diff --git a/.circleci/.gitignore b/.circleci/.gitignore new file mode 100644 index 0000000000000..2b7d56379cacd --- /dev/null +++ b/.circleci/.gitignore @@ -0,0 +1 @@ +config-staging diff --git a/.circleci/config.yml b/.circleci/config.yml index aa61845ebd672..372018b9e2532 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,1842 +1,76 @@ -# The config expects the following environment variables to be set: -# - "SLACK_WEBHOOK" Slack hook URL to send notifications. -# -# The publishing scripts expect access tokens to be defined as env vars, -# but those are not covered here. -# -# CircleCI docs on variables: -# https://circleci.com/docs/2.0/env-vars/ +version: 2.1 + +# Required for dynamic configuration +setup: true + +# Orbs +orbs: + path-filtering: circleci/path-filtering@0.1.0 + continuation: circleci/continuation@0.2.0 + +# All input parameters to pass to build config +parameters: + run-docs-only: + type: boolean + default: false + + upload-to-storage: + type: string + default: '1' + + run-build-linux: + type: boolean + default: false + + run-build-mac: + type: boolean + default: false + + run-linux-publish: + type: boolean + default: false + + linux-publish-arch-limit: + type: enum + default: all + enum: ["all", "arm", "arm64", "x64", "ia32"] + + run-macos-publish: + type: boolean + default: false + + macos-publish-arch-limit: + type: enum + default: all + enum: ["all", "osx-x64", "osx-arm64", "mas-x64", "mas-arm64"] -# Build machines configs. -docker-image: &docker-image - docker: - - image: electronbuilds/electron:0.0.10 - -machine-linux-medium: &machine-linux-medium - <<: *docker-image - resource_class: medium - -machine-linux-2xlarge: &machine-linux-2xlarge - <<: *docker-image - resource_class: 2xlarge - -machine-mac: &machine-mac - macos: - xcode: "9.4.1" - -machine-mac-large: &machine-mac-large - resource_class: large - macos: - xcode: "9.4.1" - -# Build configurations options. -env-debug-build: &env-debug-build - GN_CONFIG: //electron/build/args/debug.gn - -env-testing-build: &env-testing-build - GN_CONFIG: //electron/build/args/testing.gn - CHECK_DIST_MANIFEST: '1' - -env-release-build: &env-release-build - GN_CONFIG: //electron/build/args/release.gn - STRIP_BINARIES: true - GENERATE_SYMBOLS: true - -env-headless-testing: &env-headless-testing - DISPLAY: ':99.0' - -env-stack-dumping: &env-stack-dumping - ELECTRON_ENABLE_STACK_DUMPING: '1' - -env-browsertests: &env-browsertests - GN_CONFIG: //electron/build/args/native_tests.gn - BUILD_TARGET: electron/spec:chromium_browsertests - TESTS_CONFIG: src/electron/spec/configs/browsertests.yml - -env-unittests: &env-unittests - GN_CONFIG: //electron/build/args/native_tests.gn - BUILD_TARGET: electron/spec:chromium_unittests - TESTS_CONFIG: src/electron/spec/configs/unittests.yml - -# Build targets options. -env-ia32: &env-ia32 - GN_EXTRA_ARGS: 'target_cpu = "x86"' - NPM_CONFIG_ARCH: ia32 - TARGET_ARCH: ia32 - -env-arm: &env-arm - GN_EXTRA_ARGS: 'target_cpu = "arm"' - MKSNAPSHOT_TOOLCHAIN: //build/toolchain/linux:clang_arm - BUILD_NATIVE_MKSNAPSHOT: 1 - TARGET_ARCH: arm - -env-arm64: &env-arm64 - GN_EXTRA_ARGS: 'target_cpu = "arm64" fatal_linker_warnings = false enable_linux_installer = false' - MKSNAPSHOT_TOOLCHAIN: //build/toolchain/linux:clang_arm64 - BUILD_NATIVE_MKSNAPSHOT: 1 - TARGET_ARCH: arm64 - -env-mas: &env-mas - GN_EXTRA_ARGS: 'is_mas_build = true' - MAS_BUILD: 'true' - -# Misc build configuration options. -env-enable-sccache: &env-enable-sccache - USE_SCCACHE: true - -env-send-slack-notifications: &env-send-slack-notifications - NOTIFY_SLACK: true - -env-linux-medium: &env-linux-medium - NUMBER_OF_NINJA_PROCESSES: 3 - -env-linux-2xlarge: &env-linux-2xlarge - NUMBER_OF_NINJA_PROCESSES: 18 - -env-machine-mac: &env-machine-mac - NUMBER_OF_NINJA_PROCESSES: 6 - -env-mac-large: &env-mac-large - NUMBER_OF_NINJA_PROCESSES: 10 - -env-disable-crash-reporter-tests: &env-disable-crash-reporter-tests - DISABLE_CRASH_REPORTER_TESTS: true - -env-ninja-status: &env-ninja-status - NINJA_STATUS: "[%r processes, %f/%t @ %o/s : %es] " - -env-disable-run-as-node: &env-disable-run-as-node - GN_BUILDFLAG_ARGS: 'enable_run_as_node = false' - -# Individual (shared) steps. -step-maybe-notify-slack-failure: &step-maybe-notify-slack-failure - run: - name: Send a Slack notification on failure - command: | - if [ "$NOTIFY_SLACK" == "true" ]; then - export MESSAGE="Build failed for *<$CIRCLE_BUILD_URL|$CIRCLE_JOB>* nightly build from *$CIRCLE_BRANCH*." - curl -g -H "Content-Type: application/json" -X POST \ - -d "{\"text\": \"$MESSAGE\", \"attachments\": [{\"color\": \"#FC5C3C\",\"title\": \"$CIRCLE_JOB nightly build results\",\"title_link\": \"$CIRCLE_BUILD_URL\"}]}" $SLACK_WEBHOOK - fi - when: on_fail - -step-maybe-notify-slack-success: &step-maybe-notify-slack-success - run: - name: Send a Slack notification on success - command: | - if [ "$NOTIFY_SLACK" == "true" ]; then - export MESSAGE="Build succeeded for *<$CIRCLE_BUILD_URL|$CIRCLE_JOB>* nightly build from *$CIRCLE_BRANCH*." - curl -g -H "Content-Type: application/json" -X POST \ - -d "{\"text\": \"$MESSAGE\", \"attachments\": [{\"color\": \"good\",\"title\": \"$CIRCLE_JOB nightly build results\",\"title_link\": \"$CIRCLE_BUILD_URL\"}]}" $SLACK_WEBHOOK - fi - when: on_success - -step-checkout-electron: &step-checkout-electron - checkout: - path: src/electron - -step-depot-tools-get: &step-depot-tools-get - run: - name: Get depot tools - command: | - git clone --depth=1 https://chromium.googlesource.com/chromium/tools/depot_tools.git - -step-depot-tools-add-to-path: &step-depot-tools-add-to-path - run: - name: Add depot tools to PATH - command: echo 'export PATH="$PATH:'"$PWD"'/depot_tools"' >> $BASH_ENV - -step-gclient-sync: &step-gclient-sync - run: - name: Gclient sync - command: | - # If we did not restore a complete sync then we need to sync for realz - if [ ! -s "src/electron/.circle-sync-done" ]; then - gclient config \ - --name "src/electron" \ - --unmanaged \ - $GCLIENT_EXTRA_ARGS \ - "$CIRCLE_REPOSITORY_URL" - - gclient sync --with_branch_heads --with_tags - fi - -step-setup-env-for-build: &step-setup-env-for-build - run: - name: Setup Environment Variables - command: | - # To find `gn` executable. - echo 'export CHROMIUM_BUILDTOOLS_PATH="'"$PWD"'/src/buildtools"' >> $BASH_ENV - - if [ "$USE_SCCACHE" == "true" ]; then - # https://github.com/mozilla/sccache - SCCACHE_PATH="$PWD/src/electron/external_binaries/sccache" - echo 'export SCCACHE_PATH="'"$SCCACHE_PATH"'"' >> $BASH_ENV - if [ "$CIRCLE_PR_NUMBER" != "" ]; then - #if building a fork set readonly access to sccache - echo 'export SCCACHE_BUCKET="electronjs-sccache"' >> $BASH_ENV - echo 'export SCCACHE_TWO_TIER=true' >> $BASH_ENV - fi - fi - -step-restore-brew-cache: &step-restore-brew-cache - restore_cache: - paths: - - /usr/local/Homebrew - keys: - - v1-brew-cache-{{ arch }} - -step-get-more-space-on-mac: &step-get-more-space-on-mac - run: - name: Free up space on MacOS - command: | - if [ "`uname`" == "Darwin" ]; then - sudo rm -rf /Library/Developer/CoreSimulator - fi - -step-delete-git-directories: &step-delete-git-directories - run: - name: Delete src/.git directory on MacOS to free space - command: | - if [ "`uname`" == "Darwin" ]; then - sudo rm -rf src/.git - fi - -# On macOS the yarn install command during gclient sync was run on a linux -# machine and therefore installed a slightly different set of dependencies -# Notably "fsevents" is a macOS only dependency, we rerun yarn install once -# we are on a macOS machine to get the correct state -step-install-npm-deps-on-mac: &step-install-npm-deps-on-mac - run: - name: Install node_modules on MacOS - command: | - if [ "`uname`" == "Darwin" ]; then - cd src/electron - node script/yarn install - fi - -# This step handles the differences between the linux "gclient sync" -# and the expected state on macOS -step-fix-sync-on-mac: &step-fix-sync-on-mac - run: - name: Fix Sync on macOS - command: | - if [ "`uname`" == "Darwin" ]; then - # Fix Clang Install (wrong binary) - rm -rf src/third_party/llvm-build - python src/tools/clang/scripts/update.py - # Fix Framework Header Installs (symlinks not retained) - rm -rf src/electron/external_binaries - python src/electron/script/update-external-binaries.py - fi - -step-install-signing-cert-on-mac: &step-install-signing-cert-on-mac - run: - name: Import and trust self-signed codesigning cert on MacOS - command: | - if [ "`uname`" == "Darwin" ]; then - cd src/electron - ./script/codesign/import-testing-cert-ci.sh - fi - -step-install-gnutar-on-mac: &step-install-gnutar-on-mac - run: - name: Install gnu-tar on macos - command: | - if [ "`uname`" == "Darwin" ]; then - brew update - brew install gnu-tar - ln -fs /usr/local/bin/gtar /usr/local/bin/tar - fi - -step-gn-gen-default: &step-gn-gen-default - run: - name: Default GN gen - command: | - cd src - gn gen out/Default --args='import("'$GN_CONFIG'") cc_wrapper="'"$SCCACHE_PATH"'"'" $GN_EXTRA_ARGS $GN_BUILDFLAG_ARGS" - -step-gn-check: &step-gn-check - run: - name: GN check - command: | - cd src - gn check out/Default //electron:electron_lib - gn check out/Default //electron:electron_app - gn check out/Default //electron:manifests - gn check out/Default //electron/shell/common/api:mojo - -step-electron-build: &step-electron-build - run: - name: Electron build - no_output_timeout: 30m - command: | - cd src - ninja -C out/Default electron -j $NUMBER_OF_NINJA_PROCESSES - -step-maybe-electron-dist-strip: &step-maybe-electron-dist-strip - run: - name: Strip electron binaries - command: | - if [ "$STRIP_BINARIES" == "true" ] && [ "`uname`" != "Darwin" ]; then - cd src - electron/script/strip-binaries.py --target-cpu="$TARGET_ARCH" - fi - -step-electron-dist-build: &step-electron-dist-build - run: - name: Build dist.zip - command: | - cd src - ninja -C out/Default electron:electron_dist_zip - if [ "$CHECK_DIST_MANIFEST" == "1" ]; then - if [ "`uname`" == "Darwin" ]; then - target_os=mac - target_cpu=x64 - if [ x"$MAS_BUILD" == x"true" ]; then - target_os=mac_mas - fi - elif [ "`uname`" == "Linux" ]; then - target_os=linux - if [ x"$TARGET_ARCH" == x ]; then - target_cpu=x64 - elif [ "$TARGET_ARCH" == "ia32" ]; then - target_cpu=x86 - else - target_cpu="$TARGET_ARCH" - fi - else - echo "Unknown system: `uname`" - exit 1 - fi - electron/script/zip_manifests/check-zip-manifest.py out/Default/dist.zip electron/script/zip_manifests/dist_zip.$target_os.$target_cpu.manifest - fi - -step-electron-dist-store: &step-electron-dist-store - store_artifacts: - path: src/out/Default/dist.zip - destination: dist.zip - -step-electron-chromedriver-build: &step-electron-chromedriver-build - run: - name: Build chromedriver.zip - command: | - cd src - ninja -C out/Default chrome/test/chromedriver -j $NUMBER_OF_NINJA_PROCESSES - electron/script/strip-binaries.py --target-cpu="$TARGET_ARCH" --file $PWD/out/Default/chromedriver - ninja -C out/Default electron:electron_chromedriver_zip - -step-electron-chromedriver-store: &step-electron-chromedriver-store - store_artifacts: - path: src/out/Default/chromedriver.zip - destination: chromedriver.zip - -step-nodejs-headers-build: &step-nodejs-headers-build - run: - name: Build Node.js headers - command: | - cd src - ninja -C out/Default third_party/electron_node:headers - -step-nodejs-headers-store: &step-nodejs-headers-store - store_artifacts: - path: src/out/Default/gen/node_headers.tar.gz - destination: node_headers.tar.gz - -step-electron-publish: &step-electron-publish - run: - name: Publish Electron Dist - command: | - cd src/electron - if [ "$UPLOAD_TO_S3" == "1" ]; then - echo 'Uploading Electron release distribution to S3' - script/release/uploaders/upload.py --upload_to_s3 - else - echo 'Uploading Electron release distribution to Github releases' - script/release/uploaders/upload.py - fi - -step-persist-data-for-tests: &step-persist-data-for-tests - persist_to_workspace: - root: . - paths: - # Build artifacts - - src/out/Default/dist.zip - - src/out/Default/mksnapshot.zip - - src/out/Default/gen/node_headers - - src/out/ffmpeg/ffmpeg.zip - -step-electron-dist-unzip: &step-electron-dist-unzip - run: - name: Unzip dist.zip - command: | - cd src/out/Default - # -o overwrite files WITHOUT prompting - # TODO(alexeykuzmin): Remove '-o' when it's no longer needed. - unzip -o dist.zip - -step-ffmpeg-unzip: &step-ffmpeg-unzip - run: - name: Unzip ffmpeg.zip - command: | - cd src/out/ffmpeg - unzip -o ffmpeg.zip - -step-mksnapshot-unzip: &step-mksnapshot-unzip - run: - name: Unzip mksnapshot.zip - command: | - cd src/out/Default - unzip -o mksnapshot.zip - -step-ffmpeg-gn-gen: &step-ffmpeg-gn-gen - run: - name: ffmpeg GN gen - command: | - cd src - gn gen out/ffmpeg --args='import("//electron/build/args/ffmpeg.gn") cc_wrapper="'"$SCCACHE_PATH"'"'" $GN_EXTRA_ARGS" - -step-ffmpeg-build: &step-ffmpeg-build - run: - name: Non proprietary ffmpeg build - command: | - cd src - ninja -C out/ffmpeg electron:electron_ffmpeg_zip -j $NUMBER_OF_NINJA_PROCESSES - -step-verify-ffmpeg: &step-verify-ffmpeg - run: - name: Verify ffmpeg - command: | - cd src - python electron/script/verify-ffmpeg.py --source-root "$PWD" --build-dir out/Default --ffmpeg-path out/ffmpeg - -step-ffmpeg-store: &step-ffmpeg-store - store_artifacts: - path: src/out/ffmpeg/ffmpeg.zip - destination: ffmpeg.zip - -step-verify-mksnapshot: &step-verify-mksnapshot - run: - name: Verify mksnapshot - command: | - cd src - python electron/script/verify-mksnapshot.py --source-root "$PWD" --build-dir out/Default - -step-setup-linux-for-headless-testing: &step-setup-linux-for-headless-testing - run: - name: Setup for headless testing - command: | - if [ "`uname`" != "Darwin" ]; then - sh -e /etc/init.d/xvfb start - fi - -step-show-sccache-stats: &step-show-sccache-stats - run: - name: Check sccache stats after build - command: | - if [ "$SCCACHE_PATH" != "" ]; then - $SCCACHE_PATH -s - fi - -step-mksnapshot-build: &step-mksnapshot-build - run: - name: mksnapshot build - command: | - cd src - if [ "`uname`" != "Darwin" ]; then - if [ "$TARGET_ARCH" == "arm" ]; then - electron/script/strip-binaries.py --file $PWD/out/Default/clang_x86_v8_arm/mksnapshot - elif [ "$TARGET_ARCH" == "arm64" ]; then - electron/script/strip-binaries.py --file $PWD/out/Default/clang_x64_v8_arm64/mksnapshot - else - electron/script/strip-binaries.py --file $PWD/out/Default/mksnapshot - fi - fi - ninja -C out/Default electron:electron_mksnapshot_zip -j $NUMBER_OF_NINJA_PROCESSES - -step-mksnapshot-store: &step-mksnapshot-store - store_artifacts: - path: src/out/Default/mksnapshot.zip - destination: mksnapshot.zip - -step-maybe-generate-breakpad-symbols: &step-maybe-generate-breakpad-symbols - run: - name: Generate breakpad symbols - command: | - if [ "$GENERATE_SYMBOLS" == "true" ]; then - cd src - ninja -C out/Default electron:electron_symbols - fi - -step-maybe-zip-symbols: &step-maybe-zip-symbols - run: - name: Zip symbols - command: | - cd src - export BUILD_PATH="$PWD/out/Default" - electron/script/zip-symbols.py -b $BUILD_PATH - -step-maybe-cross-arch-snapshot: &step-maybe-cross-arch-snapshot - run: - name: Generate cross arch snapshot (arm/arm64) - command: | - if [ "$TRIGGER_ARM_TEST" == "true" ] && [ -z "$CIRCLE_PR_NUMBER" ]; then - cd src - if [ "$TARGET_ARCH" == "arm" ]; then - export MKSNAPSHOT_PATH="clang_x86_v8_arm" - elif [ "$TARGET_ARCH" == "arm64" ]; then - export MKSNAPSHOT_PATH="clang_x64_v8_arm64" - fi - cp "out/Default/$MKSNAPSHOT_PATH/mksnapshot" out/Default - cp "out/Default/$MKSNAPSHOT_PATH/libffmpeg.so" out/Default - cp "out/Default/$MKSNAPSHOT_PATH/v8_context_snapshot_generator" out/Default - python electron/script/verify-mksnapshot.py --source-root "$PWD" --build-dir out/Default --create-snapshot-only - mkdir cross-arch-snapshots - cp out/Default-mksnapshot-test/*.bin cross-arch-snapshots - fi - -step-maybe-cross-arch-snapshot-store: &step-maybe-cross-arch-snapshot-store - store_artifacts: - path: src/cross-arch-snapshots - destination: cross-arch-snapshots - -step-maybe-trigger-arm-test: &step-maybe-trigger-arm-test - run: - name: Trigger an arm test on VSTS if applicable - command: | - cd src - # Only run for non-fork prs - if [ "$TRIGGER_ARM_TEST" == "true" ] && [ -z "$CIRCLE_PR_NUMBER" ]; then - #Trigger VSTS job, passing along CircleCI job number and branch to build - echo "Triggering electron-$TARGET_ARCH-testing build on VSTS" - node electron/script/release/ci-release-build.js --job=electron-$TARGET_ARCH-testing --ci=VSTS --armTest --circleBuildNum=$CIRCLE_BUILD_NUM $CIRCLE_BRANCH - fi - -step-maybe-generate-typescript-defs: &step-maybe-generate-typescript-defs - run: - name: Generate type declarations - command: | - if [ "`uname`" == "Darwin" ]; then - cd src/electron - node script/yarn create-typescript-definitions - fi - -step-fix-known-hosts-linux: &step-fix-known-hosts-linux - run: - name: Fix Known Hosts on Linux - command: | - if [ "`uname`" == "Linux" ]; then - ./src/electron/.circleci/fix-known-hosts.sh - fi - -step-ninja-summary: &step-ninja-summary - run: - name: Print ninja summary - command: | - python depot_tools/post_build_ninja_summary.py -C src/out/Default - -# Lists of steps. -steps-lint: &steps-lint - steps: - - *step-checkout-electron - - run: - name: Setup third_party Depot Tools - command: | - # "depot_tools" has to be checkout into "//third_party/depot_tools" so pylint.py can a "pylintrc" file. - git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git src/third_party/depot_tools - echo 'export PATH="$PATH:'"$PWD"'/src/third_party/depot_tools"' >> $BASH_ENV - - run: - name: Download GN Binary - command: | - chromium_revision="$(grep -A1 chromium_version src/electron/DEPS | tr -d '\n' | cut -d\' -f4)" - gn_version="$(curl -sL "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/DEPS?format=TEXT" | base64 -d | grep gn_version | head -n1 | cut -d\' -f4)" - - cipd ensure -ensure-file - -root . <<-CIPD - \$ServiceURL https://chrome-infra-packages.appspot.com/ - @Subdir src/buildtools/linux64 - gn/gn/linux-amd64 $gn_version - CIPD - - echo 'export CHROMIUM_BUILDTOOLS_PATH="'"$PWD"'/src/buildtools"' >> $BASH_ENV - - run: - name: Download clang-format Binary - command: | - chromium_revision="$(grep -A1 chromium_version src/electron/DEPS | tr -d '\n' | cut -d\' -f4)" - - sha1_path='buildtools/linux64/clang-format.sha1' - curl -sL "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/${sha1_path}?format=TEXT" | base64 -d > "src/${sha1_path}" - - download_from_google_storage.py --no_resume --no_auth --bucket chromium-clang-format -s "src/${sha1_path}" - - run: - name: Run Lint - command: | - # gn.py tries to find a gclient root folder starting from the current dir. - # When it fails and returns "None" path, the whole script fails. Let's "fix" it. - touch .gclient - # Another option would be to checkout "buildtools" inside the Electron checkout, - # but then we would lint its contents (at least gn format), and it doesn't pass it. - - cd src/electron - node script/yarn install --frozen-lockfile - node script/yarn lint - -steps-checkout: &steps-checkout - steps: - - *step-checkout-electron - - *step-depot-tools-get - - *step-depot-tools-add-to-path - - *step-restore-brew-cache - - *step-get-more-space-on-mac - - *step-install-gnutar-on-mac - - - run: - name: Generate DEPS Hash - command: node src/electron/script/generate-deps-hash.js - - run: - name: Touch Sync Done - command: touch src/electron/.circle-sync-done - # Restore exact src cache based on the hash of DEPS and patches/* - # If no cache is matched EXACTLY then the .circle-sync-done file is empty - # If a cache is matched EXACTLY then the .circle-sync-done file contains "done" - - restore_cache: - paths: - - ./src - keys: - - v5-src-cache-{{ arch }}-{{ checksum "src/electron/.depshash" }} - name: Restoring src cache - # Restore exact or closest git cache based on the hash of DEPS and .circle-sync-done - # If the src cache was restored above then this will match an empty cache - # If the src cache was not restored above then this will match a close git cache - - restore_cache: - paths: - - ~/.gclient-cache - keys: - - v2-gclient-cache-{{ arch }}-{{ checksum "src/electron/.circle-sync-done" }}-{{ checksum "src/electron/DEPS" }} - - v2-gclient-cache-{{ arch }}-{{ checksum "src/electron/.circle-sync-done" }} - name: Conditionally restoring git cache - - run: - name: Set GIT_CACHE_PATH to make gclient to use the cache - command: | - # CircleCI does not support interpolation when setting environment variables. - # https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-shell-command - echo 'export GIT_CACHE_PATH="$HOME/.gclient-cache"' >> $BASH_ENV - # This sync call only runs if .circle-sync-done is an EMPTY file - - *step-gclient-sync - # Persist the git cache based on the hash of DEPS and .circle-sync-done - # If the src cache was restored above then this will persist an empty cache - - save_cache: - paths: - - ~/.gclient-cache - key: v2-gclient-cache-{{ arch }}-{{ checksum "src/electron/.circle-sync-done" }}-{{ checksum "src/electron/DEPS" }} - name: Persisting git cache - # These next few steps reset Electron to the correct commit regardless of which cache was restored - - run: - name: Wipe Electron - command: rm -rf src/electron - - *step-checkout-electron - - run: - name: Run Electron Only Hooks - command: gclient runhooks --spec="solutions=[{'name':'src/electron','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':False},'managed':False}]" - - run: - name: Generate DEPS Hash - command: (cd src/electron && git checkout .) && node src/electron/script/generate-deps-hash.js - # Mark the sync as done for future cache saving - - run: - name: Mark Sync Done - command: echo DONE > src/electron/.circle-sync-done - # Minimize the size of the cache - - run: - name: Remove some unused data to avoid storing it in the workspace/cache - command: | - rm -rf src/android_webview - rm -rf src/ios - rm -rf src/third_party/blink/web_tests - rm -rf src/third_party/blink/perf_tests - rm -rf src/third_party/hunspell_dictionaries - rm -rf src/third_party/WebKit/LayoutTests - # Save the src cache based on the deps hash - - save_cache: - paths: - - ./src - key: v5-src-cache-{{ arch }}-{{ checksum "src/electron/.depshash" }} - name: Persisting src cache - - save_cache: - paths: - - /usr/local/Homebrew - key: v1-brew-cache-{{ arch }} - name: Persisting brew cache - - persist_to_workspace: - root: . - paths: - - depot_tools - - src - -steps-electron-gn-check: &steps-electron-gn-check - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-setup-env-for-build - - *step-gn-gen-default - - *step-gn-check - -steps-electron-build: &steps-electron-build - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-setup-env-for-build - - *step-gn-gen-default - - # Electron app - - *step-electron-build - - *step-electron-dist-build - - *step-electron-dist-store - - *step-ninja-summary - - # Node.js headers - - *step-nodejs-headers-build - - *step-nodejs-headers-store - - - *step-show-sccache-stats - -steps-electron-build-for-tests: &steps-electron-build-for-tests - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-setup-env-for-build - - *step-restore-brew-cache - - *step-install-npm-deps-on-mac - - *step-fix-sync-on-mac - - *step-gn-gen-default - - # Electron app - - *step-electron-build - - *step-maybe-electron-dist-strip - - *step-electron-dist-build - - *step-electron-dist-store - - *step-ninja-summary - - # Node.js headers - - *step-nodejs-headers-build - - *step-nodejs-headers-store - - - *step-show-sccache-stats - - # mksnapshot - - *step-mksnapshot-build - - *step-mksnapshot-store - - *step-maybe-cross-arch-snapshot - - *step-maybe-cross-arch-snapshot-store - - # ffmpeg - - *step-ffmpeg-gn-gen - - *step-ffmpeg-build - - *step-ffmpeg-store - - # Save all data needed for a further tests run. - - *step-persist-data-for-tests - - - *step-maybe-generate-breakpad-symbols - - *step-maybe-zip-symbols - - # Trigger tests on arm hardware if needed - - *step-maybe-trigger-arm-test - - - *step-maybe-notify-slack-failure - -steps-electron-build-for-publish: &steps-electron-build-for-publish - steps: - - *step-checkout-electron - - *step-depot-tools-get - - *step-depot-tools-add-to-path - - *step-restore-brew-cache - - *step-get-more-space-on-mac - - *step-gclient-sync - - *step-setup-env-for-build - - *step-gn-gen-default - - *step-delete-git-directories - - # Electron app - - *step-electron-build - - *step-maybe-electron-dist-strip - - *step-electron-dist-build - - *step-electron-dist-store - - *step-maybe-generate-breakpad-symbols - - *step-maybe-zip-symbols - - # mksnapshot - - *step-mksnapshot-build - - *step-mksnapshot-store - - # chromedriver - - *step-electron-chromedriver-build - - *step-electron-chromedriver-store - - # Node.js headers - - *step-nodejs-headers-build - - *step-nodejs-headers-store - - # ffmpeg - - *step-ffmpeg-gn-gen - - *step-ffmpeg-build - - *step-ffmpeg-store - - # typescript defs - - *step-maybe-generate-typescript-defs - - # Publish - - *step-electron-publish - -steps-chromedriver-build: &steps-chromedriver-build - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-setup-env-for-build - - *step-fix-sync-on-mac - - *step-gn-gen-default - - - *step-electron-chromedriver-build - - *step-electron-chromedriver-store - - - *step-maybe-notify-slack-failure - -steps-native-tests: &steps-native-tests - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-setup-env-for-build - - *step-gn-gen-default - - - run: - name: Build tests - command: | - cd src - ninja -C out/Default $BUILD_TARGET - - *step-show-sccache-stats - - - *step-setup-linux-for-headless-testing - - run: - name: Run tests - command: | - mkdir test_results - python src/electron/script/native-tests.py run \ - --config $TESTS_CONFIG \ - --tests-dir src/out/Default \ - --output-dir test_results \ - $TESTS_ARGS - - - store_artifacts: - path: test_results - destination: test_results # Put it in the root folder. - - store_test_results: - path: test_results - -steps-verify-ffmpeg: &steps-verify-ffmpeg - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-electron-dist-unzip - - *step-ffmpeg-unzip - - *step-setup-linux-for-headless-testing - - - *step-verify-ffmpeg - - *step-maybe-notify-slack-failure - -steps-verify-mksnapshot: &steps-verify-mksnapshot - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-electron-dist-unzip - - *step-mksnapshot-unzip - - *step-setup-linux-for-headless-testing - - - *step-verify-mksnapshot - - *step-maybe-notify-slack-failure - -steps-tests: &steps-tests - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-electron-dist-unzip - - *step-mksnapshot-unzip - - *step-setup-linux-for-headless-testing - - *step-restore-brew-cache - - *step-fix-known-hosts-linux - - *step-install-signing-cert-on-mac - - - run: - name: Run Electron tests - environment: - MOCHA_REPORTER: mocha-multi-reporters - ELECTRON_TEST_RESULTS_DIR: junit - MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap - ELECTRON_DISABLE_SECURITY_WARNINGS: 1 - command: | - cd src - export ELECTRON_OUT_DIR=Default - (cd electron && node script/yarn test -- --ci --enable-logging) - - run: - name: Check test results existence - command: | - cd src - - # Check if test results exist and are not empty. - if [ ! -s "junit/test-results-remote.xml" ]; then - exit 1 - fi - if [ ! -s "junit/test-results-main.xml" ]; then - exit 1 - fi - - store_test_results: - path: src/junit - - - *step-verify-mksnapshot - - - *step-maybe-notify-slack-failure - -steps-test-nan: &steps-test-nan - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-electron-dist-unzip - - *step-setup-linux-for-headless-testing - - *step-fix-known-hosts-linux - - run: - name: Run Nan Tests - command: | - cd src - export ELECTRON_OUT_DIR=Default - node electron/script/nan-spec-runner.js - -steps-test-node: &steps-test-node - steps: - - attach_workspace: - at: . - - *step-depot-tools-add-to-path - - *step-electron-dist-unzip - - *step-setup-linux-for-headless-testing - - *step-fix-known-hosts-linux - - run: - name: Run Node Tests - command: | - cd src - export ELECTRON_OUT_DIR=Default - node electron/script/node-spec-runner.js junit - - store_test_results: - path: src/junit - -chromium-upgrade-branches: &chromium-upgrade-branches - /chromium\-upgrade\/[0-9]+/ - -# List of all jobs. -version: 2 jobs: - # Layer 0: Lint. Standalone. - lint: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *steps-lint - - # Layer 1: Checkout. - linux-checkout: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' - <<: *steps-checkout - - linux-checkout-for-native-tests: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_pyyaml=True' - <<: *steps-checkout - - linux-checkout-for-native-tests-with-no-patches: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - GCLIENT_EXTRA_ARGS: '--custom-var=apply_patches=False --custom-var=checkout_pyyaml=True' - <<: *steps-checkout - - mac-checkout: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' - <<: *steps-checkout - - # Layer 2: Builds. - linux-x64-debug: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-debug-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build - - linux-x64-debug-gn-check: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-debug-build - <<: *steps-electron-gn-check - - linux-x64-testing: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - linux-x64-testing-no-run-as-node: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *env-disable-run-as-node - <<: *steps-electron-build-for-tests - - linux-x64-testing-gn-check: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-testing-build - <<: *steps-electron-gn-check - - linux-x64-chromedriver: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *steps-chromedriver-build - - linux-x64-release: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - linux-x64-publish: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_boto=True --custom-var=checkout_requests=True' - <<: *env-release-build - <<: *steps-electron-build-for-publish - - linux-ia32-debug: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-ia32 - <<: *env-debug-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build - - linux-ia32-testing: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-ia32 - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - linux-ia32-chromedriver: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *steps-chromedriver-build - - linux-ia32-release: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-ia32 - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - linux-ia32-publish: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_boto=True --custom-var=checkout_requests=True' - <<: *env-ia32 - <<: *env-release-build - <<: *steps-electron-build-for-publish - - linux-arm-debug: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm - <<: *env-debug-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build - - linux-arm-testing: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - TRIGGER_ARM_TEST: true - <<: *steps-electron-build-for-tests - - linux-arm-chromedriver: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-arm - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *steps-chromedriver-build - - linux-arm-release: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - linux-arm-publish: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm - <<: *env-release-build - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_boto=True --custom-var=checkout_requests=True' - <<: *steps-electron-build-for-publish - - linux-arm64-debug: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm64 - <<: *env-debug-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build - - linux-arm64-debug-gn-check: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-arm64 - <<: *env-debug-build - <<: *steps-electron-gn-check - - linux-arm64-testing: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm64 - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - TRIGGER_ARM_TEST: true - <<: *steps-electron-build-for-tests - - linux-arm64-testing-gn-check: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-arm64 - <<: *env-testing-build - <<: *steps-electron-gn-check - - linux-arm64-chromedriver: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-arm64 - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *steps-chromedriver-build - - linux-arm64-release: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm64 - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - linux-arm64-publish: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-arm64 - <<: *env-release-build - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm64=True --custom-var=checkout_boto=True --custom-var=checkout_requests=True' - <<: *steps-electron-build-for-publish - - osx-testing: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - osx-debug-gn-check: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-debug-build - <<: *steps-electron-gn-check - - osx-testing-gn-check: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-testing-build - <<: *steps-electron-gn-check - - osx-chromedriver: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *steps-chromedriver-build - - osx-release: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - osx-publish: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-release-build - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_boto=True --custom-var=checkout_requests=True' - <<: *steps-electron-build-for-publish - - mas-testing: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-mas - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - mas-debug-gn-check: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-mas - <<: *env-debug-build - <<: *steps-electron-gn-check - - mas-testing-gn-check: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-mas - <<: *env-testing-build - <<: *steps-electron-gn-check - - mas-chromedriver: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-send-slack-notifications - <<: *steps-chromedriver-build - - mas-release: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-mas - <<: *env-release-build - <<: *env-enable-sccache - <<: *env-ninja-status - <<: *steps-electron-build-for-tests - - mas-publish: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-mas - <<: *env-release-build - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_boto=True --custom-var=checkout_requests=True' - <<: *steps-electron-build-for-publish - - # Layer 3: Tests. - linux-x64-unittests: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-unittests - <<: *env-enable-sccache - <<: *env-headless-testing - <<: *steps-native-tests - - linux-x64-disabled-unittests: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-unittests - <<: *env-enable-sccache - <<: *env-headless-testing - TESTS_ARGS: '--only-disabled-tests' - <<: *steps-native-tests - - linux-x64-chromium-unittests: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-unittests - <<: *env-enable-sccache - <<: *env-headless-testing - TESTS_ARGS: '--include-disabled-tests' - <<: *steps-native-tests - - linux-x64-browsertests: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-2xlarge - <<: *env-browsertests - <<: *env-testing-build - <<: *env-enable-sccache - <<: *env-headless-testing - <<: *steps-native-tests - - linux-x64-testing-tests: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-headless-testing - <<: *env-stack-dumping - <<: *steps-tests - - linux-x64-testing-nan: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-headless-testing - <<: *env-stack-dumping - <<: *steps-test-nan - - linux-x64-testing-node: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-medium - <<: *env-headless-testing - <<: *env-stack-dumping - <<: *steps-test-node - - linux-x64-release-tests: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-headless-testing - <<: *env-send-slack-notifications - <<: *steps-tests - - linux-x64-verify-ffmpeg: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-headless-testing - <<: *env-send-slack-notifications - <<: *steps-verify-ffmpeg - - linux-x64-verify-mksnapshot: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-headless-testing - <<: *env-send-slack-notifications - <<: *steps-verify-mksnapshot - - linux-ia32-testing-tests: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-headless-testing - <<: *env-stack-dumping - <<: *steps-tests - - linux-ia32-testing-nan: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-headless-testing - <<: *env-stack-dumping - <<: *steps-test-nan - - linux-ia32-testing-node: - <<: *machine-linux-2xlarge - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-headless-testing - <<: *env-stack-dumping - <<: *steps-test-node - - linux-ia32-release-tests: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-headless-testing - <<: *env-send-slack-notifications - <<: *steps-tests - - linux-ia32-verify-ffmpeg: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-headless-testing - <<: *env-send-slack-notifications - <<: *steps-verify-ffmpeg - - linux-ia32-verify-mksnapshot: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-ia32 - <<: *env-headless-testing - <<: *env-send-slack-notifications - <<: *steps-verify-mksnapshot - - osx-testing-tests: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-stack-dumping - <<: *env-disable-crash-reporter-tests - <<: *steps-tests - - osx-release-tests: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-stack-dumping - <<: *env-send-slack-notifications - <<: *env-disable-crash-reporter-tests - <<: *steps-tests - - osx-verify-ffmpeg: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-send-slack-notifications - <<: *steps-verify-ffmpeg - - osx-verify-mksnapshot: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-send-slack-notifications - <<: *steps-verify-mksnapshot - - mas-testing-tests: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-stack-dumping - <<: *steps-tests - - mas-release-tests: - <<: *machine-mac-large - environment: - <<: *env-mac-large - <<: *env-stack-dumping - <<: *env-send-slack-notifications - <<: *steps-tests - - mas-verify-ffmpeg: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-send-slack-notifications - <<: *steps-verify-ffmpeg - - mas-verify-mksnapshot: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-send-slack-notifications - <<: *steps-verify-mksnapshot - - # Layer 4: Summary. - linux-x64-release-summary: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-send-slack-notifications + generate-config: + docker: + - image: cimg/node:16.14 steps: - - *step-maybe-notify-slack-success - - linux-ia32-release-summary: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-send-slack-notifications - steps: - - *step-maybe-notify-slack-success - - linux-arm-release-summary: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-send-slack-notifications - steps: - - *step-maybe-notify-slack-success - - linux-arm64-release-summary: - <<: *machine-linux-medium - environment: - <<: *env-linux-medium - <<: *env-send-slack-notifications - steps: - - *step-maybe-notify-slack-success - - mas-release-summary: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-send-slack-notifications - steps: - - *step-maybe-notify-slack-success - - osx-release-summary: - <<: *machine-mac - environment: - <<: *env-machine-mac - <<: *env-send-slack-notifications - steps: - - *step-maybe-notify-slack-success - + - checkout + - path-filtering/set-parameters: + base-revision: main + mapping: | + ^((?!docs/).)*$ run-build-mac true + ^((?!docs/).)*$ run-build-linux true + docs/.* run-docs-only true + ^((?!docs/).)*$ run-docs-only false + - run: + command: | + cd .circleci/config + yarn + export CIRCLECI_BINARY="$HOME/circleci" + curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh | DESTDIR=$CIRCLECI_BINARY bash + node build.js + name: Pack config.yml + - continuation/continue: + configuration_path: .circleci/config-staging/built.yml + parameters: /tmp/pipeline-parameters.json + +# Initial setup workflow workflows: - version: 2 - lint: + setup: jobs: - - lint - - build-linux: - jobs: - - linux-checkout - - - linux-x64-debug: - requires: - - linux-checkout - - linux-x64-debug-gn-check: - requires: - - linux-checkout - - linux-x64-testing: - requires: - - linux-checkout - - linux-x64-testing-no-run-as-node: - requires: - - linux-checkout - - linux-x64-testing-gn-check: - requires: - - linux-checkout - - linux-x64-testing-tests: - requires: - - linux-x64-testing - - linux-x64-testing-nan: - requires: - - linux-x64-testing - - linux-x64-testing-node: - requires: - - linux-x64-testing - - - linux-ia32-debug: - requires: - - linux-checkout - - linux-ia32-testing: - requires: - - linux-checkout - - linux-ia32-testing-tests: - requires: - - linux-ia32-testing - - linux-ia32-testing-nan: - requires: - - linux-ia32-testing - - linux-ia32-testing-node: - requires: - - linux-ia32-testing - - - linux-arm-debug: - requires: - - linux-checkout - - linux-arm-testing: - requires: - - linux-checkout - - - linux-arm64-debug: - requires: - - linux-checkout - - linux-arm64-debug-gn-check: - requires: - - linux-checkout - - linux-arm64-testing: - requires: - - linux-checkout - - linux-arm64-testing-gn-check: - requires: - - linux-checkout - - build-mac: - jobs: - - mac-checkout - - osx-testing: - requires: - - mac-checkout - - - osx-debug-gn-check: - requires: - - mac-checkout - - osx-testing-gn-check: - requires: - - mac-checkout - - - osx-testing-tests: - requires: - - osx-testing - - - mas-testing: - requires: - - mac-checkout - - - mas-debug-gn-check: - requires: - - mac-checkout - - mas-testing-gn-check: - requires: - - mac-checkout - - - mas-testing-tests: - requires: - - mas-testing - - nightly-linux-release-test: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - - *chromium-upgrade-branches - jobs: - - linux-checkout - - - linux-x64-release: - requires: - - linux-checkout - - linux-x64-release-tests: - requires: - - linux-x64-release - - linux-x64-verify-ffmpeg: - requires: - - linux-x64-release - - linux-x64-verify-mksnapshot: - requires: - - linux-x64-release - - linux-x64-chromedriver: - requires: - - linux-checkout - - linux-x64-release-summary: - requires: - - linux-x64-release - - linux-x64-release-tests - - linux-x64-verify-ffmpeg - - linux-x64-chromedriver - - - linux-ia32-release: - requires: - - linux-checkout - - linux-ia32-release-tests: - requires: - - linux-ia32-release - - linux-ia32-verify-ffmpeg: - requires: - - linux-ia32-release - - linux-ia32-verify-mksnapshot: - requires: - - linux-ia32-release - - linux-ia32-chromedriver: - requires: - - linux-checkout - - linux-ia32-release-summary: - requires: - - linux-ia32-release - - linux-ia32-release-tests - - linux-ia32-verify-ffmpeg - - linux-ia32-chromedriver - - - linux-arm-release: - requires: - - linux-checkout - - linux-arm-chromedriver: - requires: - - linux-checkout - - linux-arm-release-summary: - requires: - - linux-arm-release - - linux-arm-chromedriver - - - - linux-arm64-release: - requires: - - linux-checkout - - linux-arm64-chromedriver: - requires: - - linux-checkout - - linux-arm64-release-summary: - requires: - - linux-arm64-release - - linux-arm64-chromedriver - - nightly-mac-release-test: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - - *chromium-upgrade-branches - jobs: - - mac-checkout - - - osx-release: - requires: - - mac-checkout - - osx-release-tests: - requires: - - osx-release - - osx-verify-ffmpeg: - requires: - - osx-release - - osx-verify-mksnapshot: - requires: - - osx-release - - osx-chromedriver: - requires: - - mac-checkout - - osx-release-summary: - requires: - - osx-release - - osx-release-tests - - osx-verify-ffmpeg - - osx-chromedriver - - - mas-release: - requires: - - mac-checkout - - mas-release-tests: - requires: - - mas-release - - mas-verify-ffmpeg: - requires: - - mas-release - - mas-verify-mksnapshot: - requires: - - mas-release - - mas-chromedriver: - requires: - - mac-checkout - - mas-release-summary: - requires: - - mas-release - - mas-release-tests - - mas-verify-ffmpeg - - mas-chromedriver - - # Various slow and non-essential checks we run only nightly. - # Sanitizer jobs should be added here. - linux-checks-nightly: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - - *chromium-upgrade-branches - jobs: - - linux-checkout-for-native-tests - - # TODO(alexeykuzmin): Enable it back. - # Tons of crashes right now, see - # https://circleci.com/gh/electron/electron/67463 -# - linux-x64-browsertests: -# requires: -# - linux-checkout-for-native-tests - - - linux-x64-unittests: - requires: - - linux-checkout-for-native-tests - - - linux-x64-disabled-unittests: - requires: - - linux-checkout-for-native-tests - - - linux-checkout-for-native-tests-with-no-patches - - - linux-x64-chromium-unittests: - requires: - - linux-checkout-for-native-tests-with-no-patches + - generate-config diff --git a/.circleci/config/base.yml b/.circleci/config/base.yml new file mode 100644 index 0000000000000..b018b441d2bcd --- /dev/null +++ b/.circleci/config/base.yml @@ -0,0 +1,2416 @@ +version: 2.1 + +parameters: + run-docs-only: + type: boolean + default: false + + upload-to-storage: + type: string + default: '1' + + run-build-linux: + type: boolean + default: false + + run-build-mac: + type: boolean + default: false + + run-linux-publish: + type: boolean + default: false + + linux-publish-arch-limit: + type: enum + default: all + enum: ["all", "arm", "arm64", "x64", "ia32"] + + run-macos-publish: + type: boolean + default: false + + macos-publish-arch-limit: + type: enum + default: all + enum: ["all", "osx-x64", "osx-arm64", "mas-x64", "mas-arm64"] + +# Executors +executors: + linux-docker: + parameters: + size: + description: "Docker executor size" + type: enum + enum: ["medium", "xlarge", "2xlarge"] + docker: + - image: ghcr.io/electron/build:e6bebd08a51a0d78ec23e5b3fd7e7c0846412328 + resource_class: << parameters.size >> + + macos: + parameters: + size: + description: "macOS executor size" + type: enum + enum: ["macos.x86.medium.gen2", "large"] + macos: + xcode: 13.3.0 + resource_class: << parameters.size >> + + # Electron Runners + apple-silicon: + resource_class: electronjs/macos-arm64 + machine: true + + linux-arm: + resource_class: electronjs/linux-arm + machine: true + + linux-arm64: + resource_class: electronjs/linux-arm64 + machine: true + +# The config expects the following environment variables to be set: +# - "SLACK_WEBHOOK" Slack hook URL to send notifications. +# +# The publishing scripts expect access tokens to be defined as env vars, +# but those are not covered here. +# +# CircleCI docs on variables: +# https://circleci.com/docs/2.0/env-vars/ + +# Build configurations options. +env-testing-build: &env-testing-build + GN_CONFIG: //electron/build/args/testing.gn + CHECK_DIST_MANIFEST: '1' + +env-release-build: &env-release-build + GN_CONFIG: //electron/build/args/release.gn + STRIP_BINARIES: true + GENERATE_SYMBOLS: true + CHECK_DIST_MANIFEST: '1' + IS_RELEASE: true + +env-headless-testing: &env-headless-testing + DISPLAY: ':99.0' + +env-stack-dumping: &env-stack-dumping + ELECTRON_ENABLE_STACK_DUMPING: '1' + +env-browsertests: &env-browsertests + GN_CONFIG: //electron/build/args/native_tests.gn + BUILD_TARGET: electron/spec:chromium_browsertests + TESTS_CONFIG: src/electron/spec/configs/browsertests.yml + +env-unittests: &env-unittests + GN_CONFIG: //electron/build/args/native_tests.gn + BUILD_TARGET: electron/spec:chromium_unittests + TESTS_CONFIG: src/electron/spec/configs/unittests.yml + +# Build targets options. +env-ia32: &env-ia32 + GN_EXTRA_ARGS: 'target_cpu = "x86"' + NPM_CONFIG_ARCH: ia32 + TARGET_ARCH: ia32 + +env-arm: &env-arm + GN_EXTRA_ARGS: 'target_cpu = "arm"' + MKSNAPSHOT_TOOLCHAIN: //build/toolchain/linux:clang_arm + BUILD_NATIVE_MKSNAPSHOT: 1 + TARGET_ARCH: arm + +env-apple-silicon: &env-apple-silicon + GN_EXTRA_ARGS: 'target_cpu = "arm64" use_prebuilt_v8_context_snapshot = true' + TARGET_ARCH: arm64 + USE_PREBUILT_V8_CONTEXT_SNAPSHOT: 1 + npm_config_arch: arm64 + +env-runner: &env-runner + IS_ELECTRON_RUNNER: 1 + +env-arm64: &env-arm64 + GN_EXTRA_ARGS: 'target_cpu = "arm64" fatal_linker_warnings = false enable_linux_installer = false' + MKSNAPSHOT_TOOLCHAIN: //build/toolchain/linux:clang_arm64 + BUILD_NATIVE_MKSNAPSHOT: 1 + TARGET_ARCH: arm64 + +env-mas: &env-mas + GN_EXTRA_ARGS: 'is_mas_build = true' + MAS_BUILD: 'true' + +env-mas-apple-silicon: &env-mas-apple-silicon + GN_EXTRA_ARGS: 'target_cpu = "arm64" is_mas_build = true use_prebuilt_v8_context_snapshot = true' + MAS_BUILD: 'true' + TARGET_ARCH: arm64 + USE_PREBUILT_V8_CONTEXT_SNAPSHOT: 1 + +env-send-slack-notifications: &env-send-slack-notifications + NOTIFY_SLACK: true + +env-global: &env-global + ELECTRON_OUT_DIR: Default + +env-linux-medium: &env-linux-medium + <<: *env-global + NUMBER_OF_NINJA_PROCESSES: 3 + +env-linux-2xlarge: &env-linux-2xlarge + <<: *env-global + NUMBER_OF_NINJA_PROCESSES: 34 + +env-linux-2xlarge-release: &env-linux-2xlarge-release + <<: *env-global + NUMBER_OF_NINJA_PROCESSES: 16 + +env-machine-mac: &env-machine-mac + <<: *env-global + NUMBER_OF_NINJA_PROCESSES: 6 + +env-mac-large: &env-mac-large + <<: *env-global + NUMBER_OF_NINJA_PROCESSES: 18 + +env-mac-large-release: &env-mac-large-release + <<: *env-global + NUMBER_OF_NINJA_PROCESSES: 8 + +env-ninja-status: &env-ninja-status + NINJA_STATUS: "[%r processes, %f/%t @ %o/s : %es] " + +env-disable-run-as-node: &env-disable-run-as-node + GN_BUILDFLAG_ARGS: 'enable_run_as_node = false' + +env-32bit-release: &env-32bit-release + # Set symbol level to 1 for 32 bit releases because of https://crbug.com/648948 + GN_BUILDFLAG_ARGS: 'symbol_level = 1' + +env-macos-build: &env-macos-build + # Disable pre-compiled headers to reduce out size, only useful for rebuilds + GN_BUILDFLAG_ARGS: 'enable_precompiled_headers = false' + +# Individual (shared) steps. +step-maybe-notify-slack-failure: &step-maybe-notify-slack-failure + run: + name: Send a Slack notification on failure + command: | + if [ "$NOTIFY_SLACK" == "true" ]; then + export MESSAGE="Build failed for *<$CIRCLE_BUILD_URL|$CIRCLE_JOB>* nightly build from *$CIRCLE_BRANCH*." + curl -g -H "Content-Type: application/json" -X POST \ + -d "{\"text\": \"$MESSAGE\", \"attachments\": [{\"color\": \"#FC5C3C\",\"title\": \"$CIRCLE_JOB nightly build results\",\"title_link\": \"$CIRCLE_BUILD_URL\"}]}" $SLACK_WEBHOOK + fi + when: on_fail + +step-maybe-notify-slack-success: &step-maybe-notify-slack-success + run: + name: Send a Slack notification on success + command: | + if [ "$NOTIFY_SLACK" == "true" ]; then + export MESSAGE="Build succeeded for *<$CIRCLE_BUILD_URL|$CIRCLE_JOB>* nightly build from *$CIRCLE_BRANCH*." + curl -g -H "Content-Type: application/json" -X POST \ + -d "{\"text\": \"$MESSAGE\", \"attachments\": [{\"color\": \"good\",\"title\": \"$CIRCLE_JOB nightly build results\",\"title_link\": \"$CIRCLE_BUILD_URL\"}]}" $SLACK_WEBHOOK + fi + when: on_success + +step-maybe-cleanup-arm64-mac: &step-maybe-cleanup-arm64-mac + run: + name: Cleanup after testing + command: | + if [ "$TARGET_ARCH" == "arm64" ] &&[ "`uname`" == "Darwin" ]; then + killall Electron || echo "No Electron processes left running" + killall Safari || echo "No Safari processes left running" + rm -rf ~/Library/Application\ Support/Electron* + rm -rf ~/Library/Application\ Support/electron* + security delete-generic-password -l "Chromium Safe Storage" || echo "✓ Keychain does not contain password from tests" + security delete-generic-password -l "Electron Test Main Safe Storage" || echo "✓ Keychain does not contain password from tests" + elif [ "$TARGET_ARCH" == "arm" ] || [ "$TARGET_ARCH" == "arm64" ]; then + XVFB=/usr/bin/Xvfb + /sbin/start-stop-daemon --stop --exec $XVFB || echo "Xvfb not running" + pkill electron || echo "electron not running" + rm -rf ~/.config/Electron* + rm -rf ~/.config/electron* + fi + + when: always + +step-checkout-electron: &step-checkout-electron + checkout: + path: src/electron + +step-depot-tools-get: &step-depot-tools-get + run: + name: Get depot tools + command: | + git clone --depth=1 https://chromium.googlesource.com/chromium/tools/depot_tools.git + # remove ninjalog_uploader_wrapper.py from autoninja since we don't use it and it causes problems + if [ "`uname`" == "Darwin" ]; then + sed -i '' '/ninjalog_uploader_wrapper.py/d' ./depot_tools/autoninja + else + sed -i '/ninjalog_uploader_wrapper.py/d' ./depot_tools/autoninja + fi + +step-depot-tools-add-to-path: &step-depot-tools-add-to-path + run: + name: Add depot tools to PATH + command: echo 'export PATH="$PATH:'"$PWD"'/depot_tools"' >> $BASH_ENV + +step-gclient-sync: &step-gclient-sync + run: + name: Gclient sync + command: | + # If we did not restore a complete sync then we need to sync for realz + if [ ! -s "src/electron/.circle-sync-done" ]; then + gclient config \ + --name "src/electron" \ + --unmanaged \ + $GCLIENT_EXTRA_ARGS \ + "$CIRCLE_REPOSITORY_URL" + + ELECTRON_USE_THREE_WAY_MERGE_FOR_PATCHES=1 gclient sync --with_branch_heads --with_tags + if [ "$IS_RELEASE" != "true" ]; then + # Re-export all the patches to check if there were changes. + python src/electron/script/export_all_patches.py src/electron/patches/config.json + cd src/electron + git update-index --refresh || true + if ! git diff-index --quiet HEAD --; then + # There are changes to the patches. Make a git commit with the updated patches + git add patches + GIT_COMMITTER_NAME="PatchUp" GIT_COMMITTER_EMAIL="73610968+patchup[bot]@users.noreply.github.com" git commit -m "chore: update patches" --author="PatchUp <73610968+patchup[bot]@users.noreply.github.com>" + # Export it + mkdir -p ../../patches + git format-patch -1 --stdout --keep-subject --no-stat --full-index > ../../patches/update-patches.patch + if (node ./script/push-patch.js 2> /dev/null > /dev/null); then + echo + echo "======================================================================" + echo "Changes to the patches when applying, we have auto-pushed the diff to the current branch" + echo "A new CI job will kick off shortly" + echo "======================================================================" + exit 1 + else + echo + echo "======================================================================" + echo "There were changes to the patches when applying." + echo "Check the CI artifacts for a patch you can apply to fix it." + echo "======================================================================" + exit 1 + fi + fi + fi + fi + +step-setup-env-for-build: &step-setup-env-for-build + run: + name: Setup Environment Variables + command: | + # To find `gn` executable. + echo 'export CHROMIUM_BUILDTOOLS_PATH="'"$PWD"'/src/buildtools"' >> $BASH_ENV + +step-setup-goma-for-build: &step-setup-goma-for-build + run: + name: Setup Goma + command: | + echo 'export NUMBER_OF_NINJA_PROCESSES=300' >> $BASH_ENV + if [ "`uname`" == "Darwin" ]; then + echo 'ulimit -n 10000' >> $BASH_ENV + echo 'sudo launchctl limit maxfiles 65536 200000' >> $BASH_ENV + fi + if [ ! -z "$RAW_GOMA_AUTH" ]; then + echo $RAW_GOMA_AUTH > ~/.goma_oauth2_config + fi + git clone https://github.com/electron/build-tools.git + cd build-tools + npm install + mkdir third_party + node -e "require('./src/utils/goma.js').downloadAndPrepare({ gomaOneForAll: true })" + export GOMA_FALLBACK_ON_AUTH_FAILURE=true + third_party/goma/goma_ctl.py ensure_start + if [ ! -z "$RAW_GOMA_AUTH" ] && [ "`third_party/goma/goma_auth.py info`" != "Login as Fermi Planck" ]; then + echo "WARNING!!!!!! Goma authentication is incorrect; please update Goma auth token." + exit 1 + fi + echo 'export GN_GOMA_FILE='`node -e "console.log(require('./src/utils/goma.js').gnFilePath)"` >> $BASH_ENV + echo 'export LOCAL_GOMA_DIR='`node -e "console.log(require('./src/utils/goma.js').dir)"` >> $BASH_ENV + echo 'export GOMA_FALLBACK_ON_AUTH_FAILURE=true' >> $BASH_ENV + cd .. + touch "${TMPDIR:=/tmp}"/.goma-ready + background: true + +step-wait-for-goma: &step-wait-for-goma + run: + name: Wait for Goma + command: | + until [ -f "${TMPDIR:=/tmp}"/.goma-ready ] + do + sleep 5 + done + echo "Goma ready" + no_output_timeout: 5m + +step-restore-brew-cache: &step-restore-brew-cache + restore_cache: + paths: + - /usr/local/Cellar/gnu-tar + - /usr/local/bin/gtar + keys: + - v4-brew-cache-{{ arch }} + +step-save-brew-cache: &step-save-brew-cache + save_cache: + paths: + - /usr/local/Cellar/gnu-tar + - /usr/local/bin/gtar + key: v4-brew-cache-{{ arch }} + name: Persisting brew cache + +step-get-more-space-on-mac: &step-get-more-space-on-mac + run: + name: Free up space on MacOS + command: | + if [ "`uname`" == "Darwin" ]; then + sudo mkdir -p $TMPDIR/del-target + tmpify() { + if [ -d "$1" ]; then + sudo mv "$1" $TMPDIR/del-target/$(echo $1|shasum -a 256|head -n1|cut -d " " -f1) + fi + } + + strip_arm_deep() { + opwd=$(pwd) + cd $1 + f=$(find . -perm +111 -type f) + for fp in $f + do + if [[ $(file "$fp") == *"universal binary"* ]]; then + if [[ $(file "$fp") == *"arm64e)"* ]]; then + sudo lipo -remove arm64e "$fp" -o "$fp" || true + fi + if [[ $(file "$fp") == *"arm64)"* ]]; then + sudo lipo -remove arm64 "$fp" -o "$fp" || true + fi + fi + done + + cd $opwd + } + + tmpify /Library/Developer/CoreSimulator + tmpify ~/Library/Developer/CoreSimulator + tmpify $(xcode-select -p)/Platforms/AppleTVOS.platform + tmpify $(xcode-select -p)/Platforms/iPhoneOS.platform + tmpify $(xcode-select -p)/Platforms/WatchOS.platform + tmpify $(xcode-select -p)/Platforms/WatchSimulator.platform + tmpify $(xcode-select -p)/Platforms/AppleTVSimulator.platform + tmpify $(xcode-select -p)/Platforms/iPhoneSimulator.platform + tmpify $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/metal/ios + tmpify $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift + tmpify $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0 + tmpify ~/.rubies + tmpify ~/Library/Caches/Homebrew + tmpify /usr/local/Homebrew + sudo rm -rf $TMPDIR/del-target + + # sudo rm -rf "/System/Library/Desktop Pictures" + # sudo rm -rf /System/Library/Templates/Data + # sudo rm -rf /System/Library/Speech/Voices + # sudo rm -rf "/System/Library/Screen Savers" + # sudo rm -rf /System/Volumes/Data/Library/Developer/CommandLineTools/SDKs + # sudo rm -rf "/System/Volumes/Data/Library/Application Support/Apple/Photos/Print Products" + # sudo rm -rf /System/Volumes/Data/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/ + # sudo rm -rf /System/Volumes/Data/Library/Java + # sudo rm -rf /System/Volumes/Data/Library/Ruby + # sudo rm -rf /System/Volumes/Data/Library/Printers + # sudo rm -rf /System/iOSSupport + # sudo rm -rf /System/Applications/*.app + # sudo rm -rf /System/Applications/Utilities/*.app + # sudo rm -rf /System/Library/LinguisticData + # sudo rm -rf /System/Volumes/Data/private/var/db/dyld/* + # sudo rm -rf /System/Library/Fonts/* + # sudo rm -rf /System/Library/PreferencePanes + # sudo rm -rf /System/Library/AssetsV2/* + sudo rm -rf /Applications/Safari.app + sudo rm -rf ~/project/src/build/linux + sudo rm -rf ~/project/src/third_party/catapult/tracing/test_data + sudo rm -rf ~/project/src/third_party/angle/third_party/VK-GL-CTS + + # lipo off some huge binaries arm64 versions to save space + strip_arm_deep $(xcode-select -p)/../SharedFrameworks + # strip_arm_deep /System/Volumes/Data/Library/Developer/CommandLineTools/usr + fi + background: true + +# On macOS delete all .git directories under src/ expect for +# third_party/angle/ because of build time generation of file +# gen/angle/commit.h depends on third_party/angle/.git/HEAD +# https://chromium-review.googlesource.com/c/angle/angle/+/2074924 +# TODO: maybe better to always leave out */.git/HEAD file for all targets ? +step-delete-git-directories: &step-delete-git-directories + run: + name: Delete all .git directories under src on MacOS to free space + command: | + if [ "`uname`" == "Darwin" ]; then + cd src + ( find . -type d -name ".git" -not -path "./third_party/angle/*" ) | xargs rm -rf + fi + +# On macOS the yarn install command during gclient sync was run on a linux +# machine and therefore installed a slightly different set of dependencies +# Notably "fsevents" is a macOS only dependency, we rerun yarn install once +# we are on a macOS machine to get the correct state +step-install-npm-deps-on-mac: &step-install-npm-deps-on-mac + run: + name: Install node_modules on MacOS + command: | + if [ "`uname`" == "Darwin" ]; then + cd src/electron + node script/yarn install + fi + +# This step handles the differences between the linux "gclient sync" +# and the expected state on macOS +step-fix-sync: &step-fix-sync + run: + name: Fix Sync + command: | + if [ "`uname`" == "Darwin" ]; then + # Fix Clang Install (wrong binary) + rm -rf src/third_party/llvm-build + python3 src/tools/clang/scripts/update.py + + # Fix esbuild (wrong binary) + echo 'infra/3pp/tools/esbuild/${platform}' `gclient getdep --deps-file=src/third_party/devtools-frontend/src/DEPS -r 'third_party/esbuild:infra/3pp/tools/esbuild/${platform}'` > esbuild_ensure_file + # Remove extra output from calling gclient getdep which always calls update_depot_tools + sed -i '' "s/Updating depot_tools... //g" esbuild_ensure_file + cipd ensure --root src/third_party/devtools-frontend/src/third_party/esbuild -ensure-file esbuild_ensure_file + fi + + cd src/third_party/angle + rm .git/objects/info/alternates + git remote set-url origin https://chromium.googlesource.com/angle/angle.git + cp .git/config .git/config.backup + git remote remove origin + mv .git/config.backup .git/config + git fetch + +step-install-signing-cert-on-mac: &step-install-signing-cert-on-mac + run: + name: Import and trust self-signed codesigning cert on MacOS + command: | + if [ "$TARGET_ARCH" != "arm64" ] && [ "`uname`" == "Darwin" ]; then + sudo security authorizationdb write com.apple.trust-settings.admin allow + cd src/electron + ./script/codesign/generate-identity.sh + fi + +step-install-gnutar-on-mac: &step-install-gnutar-on-mac + run: + name: Install gnu-tar on macos + command: | + if [ "`uname`" == "Darwin" ]; then + if [ ! -d /usr/local/Cellar/gnu-tar/ ]; then + brew update + brew install gnu-tar + fi + ln -fs /usr/local/bin/gtar /usr/local/bin/tar + fi + +step-gn-gen-default: &step-gn-gen-default + run: + name: Default GN gen + command: | + cd src + gn gen out/Default --args="import(\"$GN_CONFIG\") import(\"$GN_GOMA_FILE\") $GN_EXTRA_ARGS $GN_BUILDFLAG_ARGS" + +step-gn-check: &step-gn-check + run: + name: GN check + command: | + cd src + gn check out/Default //electron:electron_lib + gn check out/Default //electron:electron_app + gn check out/Default //electron/shell/common/api:mojo + # Check the hunspell filenames + node electron/script/gen-hunspell-filenames.js --check + node electron/script/gen-libc++-filenames.js --check + +step-electron-build: &step-electron-build + run: + name: Electron build + no_output_timeout: 30m + command: | + # On arm platforms we generate a cross-arch ffmpeg that ninja does not seem + # to realize is not correct / should be rebuilt. We delete it here so it is + # rebuilt + if [ "$TRIGGER_ARM_TEST" == "true" ]; then + rm -f src/out/Default/libffmpeg.so + fi + cd src + # Enable if things get really bad + # if [ "$TARGET_ARCH" == "arm64" ] &&[ "`uname`" == "Darwin" ]; then + # diskutil erasevolume HFS+ "xcode_disk" `hdiutil attach -nomount ram://12582912` + # mv /Applications/Xcode-12.beta.5.app /Volumes/xcode_disk/ + # ln -s /Volumes/xcode_disk/Xcode-12.beta.5.app /Applications/Xcode-12.beta.5.app + # fi + + # Lets generate a snapshot and mksnapshot and then delete all the x-compiled generated files to save space + if [ "$USE_PREBUILT_V8_CONTEXT_SNAPSHOT" == "1" ]; then + ninja -C out/Default electron:electron_mksnapshot_zip -j $NUMBER_OF_NINJA_PROCESSES + ninja -C out/Default tools/v8_context_snapshot -j $NUMBER_OF_NINJA_PROCESSES + gn desc out/Default v8:run_mksnapshot_default args > out/Default/mksnapshot_args + (cd out/Default; zip mksnapshot.zip mksnapshot_args clang_x64_v8_arm64/gen/v8/embedded.S) + rm -rf out/Default/clang_x64_v8_arm64/gen + rm -rf out/Default/clang_x64_v8_arm64/obj + rm -rf out/Default/clang_x64_v8_arm64/thinlto-cache + rm -rf out/Default/clang_x64/obj + + # Regenerate because we just deleted some ninja files + gn gen out/Default --args="import(\"$GN_CONFIG\") import(\"$GN_GOMA_FILE\") $GN_EXTRA_ARGS $GN_BUILDFLAG_ARGS" + fi + NINJA_SUMMARIZE_BUILD=1 autoninja -C out/Default electron -j $NUMBER_OF_NINJA_PROCESSES + cp out/Default/.ninja_log out/electron_ninja_log + node electron/script/check-symlinks.js + +step-maybe-electron-dist-strip: &step-maybe-electron-dist-strip + run: + name: Strip electron binaries + command: | + if [ "$STRIP_BINARIES" == "true" ] && [ "`uname`" == "Linux" ]; then + if [ x"$TARGET_ARCH" == x ]; then + target_cpu=x64 + elif [ "$TARGET_ARCH" == "ia32" ]; then + target_cpu=x86 + else + target_cpu="$TARGET_ARCH" + fi + cd src + electron/script/copy-debug-symbols.py --target-cpu="$target_cpu" --out-dir=out/Default/debug --compress + electron/script/strip-binaries.py --target-cpu="$target_cpu" + electron/script/add-debug-link.py --target-cpu="$target_cpu" --debug-dir=out/Default/debug + fi + +step-electron-chromedriver-build: &step-electron-chromedriver-build + run: + name: Build chromedriver.zip + command: | + cd src + if [ "$TARGET_ARCH" == "arm" ] || [ "$TARGET_ARCH" == "arm64" ]; then + gn gen out/chromedriver --args="import(\"$GN_CONFIG\") import(\"$GN_GOMA_FILE\") is_component_ffmpeg=false proprietary_codecs=false $GN_EXTRA_ARGS $GN_BUILDFLAG_ARGS" + export CHROMEDRIVER_DIR="out/chromedriver" + else + export CHROMEDRIVER_DIR="out/Default" + fi + ninja -C $CHROMEDRIVER_DIR electron:electron_chromedriver -j $NUMBER_OF_NINJA_PROCESSES + if [ "`uname`" == "Linux" ]; then + electron/script/strip-binaries.py --target-cpu="$TARGET_ARCH" --file $PWD/$CHROMEDRIVER_DIR/chromedriver + fi + ninja -C $CHROMEDRIVER_DIR electron:electron_chromedriver_zip + if [ "$TARGET_ARCH" == "arm" ] || [ "$TARGET_ARCH" == "arm64" ]; then + cp out/chromedriver/chromedriver.zip out/Default + fi + +step-nodejs-headers-build: &step-nodejs-headers-build + run: + name: Build Node.js headers + command: | + cd src + ninja -C out/Default third_party/electron_node:headers + +step-electron-publish: &step-electron-publish + run: + name: Publish Electron Dist + command: | + if [ "`uname`" == "Darwin" ]; then + rm -rf src/out/Default/obj + fi + + cd src/electron + if [ "$UPLOAD_TO_STORAGE" == "1" ]; then + echo 'Uploading Electron release distribution to Azure' + script/release/uploaders/upload.py --verbose --UPLOAD_TO_STORAGE + else + echo 'Uploading Electron release distribution to GitHub releases' + script/release/uploaders/upload.py --verbose + fi + +step-persist-data-for-tests: &step-persist-data-for-tests + persist_to_workspace: + root: . + paths: + # Build artifacts + - src/out/Default/dist.zip + - src/out/Default/mksnapshot.zip + - src/out/Default/chromedriver.zip + - src/out/Default/gen/node_headers + - src/out/ffmpeg/ffmpeg.zip + - src/electron + - src/third_party/electron_node + - src/third_party/nan + - src/cross-arch-snapshots + - src/third_party/llvm-build + - src/build/linux + - src/buildtools/third_party/libc++ + - src/buildtools/third_party/libc++abi + - src/out/Default/obj/buildtools/third_party + +step-electron-dist-unzip: &step-electron-dist-unzip + run: + name: Unzip dist.zip + command: | + cd src/out/Default + # -o overwrite files WITHOUT prompting + # TODO(alexeykuzmin): Remove '-o' when it's no longer needed. + # -: allows to extract archive members into locations outside + # of the current ``extraction root folder''. + # ASan builds have the llvm-symbolizer binaries listed as + # runtime_deps, with their paths as `../../third_party/...` + # unzip exits with non-zero code on such zip files unless -: is + # passed. + unzip -:o dist.zip + +step-ffmpeg-unzip: &step-ffmpeg-unzip + run: + name: Unzip ffmpeg.zip + command: | + cd src/out/ffmpeg + unzip -:o ffmpeg.zip + +step-mksnapshot-unzip: &step-mksnapshot-unzip + run: + name: Unzip mksnapshot.zip + command: | + cd src/out/Default + unzip -:o mksnapshot.zip + +step-chromedriver-unzip: &step-chromedriver-unzip + run: + name: Unzip chromedriver.zip + command: | + cd src/out/Default + unzip -:o chromedriver.zip + +step-ffmpeg-gn-gen: &step-ffmpeg-gn-gen + run: + name: ffmpeg GN gen + command: | + cd src + gn gen out/ffmpeg --args="import(\"//electron/build/args/ffmpeg.gn\") import(\"$GN_GOMA_FILE\") $GN_EXTRA_ARGS" + +step-ffmpeg-build: &step-ffmpeg-build + run: + name: Non proprietary ffmpeg build + command: | + cd src + ninja -C out/ffmpeg electron:electron_ffmpeg_zip -j $NUMBER_OF_NINJA_PROCESSES + +step-verify-ffmpeg: &step-verify-ffmpeg + run: + name: Verify ffmpeg + command: | + cd src + python electron/script/verify-ffmpeg.py --source-root "$PWD" --build-dir out/Default --ffmpeg-path out/ffmpeg + +step-verify-mksnapshot: &step-verify-mksnapshot + run: + name: Verify mksnapshot + command: | + if [ "$IS_ASAN" != "1" ]; then + cd src + if [ "$TARGET_ARCH" == "arm" ] || [ "$TARGET_ARCH" == "arm64" ]; then + python electron/script/verify-mksnapshot.py --source-root "$PWD" --build-dir out/Default --snapshot-files-dir $PWD/cross-arch-snapshots + else + python electron/script/verify-mksnapshot.py --source-root "$PWD" --build-dir out/Default + fi + fi + +step-verify-chromedriver: &step-verify-chromedriver + run: + name: Verify ChromeDriver + command: | + if [ "$IS_ASAN" != "1" ]; then + cd src + python electron/script/verify-chromedriver.py --source-root "$PWD" --build-dir out/Default + fi + +step-setup-linux-for-headless-testing: &step-setup-linux-for-headless-testing + run: + name: Setup for headless testing + command: | + if [ "`uname`" != "Darwin" ]; then + sh -e /etc/init.d/xvfb start + fi + +step-show-goma-stats: &step-show-goma-stats + run: + shell: /bin/bash + name: Check goma stats after build + command: | + set +e + set +o pipefail + $LOCAL_GOMA_DIR/goma_ctl.py stat + $LOCAL_GOMA_DIR/diagnose_goma_log.py + true + when: always + background: true + +step-mksnapshot-build: &step-mksnapshot-build + run: + name: mksnapshot build + no_output_timeout: 30m + command: | + cd src + if [ "$USE_PREBUILT_V8_CONTEXT_SNAPSHOT" != "1" ]; then + ninja -C out/Default electron:electron_mksnapshot -j $NUMBER_OF_NINJA_PROCESSES + gn desc out/Default v8:run_mksnapshot_default args > out/Default/mksnapshot_args + fi + if [ "`uname`" != "Darwin" ]; then + if [ "$TARGET_ARCH" == "arm" ]; then + electron/script/strip-binaries.py --file $PWD/out/Default/clang_x86_v8_arm/mksnapshot + electron/script/strip-binaries.py --file $PWD/out/Default/clang_x86_v8_arm/v8_context_snapshot_generator + elif [ "$TARGET_ARCH" == "arm64" ]; then + electron/script/strip-binaries.py --file $PWD/out/Default/clang_x64_v8_arm64/mksnapshot + electron/script/strip-binaries.py --file $PWD/out/Default/clang_x64_v8_arm64/v8_context_snapshot_generator + else + electron/script/strip-binaries.py --file $PWD/out/Default/mksnapshot + electron/script/strip-binaries.py --file $PWD/out/Default/v8_context_snapshot_generator + fi + fi + if [ "$USE_PREBUILT_V8_CONTEXT_SNAPSHOT" != "1" ] && [ "$SKIP_DIST_ZIP" != "1" ]; then + ninja -C out/Default electron:electron_mksnapshot_zip -j $NUMBER_OF_NINJA_PROCESSES + (cd out/Default; zip mksnapshot.zip mksnapshot_args gen/v8/embedded.S) + fi + +step-hunspell-build: &step-hunspell-build + run: + name: hunspell build + command: | + cd src + if [ "$SKIP_DIST_ZIP" != "1" ]; then + ninja -C out/Default electron:hunspell_dictionaries_zip -j $NUMBER_OF_NINJA_PROCESSES + fi + +step-maybe-generate-libcxx: &step-maybe-generate-libcxx + run: + name: maybe generate libcxx + command: | + cd src + if [ "`uname`" == "Linux" ]; then + ninja -C out/Default electron:libcxx_headers_zip -j $NUMBER_OF_NINJA_PROCESSES + ninja -C out/Default electron:libcxxabi_headers_zip -j $NUMBER_OF_NINJA_PROCESSES + ninja -C out/Default electron:libcxx_objects_zip -j $NUMBER_OF_NINJA_PROCESSES + fi + +step-maybe-generate-breakpad-symbols: &step-maybe-generate-breakpad-symbols + run: + name: Generate breakpad symbols + no_output_timeout: 30m + command: | + if [ "$GENERATE_SYMBOLS" == "true" ]; then + cd src + ninja -C out/Default electron:electron_symbols + fi + +step-maybe-zip-symbols: &step-maybe-zip-symbols + run: + name: Zip symbols + command: | + cd src + export BUILD_PATH="$PWD/out/Default" + ninja -C out/Default electron:licenses + ninja -C out/Default electron:electron_version + DELETE_DSYMS_AFTER_ZIP=1 electron/script/zip-symbols.py -b $BUILD_PATH + +step-maybe-cross-arch-snapshot: &step-maybe-cross-arch-snapshot + run: + name: Generate cross arch snapshot (arm/arm64) + command: | + if [ "$GENERATE_CROSS_ARCH_SNAPSHOT" == "true" ] && [ -z "$CIRCLE_PR_NUMBER" ]; then + cd src + if [ "$TARGET_ARCH" == "arm" ]; then + export MKSNAPSHOT_PATH="clang_x86_v8_arm" + elif [ "$TARGET_ARCH" == "arm64" ]; then + export MKSNAPSHOT_PATH="clang_x64_v8_arm64" + fi + cp "out/Default/$MKSNAPSHOT_PATH/mksnapshot" out/Default + cp "out/Default/$MKSNAPSHOT_PATH/v8_context_snapshot_generator" out/Default + if [ "`uname`" == "Linux" ]; then + cp "out/Default/$MKSNAPSHOT_PATH/libffmpeg.so" out/Default + elif [ "`uname`" == "Darwin" ]; then + cp "out/Default/$MKSNAPSHOT_PATH/libffmpeg.dylib" out/Default + fi + python electron/script/verify-mksnapshot.py --source-root "$PWD" --build-dir out/Default --create-snapshot-only + mkdir cross-arch-snapshots + cp out/Default-mksnapshot-test/*.bin cross-arch-snapshots + fi + +step-maybe-generate-typescript-defs: &step-maybe-generate-typescript-defs + run: + name: Generate type declarations + command: | + if [ "`uname`" == "Darwin" ]; then + cd src/electron + node script/yarn create-typescript-definitions + fi + +step-fix-known-hosts-linux: &step-fix-known-hosts-linux + run: + name: Fix Known Hosts on Linux + command: | + if [ "`uname`" == "Linux" ]; then + ./src/electron/.circleci/fix-known-hosts.sh + fi + +# Checkout Steps +step-generate-deps-hash: &step-generate-deps-hash + run: + name: Generate DEPS Hash + command: node src/electron/script/generate-deps-hash.js && cat src/electron/.depshash-target + +step-touch-sync-done: &step-touch-sync-done + run: + name: Touch Sync Done + command: touch src/electron/.circle-sync-done + +# Restore exact src cache based on the hash of DEPS and patches/* +# If no cache is matched EXACTLY then the .circle-sync-done file is empty +# If a cache is matched EXACTLY then the .circle-sync-done file contains "done" +step-maybe-restore-src-cache: &step-maybe-restore-src-cache + restore_cache: + keys: + - v14-src-cache-{{ checksum "src/electron/.depshash" }} + name: Restoring src cache +step-maybe-restore-src-cache-marker: &step-maybe-restore-src-cache-marker + restore_cache: + keys: + - v14-src-cache-marker-{{ checksum "src/electron/.depshash" }} + name: Restoring src cache marker + +# Restore exact or closest git cache based on the hash of DEPS and .circle-sync-done +# If the src cache was restored above then this will match an empty cache +# If the src cache was not restored above then this will match a close git cache +step-maybe-restore-git-cache: &step-maybe-restore-git-cache + restore_cache: + paths: + - git-cache + keys: + - v1-git-cache-{{ checksum "src/electron/.circle-sync-done" }}-{{ checksum "src/electron/DEPS" }} + - v1-git-cache-{{ checksum "src/electron/.circle-sync-done" }} + name: Conditionally restoring git cache + +step-restore-out-cache: &step-restore-out-cache + restore_cache: + paths: + - ./src/out/Default + keys: + - v9-out-cache-{{ checksum "src/electron/.depshash" }}-{{ checksum "src/electron/.depshash-target" }} + name: Restoring out cache + +step-set-git-cache-path: &step-set-git-cache-path + run: + name: Set GIT_CACHE_PATH to make gclient to use the cache + command: | + # CircleCI does not support interpolation when setting environment variables. + # https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-shell-command + echo 'export GIT_CACHE_PATH="$PWD/git-cache"' >> $BASH_ENV + +# Persist the git cache based on the hash of DEPS and .circle-sync-done +# If the src cache was restored above then this will persist an empty cache +step-save-git-cache: &step-save-git-cache + save_cache: + paths: + - git-cache + key: v1-git-cache-{{ checksum "src/electron/.circle-sync-done" }}-{{ checksum "src/electron/DEPS" }} + name: Persisting git cache + +step-save-out-cache: &step-save-out-cache + save_cache: + paths: + - ./src/out/Default + key: v9-out-cache-{{ checksum "src/electron/.depshash" }}-{{ checksum "src/electron/.depshash-target" }} + name: Persisting out cache + +step-run-electron-only-hooks: &step-run-electron-only-hooks + run: + name: Run Electron Only Hooks + command: gclient runhooks --spec="solutions=[{'name':'src/electron','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':False},'managed':False}]" + +step-generate-deps-hash-cleanly: &step-generate-deps-hash-cleanly + run: + name: Generate DEPS Hash + command: (cd src/electron && git checkout .) && node src/electron/script/generate-deps-hash.js && cat src/electron/.depshash-target + +# Mark the sync as done for future cache saving +step-mark-sync-done: &step-mark-sync-done + run: + name: Mark Sync Done + command: echo DONE > src/electron/.circle-sync-done + +# Minimize the size of the cache +step-minimize-workspace-size-from-checkout: &step-minimize-workspace-size-from-checkout + run: + name: Remove some unused data to avoid storing it in the workspace/cache + command: | + rm -rf src/android_webview + rm -rf src/ios/chrome + rm -rf src/third_party/blink/web_tests + rm -rf src/third_party/blink/perf_tests + rm -rf src/third_party/WebKit/LayoutTests + rm -rf third_party/electron_node/deps/openssl + rm -rf third_party/electron_node/deps/v8 + rm -rf chrome/test/data/xr/webvr_info + +# Save the src cache based on the deps hash +step-save-src-cache: &step-save-src-cache + save_cache: + paths: + - /var/portal + key: v14-src-cache-{{ checksum "/var/portal/src/electron/.depshash" }} + name: Persisting src cache +step-make-src-cache-marker: &step-make-src-cache-marker + run: + name: Making src cache marker + command: touch .src-cache-marker +step-save-src-cache-marker: &step-save-src-cache-marker + save_cache: + paths: + - .src-cache-marker + key: v14-src-cache-marker-{{ checksum "/var/portal/src/electron/.depshash" }} + +step-maybe-early-exit-no-doc-change: &step-maybe-early-exit-no-doc-change + run: + name: Shortcircuit job if change is not doc only + command: | + if [ ! -s src/electron/.skip-ci-build ]; then + circleci-agent step halt + fi + +step-ts-compile: &step-ts-compile + run: + name: Run TS/JS compile on doc only change + command: | + cd src + ninja -C out/Default electron:default_app_js -j $NUMBER_OF_NINJA_PROCESSES + ninja -C out/Default electron:electron_js2c -j $NUMBER_OF_NINJA_PROCESSES + +# List of all steps. +steps-electron-gn-check: &steps-electron-gn-check + steps: + - *step-checkout-electron + - *step-depot-tools-get + - *step-depot-tools-add-to-path + - install-python2-mac + - *step-setup-env-for-build + - *step-setup-goma-for-build + - *step-generate-deps-hash + - *step-touch-sync-done + - maybe-restore-portaled-src-cache + - *step-wait-for-goma + - *step-gn-gen-default + - *step-gn-check + +steps-electron-ts-compile-for-doc-change: &steps-electron-ts-compile-for-doc-change + steps: + # Checkout - Copied from steps-checkout + - *step-checkout-electron + - *step-depot-tools-get + - *step-depot-tools-add-to-path + - *step-restore-brew-cache + - *step-install-gnutar-on-mac + - *step-get-more-space-on-mac + - *step-setup-goma-for-build + - *step-generate-deps-hash + - *step-touch-sync-done + - maybe-restore-portaled-src-cache + - *step-maybe-restore-git-cache + - *step-set-git-cache-path + # This sync call only runs if .circle-sync-done is an EMPTY file + - *step-gclient-sync + # These next few steps reset Electron to the correct commit regardless of which cache was restored + - run: + name: Wipe Electron + command: rm -rf src/electron + - *step-checkout-electron + - *step-run-electron-only-hooks + - *step-generate-deps-hash-cleanly + - *step-mark-sync-done + - *step-minimize-workspace-size-from-checkout + + - *step-depot-tools-add-to-path + - *step-setup-env-for-build + - *step-wait-for-goma + - *step-get-more-space-on-mac + - *step-install-npm-deps-on-mac + - *step-fix-sync + - *step-gn-gen-default + + #Compile ts/js to verify doc change didn't break anything + - *step-ts-compile + +steps-native-tests: &steps-native-tests + steps: + - attach_workspace: + at: . + - *step-depot-tools-add-to-path + - *step-setup-env-for-build + - *step-setup-goma-for-build + - *step-wait-for-goma + - *step-gn-gen-default + + - run: + name: Build tests + command: | + cd src + ninja -C out/Default $BUILD_TARGET + - *step-show-goma-stats + + - *step-setup-linux-for-headless-testing + - run: + name: Run tests + command: | + mkdir test_results + python src/electron/script/native-tests.py run \ + --config $TESTS_CONFIG \ + --tests-dir src/out/Default \ + --output-dir test_results \ + $TESTS_ARGS + + - store_artifacts: + path: test_results + destination: test_results # Put it in the root folder. + - store_test_results: + path: test_results + +steps-verify-ffmpeg: &steps-verify-ffmpeg + steps: + - attach_workspace: + at: . + - *step-depot-tools-add-to-path + - *step-electron-dist-unzip + - *step-ffmpeg-unzip + - *step-setup-linux-for-headless-testing + + - *step-verify-ffmpeg + - *step-maybe-notify-slack-failure + +steps-tests: &steps-tests + steps: + - attach_workspace: + at: . + - *step-depot-tools-add-to-path + - *step-electron-dist-unzip + - *step-mksnapshot-unzip + - *step-chromedriver-unzip + - *step-setup-linux-for-headless-testing + - *step-restore-brew-cache + - *step-fix-known-hosts-linux + - install-python2-mac + - *step-install-signing-cert-on-mac + + - run: + name: Run Electron tests + environment: + MOCHA_REPORTER: mocha-multi-reporters + ELECTRON_TEST_RESULTS_DIR: junit + MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap + ELECTRON_DISABLE_SECURITY_WARNINGS: 1 + command: | + cd src + if [ "$IS_ASAN" == "1" ]; then + ASAN_SYMBOLIZE="$PWD/tools/valgrind/asan/asan_symbolize.py --executable-path=$PWD/out/Default/electron" + export ASAN_OPTIONS="symbolize=0 handle_abort=1" + export G_SLICE=always-malloc + export NSS_DISABLE_ARENA_FREE_LIST=1 + export NSS_DISABLE_UNLOAD=1 + export LLVM_SYMBOLIZER_PATH=$PWD/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer + export MOCHA_TIMEOUT=180000 + echo "Piping output to ASAN_SYMBOLIZE ($ASAN_SYMBOLIZE)" + (cd electron && node script/yarn test --runners=main --trace-uncaught --enable-logging --files $(circleci tests glob spec-main/*-spec.ts | circleci tests split --split-by=timings)) 2>&1 | $ASAN_SYMBOLIZE + (cd electron && node script/yarn test --runners=remote --trace-uncaught --enable-logging --files $(circleci tests glob spec/*-spec.js | circleci tests split --split-by=timings)) 2>&1 | $ASAN_SYMBOLIZE + else + if [ "$TARGET_ARCH" == "arm" ] || [ "$TARGET_ARCH" == "arm64" ]; then + export ELECTRON_SKIP_NATIVE_MODULE_TESTS=true + (cd electron && node script/yarn test --runners=main --trace-uncaught --enable-logging) + (cd electron && node script/yarn test --runners=remote --trace-uncaught --enable-logging) + else + if [ "$TARGET_ARCH" == "ia32" ]; then + npm_config_arch=x64 node electron/node_modules/dugite/script/download-git.js + fi + (cd electron && node script/yarn test --runners=main --trace-uncaught --enable-logging --files $(circleci tests glob spec-main/*-spec.ts | circleci tests split --split-by=timings)) + (cd electron && node script/yarn test --runners=remote --trace-uncaught --enable-logging --files $(circleci tests glob spec/*-spec.js | circleci tests split --split-by=timings)) + fi + fi + - run: + name: Check test results existence + command: | + cd src + + # Check if test results exist and are not empty. + if [ ! -s "junit/test-results-remote.xml" ]; then + exit 1 + fi + if [ ! -s "junit/test-results-main.xml" ]; then + exit 1 + fi + - store_test_results: + path: src/junit + + - *step-verify-mksnapshot + - *step-verify-chromedriver + + - *step-maybe-notify-slack-failure + + - *step-maybe-cleanup-arm64-mac + +steps-test-nan: &steps-test-nan + steps: + - attach_workspace: + at: . + - *step-depot-tools-add-to-path + - *step-electron-dist-unzip + - *step-setup-linux-for-headless-testing + - *step-fix-known-hosts-linux + - run: + name: Run Nan Tests + command: | + cd src + node electron/script/nan-spec-runner.js + +steps-test-node: &steps-test-node + steps: + - attach_workspace: + at: . + - *step-depot-tools-add-to-path + - *step-electron-dist-unzip + - *step-setup-linux-for-headless-testing + - *step-fix-known-hosts-linux + - run: + name: Run Node Tests + command: | + cd src + node electron/script/node-spec-runner.js --default --jUnitDir=junit + - store_test_results: + path: src/junit + +# Command Aliases +commands: + install-python2-mac: + steps: + - restore_cache: + keys: + - v2.7.18-python-cache-{{ arch }} + name: Restore python cache + - run: + name: Install python2 on macos + command: | + if [ "`uname`" == "Darwin" ] && [ "$IS_ELECTRON_RUNNER" != "1" ]; then + if [ ! -f "python-downloads/python-2.7.18-macosx10.9.pkg" ]; then + mkdir python-downloads + echo 'Downloading Python 2.7.18' + curl -O https://dev-cdn.electronjs.org/python/python-2.7.18-macosx10.9.pkg + mv python-2.7.18-macosx10.9.pkg python-downloads + else + echo 'Using Python install from cache' + fi + sudo installer -pkg python-downloads/python-2.7.18-macosx10.9.pkg -target / + fi + - save_cache: + paths: + - python-downloads + key: v2.7.18-python-cache-{{ arch }} + name: Persisting python cache + maybe-restore-portaled-src-cache: + parameters: + halt-if-successful: + type: boolean + default: false + steps: + - run: + name: Prepare for cross-OS sync restore + command: | + sudo mkdir -p /var/portal + sudo chown -R $(id -u):$(id -g) /var/portal + - when: + condition: << parameters.halt-if-successful >> + steps: + - *step-maybe-restore-src-cache-marker + - run: + name: Halt the job early if the src cache exists + command: | + if [ -f ".src-cache-marker" ]; then + circleci-agent step halt + fi + - *step-maybe-restore-src-cache + - run: + name: Fix the src cache restore point on macOS + command: | + if [ -d "/var/portal/src" ]; then + echo Relocating Cache + rm -rf src + mv /var/portal/src ./ + fi + + move_and_store_all_artifacts: + steps: + - run: + name: Move all generated artifacts to upload folder + command: | + rm -rf generated_artifacts + mkdir generated_artifacts + mv_if_exist() { + if [ -f "$1" ] || [ -d "$1" ]; then + echo Storing $1 + mv $1 generated_artifacts + else + echo Skipping $1 - It is not present on disk + fi + } + mv_if_exist src/out/Default/dist.zip + mv_if_exist src/out/Default/gen/node_headers.tar.gz + mv_if_exist src/out/Default/symbols.zip + mv_if_exist src/out/Default/mksnapshot.zip + mv_if_exist src/out/Default/chromedriver.zip + mv_if_exist src/out/ffmpeg/ffmpeg.zip + mv_if_exist src/out/Default/hunspell_dictionaries.zip + mv_if_exist src/cross-arch-snapshots + mv_if_exist src/out/electron_ninja_log + when: always + - store_artifacts: + path: generated_artifacts + destination: ./ + - store_artifacts: + path: generated_artifacts/cross-arch-snapshots + destination: cross-arch-snapshots + + checkout-from-cache: + steps: + - *step-checkout-electron + - *step-depot-tools-get + - *step-depot-tools-add-to-path + - *step-generate-deps-hash + - maybe-restore-portaled-src-cache + - run: + name: Ensure src checkout worked + command: | + if [ ! -d "src/third_party/blink" ]; then + echo src cache was not restored for some reason, idk what happened here... + exit 1 + fi + - run: + name: Wipe Electron + command: rm -rf src/electron + - *step-checkout-electron + - *step-run-electron-only-hooks + - *step-generate-deps-hash-cleanly + + step-electron-dist-build: + parameters: + additional-targets: + type: string + default: '' + steps: + - run: + name: Build dist.zip + command: | + cd src + if [ "$SKIP_DIST_ZIP" != "1" ]; then + ninja -C out/Default electron:electron_dist_zip << parameters.additional-targets >> + if [ "$CHECK_DIST_MANIFEST" == "1" ]; then + if [ "`uname`" == "Darwin" ]; then + target_os=mac + target_cpu=x64 + if [ x"$MAS_BUILD" == x"true" ]; then + target_os=mac_mas + fi + if [ "$TARGET_ARCH" == "arm64" ]; then + target_cpu=arm64 + fi + elif [ "`uname`" == "Linux" ]; then + target_os=linux + if [ x"$TARGET_ARCH" == x ]; then + target_cpu=x64 + elif [ "$TARGET_ARCH" == "ia32" ]; then + target_cpu=x86 + else + target_cpu="$TARGET_ARCH" + fi + else + echo "Unknown system: `uname`" + exit 1 + fi + electron/script/zip_manifests/check-zip-manifest.py out/Default/dist.zip electron/script/zip_manifests/dist_zip.$target_os.$target_cpu.manifest + fi + fi + + electron-build: + parameters: + attach: + type: boolean + default: false + persist: + type: boolean + default: true + persist-checkout: + type: boolean + default: false + checkout: + type: boolean + default: true + checkout-and-assume-cache: + type: boolean + default: false + save-git-cache: + type: boolean + default: false + checkout-to-create-src-cache: + type: boolean + default: false + build: + type: boolean + default: true + use-out-cache: + type: boolean + default: true + restore-src-cache: + type: boolean + default: true + build-nonproprietary-ffmpeg: + type: boolean + default: true + steps: + - when: + condition: << parameters.attach >> + steps: + - attach_workspace: + at: . + - run: rm -rf src/electron + - *step-restore-brew-cache + - *step-install-gnutar-on-mac + - install-python2-mac + - *step-save-brew-cache + - when: + condition: << parameters.build >> + steps: + - *step-setup-goma-for-build + - when: + condition: << parameters.checkout-and-assume-cache >> + steps: + - checkout-from-cache + - when: + condition: << parameters.checkout >> + steps: + # Checkout - Copied from steps-checkout + - *step-checkout-electron + - *step-depot-tools-get + - *step-depot-tools-add-to-path + - *step-get-more-space-on-mac + - *step-generate-deps-hash + - *step-touch-sync-done + - when: + condition: << parameters.restore-src-cache >> + steps: + - maybe-restore-portaled-src-cache: + halt-if-successful: << parameters.checkout-to-create-src-cache >> + - *step-maybe-restore-git-cache + - *step-set-git-cache-path + # This sync call only runs if .circle-sync-done is an EMPTY file + - *step-gclient-sync + - store_artifacts: + path: patches + # These next few steps reset Electron to the correct commit regardless of which cache was restored + - run: + name: Wipe Electron + command: rm -rf src/electron + - *step-checkout-electron + - *step-run-electron-only-hooks + - *step-generate-deps-hash-cleanly + - *step-touch-sync-done + - when: + condition: << parameters.save-git-cache >> + steps: + - *step-save-git-cache + # Mark sync as done _after_ saving the git cache so that it is uploaded + # only when the src cache was not present + # Their are theoretically two cases for this cache key + # 1. `vX-git-cache-DONE-{deps_hash} + # 2. `vX-git-cache-EMPTY-{deps_hash} + # + # Case (1) occurs when the flag file has "DONE" in it + # which only occurs when "step-mark-sync-done" is run + # or when the src cache was restored successfully as that + # flag file contains "DONE" in the src cache. + # + # Case (2) occurs when the flag file is empty, this occurs + # when the src cache was not restored and "step-mark-sync-done" + # has not run yet. + # + # Notably both of these cases also have completely different + # gclient cache states. + # In (1) the git cache is completely empty as we didn't run + # "gclient sync" because the src cache was restored. + # In (2) the git cache is full as we had to run "gclient sync" + # + # This allows us to do make the follow transitive assumption: + # In cases where the src cache is restored, saving the git cache + # will save an empty cache. In cases where the src cache is built + # during this build the git cache will save a full cache. + # + # In order words if there is a src cache for a given DEPS hash + # the git cache restored will be empty. But if the src cache + # is missing we will restore a useful git cache. + - *step-mark-sync-done + - *step-minimize-workspace-size-from-checkout + - *step-delete-git-directories + - when: + condition: << parameters.persist-checkout >> + steps: + - persist_to_workspace: + root: . + paths: + - depot_tools + - src + - when: + condition: << parameters.checkout-to-create-src-cache >> + steps: + - run: + name: Move src folder to the cross-OS portal + command: | + sudo mkdir -p /var/portal + sudo chown -R $(id -u):$(id -g) /var/portal + mv ./src /var/portal + - *step-save-src-cache + - *step-make-src-cache-marker + - *step-save-src-cache-marker + + - when: + condition: << parameters.build >> + steps: + - *step-depot-tools-add-to-path + - *step-setup-env-for-build + - *step-wait-for-goma + - *step-get-more-space-on-mac + - *step-fix-sync + - *step-delete-git-directories + + # Electron app + - when: + condition: << parameters.use-out-cache >> + steps: + - *step-restore-out-cache + - *step-gn-gen-default + - *step-electron-build + - *step-maybe-electron-dist-strip + - step-electron-dist-build: + additional-targets: shell_browser_ui_unittests third_party/electron_node:headers electron:hunspell_dictionaries_zip + + - *step-show-goma-stats + + # mksnapshot + - *step-mksnapshot-build + - *step-maybe-cross-arch-snapshot + + # chromedriver + - *step-electron-chromedriver-build + + - when: + condition: << parameters.build-nonproprietary-ffmpeg >> + steps: + # ffmpeg + - *step-ffmpeg-gn-gen + - *step-ffmpeg-build + + # Save all data needed for a further tests run. + - when: + condition: << parameters.persist >> + steps: + - *step-minimize-workspace-size-from-checkout + - run: | + rm -rf src/third_party/electron_node/deps/openssl + rm -rf src/third_party/electron_node/deps/v8 + - *step-persist-data-for-tests + + - when: + condition: << parameters.build >> + steps: + - *step-maybe-generate-breakpad-symbols + - *step-maybe-zip-symbols + + - when: + condition: << parameters.build >> + steps: + - move_and_store_all_artifacts + - run: + name: Remove the big things on macOS, this seems to be better on average + command: | + if [ "`uname`" == "Darwin" ]; then + mkdir -p src/out/Default + cd src/out/Default + find . -type f -size +50M -delete + mkdir -p gen/electron + cd gen/electron + # These files do not seem to like being in a cache, let us remove them + find . -type f -name '*_pkg_info' -delete + fi + - when: + condition: << parameters.use-out-cache >> + steps: + - *step-save-out-cache + + - *step-maybe-notify-slack-failure + + electron-publish: + parameters: + attach: + type: boolean + default: false + checkout: + type: boolean + default: true + steps: + - when: + condition: << parameters.attach >> + steps: + - attach_workspace: + at: . + - when: + condition: << parameters.checkout >> + steps: + - *step-depot-tools-get + - *step-depot-tools-add-to-path + - *step-restore-brew-cache + - install-python2-mac + - *step-get-more-space-on-mac + - when: + condition: << parameters.checkout >> + steps: + - *step-checkout-electron + - *step-touch-sync-done + - *step-maybe-restore-git-cache + - *step-set-git-cache-path + - *step-gclient-sync + - *step-delete-git-directories + - *step-minimize-workspace-size-from-checkout + - *step-fix-sync + - *step-setup-env-for-build + - *step-setup-goma-for-build + - *step-wait-for-goma + - *step-gn-gen-default + + # Electron app + - *step-electron-build + - *step-show-goma-stats + - *step-maybe-generate-breakpad-symbols + - *step-maybe-electron-dist-strip + - step-electron-dist-build + - *step-maybe-zip-symbols + + # mksnapshot + - *step-mksnapshot-build + + # chromedriver + - *step-electron-chromedriver-build + + # Node.js headers + - *step-nodejs-headers-build + + # ffmpeg + - *step-ffmpeg-gn-gen + - *step-ffmpeg-build + + # hunspell + - *step-hunspell-build + + # libcxx + - *step-maybe-generate-libcxx + + # typescript defs + - *step-maybe-generate-typescript-defs + + # Publish + - *step-electron-publish + - move_and_store_all_artifacts + +# List of all jobs. +jobs: + # Layer 0: Docs. Standalone. + ts-compile-doc-change: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-2xlarge + <<: *env-testing-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + <<: *steps-electron-ts-compile-for-doc-change + + # Layer 1: Checkout. + linux-make-src-cache: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-2xlarge + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - electron-build: + persist: false + build: false + checkout: true + save-git-cache: true + checkout-to-create-src-cache: true + + mac-checkout: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-2xlarge + <<: *env-testing-build + <<: *env-macos-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + steps: + - electron-build: + persist: false + build: false + checkout: true + persist-checkout: true + restore-src-cache: false + + mac-make-src-cache: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-2xlarge + <<: *env-testing-build + <<: *env-macos-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + steps: + - electron-build: + persist: false + build: false + checkout: true + save-git-cache: true + checkout-to-create-src-cache: true + + # Layer 2: Builds. + linux-x64-testing: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-global + <<: *env-testing-build + <<: *env-ninja-status + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + use-out-cache: false + + linux-x64-testing-asan: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-global + <<: *env-testing-build + <<: *env-ninja-status + CHECK_DIST_MANIFEST: '0' + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + GN_EXTRA_ARGS: 'is_asan = true' + steps: + - electron-build: + persist: true + checkout: true + use-out-cache: false + build-nonproprietary-ffmpeg: false + + linux-x64-testing-no-run-as-node: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-2xlarge + <<: *env-testing-build + <<: *env-ninja-status + <<: *env-disable-run-as-node + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - electron-build: + persist: false + checkout: true + use-out-cache: false + + linux-x64-testing-gn-check: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-testing-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + <<: *steps-electron-gn-check + + linux-x64-publish: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-linux-2xlarge-release + <<: *env-release-build + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.linux-publish-arch-limit >>] + - equal: ["x64", << pipeline.parameters.linux-publish-arch-limit >>] + steps: + - electron-publish: + attach: false + checkout: true + + linux-ia32-testing: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-global + <<: *env-ia32 + <<: *env-testing-build + <<: *env-ninja-status + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - electron-build: + persist: true + checkout: true + use-out-cache: false + + linux-ia32-publish: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-linux-2xlarge-release + <<: *env-ia32 + <<: *env-release-build + <<: *env-32bit-release + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.linux-publish-arch-limit >>] + - equal: ["ia32", << pipeline.parameters.linux-publish-arch-limit >>] + steps: + - electron-publish: + attach: false + checkout: true + + linux-arm-testing: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-global + <<: *env-arm + <<: *env-testing-build + <<: *env-ninja-status + TRIGGER_ARM_TEST: true + GENERATE_CROSS_ARCH_SNAPSHOT: true + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + use-out-cache: false + + linux-arm-publish: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-linux-2xlarge-release + <<: *env-arm + <<: *env-release-build + <<: *env-32bit-release + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True' + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.linux-publish-arch-limit >>] + - equal: ["arm", << pipeline.parameters.linux-publish-arch-limit >>] + steps: + - electron-publish: + attach: false + checkout: true + + linux-arm64-testing: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-global + <<: *env-arm64 + <<: *env-testing-build + <<: *env-ninja-status + TRIGGER_ARM_TEST: true + GENERATE_CROSS_ARCH_SNAPSHOT: true + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + use-out-cache: false + + linux-arm64-testing-gn-check: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-arm64 + <<: *env-testing-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + <<: *steps-electron-gn-check + + linux-arm64-publish: + executor: + name: linux-docker + size: 2xlarge + environment: + <<: *env-linux-2xlarge-release + <<: *env-arm64 + <<: *env-release-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm64=True' + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.linux-publish-arch-limit >>] + - equal: ["arm64", << pipeline.parameters.linux-publish-arch-limit >>] + steps: + - electron-publish: + attach: false + checkout: true + + osx-testing-x64: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large + <<: *env-testing-build + <<: *env-ninja-status + <<: *env-macos-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + attach: true + + osx-testing-x64-gn-check: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-machine-mac + <<: *env-testing-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + <<: *steps-electron-gn-check + + osx-publish-x64-skip-checkout: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large-release + <<: *env-release-build + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.macos-publish-arch-limit >>] + - equal: ["osx-x64", << pipeline.parameters.macos-publish-arch-limit >>] + steps: + - electron-publish: + attach: true + checkout: false + + osx-publish-arm64-skip-checkout: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large-release + <<: *env-release-build + <<: *env-apple-silicon + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.macos-publish-arch-limit >>] + - equal: ["osx-arm64", << pipeline.parameters.macos-publish-arch-limit >>] + steps: + - electron-publish: + attach: true + checkout: false + + osx-testing-arm64: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large + <<: *env-testing-build + <<: *env-ninja-status + <<: *env-macos-build + <<: *env-apple-silicon + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + GENERATE_CROSS_ARCH_SNAPSHOT: true + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + attach: true + + mas-testing-x64: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large + <<: *env-mas + <<: *env-testing-build + <<: *env-ninja-status + <<: *env-macos-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + attach: true + + mas-testing-x64-gn-check: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-machine-mac + <<: *env-mas + <<: *env-testing-build + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + <<: *steps-electron-gn-check + + mas-publish-x64-skip-checkout: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large-release + <<: *env-mas + <<: *env-release-build + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.macos-publish-arch-limit >>] + - equal: ["mas-x64", << pipeline.parameters.macos-publish-arch-limit >>] + steps: + - electron-publish: + attach: true + checkout: false + + mas-publish-arm64-skip-checkout: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large-release + <<: *env-mas-apple-silicon + <<: *env-release-build + UPLOAD_TO_STORAGE: << pipeline.parameters.upload-to-storage >> + <<: *env-ninja-status + steps: + - run: echo running + - when: + condition: + or: + - equal: ["all", << pipeline.parameters.macos-publish-arch-limit >>] + - equal: ["mas-arm64", << pipeline.parameters.macos-publish-arch-limit >>] + steps: + - electron-publish: + attach: true + checkout: false + + mas-testing-arm64: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large + <<: *env-testing-build + <<: *env-ninja-status + <<: *env-macos-build + <<: *env-mas-apple-silicon + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac' + GENERATE_CROSS_ARCH_SNAPSHOT: true + steps: + - electron-build: + persist: true + checkout: false + checkout-and-assume-cache: true + attach: true + + # Layer 3: Tests. + linux-x64-testing-tests: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-headless-testing + <<: *env-stack-dumping + parallelism: 3 + <<: *steps-tests + + linux-x64-testing-asan-tests: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-medium + <<: *env-headless-testing + <<: *env-stack-dumping + IS_ASAN: '1' + DISABLE_CRASH_REPORTER_TESTS: '1' + parallelism: 3 + <<: *steps-tests + + linux-x64-testing-nan: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-headless-testing + <<: *env-stack-dumping + <<: *steps-test-nan + + linux-x64-testing-node: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-medium + <<: *env-headless-testing + <<: *env-stack-dumping + <<: *steps-test-node + + linux-x64-verify-ffmpeg: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-headless-testing + <<: *env-send-slack-notifications + <<: *steps-verify-ffmpeg + + linux-ia32-testing-tests: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-ia32 + <<: *env-headless-testing + <<: *env-stack-dumping + parallelism: 3 + <<: *steps-tests + + linux-ia32-testing-nan: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-ia32 + <<: *env-headless-testing + <<: *env-stack-dumping + <<: *steps-test-nan + + linux-ia32-testing-node: + executor: + name: linux-docker + size: xlarge + environment: + <<: *env-linux-medium + <<: *env-ia32 + <<: *env-headless-testing + <<: *env-stack-dumping + <<: *steps-test-node + + linux-ia32-verify-ffmpeg: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-ia32 + <<: *env-headless-testing + <<: *env-send-slack-notifications + <<: *steps-verify-ffmpeg + + linux-arm-testing-tests: + executor: linux-arm + environment: + <<: *env-arm + <<: *env-global + <<: *env-headless-testing + <<: *env-stack-dumping + <<: *steps-tests + + linux-arm64-testing-tests: + executor: linux-arm64 + environment: + <<: *env-arm64 + <<: *env-global + <<: *env-headless-testing + <<: *env-stack-dumping + <<: *steps-tests + + osx-testing-x64-tests: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large + <<: *env-stack-dumping + parallelism: 2 + <<: *steps-tests + + osx-testing-arm64-tests: + executor: apple-silicon + environment: + <<: *env-mac-large + <<: *env-stack-dumping + <<: *env-apple-silicon + <<: *env-runner + <<: *steps-tests + + mas-testing-x64-tests: + executor: + name: macos + size: macos.x86.medium.gen2 + environment: + <<: *env-mac-large + <<: *env-stack-dumping + parallelism: 2 + <<: *steps-tests + + mas-testing-arm64-tests: + executor: apple-silicon + environment: + <<: *env-mac-large + <<: *env-stack-dumping + <<: *env-apple-silicon + <<: *env-runner + <<: *steps-tests + + # Layer 4: Summary. + linux-release-summary: + executor: + name: linux-docker + size: medium + environment: + <<: *env-linux-medium + <<: *env-send-slack-notifications + steps: + - *step-maybe-notify-slack-success + +# List all workflows +workflows: + docs-only: + when: + and: + - equal: [false, << pipeline.parameters.run-macos-publish >>] + - equal: [false, << pipeline.parameters.run-linux-publish >>] + - equal: [true, << pipeline.parameters.run-docs-only >>] + jobs: + - ts-compile-doc-change + + publish-linux: + when: << pipeline.parameters.run-linux-publish >> + jobs: + - linux-x64-publish: + context: release-env + - linux-ia32-publish: + context: release-env + - linux-arm-publish: + context: release-env + - linux-arm64-publish: + context: release-env + + publish-macos: + when: << pipeline.parameters.run-macos-publish >> + jobs: + - mac-checkout + - osx-publish-x64-skip-checkout: + requires: + - mac-checkout + context: release-env + - mas-publish-x64-skip-checkout: + requires: + - mac-checkout + context: release-env + - osx-publish-arm64-skip-checkout: + requires: + - mac-checkout + context: release-env + - mas-publish-arm64-skip-checkout: + requires: + - mac-checkout + context: release-env + + build-linux: + when: + and: + - equal: [false, << pipeline.parameters.run-macos-publish >>] + - equal: [false, << pipeline.parameters.run-linux-publish >>] + - equal: [true, << pipeline.parameters.run-build-linux >>] + jobs: + - linux-make-src-cache + - linux-x64-testing: + requires: + - linux-make-src-cache + - linux-x64-testing-asan: + requires: + - linux-make-src-cache + - linux-x64-testing-no-run-as-node: + requires: + - linux-make-src-cache + - linux-x64-testing-gn-check: + requires: + - linux-make-src-cache + - linux-x64-testing-tests: + requires: + - linux-x64-testing + - linux-x64-testing-asan-tests: + requires: + - linux-x64-testing-asan + - linux-x64-testing-nan: + requires: + - linux-x64-testing + - linux-x64-testing-node: + requires: + - linux-x64-testing + - linux-ia32-testing: + requires: + - linux-make-src-cache + - linux-ia32-testing-tests: + requires: + - linux-ia32-testing + - linux-ia32-testing-nan: + requires: + - linux-ia32-testing + - linux-ia32-testing-node: + requires: + - linux-ia32-testing + - linux-arm-testing: + requires: + - linux-make-src-cache + - linux-arm-testing-tests: + filters: + branches: + # Do not run this on forked pull requests + ignore: /pull\/[0-9]+/ + requires: + - linux-arm-testing + - linux-arm64-testing: + requires: + - linux-make-src-cache + - linux-arm64-testing-tests: + filters: + branches: + # Do not run this on forked pull requests + ignore: /pull\/[0-9]+/ + requires: + - linux-arm64-testing + - linux-arm64-testing-gn-check: + requires: + - linux-make-src-cache + + build-mac: + when: + and: + - equal: [false, << pipeline.parameters.run-macos-publish >>] + - equal: [false, << pipeline.parameters.run-linux-publish >>] + - equal: [true, << pipeline.parameters.run-build-mac >>] + jobs: + - mac-make-src-cache + - osx-testing-x64: + requires: + - mac-make-src-cache + - osx-testing-x64-gn-check: + requires: + - mac-make-src-cache + - osx-testing-x64-tests: + requires: + - osx-testing-x64 + - osx-testing-arm64: + requires: + - mac-make-src-cache + - osx-testing-arm64-tests: + filters: + branches: + # Do not run this on forked pull requests + ignore: /pull\/[0-9]+/ + requires: + - osx-testing-arm64 + - mas-testing-x64: + requires: + - mac-make-src-cache + - mas-testing-x64-gn-check: + requires: + - mac-make-src-cache + - mas-testing-x64-tests: + requires: + - mas-testing-x64 + - mas-testing-arm64: + requires: + - mac-make-src-cache + - mas-testing-arm64-tests: + filters: + branches: + # Do not run this on forked pull requests + ignore: /pull\/[0-9]+/ + requires: + - mas-testing-arm64 + lint: + jobs: + - lint diff --git a/.circleci/config/build.js b/.circleci/config/build.js new file mode 100644 index 0000000000000..4e255608a0905 --- /dev/null +++ b/.circleci/config/build.js @@ -0,0 +1,34 @@ +const cp = require('child_process'); +const fs = require('fs-extra'); +const path = require('path'); +const yaml = require('js-yaml'); + +const STAGING_DIR = path.resolve(__dirname, '..', 'config-staging'); + +function copyAndExpand(dir = './') { + const absDir = path.resolve(__dirname, dir); + const targetDir = path.resolve(STAGING_DIR, dir); + + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir); + } + + for (const file of fs.readdirSync(absDir)) { + if (!file.endsWith('.yml')) { + if (fs.statSync(path.resolve(absDir, file)).isDirectory()) { + copyAndExpand(path.join(dir, file)); + } + continue; + } + + fs.writeFileSync(path.resolve(targetDir, file), yaml.dump(yaml.load(fs.readFileSync(path.resolve(absDir, file), 'utf8')), { + noRefs: true, + })); + } +} + +if (fs.pathExists(STAGING_DIR)) fs.removeSync(STAGING_DIR); +copyAndExpand(); + +const output = cp.spawnSync(process.env.CIRCLECI_BINARY || 'circleci', ['config', 'pack', STAGING_DIR]); +fs.writeFileSync(path.resolve(STAGING_DIR, 'built.yml'), output.stdout.toString()); diff --git a/.circleci/config/jobs/lint.yml b/.circleci/config/jobs/lint.yml new file mode 100644 index 0000000000000..2137566547120 --- /dev/null +++ b/.circleci/config/jobs/lint.yml @@ -0,0 +1,51 @@ +executor: + name: linux-docker + size: medium +steps: + - checkout: + path: src/electron + - run: + name: Setup third_party Depot Tools + command: | + # "depot_tools" has to be checkout into "//third_party/depot_tools" so pylint.py can a "pylintrc" file. + git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git src/third_party/depot_tools + echo 'export PATH="$PATH:'"$PWD"'/src/third_party/depot_tools"' >> $BASH_ENV + - run: + name: Download GN Binary + command: | + chromium_revision="$(grep -A1 chromium_version src/electron/DEPS | tr -d '\n' | cut -d\' -f4)" + gn_version="$(curl -sL "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/DEPS?format=TEXT" | base64 -d | grep gn_version | head -n1 | cut -d\' -f4)" + + cipd ensure -ensure-file - -root . \<<-CIPD + \$ServiceURL https://chrome-infra-packages.appspot.com/ + @Subdir src/buildtools/linux64 + gn/gn/linux-amd64 $gn_version + CIPD + + echo 'export CHROMIUM_BUILDTOOLS_PATH="'"$PWD"'/src/buildtools"' >> $BASH_ENV + - run: + name: Download clang-format Binary + command: | + chromium_revision="$(grep -A1 chromium_version src/electron/DEPS | tr -d '\n' | cut -d\' -f4)" + + sha1_path='buildtools/linux64/clang-format.sha1' + curl -sL "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/${sha1_path}?format=TEXT" | base64 -d > "src/${sha1_path}" + + download_from_google_storage.py --no_resume --no_auth --bucket chromium-clang-format -s "src/${sha1_path}" + - run: + name: Run Lint + command: | + # gn.py tries to find a gclient root folder starting from the current dir. + # When it fails and returns "None" path, the whole script fails. Let's "fix" it. + touch .gclient + # Another option would be to checkout "buildtools" inside the Electron checkout, + # but then we would lint its contents (at least gn format), and it doesn't pass it. + + cd src/electron + node script/yarn install --frozen-lockfile + node script/yarn lint + - run: + name: Run Script Typechecker + command: | + cd src/electron + node script/yarn tsc -p tsconfig.script.json diff --git a/.circleci/config/package.json b/.circleci/config/package.json new file mode 100644 index 0000000000000..054dd7c11cc9e --- /dev/null +++ b/.circleci/config/package.json @@ -0,0 +1,10 @@ +{ + "name": "@electron/circleci-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } +} diff --git a/.circleci/config/yarn.lock b/.circleci/config/yarn.lock new file mode 100644 index 0000000000000..51b2d9877643f --- /dev/null +++ b/.circleci/config/yarn.lock @@ -0,0 +1,43 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000000000..c12625a1481e8 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,59 @@ +# Electron Dev on Codespaces + +Welcome to the Codespaces Electron Developer Environment. + +## Quick Start + +Upon creation of your codespace you should have [build tools](https://github.com/electron/build-tools) installed and an initialized gclient checkout of Electron. In order to build electron you'll need to run the following commands. + +```bash +e sync -vv +e build +``` + +The initial sync will take approximately ~30 minutes and the build will take ~8 minutes. Incremental syncs and incremental builds are substantially quicker. + +## Directory Structure + +Codespaces doesn't lean very well into gclient based checkouts, the directory structure is slightly strange. There are two locations for the `electron` checkout that both map to the same files under the hood. + +```graphql +# Primary gclient checkout container +/workspaces/gclient/* + └─ src/* - # Chromium checkout + └─ electron - # Electron checkout +# Symlinked Electron checkout (identical to the above) +/workspaces/electron +``` + +## Goma + +If you are a maintainer [with Goma access](../docs/development/goma.md) it should be automatically configured and authenticated when you spin up a new codespaces instance. You can validate this by checking `e d goma_auth info` or by checking that your build-tools configuration has a goma mode of `cluster`. + +## Running Electron + +You can run Electron in a few ways. If you just want to see if it launches: + +```bash +# Enter an interactive JS prompt headlessly +xvfb-run e start -i +``` + +But if you want to actually see Electron you will need to use the built-in VNC capability. If you click "Ports" in codespaces and then open the `VNC web client` forwarded port you should see a web based VNC portal in your browser. When you are asked for a password use `builduser`. + +Once in the VNC UI you can open `Applications -> System -> XTerm` which will open a VNC based terminal app and then you can run `e start` like normal and Electron will open in your VNC session. + +## Running Tests + +You run tests via build-tools and `xvfb`. + +```bash +# Run all tests +xvfb-run e test + +# Run the main process tests +xvfb-run e test --runners=main + +# Run the old remote tests +xvfb-run e test --runners=remote +``` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000..5c800028282d8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +{ + "dockerComposeFile": "docker-compose.yml", + "service": "buildtools", + "onCreateCommand": ".devcontainer/on-create-command.sh", + "workspaceFolder": "/workspaces/gclient/src/electron", + "extensions": [ + "joeleinbinder.mojom-language", + "rafaelmaiolla.diff", + "surajbarkale.ninja", + "ms-vscode.cpptools", + "mutantdino.resourcemonitor", + "dbaeumer.vscode-eslint", + "shakram02.bash-beautify", + "marshallofsound.gnls-electron" + ], + "settings": { + "[gn]": { + "editor.formatOnSave": true + }, + "editor.tabSize": 2, + "bashBeautify.tabSize": 2 + }, + "forwardPorts": [8088, 6080, 5901], + "portsAttributes": { + "8088": { + "label": "Goma Control Panel", + "onAutoForward": "silent" + }, + "6080": { + "label": "VNC web client (noVNC)", + "onAutoForward": "silent" + }, + "5901": { + "label": "VNC TCP port", + "onAutoForward": "silent" + } + }, + "hostRequirements": { + "storage": "32gb", + "cpus": 8 + }, + "remoteUser": "builduser" +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000000..794577745512d --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3' + +services: + buildtools: + image: ghcr.io/electron/devcontainer:27db4a3e3512bfd2e47f58cea69922da0835f1d9 + + volumes: + - ..:/workspaces/gclient/src/electron:cached + + - /var/run/docker.sock:/var/run/docker.sock + + command: /bin/sh -c "while sleep 1000; do :; done" + + user: builduser + + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined diff --git a/.devcontainer/on-create-command.sh b/.devcontainer/on-create-command.sh new file mode 100755 index 0000000000000..8899906dff20e --- /dev/null +++ b/.devcontainer/on-create-command.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -eo pipefail + +buildtools=$HOME/.electron_build_tools +gclient_root=/workspaces/gclient +buildtools_configs=/workspaces/buildtools-configs + +export PATH="$PATH:$buildtools/src" + +# Create the persisted buildtools config folder +mkdir -p $buildtools_configs +rm -f $buildtools/configs +ln -s $buildtools_configs $buildtools/configs + +# Write the gclient config if it does not already exist +if [ ! -f $gclient_root/.gclient ]; then + echo "solutions = [ + { \"name\" : \"src/electron\", + \"url\" : \"https://github.com/electron/electron\", + \"deps_file\" : \"DEPS\", + \"managed\" : False, + \"custom_deps\" : { + }, + \"custom_vars\": {}, + }, + ] + " >$gclient_root/.gclient +fi + +# Write the default buildtools config file if it does +# not already exist +if [ ! -f $buildtools/configs/evm.testing.json ]; then + write_config() { + echo " + { + \"root\": \"/workspaces/gclient\", + \"goma\": \"$1\", + \"gen\": { + \"args\": [ + \"import(\\\"//electron/build/args/testing.gn\\\")\", + \"import(\\\"/home/builduser/.electron_build_tools/third_party/goma.gn\\\")\" + ], + \"out\": \"Testing\" + }, + \"env\": { + \"CHROMIUM_BUILDTOOLS_PATH\": \"/workspaces/gclient/src/buildtools\", + \"GIT_CACHE_PATH\": \"/workspaces/gclient/.git-cache\" + }, + \"remotes\": { + \"electron\": { + \"origin\": \"https://github.com/electron/electron.git\" + } + } + } + " >$buildtools/configs/evm.testing.json + } + + # Start out as cache only + write_config cache-only + + e use testing + + # Attempt to auth to the goma service via codespaces tokens + # if it works we can use the goma cluster + export NOTGOMA_CODESPACES_TOKEN=$GITHUB_TOKEN + if e d goma_auth login; then + write_config cluster + fi +else + # Even if the config file existed we still need to re-auth with the goma + # cluster + NOTGOMA_CODESPACES_TOKEN=$GITHUB_TOKEN e d goma_auth login || true +fi diff --git a/.dockerignore b/.dockerignore index fa43feee9940a..7307e769482a1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,2 @@ * !tools/xvfb-init.sh -!build/install-build-deps.sh diff --git a/.env.example b/.env.example index eb3df4b6bdf9c..4d218327bd60e 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,3 @@ APPVEYOR_CLOUD_TOKEN= CIRCLE_TOKEN= ELECTRON_GITHUB_TOKEN= -VSTS_TOKEN= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 08b7ff0923125..55d0f1712134a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,18 +6,21 @@ "browser": true }, "rules": { + "semi": ["error", "always"], "no-var": "error", - "no-unused-vars": 0, - "no-global-assign": 0, + "no-unused-vars": "off", + "no-global-assign": "off", + "guard-for-in": "error", "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "after-used", - "ignoreRestSiblings": false + "ignoreRestSiblings": true }], "prefer-const": ["error", { "destructuring": "all" }], - "node/no-deprecated-api": 0 + "standard/no-callback-literal": "off", + "node/no-deprecated-api": "off" }, "parserOptions": { "ecmaVersion": 6, @@ -27,7 +30,23 @@ { "files": "*.js", "rules": { - "@typescript-eslint/no-unused-vars": "off" + "@typescript-eslint/no-unused-vars": "off" + } + }, + { + "files": "*.ts", + "rules": { + "no-undef": "off", + "no-redeclare": "off", + "@typescript-eslint/no-redeclare": ["error"], + "no-use-before-define": "off" + } + }, + { + "files": "*.d.ts", + "rules": { + "no-useless-constructor": "off", + "@typescript-eslint/no-unused-vars": "off" } } ] diff --git a/.gitattributes b/.gitattributes index a8cc2eac7063f..88189455c32c1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,14 @@ # `git apply` and friends don't understand CRLF, even on windows. Force those # files to be checked out with LF endings even if core.autocrlf is true. *.patch text eol=lf +patches/**/.patches merge=union + +# Source code and markdown files should always use LF as line ending. +*.cc text eol=lf +*.mm text eol=lf +*.h text eol=lf +*.js text eol=lf +*.ts text eol=lf +*.py text eol=lf +*.ps1 text eol=lf +*.md text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 16c6bbed6ff3c..6bfa64f5a2483 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,19 +3,17 @@ # https://help.github.com/articles/about-codeowners # https://git-scm.com/docs/gitignore -# Most stuff in here is owned by the Community & Safety WG... -/.github/* @electron/wg-community - -# ...except the Admin WG maintains this file. -/.github/CODEOWNERS @electron/wg-admin - # Upgrades WG -/patches/ @electron/wg-upgrades - -# Docs & Tooling WG -/default_app/ @electron/wg-docs-tools -/docs/ @electron/wg-docs-tools +/patches/ @electron/wg-upgrades @electron/wg-security +DEPS @electron/wg-upgrades # Releases WG /npm/ @electron/wg-releases -/script/release @electron/wg-releases \ No newline at end of file +/script/release @electron/wg-releases + +# Security WG +/lib/browser/devtools.ts @electron/wg-security +/lib/browser/guest-view-manager.ts @electron/wg-security +/lib/browser/guest-window-proxy.ts @electron/wg-security +/lib/browser/rpc-server.ts @electron/wg-security +/lib/renderer/security-warnings.ts @electron/wg-security diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index a1a336a131e19..0000000000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve Electron - ---- - - - -### Preflight Checklist - - -* [ ] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md) for this project. -* [ ] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. -* [ ] I have searched the issue tracker for an issue that matches the one I want to file, without success. - -### Issue Details - -* **Electron Version:** - * -* **Operating System:** - * -* **Last Known Working Electron version:** - * - -### Expected Behavior - - -### Actual Behavior - - -### To Reproduce - - - - - - - - -### Screenshots - - -### Additional Information - diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index 20fc958e2ce57..0000000000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for Electron - ---- - - - -### Preflight Checklist - - -* [ ] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md) for this project. -* [ ] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. -* [ ] I have searched the issue tracker for a feature request that matches the one I want to file, without success. - -### Problem Description - - -### Proposed Solution - - -### Alternatives Considered - - -### Additional Information - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000000..706d63103ed76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,77 @@ +name: Bug Report +description: Report an Electron bug +title: "[Bug]: " +labels: "bug :beetle:" +body: +- type: checkboxes + attributes: + label: Preflight Checklist + description: Please ensure you've completed all of the following. + options: + - label: I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project. + required: true + - label: I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + required: true + - label: I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a bug report that matches the one I want to file, without success. + required: true +- type: input + attributes: + label: Electron Version + description: What version of Electron are you using? + placeholder: 12.0.0 + validations: + required: true +- type: dropdown + attributes: + label: What operating system are you using? + options: + - Windows + - macOS + - Ubuntu + - Other Linux + - Other (specify below) + validations: + required: true +- type: input + attributes: + label: Operating System Version + description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a. + placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04" + validations: + required: true +- type: dropdown + attributes: + label: What arch are you using? + options: + - x64 + - ia32 + - arm64 (including Apple Silicon) + - Other (specify below) + validations: + required: true +- type: input + attributes: + label: Last Known Working Electron version + description: What is the last version of Electron this worked in, if applicable? + placeholder: 11.0.0 +- type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Actual Behavior + description: A clear description of what actually happens. + validations: + required: true +- type: input + attributes: + label: Testcase Gist URL + description: If you can reproduce the issue in a standalone test case, please use [Electron Fiddle](https://github.com/electron/fiddle) to create one and to publish it as a [GitHub gist](https://gist.github.com) and put the gist URL here. This is **the best way** to ensure this issue is triaged quickly. + placeholder: https://gist.github.com/... +- type: textarea + attributes: + label: Additional Information + description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000000..9e8c1cd168b5e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,40 @@ +name: Feature Request +description: Suggest an idea for Electron +title: "[Feature Request]: " +labels: "enhancement :sparkles:" +body: +- type: checkboxes + attributes: + label: Preflight Checklist + description: Please ensure you've completed all of the following. + options: + - label: I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project. + required: true + - label: I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + required: true + - label: I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a feature request that matches the one I want to file, without success. + required: true +- type: textarea + attributes: + label: Problem Description + description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. + validations: + required: true +- type: textarea + attributes: + label: Proposed Solution + description: Describe the solution you'd like in a clear and concise manner. + validations: + required: true +- type: textarea + attributes: + label: Alternatives Considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true +- type: textarea + attributes: + label: Additional Information + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/mac_app_store_private_api_rejection.md b/.github/ISSUE_TEMPLATE/mac_app_store_private_api_rejection.md deleted file mode 100644 index cefd5800e2a73..0000000000000 --- a/.github/ISSUE_TEMPLATE/mac_app_store_private_api_rejection.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Mac App Store Private API Rejection -about: Your app was rejected from the Mac App Store for using private API's - ---- - - - -### Preflight Checklist - - -* [ ] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md) for this project. -* [ ] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. - -### Issue Details - -* **Electron Version:** - * - -### Rejection Email - - -### Additional Information - diff --git a/.github/ISSUE_TEMPLATE/mac_app_store_private_api_rejection.yml b/.github/ISSUE_TEMPLATE/mac_app_store_private_api_rejection.yml new file mode 100644 index 0000000000000..df6f0fc972877 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/mac_app_store_private_api_rejection.yml @@ -0,0 +1,30 @@ +name: Report Mac App Store Private API Rejection +description: Your app was rejected from the Mac App Store for using private API's +title: "[MAS Rejection]: " +body: +- type: checkboxes + attributes: + label: Preflight Checklist + description: Please ensure you've completed all of the following. + options: + - label: I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project. + required: true + - label: I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + required: true +- type: input + attributes: + label: Electron Version + description: What version of Electron are you using? + placeholder: 12.0.0 + validations: + required: true +- type: textarea + attributes: + label: Rejection Email + description: Paste the contents of your rejection email here, censoring any private information such as app names. + validations: + required: true +- type: textarea + attributes: + label: Additional Information + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/security_report.md b/.github/ISSUE_TEMPLATE/security_report.md deleted file mode 100644 index 8c2498da2e2bc..0000000000000 --- a/.github/ISSUE_TEMPLATE/security_report.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Security report -about: Do not create an issue for security reports, send an email to security@electronjs.org - ---- - -### Notice - -**DO NOT** create an issue for security reports. -Send an email to: **security@electronjs.org**. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e8215e0dc5342..3551177321e6d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ Thank you for your Pull Request. Please provide a description above and review the requirements below. -Contributors guide: https://github.com/electron/electron/blob/master/CONTRIBUTING.md +Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md --> #### Checklist @@ -11,11 +11,10 @@ Contributors guide: https://github.com/electron/electron/blob/master/CONTRIBUTIN - [ ] PR description included and stakeholders cc'd - [ ] `npm test` passes -- [ ] tests are [changed or added](https://github.com/electron/electron/blob/master/docs/development/testing.md) +- [ ] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md) - [ ] relevant documentation is changed or added -- [ ] PR title follows semantic [commit guidelines](https://github.com/electron/electron/blob/master/docs/development/pull-requests.md#commit-message-guidelines) - [ ] [PR release notes](https://github.com/electron/clerk/blob/master/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/master/README.md#examples). #### Release Notes -Notes: +Notes: diff --git a/.github/config.yml b/.github/config.yml index 62fe777031a70..b6bb25d021b24 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -2,7 +2,7 @@ newPRWelcomeComment: | 💖 Thanks for opening this pull request! 💖 - We use [semantic commit messages](https://github.com/electron/electron/blob/master/docs/development/pull-requests.md#commit-message-guidelines) to streamline the release process. Before your pull request can be merged, you should **update your pull request title** to start with a semantic prefix. + We use [semantic commit messages](https://github.com/electron/electron/blob/main/docs/development/pull-requests.md#commit-message-guidelines) to streamline the release process. Before your pull request can be merged, you should **update your pull request title** to start with a semantic prefix. Examples of commit messages with semantic prefixes: @@ -12,9 +12,9 @@ newPRWelcomeComment: | Things that will help get your PR across the finish line: - - Follow the JavaScript, C++, and Python [coding style](https://github.com/electron/electron/blob/master/docs/development/coding-style.md). + - Follow the JavaScript, C++, and Python [coding style](https://github.com/electron/electron/blob/main/docs/development/coding-style.md). - Run `npm run lint` locally to catch formatting errors earlier. - - Document any user-facing changes you've made following the [documentation styleguide](https://github.com/electron/electron/blob/master/docs/styleguide.md). + - Document any user-facing changes you've made following the [documentation styleguide](https://github.com/electron/electron/blob/main/docs/styleguide.md). - Include tests when adding/changing behavior. - Include screenshots and animated GIFs whenever possible. @@ -29,13 +29,13 @@ firstPRMergeComment: > # Users authorized to run manual trop backports authorizedUsers: - alexeykuzmin - - BinaryMuse - ckerr - codebytere - deepak1556 - jkleinsc + - loc - MarshallOfSound - miniak - - nitsakh + - mlaurencin - nornagon - zcbenz diff --git a/.github/main.workflow b/.github/main.workflow deleted file mode 100644 index f33f94bd75896..0000000000000 --- a/.github/main.workflow +++ /dev/null @@ -1,10 +0,0 @@ -workflow "Clerk" { - #TODO(codebytere): make this work properly on pull_request - on = "repository_dispatch" - resolves = "Check release notes" -} - -action "Check release notes" { - uses = "electron/clerk@master" - secrets = [ "GITHUB_TOKEN" ] -} diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000000000..4168a3cdeed9e --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,2 @@ +# Always validate the PR title, and ignore the commits +titleOnly: true diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index e39e44f5eff05..0000000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 45 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - fixme/bug - - fixme/crash - - fixme/regression - - fixme/security - - blocked - - blocking-stable - - needs-review -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity and is not currently prioritized. It will be closed - in a week if no further activity occurs :) - -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: > - If you still think this issue is relevant, please ping a maintainer or - leave a comment! \ No newline at end of file diff --git a/.github/workflows/electron_woa_testing.yml b/.github/workflows/electron_woa_testing.yml new file mode 100644 index 0000000000000..01c2b9a08ca71 --- /dev/null +++ b/.github/workflows/electron_woa_testing.yml @@ -0,0 +1,178 @@ +name: Electron WOA Testing + +on: + push: + branches: '**' + workflow_dispatch: + inputs: + appveyor_job_id: + description: 'Job Id of Appveyor WOA job to test' + type: text + required: true + +jobs: + electron-woa-init: + if: ${{ github.event_name == 'push' && github.repository == 'electron/electron' }} + runs-on: ubuntu-latest + steps: + - name: Dummy step for push event + run: | + echo "This job is a needed initialization step for Electron WOA testing. Another test result will appear once the electron-woa-testing build is done." + + electron-woa-testing: + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'electron/electron' }} + runs-on: [self-hosted, woa] + permissions: + checks: write + pull-requests: write + steps: + - uses: LouisBrunner/checks-action@v1.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: electron-woa-testing + status: in_progress + details_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + output: | + {"summary":"Test In Progress","text_description":"See job details here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"} + - name: Clean Workspace + run: | + Remove-Item * -Recurse -Force + shell: powershell + - name: Checkout + uses: actions/checkout@v3 + with: + path: src\electron + fetch-depth: 0 + - name: Yarn install + run: | + cd src\electron + node script/yarn.js install --frozen-lockfile + - name: Download and extract dist.zip for test + run: | + $localArtifactPath = "$pwd\dist.zip" + $serverArtifactPath = "https://ci.appveyor.com/api/buildjobs/${{ inputs.appveyor_job_id }}/artifacts/dist.zip" + Invoke-RestMethod -Method Get -Uri $serverArtifactPath -OutFile $localArtifactPath -Headers @{ "Authorization" = "Bearer ${{ secrets.APPVEYOR_TOKEN }}" } + & "${env:ProgramFiles(x86)}\7-Zip\7z.exe" x -osrc\out\Default -y $localArtifactPath + shell: powershell + - name: Download and extract native test executables for test + run: | + $localArtifactPath = "src\out\Default\shell_browser_ui_unittests.exe" + $serverArtifactPath = "https://ci.appveyor.com/api/buildjobs/${{ inputs.appveyor_job_id }}/artifacts/shell_browser_ui_unittests.exe" + Invoke-RestMethod -Method Get -Uri $serverArtifactPath -OutFile $localArtifactPath -Headers @{ "Authorization" = "Bearer ${{ secrets.APPVEYOR_TOKEN }}" } + shell: powershell + - name: Download and extract ffmpeg.zip for test + run: | + $localArtifactPath = "$pwd\ffmpeg.zip" + $serverArtifactPath = "https://ci.appveyor.com/api/buildjobs/${{ inputs.appveyor_job_id }}/artifacts/ffmpeg.zip" + Invoke-RestMethod -Method Get -Uri $serverArtifactPath -OutFile $localArtifactPath -Headers @{ "Authorization" = "Bearer ${{ secrets.APPVEYOR_TOKEN }}" } + & "${env:ProgramFiles(x86)}\7-Zip\7z.exe" x -osrc\out\ffmpeg $localArtifactPath + shell: powershell + - name: Download node headers for test + run: | + $localArtifactPath = "src\node_headers.zip" + $serverArtifactPath = "https://ci.appveyor.com/api/buildjobs/${{ inputs.appveyor_job_id }}/artifacts/node_headers.zip" + Invoke-RestMethod -Method Get -Uri $serverArtifactPath -OutFile $localArtifactPath -Headers @{ "Authorization" = "Bearer ${{ secrets.APPVEYOR_TOKEN }}" } + cd src + & "${env:ProgramFiles(x86)}\7-Zip\7z.exe" x -y node_headers.zip + shell: powershell + - name: Download electron.lib for test + run: | + $localArtifactPath = "src\out\Default\electron.lib" + $serverArtifactPath = "https://ci.appveyor.com/api/buildjobs/${{ inputs.appveyor_job_id }}/artifacts/electron.lib" + Invoke-RestMethod -Method Get -Uri $serverArtifactPath -OutFile $localArtifactPath -Headers @{ "Authorization" = "Bearer ${{ secrets.APPVEYOR_TOKEN }}" } + shell: powershell + # Uncomment the following block if pdb files are needed to debug issues + # - name: Download pdb files for detailed stacktraces + # if: ${{ github.event_name == 'workflow_dispatch' }} + # run: | + # try { + # $localArtifactPath = "src\pdb.zip" + # $serverArtifactPath = "https://ci.appveyor.com/api/buildjobs/${{ inputs.appveyor_job_id }}/artifacts/pdb.zip" + # Invoke-RestMethod -Method Get -Uri $serverArtifactPath -OutFile $localArtifactPath -Headers @{ "Authorization" = "Bearer ${{ secrets.APPVEYOR_TOKEN }}" } + # cd src + # & "${env:ProgramFiles(x86)}\7-Zip\7z.exe" x -y pdb.zip + # } catch { + # Write-Host "There was an exception encountered while downloading pdb files:" $_.Exception.Message + # } finally { + # $global:LASTEXITCODE = 0 + # } + # shell: powershell + - name: Setup node headers + run: | + New-Item src\out\Default\gen\node_headers\Release -Type directory + Copy-Item -path src\out\Default\electron.lib -destination src\out\Default\gen\node_headers\Release\node.lib + shell: powershell + - name: Run Electron Main process tests + run: | + cd src + set npm_config_nodedir=%cd%\out\Default\gen\node_headers + set npm_config_arch=arm64 + cd electron + node script/yarn test --runners=main --enable-logging --disable-features=CalculateNativeWinOcclusion + env: + ELECTRON_ENABLE_STACK_DUMPING: true + ELECTRON_OUT_DIR: Default + IGNORE_YARN_INSTALL_ERROR: 1 + ELECTRON_TEST_RESULTS_DIR: junit + MOCHA_MULTI_REPORTERS: 'mocha-junit-reporter, tap' + MOCHA_REPORTER: mocha-multi-reporters + ELECTRON_SKIP_NATIVE_MODULE_TESTS: true + - name: Run Electron Remote based tests + if: ${{ success() || failure() }} + run: | + cd src + set npm_config_nodedir=%cd%\out\Default\gen\node_headers + set npm_config_arch=arm64 + cd electron + node script/yarn test --runners=remote --enable-logging --disable-features=CalculateNativeWinOcclusion + env: + ELECTRON_OUT_DIR: Default + IGNORE_YARN_INSTALL_ERROR: 1 + ELECTRON_TEST_RESULTS_DIR: junit + MOCHA_MULTI_REPORTERS: 'mocha-junit-reporter, tap' + MOCHA_REPORTER: mocha-multi-reporters + ELECTRON_SKIP_NATIVE_MODULE_TESTS: true + - name: Verify ffmpeg + run: | + cd src + echo "Verifying non proprietary ffmpeg" + python electron\script\verify-ffmpeg.py --build-dir out\Default --source-root %cd% --ffmpeg-path out\ffmpeg + shell: cmd + - name: Kill processes left running from last test run + if: ${{ always() }} + run: | + Get-Process | Where Name -Like "electron*" | Stop-Process + Get-Process | Where Name -Like "msedge*" | Stop-Process + shell: powershell + - name: Delete user app data directories + if: ${{ always() }} + run: | + Remove-Item -path $env:APPDATA/Electron* -Recurse -Force -ErrorAction Ignore + shell: powershell + - uses: LouisBrunner/checks-action@v1.1.1 + if: ${{ success() }} + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: electron-woa-testing + conclusion: "${{ job.status }}" + details_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + output: | + {"summary":"${{ job.status }}","text_description":"See job details here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"} + - uses: LouisBrunner/checks-action@v1.1.1 + if: ${{ success() }} + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: electron-woa-testing + conclusion: "${{ job.status }}" + details_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + output: | + {"summary":"Job Succeeded","text_description":"See job details here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"} + - uses: LouisBrunner/checks-action@v1.1.1 + if: ${{ ! success() }} + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: electron-woa-testing + conclusion: "${{ job.status }}" + details_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + output: | + {"summary":"Job Failed","text_description":"See job details here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"} \ No newline at end of file diff --git a/.github/workflows/semantic.yml b/.github/workflows/semantic.yml new file mode 100644 index 0000000000000..11d62c9d62855 --- /dev/null +++ b/.github/workflows/semantic.yml @@ -0,0 +1,20 @@ +name: "Check Semantic Commit" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - name: semantic-pull-request + uses: amannn/action-semantic-pull-request@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: false diff --git a/.gitignore b/.gitignore index 218dd25f4dd03..e2168896e2ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,24 +17,6 @@ *.xcodeproj /.idea/ /dist/ -/external_binaries/ -/out/ -/vendor/.gclient -/vendor/debian_jessie_mips64-sysroot/ -/vendor/debian_stretch_amd64-sysroot/ -/vendor/debian_stretch_arm-sysroot/ -/vendor/debian_stretch_arm64-sysroot/ -/vendor/debian_stretch_i386-sysroot/ -/vendor/gcc-4.8.3-d197-n64-loongson/ -/vendor/readme-gcc483-loongson.txt -/vendor/download/ -/vendor/llvm-build/ -/vendor/llvm/ -/vendor/npm/ -/vendor/python_26/ -/vendor/native_mksnapshot -/vendor/LICENSES.chromium.html -/vendor/pyyaml node_modules/ SHASUMS256.txt **/package-lock.json @@ -44,6 +26,7 @@ compile_commands.json # npm package /npm/dist /npm/path.txt +/npm/checksums.json .npmrc @@ -55,13 +38,19 @@ electron.d.ts spec/.hash # Eslint Cache -.eslintcache +.eslintcache* # Generated native addon files -/spec/fixtures/native-addon/echo/build/ +/spec-main/fixtures/native-addon/echo/build/ # If someone runs tsc this is where stuff will end up ts-gen # Used to accelerate CI builds .depshash +.depshash-target + +# Used to accelerate builds after sync +patches/mtime-cache.json + +spec/fixtures/logo.png \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f6b899f682ac8..0000000000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "vendor/requests"] - path = vendor/requests - url = https://github.com/kennethreitz/requests -[submodule "vendor/boto"] - path = vendor/boto - url = https://github.com/boto/boto.git diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000000000..31354ec138999 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000000..feac116af9ca6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run precommit \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000000000..7482fdf10c359 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run prepack diff --git a/.markdownlint.autofix.json b/.markdownlint.autofix.json new file mode 100644 index 0000000000000..7bb678b286387 --- /dev/null +++ b/.markdownlint.autofix.json @@ -0,0 +1,6 @@ +{ + "default": false, + "no-trailing-spaces": { + "br_spaces": 0 + } +} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000000000..495df656c2cd3 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,27 @@ +{ + "commands-show-output": false, + "first-line-h1": false, + "header-increment": false, + "line-length": { + "code_blocks": false, + "tables": false, + "stern": true, + "line_length": -1 + }, + "no-bare-urls": false, + "no-blanks-blockquote": false, + "no-duplicate-header": { + "allow_different_nesting": true + }, + "no-emphasis-as-header": false, + "no-hard-tabs": { + "code_blocks": false + }, + "no-space-in-emphasis": false, + "no-trailing-punctuation": false, + "no-trailing-spaces": { + "br_spaces": 0 + }, + "single-h1": false, + "no-inline-html": false +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000..8351c19397f4f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +14 diff --git a/BUILD.gn b/BUILD.gn index 425efe604a91e..29d5cbf06226d 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -1,9 +1,14 @@ import("//build/config/locales.gni") import("//build/config/ui.gni") import("//build/config/win/manifest.gni") +import("//components/os_crypt/features.gni") +import("//components/spellcheck/spellcheck_build_features.gni") import("//content/public/app/mac_helpers.gni") +import("//extensions/buildflags/buildflags.gni") import("//pdf/features.gni") +import("//ppapi/buildflags/buildflags.gni") import("//printing/buildflags/buildflags.gni") +import("//testing/test.gni") import("//third_party/ffmpeg/ffmpeg_options.gni") import("//tools/generate_library_loader/generate_library_loader.gni") import("//tools/grit/grit_rule.gni") @@ -20,16 +25,25 @@ import("buildflags/buildflags.gni") import("electron_paks.gni") import("filenames.auto.gni") import("filenames.gni") +import("filenames.hunspell.gni") +import("filenames.libcxx.gni") +import("filenames.libcxxabi.gni") if (is_mac) { import("//build/config/mac/rules.gni") import("//third_party/icu/config.gni") import("//ui/gl/features.gni") import("//v8/gni/v8.gni") + import("build/rules.gni") + + assert( + mac_deployment_target == "10.11.0", + "Chromium has updated the mac_deployment_target, please update this assert, update the supported versions documentation (docs/tutorial/support.md) and flag this as a breaking change") } if (is_linux) { import("//build/config/linux/pkg_config.gni") + import("//tools/generate_stubs/rules.gni") pkg_config("gio_unix") { packages = [ "gio-unix-2.0" ] @@ -41,6 +55,45 @@ if (is_linux) { "gdk-pixbuf-2.0", ] } + + generate_library_loader("libnotify_loader") { + name = "LibNotifyLoader" + output_h = "libnotify_loader.h" + output_cc = "libnotify_loader.cc" + header = "" + config = ":libnotify_config" + + functions = [ + "notify_is_initted", + "notify_init", + "notify_get_server_caps", + "notify_get_server_info", + "notify_notification_new", + "notify_notification_add_action", + "notify_notification_set_image_from_pixbuf", + "notify_notification_set_timeout", + "notify_notification_set_urgency", + "notify_notification_set_hint_string", + "notify_notification_show", + "notify_notification_close", + ] + } + + generate_stubs("electron_gtk_stubs") { + sigs = [ + "shell/browser/ui/electron_gdk_pixbuf.sigs", + "shell/browser/ui/electron_gtk.sigs", + ] + extra_header = "shell/browser/ui/electron_gtk.fragment" + output_name = "electron_gtk_stubs" + public_deps = [ "//ui/gtk:gtk_config" ] + logging_function = "LogNoop()" + logging_include = "ui/gtk/log_noop.h" + } +} + +declare_args() { + use_prebuilt_v8_context_snapshot = false } branding = read_file("shell/app/BRANDING.json", "json") @@ -53,6 +106,17 @@ if (is_mas_build) { "It doesn't make sense to build a MAS build on a non-mac platform") } +if (enable_pdf_viewer) { + assert(enable_pdf, "PDF viewer support requires enable_pdf=true") + assert(enable_electron_extensions, + "PDF viewer support requires enable_electron_extensions=true") +} + +if (enable_electron_extensions) { + assert(enable_extensions, + "Chrome extension support requires enable_extensions=true") +} + config("branding") { defines = [ "ELECTRON_PRODUCT_NAME=\"$electron_product_name\"", @@ -60,6 +124,10 @@ config("branding") { ] } +config("electron_lib_config") { + include_dirs = [ "." ] +} + # We geneate the definitions twice here, once in //electron/electron.d.ts # and once in $target_gen_dir # The one in $target_gen_dir is used for the actual TSC build later one @@ -70,15 +138,20 @@ npm_action("build_electron_definitions") { args = [ rebase_path("$target_gen_dir/tsc/typings/electron.d.ts") ] inputs = auto_filenames.api_docs + [ "yarn.lock" ] - outputs = [ - "$target_gen_dir/tsc/typings/electron.d.ts", - ] + outputs = [ "$target_gen_dir/tsc/typings/electron.d.ts" ] +} + +webpack_build("electron_asar_bundle") { + deps = [ ":build_electron_definitions" ] + + inputs = auto_filenames.asar_bundle_deps + + config_file = "//electron/build/webpack/webpack.config.asar.js" + out_file = "$target_gen_dir/js2c/asar_bundle.js" } webpack_build("electron_browser_bundle") { - deps = [ - ":build_electron_definitions", - ] + deps = [ ":build_electron_definitions" ] inputs = auto_filenames.browser_bundle_deps @@ -87,9 +160,7 @@ webpack_build("electron_browser_bundle") { } webpack_build("electron_renderer_bundle") { - deps = [ - ":build_electron_definitions", - ] + deps = [ ":build_electron_definitions" ] inputs = auto_filenames.renderer_bundle_deps @@ -98,9 +169,7 @@ webpack_build("electron_renderer_bundle") { } webpack_build("electron_worker_bundle") { - deps = [ - ":build_electron_definitions", - ] + deps = [ ":build_electron_definitions" ] inputs = auto_filenames.worker_bundle_deps @@ -109,9 +178,7 @@ webpack_build("electron_worker_bundle") { } webpack_build("electron_sandboxed_renderer_bundle") { - deps = [ - ":build_electron_definitions", - ] + deps = [ ":build_electron_definitions" ] inputs = auto_filenames.sandbox_bundle_deps @@ -120,9 +187,7 @@ webpack_build("electron_sandboxed_renderer_bundle") { } webpack_build("electron_isolated_renderer_bundle") { - deps = [ - ":build_electron_definitions", - ] + deps = [ ":build_electron_definitions" ] inputs = auto_filenames.isolated_bundle_deps @@ -130,70 +195,44 @@ webpack_build("electron_isolated_renderer_bundle") { out_file = "$target_gen_dir/js2c/isolated_bundle.js" } -webpack_build("electron_content_script_bundle") { - deps = [ - ":build_electron_definitions", - ] - - inputs = auto_filenames.content_script_bundle_deps - - config_file = "//electron/build/webpack/webpack.config.content_script.js" - out_file = "$target_gen_dir/js2c/content_script_bundle.js" -} - -copy("atom_js2c_copy") { - sources = [ - "lib/common/asar.js", - "lib/common/asar_init.js", - ] - outputs = [ - "$target_gen_dir/js2c/{{source_file_part}}", - ] -} - -action("atom_js2c") { +action("electron_js2c") { deps = [ - ":atom_js2c_copy", + ":electron_asar_bundle", ":electron_browser_bundle", - ":electron_content_script_bundle", ":electron_isolated_renderer_bundle", ":electron_renderer_bundle", ":electron_sandboxed_renderer_bundle", ":electron_worker_bundle", ] - webpack_sources = [ + sources = [ + "$target_gen_dir/js2c/asar_bundle.js", "$target_gen_dir/js2c/browser_init.js", - "$target_gen_dir/js2c/renderer_init.js", - "$target_gen_dir/js2c/worker_init.js", - "$target_gen_dir/js2c/content_script_bundle.js", "$target_gen_dir/js2c/isolated_bundle.js", + "$target_gen_dir/js2c/renderer_init.js", "$target_gen_dir/js2c/sandbox_bundle.js", + "$target_gen_dir/js2c/worker_init.js", ] - sources = webpack_sources + [ - "$target_gen_dir/js2c/asar.js", - "$target_gen_dir/js2c/asar_init.js", - ] - inputs = sources + [ "//third_party/electron_node/tools/js2c.py" ] - outputs = [ - "$root_gen_dir/atom_natives.cc", - ] + outputs = [ "$root_gen_dir/electron_natives.cc" ] - script = "tools/js2c.py" + script = "build/js2c.py" args = [ rebase_path("//third_party/electron_node") ] + rebase_path(outputs, root_build_dir) + rebase_path(sources, root_build_dir) } +action("generate_config_gypi") { + outputs = [ "$root_gen_dir/config.gypi" ] + script = "script/generate-config-gypi.py" + args = rebase_path(outputs) + [ target_cpu ] +} + target_gen_default_app_js = "$target_gen_dir/js/default_app" typescript_build("default_app_js") { - deps = [ - ":build_electron_definitions", - ] - type_root = rebase_path("$target_gen_dir/tsc/electron/typings") + deps = [ ":build_electron_definitions" ] sources = filenames.default_app_ts_sources @@ -204,16 +243,12 @@ typescript_build("default_app_js") { copy("default_app_static") { sources = filenames.default_app_static_sources - outputs = [ - "$target_gen_default_app_js/{{source}}", - ] + outputs = [ "$target_gen_default_app_js/{{source}}" ] } copy("default_app_octicon_deps") { sources = filenames.default_app_octicon_sources - outputs = [ - "$target_gen_default_app_js/electron/default_app/octicon/{{source_file_part}}", - ] + outputs = [ "$target_gen_default_app_js/electron/default_app/octicon/{{source_file_part}}" ] } asar("default_app_asar") { @@ -227,9 +262,7 @@ asar("default_app_asar") { sources = get_target_outputs(":default_app_js") + get_target_outputs(":default_app_static") + get_target_outputs(":default_app_octicon_deps") - outputs = [ - "$root_out_dir/resources/default_app.asar", - ] + outputs = [ "$root_out_dir/resources/default_app.asar" ] } grit("resources") { @@ -241,129 +274,116 @@ grit("resources") { ] # Mojo manifest overlays are generated. - source_is_generated = true grit_flags = [ "-E", "target_gen_dir=" + rebase_path(target_gen_dir, root_build_dir), ] - deps = [ - ":copy_shell_devtools_discovery_page", - ] + deps = [ ":copy_shell_devtools_discovery_page" ] output_dir = "$target_gen_dir" } copy("copy_shell_devtools_discovery_page") { - sources = [ - "//content/shell/resources/shell_devtools_discovery_page.html", - ] - outputs = [ - "$target_gen_dir/shell_devtools_discovery_page.html", - ] + sources = [ "//content/shell/resources/shell_devtools_discovery_page.html" ] + outputs = [ "$target_gen_dir/shell_devtools_discovery_page.html" ] } -if (is_linux) { - generate_library_loader("libnotify_loader") { - name = "LibNotifyLoader" - output_h = "libnotify_loader.h" - output_cc = "libnotify_loader.cc" - header = "" - config = ":libnotify_config" - - functions = [ - "notify_is_initted", - "notify_init", - "notify_get_server_caps", - "notify_get_server_info", - "notify_notification_new", - "notify_notification_add_action", - "notify_notification_set_image_from_pixbuf", - "notify_notification_set_timeout", - "notify_notification_set_hint_string", - "notify_notification_show", - "notify_notification_close", - ] - } -} +npm_action("electron_version_args") { + script = "generate-version-json" -source_set("manifests") { - sources = [ - "//electron/shell/app/manifests.cc", - "//electron/shell/app/manifests.h", - ] + outputs = [ "$target_gen_dir/electron_version.args" ] - include_dirs = [ "//electron" ] + args = rebase_path(outputs) - deps = [ - "//electron/shell/common/api:mojo", - "//printing/buildflags", - "//services/service_manager/public/cpp", + inputs = [ + "ELECTRON_VERSION", + "script/generate-version-json.js", ] +} - if (enable_basic_printing) { - deps += [ "//components/services/pdf_compositor/public/cpp:manifest" ] - } +templated_file("electron_version_header") { + deps = [ ":electron_version_args" ] - if (enable_print_preview) { - deps += [ "//chrome/services/printing/public/cpp:manifest" ] - } + template = "build/templates/electron_version.tmpl" + output = "$target_gen_dir/electron_version.h" + + args_files = get_target_outputs(":electron_version_args") } -npm_action("electron_version_args") { - script = "generate-version-json" +action("electron_fuses") { + script = "build/fuses/build.py" + + inputs = [ "build/fuses/fuses.json5" ] outputs = [ - "$target_gen_dir/electron_version.args", + "$target_gen_dir/fuses.h", + "$target_gen_dir/fuses.cc", ] args = rebase_path(outputs) +} + +action("electron_generate_node_defines") { + script = "build/generate_node_defines.py" inputs = [ - "ELECTRON_VERSION", - "script/generate-version-json.js", + "//third_party/electron_node/src/tracing/trace_event_common.h", + "//third_party/electron_node/src/tracing/trace_event.h", + "//third_party/electron_node/src/util.h", ] -} -templated_file("electron_version_header") { - deps = [ - ":electron_version_args", + outputs = [ + "$target_gen_dir/push_and_undef_node_defines.h", + "$target_gen_dir/pop_node_defines.h", ] - template = "build/templates/electron_version.tmpl" - output = "$target_gen_dir/electron_version.h" - - args_files = get_target_outputs(":electron_version_args") + args = [ rebase_path(target_gen_dir) ] + rebase_path(inputs) } source_set("electron_lib") { configs += [ "//v8:external_startup_data" ] configs += [ "//third_party/electron_node:node_internals" ] - public_configs = [ ":branding" ] + public_configs = [ + ":branding", + ":electron_lib_config", + ] deps = [ - ":atom_js2c", + ":electron_fuses", + ":electron_generate_node_defines", + ":electron_js2c", ":electron_version_header", - ":manifests", ":resources", "buildflags", "chromium_src:chrome", - "native_mate", + "chromium_src:chrome_spellchecker", "shell/common/api:mojo", "//base:base_static", "//base/allocator:buildflags", + "//chrome/app:command_ids", "//chrome/app/resources:platform_locale_settings", + "//components/autofill/core/common:features", "//components/certificate_transparency", + "//components/embedder_support:browser_util", + "//components/language/core/browser", "//components/net_log", + "//components/network_hints/browser", + "//components/network_hints/common:mojo_bindings", + "//components/network_hints/renderer", "//components/network_session_configurator/common", + "//components/omnibox/browser:buildflags", + "//components/os_crypt", + "//components/pref_registry", "//components/prefs", - "//components/spellcheck/renderer", + "//components/security_state/content", + "//components/upload_list", + "//components/user_prefs", "//components/viz/host", "//components/viz/service", "//content/public/browser", "//content/public/child", - "//content/public/common:service_names", "//content/public/gpu", "//content/public/renderer", "//content/public/utility", @@ -371,28 +391,31 @@ source_set("electron_lib") { "//device/bluetooth/public/cpp", "//gin", "//media/capture/mojom:video_capture", - "//media/mojo/interfaces", + "//media/mojo/mojom", "//net:extras", "//net:net_resources", - "//net:net_with_v8", "//ppapi/host", "//ppapi/proxy", "//ppapi/shared_impl", "//printing/buildflags", - "//services/audio/public/mojom:constants", "//services/device/public/cpp/geolocation", + "//services/device/public/cpp/hid", "//services/device/public/mojom", "//services/proxy_resolver:lib", "//services/video_capture/public/mojom:constants", - "//services/viz/privileged/interfaces/compositing", + "//services/viz/privileged/mojom/compositing", "//skia", "//third_party/blink/public:blink", + "//third_party/blink/public:blink_devtools_inspector_resources", + "//third_party/blink/public/platform/media", "//third_party/boringssl", "//third_party/electron_node:node_lib", + "//third_party/inspector_protocol:crdtp", "//third_party/leveldatabase", "//third_party/libyuv", - "//third_party/webrtc_overrides:init_webrtc", + "//third_party/webrtc_overrides:webrtc_component", "//third_party/widevine/cdm:headers", + "//third_party/zlib/google:zip", "//ui/base/idle", "//ui/events:dom_keycode_converter", "//ui/gl", @@ -406,11 +429,10 @@ source_set("electron_lib") { public_deps = [ "//base", "//base:i18n", - "//content/public/app:both", + "//content/public/app", ] include_dirs = [ - "chromium_src", ".", "$target_gen_dir", @@ -426,41 +448,29 @@ source_set("electron_lib") { defines += [ "GDK_DISABLE_DEPRECATION_WARNINGS" ] } - extra_source_filters = [] - if (!is_linux) { - extra_source_filters += [ - "*\bx/*", - "*_x11.h", - "*_x11.cc", - "*_gtk.h", - "*_gtk.cc", - "*\blibrary_loaders/*", - ] - } - if (!is_win) { - extra_source_filters += [ - "*\bwin_*.h", - "*\bwin_*.cc", + if (!is_mas_build) { + deps += [ + "//components/crash/core/app", + "//components/crash/core/browser", ] } - if (!is_posix) { - extra_source_filters += [ - "*_posix.cc", - "*_posix.h", - ] + + sources = filenames.lib_sources + if (is_win) { + sources += filenames.lib_sources_win } if (is_mac) { - extra_source_filters += [ - "*_views.cc", - "*_views.h", - "*\bviews/*", - ] + sources += filenames.lib_sources_mac + } + if (is_posix) { + sources += filenames.lib_sources_posix + } + if (is_linux) { + sources += filenames.lib_sources_linux + } + if (!is_mac) { + sources += filenames.lib_sources_views } - - set_sources_assignment_filter( - sources_assignment_filter + extra_source_filters) - sources = filenames.lib_sources - set_sources_assignment_filter(sources_assignment_filter) if (is_component_build) { defines += [ "NODE_SHARED_MODE" ] @@ -473,65 +483,116 @@ source_set("electron_lib") { ] } + if (is_linux) { + deps += [ + "//components/crash/content/browser", + "//ui/gtk:gtk_config", + ] + } + if (is_mac) { deps += [ "//components/remote_cocoa/app_shim", + "//components/remote_cocoa/browser", "//content/common:mac_helpers", "//ui/accelerated_widget_mac", ] + + if (!is_mas_build) { + deps += [ "//third_party/crashpad/crashpad/client" ] + } + + frameworks = [ + "AVFoundation.framework", + "Carbon.framework", + "LocalAuthentication.framework", + "QuartzCore.framework", + "Quartz.framework", + "Security.framework", + "SecurityInterface.framework", + "ServiceManagement.framework", + "StoreKit.framework", + ] + sources += [ "shell/browser/ui/views/autofill_popup_view.cc", "shell/browser/ui/views/autofill_popup_view.h", ] if (is_mas_build) { - sources += [ "shell/browser/api/atom_api_app_mas.mm" ] + sources += [ "shell/browser/api/electron_api_app_mas.mm" ] + sources -= [ "shell/browser/auto_updater_mac.mm" ] + defines += [ "MAS_BUILD" ] sources -= [ - "shell/browser/auto_updater_mac.mm", - "shell/common/crash_reporter/crash_reporter_mac.h", - "shell/common/crash_reporter/crash_reporter_mac.mm", + "shell/app/electron_crash_reporter_client.cc", + "shell/app/electron_crash_reporter_client.h", + "shell/common/crash_keys.cc", + "shell/common/crash_keys.h", ] - defines += [ "MAS_BUILD" ] } else { - libs += [ + frameworks += [ "Squirrel.framework", - "ReactiveCocoa.framework", + "ReactiveObjC.framework", "Mantle.framework", ] - cflags_objcc = [ - "-F", - rebase_path("external_binaries", root_build_dir), + + deps += [ + "//third_party/squirrel.mac:reactiveobjc_framework+link", + "//third_party/squirrel.mac:squirrel_framework+link", ] - # ReactiveCocoa which is used by Squirrel requires using __weak. - cflags_objcc += [ "-fobjc-weak" ] + # ReactiveObjC which is used by Squirrel requires using __weak. + cflags_objcc = [ "-fobjc-weak" ] } } if (is_linux) { + libs = [ "xshmfence" ] deps += [ + ":electron_gtk_stubs", ":libnotify_loader", "//build/config/linux/gtk", - "//chrome/browser/ui/libgtkui", "//dbus", "//device/bluetooth", - "//third_party/breakpad:client", + "//ui/base/ime/linux", "//ui/events/devices/x11", "//ui/events/platform/x11", + "//ui/gtk", "//ui/views/controls/webview", "//ui/wm", ] + if (ozone_platform_x11) { + sources += filenames.lib_sources_linux_x11 + public_deps += [ + "//ui/base/x", + "//ui/ozone/platform/x11", + ] + } configs += [ ":gio_unix" ] - include_dirs += [ "//third_party/breakpad" ] - configs += [ "//build/config/linux:x11" ] defines += [ # Disable warnings for g_settings_list_schemas. "GLIB_DISABLE_DEPRECATION_WARNINGS", ] - sources += filenames.lib_sources_nss + sources += [ + "shell/browser/certificate_manager_model.cc", + "shell/browser/certificate_manager_model.h", + "shell/browser/ui/gtk/app_indicator_icon.cc", + "shell/browser/ui/gtk/app_indicator_icon.h", + "shell/browser/ui/gtk/app_indicator_icon_menu.cc", + "shell/browser/ui/gtk/app_indicator_icon_menu.h", + "shell/browser/ui/gtk/gtk_status_icon.cc", + "shell/browser/ui/gtk/gtk_status_icon.h", + "shell/browser/ui/gtk/menu_util.cc", + "shell/browser/ui/gtk/menu_util.h", + "shell/browser/ui/gtk/status_icon.cc", + "shell/browser/ui/gtk/status_icon.h", + "shell/browser/ui/gtk_util.cc", + "shell/browser/ui/gtk_util.h", + ] } if (is_win) { libs += [ "dwmapi.lib" ] deps += [ + "//components/crash/core/app:crash_export_thunks", "//ui/native_theme:native_theme_browser", "//ui/views/controls/webview", "//ui/wm", @@ -543,16 +604,12 @@ source_set("electron_lib") { ] } - if ((is_mac && !is_mas_build) || is_win) { + if (enable_plugins) { + deps += [ "chromium_src:plugins" ] sources += [ - "shell/common/crash_reporter/crash_reporter_crashpad.cc", - "shell/common/crash_reporter/crash_reporter_crashpad.h", + "shell/renderer/pepper_helper.cc", + "shell/renderer/pepper_helper.h", ] - deps += [ "//third_party/crashpad/crashpad/client" ] - } - - if (enable_pdf) { - deps += [ "//pdf" ] } if (enable_run_as_node) { @@ -566,7 +623,6 @@ source_set("electron_lib") { sources += [ "shell/browser/osr/osr_host_display_client.cc", "shell/browser/osr/osr_host_display_client.h", - "shell/browser/osr/osr_host_display_client_mac.mm", "shell/browser/osr/osr_render_widget_host_view.cc", "shell/browser/osr/osr_render_widget_host_view.h", "shell/browser/osr/osr_video_consumer.cc", @@ -575,43 +631,31 @@ source_set("electron_lib") { "shell/browser/osr/osr_view_proxy.h", "shell/browser/osr/osr_web_contents_view.cc", "shell/browser/osr/osr_web_contents_view.h", - "shell/browser/osr/osr_web_contents_view_mac.mm", ] + if (is_mac) { + sources += [ + "shell/browser/osr/osr_host_display_client_mac.mm", + "shell/browser/osr/osr_web_contents_view_mac.mm", + ] + } deps += [ "//components/viz/service", - "//services/viz/public/interfaces", + "//services/viz/public/mojom", "//ui/compositor", ] } if (enable_desktop_capturer) { - if (is_component_build && is_win) { - # On windows the implementation relies on unexported - # DxgiDuplicatorController class. - deps += [ "//third_party/webrtc/modules/desktop_capture" ] - } sources += [ - "shell/browser/api/atom_api_desktop_capturer.cc", - "shell/browser/api/atom_api_desktop_capturer.h", + "shell/browser/api/electron_api_desktop_capturer.cc", + "shell/browser/api/electron_api_desktop_capturer.h", ] } - if (enable_view_api) { + if (enable_views_api) { sources += [ - "shell/browser/api/views/atom_api_box_layout.cc", - "shell/browser/api/views/atom_api_box_layout.h", - "shell/browser/api/views/atom_api_button.cc", - "shell/browser/api/views/atom_api_button.h", - "shell/browser/api/views/atom_api_label_button.cc", - "shell/browser/api/views/atom_api_label_button.h", - "shell/browser/api/views/atom_api_layout_manager.cc", - "shell/browser/api/views/atom_api_layout_manager.h", - "shell/browser/api/views/atom_api_md_text_button.cc", - "shell/browser/api/views/atom_api_md_text_button.h", - "shell/browser/api/views/atom_api_resize_area.cc", - "shell/browser/api/views/atom_api_resize_area.h", - "shell/browser/api/views/atom_api_text_field.cc", - "shell/browser/api/views/atom_api_text_field.h", + "shell/browser/api/views/electron_api_image_view.cc", + "shell/browser/api/views/electron_api_image_view.h", ] } @@ -619,27 +663,73 @@ source_set("electron_lib") { sources += [ "shell/browser/printing/print_preview_message_handler.cc", "shell/browser/printing/print_preview_message_handler.h", + "shell/browser/printing/print_view_manager_electron.cc", + "shell/browser/printing/print_view_manager_electron.h", "shell/renderer/printing/print_render_frame_helper_delegate.cc", "shell/renderer/printing/print_render_frame_helper_delegate.h", ] + deps += [ + "//chrome/services/printing/public/mojom", + "//components/printing/common:mojo_interfaces", + ] + if (is_mac) { + deps += [ "//chrome/services/mac_notifications/public/mojom" ] + } } - if (enable_pepper_flash) { - deps += [ "components/pepper_flash" ] - } - - public_deps += [ "shell/common/extensions/api:extensions_features" ] - deps += [ - "//components/pref_registry", - "//components/user_prefs", - "//extensions/browser", - "//extensions/browser:core_api_provider", - "//extensions/common", - "//extensions/common:core_api_provider", - "//extensions/renderer", - ] if (enable_electron_extensions) { sources += filenames.lib_sources_extensions + deps += [ + "shell/browser/extensions/api:api_registration", + "shell/common/extensions/api", + "shell/common/extensions/api:extensions_features", + "//chrome/browser/resources:component_extension_resources", + "//components/update_client:update_client", + "//components/zoom", + "//extensions/browser", + "//extensions/browser:core_api_provider", + "//extensions/browser/updater", + "//extensions/common", + "//extensions/common:core_api_provider", + "//extensions/renderer", + ] + } + + if (enable_pdf) { + # Printing depends on some //pdf code, so it needs to be built even if the + # pdf viewer isn't enabled. + deps += [ + "//pdf", + "//pdf:features", + ] + } + if (enable_pdf_viewer) { + deps += [ + "//chrome/browser/resources/pdf:resources", + "//components/pdf/browser", + "//components/pdf/browser:interceptors", + "//components/pdf/common", + "//components/pdf/renderer", + "//pdf:pdf_ppapi", + ] + sources += [ + "shell/browser/electron_pdf_web_contents_helper_client.cc", + "shell/browser/electron_pdf_web_contents_helper_client.h", + ] + } + + sources += get_target_outputs(":electron_fuses") + + if (is_win && enable_win_dark_mode_window_ui) { + sources += [ + "shell/browser/win/dark_mode.cc", + "shell/browser/win/dark_mode.h", + ] + libs += [ "uxtheme.lib" ] + } + + if (allow_runtime_configurable_key_storage) { + defines += [ "ALLOW_RUNTIME_CONFIGURABLE_KEY_STORAGE" ] } } @@ -659,41 +749,50 @@ if (is_mac) { electron_framework_version = "A" electron_version = read_file("ELECTRON_VERSION", "trim string") - bundle_data("electron_framework_resources") { - public_deps = [ - ":packed_resources", + mac_xib_bundle_data("electron_xibs") { + sources = [ "shell/common/resources/mac/MainMenu.xib" ] + } + + action("fake_v8_context_snapshot_generator") { + script = "build/fake_v8_context_snapshot_generator.py" + args = [ + rebase_path("$root_out_dir/$v8_context_snapshot_filename"), + rebase_path("$root_out_dir/fake/$v8_context_snapshot_filename"), ] + outputs = [ "$root_out_dir/fake/$v8_context_snapshot_filename" ] + } + + bundle_data("electron_framework_resources") { + public_deps = [ ":packed_resources" ] sources = [] if (icu_use_data_file) { sources += [ "$root_out_dir/icudtl.dat" ] public_deps += [ "//third_party/icu:icudata" ] } if (v8_use_external_startup_data) { - sources += [ "$root_out_dir/natives_blob.bin" ] public_deps += [ "//v8" ] if (use_v8_context_snapshot) { - sources += [ "$root_out_dir/v8_context_snapshot.bin" ] - public_deps += [ "//tools/v8_context_snapshot" ] + if (use_prebuilt_v8_context_snapshot) { + sources += [ "$root_out_dir/fake/$v8_context_snapshot_filename" ] + public_deps += [ ":fake_v8_context_snapshot_generator" ] + } else { + sources += [ "$root_out_dir/$v8_context_snapshot_filename" ] + public_deps += [ "//tools/v8_context_snapshot" ] + } } else { sources += [ "$root_out_dir/snapshot_blob.bin" ] } } - outputs = [ - "{{bundle_resources_dir}}/{{source_file_part}}", - ] + outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ] } - if (!is_component_build) { + if (!is_component_build && is_component_ffmpeg) { bundle_data("electron_framework_libraries") { sources = [] public_deps = [] - if (is_component_ffmpeg) { - sources += [ "$root_out_dir/libffmpeg.dylib" ] - public_deps += [ "//third_party/ffmpeg:ffmpeg" ] - } - outputs = [ - "{{bundle_contents_dir}}/Libraries/{{source_file_part}}", - ] + sources += [ "$root_out_dir/libffmpeg.dylib" ] + public_deps += [ "//third_party/ffmpeg:ffmpeg" ] + outputs = [ "{{bundle_contents_dir}}/Libraries/{{source_file_part}}" ] } } else { group("electron_framework_libraries") { @@ -706,12 +805,8 @@ if (is_mac) { "$root_out_dir/egl_intermediates/libEGL.dylib", "$root_out_dir/egl_intermediates/libGLESv2.dylib", ] - outputs = [ - "{{bundle_contents_dir}}/Libraries/{{source_file_part}}", - ] - public_deps = [ - "//ui/gl:angle_library_copy", - ] + outputs = [ "{{bundle_contents_dir}}/Libraries/{{source_file_part}}" ] + public_deps = [ "//ui/gl:angle_library_copy" ] } # Add the SwiftShader .dylibs in the Libraries directory of the Framework. @@ -719,43 +814,40 @@ if (is_mac) { sources = [ "$root_out_dir/egl_intermediates/libswiftshader_libEGL.dylib", "$root_out_dir/egl_intermediates/libswiftshader_libGLESv2.dylib", + "$root_out_dir/vk_intermediates/libvk_swiftshader.dylib", + "$root_out_dir/vk_intermediates/vk_swiftshader_icd.json", ] - outputs = [ - "{{bundle_contents_dir}}/Libraries/{{source_file_part}}", - ] + outputs = [ "{{bundle_contents_dir}}/Libraries/{{source_file_part}}" ] public_deps = [ - "//ui/gl:swiftshader_library_copy", + "//ui/gl:swiftshader_egl_library_copy", + "//ui/gl:swiftshader_vk_library_copy", ] } } group("electron_angle_library") { if (use_egl) { - deps = [ - ":electron_angle_binaries", - ] + deps = [ ":electron_angle_binaries" ] } } group("electron_swiftshader_library") { if (use_egl) { - deps = [ - ":electron_swiftshader_binaries", - ] + deps = [ ":electron_swiftshader_binaries" ] } } bundle_data("electron_crashpad_helper") { - sources = [ - "$root_out_dir/crashpad_handler", - ] + sources = [ "$root_out_dir/chrome_crashpad_handler" ] - outputs = [ - "{{bundle_resources_dir}}/{{source_file_part}}", - ] + outputs = [ "{{bundle_contents_dir}}/Helpers/{{source_file_part}}" ] - public_deps = [ - "//third_party/crashpad/crashpad/handler:crashpad_handler", - ] + public_deps = [ "//components/crash/core/app:chrome_crashpad_handler" ] + + if (is_asan) { + # crashpad_handler requires the ASan runtime at its @executable_path. + sources += [ "$root_out_dir/libclang_rt.asan_osx_dynamic.dylib" ] + public_deps += [ "//build/config/sanitizers:copy_asan_runtime" ] + } } mac_framework_bundle("electron_framework") { @@ -765,7 +857,11 @@ if (is_mac) { "Resources", "Libraries", ] + if (!is_mas_build) { + framework_contents += [ "Helpers" ] + } public_deps = [ + ":electron_framework_libraries", ":electron_lib", ] deps = [ @@ -773,6 +869,7 @@ if (is_mac) { ":electron_framework_libraries", ":electron_framework_resources", ":electron_swiftshader_library", + ":electron_xibs", ] if (!is_mas_build) { deps += [ ":electron_crashpad_helper" ] @@ -786,29 +883,19 @@ if (is_mac) { include_dirs = [ "." ] sources = filenames.framework_sources - - libs = [ - "AVFoundation.framework", - "Carbon.framework", - "LocalAuthentication.framework", - "QuartzCore.framework", - "Quartz.framework", - "Security.framework", - "SecurityInterface.framework", - "ServiceManagement.framework", - "StoreKit.framework", - ] + frameworks = [] if (enable_osr) { - libs += [ "IOSurface.framework" ] + frameworks += [ "IOSurface.framework" ] } ldflags = [ - "-F", - rebase_path("external_binaries", root_build_dir), "-Wl,-install_name,@rpath/$output_name.framework/$output_name", "-rpath", "@loader_path/Libraries", + + # Required for exporting all symbols of libuv. + "-Wl,-force_load,obj/third_party/electron_node/deps/uv/libuv.a", ] if (is_component_build) { ldflags += [ @@ -823,15 +910,17 @@ if (is_mac) { assert(defined(invoker.helper_name_suffix)) output_name = electron_helper_name + invoker.helper_name_suffix - deps = [ - ":electron_framework+link", - ] + deps = [ ":electron_framework+link" ] if (!is_mas_build) { deps += [ "//sandbox/mac:seatbelt" ] } defines = [ "HELPER_EXECUTABLE" ] - sources = filenames.app_sources - sources += [ "shell/common/atom_constants.cc" ] + sources = [ + "shell/app/electron_main_mac.cc", + "shell/app/uv_stdio_fix.cc", + "shell/app/uv_stdio_fix.h", + "shell/common/electron_constants.cc", + ] include_dirs = [ "." ] info_plist = "shell/renderer/resources/mac/Info.plist" extra_substitutions = @@ -858,22 +947,48 @@ if (is_mac) { } } + template("stripped_framework") { + action(target_name) { + assert(defined(invoker.framework)) + + script = "//electron/build/strip_framework.py" + + forward_variables_from(invoker, [ "deps" ]) + inputs = [ "$root_out_dir/" + invoker.framework ] + outputs = [ "$target_out_dir/stripped_frameworks/" + invoker.framework ] + + args = rebase_path(inputs) + rebase_path(outputs) + } + } + + stripped_framework("stripped_mantle_framework") { + framework = "Mantle.framework" + deps = [ "//third_party/squirrel.mac:mantle_framework" ] + } + + stripped_framework("stripped_reactiveobjc_framework") { + framework = "ReactiveObjC.framework" + deps = [ "//third_party/squirrel.mac:reactiveobjc_framework" ] + } + + stripped_framework("stripped_squirrel_framework") { + framework = "Squirrel.framework" + deps = [ "//third_party/squirrel.mac:squirrel_framework" ] + } + bundle_data("electron_app_framework_bundle_data") { - sources = [ - "$root_out_dir/$electron_framework_name.framework", - ] + sources = [ "$root_out_dir/$electron_framework_name.framework" ] if (!is_mas_build) { - sources += [ - "external_binaries/Mantle.framework", - "external_binaries/ReactiveCocoa.framework", - "external_binaries/Squirrel.framework", - ] + sources += get_target_outputs(":stripped_mantle_framework") + + get_target_outputs(":stripped_reactiveobjc_framework") + + get_target_outputs(":stripped_squirrel_framework") } - outputs = [ - "{{bundle_contents_dir}}/Frameworks/{{source_file_part}}", - ] + outputs = [ "{{bundle_contents_dir}}/Frameworks/{{source_file_part}}" ] public_deps = [ ":electron_framework+link", + ":stripped_mantle_framework", + ":stripped_reactiveobjc_framework", + ":stripped_squirrel_framework", ] foreach(helper_params, content_mac_helpers) { @@ -887,22 +1002,17 @@ if (is_mac) { output_name = electron_login_helper_name sources = filenames.login_helper_sources include_dirs = [ "." ] - libs = [ "AppKit.framework" ] + frameworks = [ "AppKit.framework" ] info_plist = "shell/app/resources/mac/loginhelper-Info.plist" extra_substitutions = [ "ELECTRON_BUNDLE_ID=$electron_mac_bundle_id.loginhelper" ] } bundle_data("electron_login_helper_app") { - public_deps = [ - ":electron_login_helper", - ] - sources = [ - "$root_out_dir/$electron_login_helper_name.app", - ] - outputs = [ - "{{bundle_contents_dir}}/Library/LoginItems/{{source_file_part}}", - ] + public_deps = [ ":electron_login_helper" ] + sources = [ "$root_out_dir/$electron_login_helper_name.app" ] + outputs = + [ "{{bundle_contents_dir}}/Library/LoginItems/{{source_file_part}}" ] } action("electron_app_lproj_dirs") { @@ -917,15 +1027,9 @@ if (is_mac) { foreach(locale, locales_as_mac_outputs) { bundle_data("electron_app_strings_${locale}_bundle_data") { - sources = [ - "$target_gen_dir/app_infoplist_strings/$locale.lproj", - ] - outputs = [ - "{{bundle_resources_dir}}/$locale.lproj", - ] - public_deps = [ - ":electron_app_lproj_dirs", - ] + sources = [ "$target_gen_dir/app_infoplist_strings/$locale.lproj" ] + outputs = [ "{{bundle_resources_dir}}/$locale.lproj" ] + public_deps = [ ":electron_app_lproj_dirs" ] } } group("electron_app_strings_bundle_data") { @@ -944,24 +1048,34 @@ if (is_mac) { "$root_out_dir/resources/default_app.asar", "shell/browser/resources/mac/electron.icns", ] - outputs = [ - "{{bundle_resources_dir}}/{{source_file_part}}", - ] + outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ] + } + + asar_hashed_info_plist("electron_app_plist") { + keys = [ "DEFAULT_APP_ASAR_HEADER_SHA" ] + hash_targets = [ ":default_app_asar_header_hash" ] + plist_file = "shell/browser/resources/mac/Info.plist" } mac_app_bundle("electron_app") { output_name = electron_product_name - sources = filenames.app_sources - sources += [ "shell/common/atom_constants.cc" ] + sources = [ + "shell/app/electron_main_mac.cc", + "shell/app/uv_stdio_fix.cc", + "shell/app/uv_stdio_fix.h", + ] include_dirs = [ "." ] deps = [ ":electron_app_framework_bundle_data", + ":electron_app_plist", ":electron_app_resources", + ":electron_fuses", + "//electron/buildflags", ] if (is_mas_build) { deps += [ ":electron_login_helper_app" ] } - info_plist = "shell/browser/resources/mac/Info.plist" + info_plist_target = ":electron_app_plist" extra_substitutions = [ "ELECTRON_BUNDLE_ID=$electron_mac_bundle_id", "ELECTRON_VERSION=$electron_version", @@ -977,9 +1091,7 @@ if (is_mac) { binary = "$root_out_dir/$electron_framework_name.framework/Versions/$electron_framework_version/$electron_framework_name" symbol_dir = "$root_out_dir/breakpad_symbols" dsym_file = "$root_out_dir/$electron_framework_name.dSYM/Contents/Resources/DWARF/$electron_framework_name" - deps = [ - ":electron_framework", - ] + deps = [ ":electron_framework" ] } foreach(helper_params, content_mac_helpers) { @@ -990,9 +1102,7 @@ if (is_mac) { binary = "$root_out_dir/$electron_helper_name${_helper_suffix}.app/Contents/MacOS/$electron_helper_name${_helper_suffix}" symbol_dir = "$root_out_dir/breakpad_symbols" dsym_file = "$root_out_dir/$electron_helper_name${_helper_suffix}.dSYM/Contents/Resources/DWARF/$electron_helper_name${_helper_suffix}" - deps = [ - ":electron_helper_app_${_helper_target}", - ] + deps = [ ":electron_helper_app_${_helper_target}" ] } } @@ -1000,18 +1110,15 @@ if (is_mac) { binary = "$root_out_dir/$electron_product_name.app/Contents/MacOS/$electron_product_name" symbol_dir = "$root_out_dir/breakpad_symbols" dsym_file = "$root_out_dir/$electron_product_name.dSYM/Contents/Resources/DWARF/$electron_product_name" - deps = [ - ":electron_app", - ] + deps = [ ":electron_app" ] } extract_symbols("swiftshader_egl_syms") { binary = "$root_out_dir/libswiftshader_libEGL.dylib" symbol_dir = "$root_out_dir/breakpad_symbols" dsym_file = "$root_out_dir/libswiftshader_libEGL.dylib.dSYM/Contents/Resources/DWARF/libswiftshader_libEGL.dylib" - deps = [ - "//third_party/swiftshader/src/OpenGL/libEGL:swiftshader_libEGL", - ] + deps = + [ "//third_party/swiftshader/src/OpenGL/libEGL:swiftshader_libEGL" ] } extract_symbols("swiftshader_gles_syms") { @@ -1024,23 +1131,24 @@ if (is_mac) { } extract_symbols("crashpad_handler_syms") { - binary = "$root_out_dir/crashpad_handler" + binary = "$root_out_dir/chrome_crashpad_handler" symbol_dir = "$root_out_dir/breakpad_symbols" - dsym_file = "$root_out_dir/crashpad_handler.dSYM/Contents/Resources/DWARF/crashpad_handler" - deps = [ - "//third_party/crashpad/crashpad/handler:crashpad_handler", - ] + dsym_file = "$root_out_dir/chrome_crashpad_handler.dSYM/Contents/Resources/DWARF/chrome_crashpad_handler" + deps = [ "//components/crash/core/app:chrome_crashpad_handler" ] } group("electron_symbols") { deps = [ - ":crashpad_handler_syms", ":electron_app_syms", ":electron_framework_syms", ":swiftshader_egl_syms", ":swiftshader_gles_syms", ] + if (!is_mas_build) { + deps += [ ":crashpad_handler_syms" ] + } + foreach(helper_params, content_mac_helpers) { _helper_target = helper_params[0] deps += [ ":electron_helper_syms_${_helper_target}" ] @@ -1063,26 +1171,36 @@ if (is_mac) { executable("electron_app") { output_name = electron_project_name - sources = filenames.app_sources + if (is_win) { + sources = [ "shell/app/electron_main_win.cc" ] + } else if (is_linux) { + sources = [ + "shell/app/electron_main_linux.cc", + "shell/app/uv_stdio_fix.cc", + "shell/app/uv_stdio_fix.h", + ] + } include_dirs = [ "." ] deps = [ ":default_app_asar", ":electron_app_manifest", ":electron_lib", ":packed_resources", + "//components/crash/core/app", "//content:sandbox_helper_win", "//electron/buildflags", "//ui/strings", ] data = [] + data_deps = [] data += [ "$root_out_dir/resources.pak" ] data += [ "$root_out_dir/chrome_100_percent.pak" ] if (enable_hidpi) { data += [ "$root_out_dir/chrome_200_percent.pak" ] } - foreach(locale, locales) { + foreach(locale, platform_pak_locales) { data += [ "$root_out_dir/locales/$locale.pak" ] } @@ -1090,33 +1208,53 @@ if (is_mac) { data += [ "$root_out_dir/resources/default_app.asar" ] } - public_deps = [ - "//tools/v8_context_snapshot:v8_context_snapshot", - ] + if (use_v8_context_snapshot) { + public_deps = [ "//tools/v8_context_snapshot:v8_context_snapshot" ] + } + + if (is_linux) { + data_deps += [ "//components/crash/core/app:chrome_crashpad_handler" ] + } if (is_win) { sources += [ # TODO: we should be generating our .rc files more like how chrome does - "shell/browser/resources/win/atom.rc", + "shell/browser/resources/win/electron.rc", "shell/browser/resources/win/resource.h", ] + deps += [ + "//components/browser_watcher:browser_watcher_client", + "//components/crash/core/app:run_as_crashpad_handler", + ] + + ldflags = [] + libs = [ "comctl32.lib", "uiautomationcore.lib", "wtsapi32.lib", ] - configs += [ "//build/config/win:windowed" ] - - ldflags = [ - # Windows 7 doesn't have these DLLs. - # TODO: are there other DLLs we need to list here to be win7 - # compatible? - "/DELAYLOAD:api-ms-win-core-winrt-l1-1-0.dll", - "/DELAYLOAD:api-ms-win-core-winrt-string-l1-1-0.dll", + configs -= [ "//build/config/win:console" ] + configs += [ + "//build/config/win:windowed", + "//build/config/win:delayloads", ] + if (current_cpu == "x86") { + # Set the initial stack size to 0.5MiB, instead of the 1.5MiB needed by + # Chrome's main thread. This saves significant memory on threads (like + # those in the Windows thread pool, and others) whose stack size we can + # only control through this setting. Because Chrome's main thread needs + # a minimum 1.5 MiB stack, the main thread (in 32-bit builds only) uses + # fibers to switch to a 1.5 MiB stack before running any other code. + ldflags += [ "/STACK:0x80000" ] + } else { + # Increase the initial stack size. The default is 1MB, this is 8MB. + ldflags += [ "/STACK:0x800000" ] + } + # This is to support renaming of electron.exe. node-gyp has hard-coded # executable names which it will recognise as node. This module definition # file claims that the electron executable is in fact named "node.exe", @@ -1124,16 +1262,27 @@ if (is_mac) { # See https://github.com/nodejs/node-gyp/commit/52ceec3a6d15de3a8f385f43dbe5ecf5456ad07a ldflags += [ "/DEF:" + rebase_path("build/electron.def", root_build_dir) ] inputs = [ - "shell/browser/resources/win/atom.ico", + "shell/browser/resources/win/electron.ico", "build/electron.def", ] } if (is_linux) { - ldflags = [ "-pie" ] + ldflags = [ + "-pie", + + # Required for exporting all symbols of libuv. + "-Wl,--whole-archive", + "obj/third_party/electron_node/deps/uv/libuv.a", + "-Wl,--no-whole-archive", + ] if (!is_component_build && is_component_ffmpeg) { configs += [ "//build/config/gcc:rpath_for_built_shared_libraries" ] } + + if (is_linux) { + deps += [ "//sandbox/linux:chrome_sandbox" ] + } } } @@ -1149,17 +1298,14 @@ if (is_mac) { extract_symbols("electron_app_symbols") { binary = "$root_out_dir/$electron_project_name$_target_executable_suffix" symbol_dir = "$root_out_dir/breakpad_symbols" - deps = [ - ":electron_app", - ] + deps = [ ":electron_app" ] } extract_symbols("swiftshader_egl_symbols") { binary = "$root_out_dir/swiftshader/libEGL$_target_shared_library_suffix" symbol_dir = "$root_out_dir/breakpad_symbols" - deps = [ - "//third_party/swiftshader/src/OpenGL/libEGL:swiftshader_libEGL", - ] + deps = + [ "//third_party/swiftshader/src/OpenGL/libEGL:swiftshader_libEGL" ] } extract_symbols("swiftshader_gles_symbols") { @@ -1181,6 +1327,25 @@ if (is_mac) { } } +test("shell_browser_ui_unittests") { + sources = [ + "//electron/shell/browser/ui/accelerator_util_unittests.cc", + "//electron/shell/browser/ui/run_all_unittests.cc", + ] + + configs += [ ":electron_lib_config" ] + + deps = [ + ":electron_lib", + "//base", + "//base/test:test_support", + "//testing/gmock", + "//testing/gtest", + "//ui/base", + "//ui/strings", + ] +} + template("dist_zip") { _runtime_deps_target = "${target_name}__deps" _runtime_deps_file = @@ -1200,104 +1365,160 @@ template("dist_zip") { action(target_name) { script = "//electron/build/zip.py" - deps = [ - ":$_runtime_deps_target", - ] + deps = [ ":$_runtime_deps_target" ] forward_variables_from(invoker, [ "outputs", "testonly", ]) + flatten = false + flatten_relative_to = false + if (defined(invoker.flatten)) { + flatten = invoker.flatten + if (defined(invoker.flatten_relative_to)) { + flatten_relative_to = invoker.flatten_relative_to + } + } args = rebase_path(outputs + [ _runtime_deps_file ], root_build_dir) + [ target_cpu, target_os, + "$flatten", + "$flatten_relative_to", ] } } copy("electron_license") { - sources = [ - "LICENSE", - ] - outputs = [ - "$root_build_dir/{{source_file_part}}", - ] + sources = [ "LICENSE" ] + outputs = [ "$root_build_dir/{{source_file_part}}" ] } copy("chromium_licenses") { - deps = [ - "//components/resources:about_credits", - ] - sources = [ - "$root_gen_dir/components/resources/about_credits.html", - ] - outputs = [ - "$root_build_dir/LICENSES.chromium.html", - ] + deps = [ "//components/resources:about_credits" ] + sources = [ "$root_gen_dir/components/resources/about_credits.html" ] + outputs = [ "$root_build_dir/LICENSES.chromium.html" ] } group("licenses") { data_deps = [ - ":electron_license", ":chromium_licenses", + ":electron_license", ] } copy("electron_version") { - sources = [ - "ELECTRON_VERSION", - ] - outputs = [ - "$root_build_dir/version", - ] + sources = [ "ELECTRON_VERSION" ] + outputs = [ "$root_build_dir/version" ] } dist_zip("electron_dist_zip") { data_deps = [ ":electron_app", - ":licenses", ":electron_version", + ":licenses", ] if (is_linux) { data_deps += [ "//sandbox/linux:chrome_sandbox" ] } - outputs = [ - "$root_build_dir/dist.zip", - ] + deps = data_deps + outputs = [ "$root_build_dir/dist.zip" ] } dist_zip("electron_ffmpeg_zip") { - data_deps = [ - "//third_party/ffmpeg", - ] - outputs = [ - "$root_build_dir/ffmpeg.zip", - ] + data_deps = [ "//third_party/ffmpeg" ] + deps = data_deps + outputs = [ "$root_build_dir/ffmpeg.zip" ] +} + +electron_chromedriver_deps = [ + ":licenses", + "//chrome/test/chromedriver", + "//electron/buildflags", +] + +group("electron_chromedriver") { + testonly = true + public_deps = electron_chromedriver_deps } dist_zip("electron_chromedriver_zip") { testonly = true - data_deps = [ - "//chrome/test/chromedriver", - ":licenses", - ] - outputs = [ - "$root_build_dir/chromedriver.zip", - ] + data_deps = electron_chromedriver_deps + deps = data_deps + outputs = [ "$root_build_dir/chromedriver.zip" ] +} + +mksnapshot_deps = [ + ":licenses", + "//v8:mksnapshot($v8_snapshot_toolchain)", +] + +if (use_v8_context_snapshot) { + mksnapshot_deps += [ "//tools/v8_context_snapshot:v8_context_snapshot_generator($v8_snapshot_toolchain)" ] +} + +group("electron_mksnapshot") { + public_deps = mksnapshot_deps } dist_zip("electron_mksnapshot_zip") { - data_deps = [ - "//v8:mksnapshot($v8_snapshot_toolchain)", - "//tools/v8_context_snapshot:v8_context_snapshot_generator", - ":licenses", - ] - outputs = [ - "$root_build_dir/mksnapshot.zip", - ] + data_deps = mksnapshot_deps + deps = data_deps + outputs = [ "$root_build_dir/mksnapshot.zip" ] +} + +copy("hunspell_dictionaries") { + sources = hunspell_dictionaries + hunspell_licenses + outputs = [ "$target_gen_dir/electron_hunspell/{{source_file_part}}" ] +} + +dist_zip("hunspell_dictionaries_zip") { + data_deps = [ ":hunspell_dictionaries" ] + deps = data_deps + flatten = true + + outputs = [ "$root_build_dir/hunspell_dictionaries.zip" ] +} + +copy("libcxx_headers") { + sources = libcxx_headers + libcxx_licenses + + [ "//buildtools/third_party/libc++/__config_site" ] + outputs = [ "$target_gen_dir/electron_libcxx_include/{{source_root_relative_dir}}/{{source_file_part}}" ] +} + +dist_zip("libcxx_headers_zip") { + data_deps = [ ":libcxx_headers" ] + deps = data_deps + flatten = true + flatten_relative_to = rebase_path( + "$target_gen_dir/electron_libcxx_include/buildtools/third_party/libc++/trunk", + "$root_out_dir") + + outputs = [ "$root_build_dir/libcxx_headers.zip" ] +} + +copy("libcxxabi_headers") { + sources = libcxxabi_headers + libcxxabi_licenses + outputs = [ "$target_gen_dir/electron_libcxxabi_include/{{source_root_relative_dir}}/{{source_file_part}}" ] +} + +dist_zip("libcxxabi_headers_zip") { + data_deps = [ ":libcxxabi_headers" ] + deps = data_deps + flatten = true + flatten_relative_to = rebase_path( + "$target_gen_dir/electron_libcxxabi_include/buildtools/third_party/libc++abi/trunk", + "$root_out_dir") + + outputs = [ "$root_build_dir/libcxxabi_headers.zip" ] +} + +action("libcxx_objects_zip") { + deps = [ "//buildtools/third_party/libc++" ] + script = "build/zip_libcxx.py" + outputs = [ "$root_build_dir/libcxx_objects.zip" ] + args = rebase_path(outputs) } group("electron") { - public_deps = [ - ":electron_app", - ] + public_deps = [ ":electron_app" ] } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 33caac1a2f798..3a65cd1249f8a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,46 +1,135 @@ -# Contributor Covenant Code of Conduct: +# Code of Conduct -## Our Pledge +As a member project of the OpenJS Foundation, Electron uses [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) as their code of conduct. The full text is included [below](#contributor-covenant-code-of-conduct) in English, and translations are available from the Contributor Covenant organisation: -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +* [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) +* [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) -## Our Standards +## Contributor Covenant Code of Conduct -Examples of behavior that contributes to creating a positive environment include: +### Our Pledge -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. -Examples of unacceptable behavior by participants include: +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks +### Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[coc@electronjs.org](mailto:coc@electronjs.org). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. -## Our Responsibilities +#### 3. Temporary Ban -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. -## Scope +#### 4. Permanent Ban -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. -## Enforcement +**Consequence**: A permanent ban from any sort of public interaction within +the community. -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [coc@electronjs.org](mailto:coc@electronjs.org). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +### Attribution -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. -## Attribution +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). -This Code of Conduct is adapted from the [Contributor-Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] +[homepage]: https://www.contributor-covenant.org -[homepage]: https://contributor-covenant.org -[version]: https://contributor-covenant.org/version/1/4/ +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 654b3a65426e8..5c1223308ee58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,25 +20,23 @@ Issues are created [here](https://github.com/electron/electron/issues/new). * [Triaging a Bug Report](https://electronjs.org/docs/development/issues#triaging-a-bug-report) * [Resolving a Bug Report](https://electronjs.org/docs/development/issues#resolving-a-bug-report) -### Issue Maintenance and Closure -* If an issue is inactive for 45 days (no activity of any kind), it will be -marked for closure with `stale`. -* If after this label is applied, no further activity occurs in the next 7 days, -the issue will be closed. - * If an issue has been closed and you still feel it's relevant, feel free to - ping a maintainer or add a comment! +### Issue Closure + +Bug reports will be closed if the issue has been inactive and the latest affected version no longer receives support. At the moment, Electron maintains its three latest major versions, with a new major version being released every 8 weeks. (For more information on Electron's release cadence, see [this blog post](https://electronjs.org/blog/8-week-cadence).) + +_If an issue has been closed and you still feel it's relevant, feel free to ping a maintainer or add a comment!_ ### Languages We accept issues in *any* language. -When an issue is posted in a language besides English, it is acceptable and encouraged to post an English-translated copy as a reply. +When an issue is posted in a language besides English, it is acceptable and encouraged to post an English-translated copy as a reply. Anyone may post the translated reply. In most cases, a quick pass through translation software is sufficient. Having the original text _as well as_ the translation can help mitigate translation errors. Responses to posted issues may or may not be in the original language. -**Please note** that using non-English as an attempt to circumvent our [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) will be an immediate, and possibly indefinite, ban from the project. +**Please note** that using non-English as an attempt to circumvent our [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) will be an immediate, and possibly indefinite, ban from the project. ## [Pull Requests](https://electronjs.org/docs/development/pull-requests) @@ -49,17 +47,17 @@ dependencies, and tools contained in the `electron/electron` repository. * [Step 1: Fork](https://electronjs.org/docs/development/pull-requests#step-1-fork) * [Step 2: Build](https://electronjs.org/docs/development/pull-requests#step-2-build) * [Step 3: Branch](https://electronjs.org/docs/development/pull-requests#step-3-branch) -* [The Process of Making Changes](https://electronjs.org/docs/development/pull-requests#the-process-of-making-changes) +* [Making Changes](https://electronjs.org/docs/development/pull-requests#making-changes) * [Step 4: Code](https://electronjs.org/docs/development/pull-requests#step-4-code) * [Step 5: Commit](https://electronjs.org/docs/development/pull-requests#step-5-commit) * [Commit message guidelines](https://electronjs.org/docs/development/pull-requests#commit-message-guidelines) * [Step 6: Rebase](https://electronjs.org/docs/development/pull-requests#step-6-rebase) * [Step 7: Test](https://electronjs.org/docs/development/pull-requests#step-7-test) * [Step 8: Push](https://electronjs.org/docs/development/pull-requests#step-8-push) - * [Step 8: Opening the Pull Request](https://electronjs.org/docs/development/pull-requests#step-8-opening-the-pull-request) - * [Step 9: Discuss and Update](#step-9-discuss-and-update) + * [Step 9: Opening the Pull Request](https://electronjs.org/docs/development/pull-requests#step-9-opening-the-pull-request) + * [Step 10: Discuss and Update](https://electronjs.org/docs/development/pull-requests#step-10-discuss-and-update) * [Approval and Request Changes Workflow](https://electronjs.org/docs/development/pull-requests#approval-and-request-changes-workflow) - * [Step 10: Landing](https://electronjs.org/docs/development/pull-requests#step-10-landing) + * [Step 11: Landing](https://electronjs.org/docs/development/pull-requests#step-11-landing) * [Continuous Integration Testing](https://electronjs.org/docs/development/pull-requests#continuous-integration-testing) ## Style Guides @@ -68,5 +66,5 @@ See [Coding Style](https://electronjs.org/docs/development/coding-style) for inf ## Further Reading -For more in-depth guides on developing Electron, see +For more in-depth guides on developing Electron, see [/docs/development](/docs/development/README.md) diff --git a/DEPS b/DEPS index a4d6cb4805891..905119f7deae3 100644 --- a/DEPS +++ b/DEPS @@ -5,27 +5,34 @@ gclient_gn_args = [ 'checkout_android_native_support', 'checkout_libaom', 'checkout_nacl', - 'checkout_oculus_sdk' + 'checkout_pgo_profiles', + 'checkout_oculus_sdk', + 'checkout_openxr', + 'checkout_google_benchmark', + 'mac_xcode_version', + 'generate_location_tags', ] vars = { 'chromium_version': - '9eecb7a9f652bbf84f6437b49c70922b65b38bf3', + '100.0.4896.160', 'node_version': - 'v12.6.0', + 'v16.13.2', 'nan_version': - '2ee313aaca52e2b478965ac50eb5082520380d1b', + # The following commit hash of NAN is v2.14.2 with *only* changes to the + # test suite. This should be updated to a specific tag when one becomes + # available. + '65b32af46e9d7fab2e4ff657751205b3865f4920', + 'squirrel.mac_version': + '0e5d146ba13101a1302d59ea6e6e0b3cace4ae38', - 'boto_version': 'f7574aa6cc2c819430c1f05e9a1a1a666ef8169b', 'pyyaml_version': '3.12', - 'requests_version': 'e4d59bedfd3c7f4f254f4f5d036587bcd8152458', - 'boto_git': 'https://github.com/boto', 'chromium_git': 'https://chromium.googlesource.com', 'electron_git': 'https://github.com/electron', 'nodejs_git': 'https://github.com/nodejs', - 'requests_git': 'https://github.com/kennethreitz', 'yaml_git': 'https://github.com/yaml', + 'squirrel_git': 'https://github.com/Squirrel', # KEEP IN SYNC WITH utils.js FILE 'yarn_version': '1.15.2', @@ -33,39 +40,45 @@ vars = { # To be able to build clean Chromium from sources. 'apply_patches': True, - # Python interface to Amazon Web Services. Is used for releases only. - 'checkout_boto': False, + # To use an mtime cache for patched files to speed up builds. + 'use_mtime_cache': True, # To allow in-house builds to checkout those manually. 'checkout_chromium': True, 'checkout_node': True, 'checkout_nan': True, + 'checkout_pgo_profiles': True, # It's only needed to parse the native tests configurations. 'checkout_pyyaml': False, - # Python "requests" module is used for releases only. - 'checkout_requests': False, + 'use_rts': False, + + 'mac_xcode_version': 'default', + + 'generate_location_tags': False, # To allow running hooks without parsing the DEPS tree 'process_deps': True, - # It is always needed for normal Electron builds, - # but might be impossible for custom in-house builds. - 'download_external_binaries': True, - 'checkout_nacl': False, 'checkout_libaom': True, 'checkout_oculus_sdk': False, + 'checkout_openxr': + False, 'build_with_chromium': True, 'checkout_android': False, 'checkout_android_native_support': False, + 'checkout_google_benchmark': + False, + 'checkout_clang_tidy': + True, } deps = { @@ -81,71 +94,76 @@ deps = { 'url': (Var("nodejs_git")) + '/node.git@' + (Var("node_version")), 'condition': 'checkout_node and process_deps', }, - 'src/electron/vendor/pyyaml': { + 'src/third_party/pyyaml': { 'url': (Var("yaml_git")) + '/pyyaml.git@' + (Var("pyyaml_version")), 'condition': 'checkout_pyyaml and process_deps', }, - 'src/electron/vendor/boto': { - 'url': Var('boto_git') + '/boto.git' + '@' + Var('boto_version'), - 'condition': 'checkout_boto and process_deps', + 'src/third_party/squirrel.mac': { + 'url': Var("squirrel_git") + '/Squirrel.Mac.git@' + Var("squirrel.mac_version"), + 'condition': 'process_deps', }, - 'src/electron/vendor/requests': { - 'url': Var('requests_git') + '/requests.git' + '@' + Var('requests_version'), - 'condition': 'checkout_requests and process_deps', + 'src/third_party/squirrel.mac/vendor/ReactiveObjC': { + 'url': 'https://github.com/ReactiveCocoa/ReactiveObjC.git@74ab5baccc6f7202c8ac69a8d1e152c29dc1ea76', + 'condition': 'process_deps' }, + 'src/third_party/squirrel.mac/vendor/Mantle': { + 'url': 'https://github.com/Mantle/Mantle.git@78d3966b3c331292ea29ec38661b25df0a245948', + 'condition': 'process_deps', + } } +pre_deps_hooks = [ + { + 'name': 'generate_mtime_cache', + 'condition': '(checkout_chromium and apply_patches and use_mtime_cache) and process_deps', + 'pattern': 'src/electron', + 'action': [ + 'python3', + 'src/electron/script/patches-mtime-cache.py', + 'generate', + '--cache-file', + 'src/electron/patches/mtime-cache.json', + '--patches-config', + 'src/electron/patches/config.json', + ], + }, +] + hooks = [ { 'name': 'patch_chromium', 'condition': '(checkout_chromium and apply_patches) and process_deps', 'pattern': 'src/electron', 'action': [ - 'python', + 'python3', 'src/electron/script/apply_all_patches.py', 'src/electron/patches/config.json', ], }, { - 'name': 'electron_external_binaries', - 'pattern': 'src/electron/script/update-external-binaries.py', - 'condition': 'download_external_binaries', + 'name': 'apply_mtime_cache', + 'condition': '(checkout_chromium and apply_patches and use_mtime_cache) and process_deps', + 'pattern': 'src/electron', 'action': [ - 'python', - 'src/electron/script/update-external-binaries.py', + 'python3', + 'src/electron/script/patches-mtime-cache.py', + 'apply', + '--cache-file', + 'src/electron/patches/mtime-cache.json', ], }, { 'name': 'electron_npm_deps', 'pattern': 'src/electron/package.json', 'action': [ - 'python', - '-c', - 'import os, subprocess; os.chdir(os.path.join("src", "electron")); subprocess.check_call(["python", "script/lib/npx.py", "yarn@' + (Var("yarn_version")) + '", "install", "--frozen-lockfile"]);', - ], - }, - { - 'name': 'setup_boto', - 'pattern': 'src/electron', - 'condition': 'checkout_boto and process_deps', - 'action': [ - 'python', - '-c', - 'import os, subprocess; os.chdir(os.path.join("src", "electron", "vendor", "boto")); subprocess.check_call(["python", "setup.py", "build"]);', - ], - }, - { - 'name': 'setup_requests', - 'pattern': 'src/electron', - 'condition': 'checkout_requests and process_deps', - 'action': [ - 'python', + 'python3', '-c', - 'import os, subprocess; os.chdir(os.path.join("src", "electron", "vendor", "requests")); subprocess.check_call(["python", "setup.py", "build"]);', + 'import os, subprocess; os.chdir(os.path.join("src", "electron")); subprocess.check_call(["python3", "script/lib/npx.py", "yarn@' + (Var("yarn_version")) + '", "install", "--frozen-lockfile"]);', ], }, ] recursedeps = [ 'src', + 'src/third_party/squirrel.mac', ] diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 31b0633e0c4af..0000000000000 --- a/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM ubuntu:18.04 - -RUN groupadd --gid 1000 builduser \ - && useradd --uid 1000 --gid builduser --shell /bin/bash --create-home builduser - -# Set up TEMP directory -ENV TEMP=/tmp -RUN chmod a+rwx /tmp - -# Install Linux packages -ADD build/install-build-deps.sh /setup/install-build-deps.sh -RUN echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections -RUN dpkg --add-architecture i386 -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - curl \ - libnotify-bin \ - locales \ - lsb-release \ - nano \ - python-dbus \ - python-pip \ - python-setuptools \ - sudo \ - vim-nox \ - wget \ - g++-multilib \ - libgl1:i386 \ - && /setup/install-build-deps.sh --syms --no-prompt --no-chromeos-fonts --lib32 --arm \ - && rm -rf /var/lib/apt/lists/* - -# Install Node.js -RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* - -# crcmod is required by gsutil, which is used for filling the gclient git cache -RUN pip install -U crcmod - -# dbusmock is needed for Electron tests -RUN pip install python-dbusmock - -RUN mkdir /tmp/workspace -RUN chown builduser:builduser /tmp/workspace - -# Add xvfb init script -ADD tools/xvfb-init.sh /etc/init.d/xvfb -RUN chmod a+x /etc/init.d/xvfb - -USER builduser -WORKDIR /home/builduser diff --git a/Dockerfile.arm32v7 b/Dockerfile.arm32v7 deleted file mode 100644 index 8dbae81b97ca1..0000000000000 --- a/Dockerfile.arm32v7 +++ /dev/null @@ -1,59 +0,0 @@ -FROM arm32v7/ubuntu:18.04 - -RUN groupadd --gid 1000 builduser \ - && useradd --uid 1000 --gid builduser --shell /bin/bash --create-home builduser - -# Set up TEMP directory -ENV TEMP=/tmp -RUN chmod a+rwx /tmp - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - bison \ - build-essential \ - clang \ - curl \ - gperf \ - git \ - libasound2 \ - libasound2-dev \ - libcap-dev \ - libcups2-dev \ - libdbus-1-dev \ - libgnome-keyring-dev \ - libgtk2.0-0 \ - libgtk2.0-dev \ - libgtk-3-0 \ - libgtk-3-dev \ - libnotify-bin \ - libnss3 \ - libnss3-dev \ - libxss1 \ - libxtst-dev \ - libxtst6 \ - lsb-release \ - locales \ - nano \ - python-setuptools \ - python-pip \ - python-dbusmock \ - sudo \ - unzip \ - wget \ - xvfb \ -&& rm -rf /var/lib/apt/lists/* - -# Install Node.js -RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* - -# crcmod is required by gsutil, which is used for filling the gclient git cache -RUN pip install -U crcmod - -ADD tools/xvfb-init.sh /etc/init.d/xvfb -RUN chmod a+x /etc/init.d/xvfb - -RUN usermod -aG sudo builduser -RUN echo 'builduser ALL=(ALL:ALL) NOPASSWD:ALL' >> /etc/sudoers - -WORKDIR /home/builduser diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 deleted file mode 100644 index a627497f4c275..0000000000000 --- a/Dockerfile.arm64v8 +++ /dev/null @@ -1,65 +0,0 @@ -FROM arm64v8/ubuntu:18.04 - -RUN groupadd --gid 1000 builduser \ - && useradd --uid 1000 --gid builduser --shell /bin/bash --create-home builduser - -# Set up TEMP directory -ENV TEMP=/tmp -RUN chmod a+rwx /tmp - -RUN dpkg --add-architecture armhf - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - bison \ - build-essential \ - clang \ - curl \ - gperf \ - git \ - libasound2 \ - libasound2-dev \ - libc6:armhf \ - libcap-dev \ - libcups2-dev \ - libdbus-1-dev \ - libgnome-keyring-dev \ - libgtk2.0-0 \ - libgtk2.0-dev \ - libgtk-3-0 \ - libgtk-3-dev \ - libnotify-bin \ - libnss3 \ - libnss3-dev \ - libstdc++6:armhf \ - libxss1 \ - libxtst-dev \ - libxtst6 \ - lsb-release \ - locales \ - nano \ - python-setuptools \ - python-pip \ - sudo \ - unzip \ - wget \ - xvfb \ -&& rm -rf /var/lib/apt/lists/* - -# Install Node.js -RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* - -# crcmod is required by gsutil, which is used for filling the gclient git cache -RUN pip install -U crcmod - -# dbusmock is needed for Electron tests -RUN pip install python-dbusmock - -ADD tools/xvfb-init.sh /etc/init.d/xvfb -RUN chmod a+x /etc/init.d/xvfb - -RUN usermod -aG sudo builduser -RUN echo 'builduser ALL=(ALL:ALL) NOPASSWD:ALL' >> /etc/sudoers - -WORKDIR /home/builduser diff --git a/ELECTRON_VERSION b/ELECTRON_VERSION index 0315b18c84c56..4d5fa631d4df0 100644 --- a/ELECTRON_VERSION +++ b/ELECTRON_VERSION @@ -1 +1 @@ -7.0.0-nightly.20190731 \ No newline at end of file +18.3.7 \ No newline at end of file diff --git a/LICENSE b/LICENSE index 54ff135e7920e..536d54efc3fd3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2013-2019 GitHub Inc. +Copyright (c) Electron contributors +Copyright (c) 2013-2020 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index ae7c008433c5d..f0af41e5940b3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ [![Electron Logo](https://electronjs.org/images/electron-logo.svg)](https://electronjs.org) +[![CircleCI Build Status](https://circleci.com/gh/electron/electron/tree/main.svg?style=shield)](https://circleci.com/gh/electron/electron/tree/main) +[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/4lggi9dpjc1qob7k/branch/main?svg=true)](https://ci.appveyor.com/project/electron-bot/electron-ljo26/branch/main) +[![Electron Discord Invite](https://img.shields.io/discord/745037351163527189?color=%237289DA&label=chat&logo=discord&logoColor=white)](https://discord.com/invite/APGC3k5yaH) -[![CircleCI Build Status](https://circleci.com/gh/electron/electron/tree/master.svg?style=shield)](https://circleci.com/gh/electron/electron/tree/master) -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/4lggi9dpjc1qob7k/branch/master?svg=true)](https://ci.appveyor.com/project/electron-bot/electron-ljo26/branch/master) -[![devDependency Status](https://david-dm.org/electron/electron/dev-status.svg)](https://david-dm.org/electron/electron?type=dev) - -:memo: Available Translations: 🇨🇳 🇹🇼 🇧🇷 🇪🇸 🇰🇷 🇯🇵 🇷🇺 🇫🇷 🇹🇭 🇳🇱 🇹🇷 🇮🇩 🇺🇦 🇨🇿 🇮🇹 🇵🇱. +:memo: Available Translations: 🇨🇳 🇧🇷 🇪🇸 🇯🇵 🇷🇺 🇫🇷 🇺🇸 🇩🇪. View these docs in other languages at [electron/i18n](https://github.com/electron/i18n/tree/master/content/). The Electron framework lets you write cross-platform desktop applications @@ -17,7 +16,7 @@ Follow [@ElectronJS](https://twitter.com/electronjs) on Twitter for important announcements. This project adheres to the Contributor Covenant -[code of conduct](https://github.com/electron/electron/tree/master/CODE_OF_CONDUCT.md). +[code of conduct](https://github.com/electron/electron/tree/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [coc@electronjs.org](mailto:coc@electronjs.org). @@ -28,15 +27,12 @@ The preferred method is to install Electron as a development dependency in your app: ```sh -npm install electron --save-dev [--save-exact] +npm install electron --save-dev ``` -The `--save-exact` flag is recommended for Electron prior to version 2, as it does not follow semantic -versioning. As of version 2.0.0, Electron follows semver, so you don't need `--save-exact` flag. For info on how to manage Electron versions in your apps, see -[Electron versioning](docs/tutorial/electron-versioning.md). - For more installation options and troubleshooting tips, see -[installation](docs/tutorial/installation.md). +[installation](docs/tutorial/installation.md). For info on how to manage Electron versions in your apps, see +[Electron versioning](docs/tutorial/electron-versioning.md). ## Quick start & Electron Fiddle @@ -58,13 +54,12 @@ npm start ## Resources for learning Electron -- [electronjs.org/docs](https://electronjs.org/docs) - all of Electron's documentation +- [electronjs.org/docs](https://electronjs.org/docs) - All of Electron's documentation - [electron/fiddle](https://github.com/electron/fiddle) - A tool to build, run, and package small Electron experiments -- [electron/electron-quick-start](https://github.com/electron/electron-quick-start) - a very basic starter Electron app -- [electronjs.org/community#boilerplates](https://electronjs.org/community#boilerplates) - sample starter apps created by the community -- [electron/simple-samples](https://github.com/electron/simple-samples) - small applications with ideas for taking them further -- [electron/electron-api-demos](https://github.com/electron/electron-api-demos) - an Electron app that teaches you how to use Electron -- [hokein/electron-sample-apps](https://github.com/hokein/electron-sample-apps) - small demo apps for the various Electron APIs +- [electron/electron-quick-start](https://github.com/electron/electron-quick-start) - A very basic starter Electron app +- [electronjs.org/community#boilerplates](https://electronjs.org/community#boilerplates) - Sample starter apps created by the community +- [electron/simple-samples](https://github.com/electron/simple-samples) - Small applications with ideas for taking them further +- [electron/electron-api-demos](https://github.com/electron/electron-api-demos) - An Electron app that teaches you how to use Electron ## Programmatic usage @@ -85,7 +80,7 @@ const child = proc.spawn(electron) ### Mirrors -- [China](https://npm.taobao.org/mirrors/electron) +- [China](https://npmmirror.com/mirrors/electron) ## Documentation Translations @@ -102,6 +97,6 @@ and more can be found in the [support document](docs/tutorial/support.md#finding ## License -[MIT](https://github.com/electron/electron/blob/master/LICENSE) +[MIT](https://github.com/electron/electron/blob/main/LICENSE) When using the Electron or other GitHub logos, be sure to follow the [GitHub logo guidelines](https://github.com/logos). diff --git a/SECURITY.md b/SECURITY.md index c113ff00e06f0..43129ea52c03e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,12 @@ To report a security issue, email [security@electronjs.org](mailto:security@elec The Electron team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. -Report security bugs in third-party modules to the person or team maintaining the module. You can also report a vulnerability through the [Node Security Project](https://nodesecurity.io/report). +Report security bugs in third-party modules to the person or team maintaining the module. You can also report a vulnerability through the [npm contact form](https://www.npmjs.com/support) by selecting "I'm reporting a security vulnerability". + +## The Electron Security Notification Process + +For context on Electron's security notification process, please see the [Notifications](https://github.com/electron/governance/blob/main/wg-security/membership-and-notifications.md#notifications) section of the Security WG's [Membership and Notifications](https://github.com/electron/governance/blob/main/wg-security/membership-and-notifications.md) Governance document. ## Learning More About Security + To learn more about securing an Electron application, please see the [security tutorial](docs/tutorial/security.md). diff --git a/appveyor.yml b/appveyor.yml index a0821045cd2c3..072e9e8bfa3b6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ # The config expects the following environment variables to be set: -# - "GN_CONFIG" Build type. One of {'debug', 'testing', 'release'}. +# - "GN_CONFIG" Build type. One of {'testing', 'release'}. # - "GN_EXTRA_ARGS" Additional gn arguments for a build config, # e.g. 'target_cpu="x86"' to build for a 32bit platform. # https://gn.googlesource.com/gn/+/master/docs/reference.md#target_cpu @@ -11,7 +11,7 @@ # - "TARGET_ARCH" Choose from {'ia32', 'x64', 'arm', 'arm64', 'mips64el'}. # Is used in some publishing scripts, but does NOT affect the Electron binary. # Must match 'target_cpu' passed to "GN_EXTRA_ARGS" and "NPM_CONFIG_ARCH" value. -# - "UPLOAD_TO_S3" Set it to '1' upload a release to the S3 bucket. +# - "UPLOAD_TO_STORAGE" Set it to '1' upload a release to the Azure bucket. # Otherwise the release will be uploaded to the Github Releases. # (The value is only checked if "ELECTRON_RELEASE" is defined.) # @@ -28,40 +28,63 @@ # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) version: 1.0.{build} -build_cloud: libcc-20 -image: vs2017-15.9-10.0.18362 +build_cloud: electron-16-core +image: vs2019bt-16.6.2 environment: GIT_CACHE_PATH: C:\Users\electron\libcc_cache ELECTRON_OUT_DIR: Default ELECTRON_ENABLE_STACK_DUMPING: 1 + ELECTRON_ALSO_LOG_TO_STDERR: 1 MOCHA_REPORTER: mocha-multi-reporters MOCHA_MULTI_REPORTERS: mocha-appveyor-reporter, tap -notifications: - - provider: Webhook - url: https://electron-mission-control.herokuapp.com/rest/appveyor-hook - method: POST - headers: - x-mission-control-secret: - secure: 90BLVPcqhJPG7d24v0q/RRray6W3wDQ8uVQlQjOHaBWkw1i8FoA1lsjr2C/v1dVok+tS2Pi6KxDctPUkwIb4T27u4RhvmcPzQhVpfwVJAG9oNtq+yKN7vzHfg7k/pojEzVdJpQLzeJGcSrZu7VY39Q== - on_build_success: false - on_build_failure: true - on_build_status_changed: false + GOMA_FALLBACK_ON_AUTH_FAILURE: true build_script: - ps: >- if(($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME -split "/")[0] -eq ($env:APPVEYOR_REPO_NAME -split "/")[0]) { Write-warning "Skipping PR build for branch"; Exit-AppveyorBuild + } else { + node script/yarn.js install --frozen-lockfile + + $result = node script/doc-only-change.js --prNumber=$env:APPVEYOR_PULL_REQUEST_NUMBER --prBranch=$env:APPVEYOR_REPO_BRANCH + Write-Output $result + if ($result.ExitCode -eq 0) { + Write-warning "Skipping build for doc only change"; Exit-AppveyorBuild + } } - echo "Building $env:GN_CONFIG build" - git config --global core.longpaths true - cd .. - mkdir src + - update_depot_tools.bat - ps: Move-Item $env:APPVEYOR_BUILD_FOLDER -Destination src\electron + - ps: >- + if (Test-Path 'env:RAW_GOMA_AUTH') { + $env:GOMA_OAUTH2_CONFIG_FILE = "$pwd\.goma_oauth2_config" + $env:RAW_GOMA_AUTH | Set-Content $env:GOMA_OAUTH2_CONFIG_FILE + } + - git clone https://github.com/electron/build-tools.git + - cd build-tools + - npm install + - mkdir third_party + - ps: >- + node -e "require('./src/utils/goma.js').downloadAndPrepare({ gomaOneForAll: true })" + - ps: $env:GN_GOMA_FILE = node -e "console.log(require('./src/utils/goma.js').gnFilePath)" + - ps: $env:LOCAL_GOMA_DIR = node -e "console.log(require('./src/utils/goma.js').dir)" + - cd .. + - ps: .\src\electron\script\start-goma.ps1 -gomaDir $env:LOCAL_GOMA_DIR + - ps: >- + if (Test-Path 'env:RAW_GOMA_AUTH') { + $goma_login = python $env:LOCAL_GOMA_DIR\goma_auth.py info + if ($goma_login -eq 'Login as Fermi Planck') { + Write-warning "Goma authentication is correct"; + } else { + Write-warning "WARNING!!!!!! Goma authentication is incorrect; please update Goma auth token."; + $host.SetShouldExit(1) + } + } - ps: $env:CHROMIUM_BUILDTOOLS_PATH="$pwd\src\buildtools" - - ps: $env:SCCACHE_PATH="$pwd\src\electron\external_binaries\sccache.exe" - ps: >- - if ($env:GN_CONFIG -eq 'release') { - $env:GCLIENT_EXTRA_ARGS="--custom-var=checkout_boto=True --custom-var=checkout_requests=True" - } else { + if ($env:GN_CONFIG -ne 'release') { $env:NINJA_STATUS="[%r processes, %f/%t @ %o/s : %es] " } - >- @@ -70,25 +93,90 @@ build_script: --unmanaged %GCLIENT_EXTRA_ARGS% "https://github.com/electron/electron" - - gclient sync --with_branch_heads --with_tags --reset + - ps: >- + if ($env:GN_CONFIG -eq 'release') { + $env:RUN_GCLIENT_SYNC="true" + } else { + cd src\electron + node script\generate-deps-hash.js + $depshash = Get-Content .\.depshash -Raw + $zipfile = "Z:\$depshash.7z" + cd ..\.. + if (Test-Path -Path $zipfile) { + # file exists, unzip and then gclient sync + 7z x -y $zipfile -mmt=30 -aoa + if (-not (Test-Path -Path "src\buildtools")) { + # the zip file must be corrupt - resync + $env:RUN_GCLIENT_SYNC="true" + if ($env:TARGET_ARCH -ne 'ia32') { + # only save on x64/woa to avoid contention saving + $env:SAVE_GCLIENT_SRC="true" + } + } else { + # update angle + cd src\third_party\angle + git remote set-url origin https://chromium.googlesource.com/angle/angle.git + git fetch + cd ..\..\.. + } + } else { + # file does not exist, gclient sync, then zip + $env:RUN_GCLIENT_SYNC="true" + if ($env:TARGET_ARCH -ne 'ia32') { + # only save on x64/woa to avoid contention saving + $env:SAVE_GCLIENT_SRC="true" + } + } + } + - if "%RUN_GCLIENT_SYNC%"=="true" ( gclient sync ) + - ps: >- + if ($env:SAVE_GCLIENT_SRC -eq 'true') { + # archive current source for future use + # only run on x64/woa to avoid contention saving + $(7z a $zipfile src -xr!android_webview -xr!electron -xr'!*\.git' -xr!third_party\WebKit\LayoutTests! -xr!third_party\blink\web_tests -xr!third_party\blink\perf_tests -slp -t7z -mmt=30) + if ($LASTEXITCODE -ne 0) { + Write-warning "Could not save source to shared drive; continuing anyway" + } + # build time generation of file gen/angle/angle_commit.h depends on + # third_party/angle/.git + # https://chromium-review.googlesource.com/c/angle/angle/+/2074924 + $(7z a $zipfile src\third_party\angle\.git) + if ($LASTEXITCODE -ne 0) { + Write-warning "Failed to add third_party\angle\.git; continuing anyway" + } + } - cd src - - ps: $env:BUILD_CONFIG_PATH="//electron/build/args/%GN_CONFIG%.gn" - - gn gen out/Default "--args=import(\"%BUILD_CONFIG_PATH%\") %GN_EXTRA_ARGS%" + - set BUILD_CONFIG_PATH=//electron/build/args/%GN_CONFIG%.gn + - gn gen out/Default "--args=import(\"%BUILD_CONFIG_PATH%\") import(\"%GN_GOMA_FILE%\") %GN_EXTRA_ARGS% " - gn check out/Default //electron:electron_lib - gn check out/Default //electron:electron_app - - gn check out/Default //electron:manifests - gn check out/Default //electron/shell/common/api:mojo - - ninja -C out/Default electron:electron_app - - if "%GN_CONFIG%"=="testing" ( python C:\Users\electron\depot_tools\post_build_ninja_summary.py -C out\Default ) + - if DEFINED GN_GOMA_FILE (ninja -j 300 -C out/Default electron:electron_app) else (ninja -C out/Default electron:electron_app) + - if "%GN_CONFIG%"=="testing" ( python C:\depot_tools\post_build_ninja_summary.py -C out\Default ) - gn gen out/ffmpeg "--args=import(\"//electron/build/args/ffmpeg.gn\") %GN_EXTRA_ARGS%" - ninja -C out/ffmpeg electron:electron_ffmpeg_zip - ninja -C out/Default electron:electron_dist_zip + - ninja -C out/Default shell_browser_ui_unittests + - gn desc out/Default v8:run_mksnapshot_default args > out/Default/mksnapshot_args - ninja -C out/Default electron:electron_mksnapshot_zip + - cd out\Default + - 7z a mksnapshot.zip mksnapshot_args gen\v8\embedded.S + - cd ..\.. + - ninja -C out/Default electron:hunspell_dictionaries_zip - ninja -C out/Default electron:electron_chromedriver_zip - ninja -C out/Default third_party/electron_node:headers + - python %LOCAL_GOMA_DIR%\goma_ctl.py stat + - python electron/build/profile_toolchain.py --output-json=out/Default/windows_toolchain_profile.json + - appveyor PushArtifact out/Default/windows_toolchain_profile.json - appveyor PushArtifact out/Default/dist.zip + - appveyor PushArtifact out/Default/shell_browser_ui_unittests.exe - appveyor PushArtifact out/Default/chromedriver.zip - appveyor PushArtifact out/ffmpeg/ffmpeg.zip + - 7z a node_headers.zip out\Default\gen\node_headers + - appveyor PushArtifact node_headers.zip + - appveyor PushArtifact out/Default/mksnapshot.zip + - appveyor PushArtifact out/Default/hunspell_dictionaries.zip + - appveyor PushArtifact out/Default/electron.lib - ps: >- if ($env:GN_CONFIG -eq 'release') { # Needed for msdia140.dll on 64-bit windows @@ -98,14 +186,19 @@ build_script: - ps: >- if ($env:GN_CONFIG -eq 'release') { python electron\script\zip-symbols.py - appveyor PushArtifact out/Default/symbols.zip + appveyor-retry appveyor PushArtifact out/Default/symbols.zip + } else { + # It's useful to have pdb files when debugging testing builds that are + # built on CI. + 7z a pdb.zip out\Default\*.pdb + appveyor-retry appveyor PushArtifact pdb.zip } - python electron/script/zip_manifests/check-zip-manifest.py out/Default/dist.zip electron/script/zip_manifests/dist_zip.win.%TARGET_ARCH%.manifest test_script: # Workaround for https://github.com/appveyor/ci/issues/2420 - set "PATH=%PATH%;C:\Program Files\Git\mingw64\libexec\git-core" - ps: >- - if ((-Not (Test-Path Env:\ELECTRON_RELEASE)) -And ($env:GN_CONFIG -in "testing", "release")) { + if ((-Not (Test-Path Env:\TEST_WOA)) -And (-Not (Test-Path Env:\ELECTRON_RELEASE)) -And ($env:GN_CONFIG -in "testing", "release")) { $env:RUN_TESTS="true" } - ps: >- @@ -116,21 +209,31 @@ test_script: echo "Skipping tests for $env:GN_CONFIG build" } - cd electron - - if "%RUN_TESTS%"=="true" ( echo Running test suite & node script/yarn test -- --ci --enable-logging) + # CalculateNativeWinOcclusion is disabled due to https://bugs.chromium.org/p/chromium/issues/detail?id=1139022 + - if "%RUN_TESTS%"=="true" ( echo Running main test suite & node script/yarn test -- --trace-uncaught --runners=main --enable-logging=file --log-file=%cd%\electron.log --disable-features=CalculateNativeWinOcclusion ) + - if "%RUN_TESTS%"=="true" ( echo Running remote test suite & node script/yarn test -- --trace-uncaught --runners=remote --runTestFilesSeperately --enable-logging=file --log-file=%cd%\electron.log --disable-features=CalculateNativeWinOcclusion ) + - if "%RUN_TESTS%"=="true" ( echo Running native test suite & node script/yarn test -- --trace-uncaught --runners=native --enable-logging=file --log-file=%cd%\electron.log --disable-features=CalculateNativeWinOcclusion ) - cd .. - if "%RUN_TESTS%"=="true" ( echo Verifying non proprietary ffmpeg & python electron\script\verify-ffmpeg.py --build-dir out\Default --source-root %cd% --ffmpeg-path out\ffmpeg ) - echo "About to verify mksnapshot" - if "%RUN_TESTS%"=="true" ( echo Verifying mksnapshot & python electron\script\verify-mksnapshot.py --build-dir out\Default --source-root %cd% ) - echo "Done verifying mksnapshot" + - if "%RUN_TESTS%"=="true" ( echo Verifying chromedriver & python electron\script\verify-chromedriver.py --build-dir out\Default --source-root %cd% ) + - echo "Done verifying chromedriver" + - if exist %cd%\electron.log ( appveyor-retry appveyor PushArtifact %cd%\electron.log ) deploy_script: - cd electron - ps: >- if (Test-Path Env:\ELECTRON_RELEASE) { - if (Test-Path Env:\UPLOAD_TO_S3) { - Write-Output "Uploading Electron release distribution to s3" - & python script\release\uploaders\upload.py --upload_to_s3 + if (Test-Path Env:\UPLOAD_TO_STORAGE) { + Write-Output "Uploading Electron release distribution to azure" + & python script\release\uploaders\upload.py --verbose --upload_to_storage } else { Write-Output "Uploading Electron release distribution to github releases" - & python script\release\uploaders\upload.py + & python script\release\uploaders\upload.py --verbose } + } elseif (Test-Path Env:\TEST_WOA) { + node script/release/ci-release-build.js --job=electron-woa-testing --ci=GHA --appveyorJobId=$env:APPVEYOR_JOB_ID $env:APPVEYOR_REPO_BRANCH } +on_finish: + - if exist src\electron\electron.log ( appveyor-retry appveyor PushArtifact src\electron\electron.log ) diff --git a/build/args/all.gn b/build/args/all.gn index ad391b658cc9c..0d3ef6beedbfc 100644 --- a/build/args/all.gn +++ b/build/args/all.gn @@ -1,26 +1,43 @@ is_electron_build = true -use_jumbo_build = true root_extra_deps = [ "//electron" ] # Registry of NMVs --> https://github.com/nodejs/node/blob/master/doc/abi_version_registry.json -node_module_version = 75 +node_module_version = 103 v8_promise_internal_field_count = 1 -v8_typed_array_max_size_in_heap = 0 v8_embedder_string = "-electron.0" # TODO: this breaks mksnapshot v8_enable_snapshot_native_code_counters = false +# TODO(codebytere): remove when Node.js handles https://chromium-review.googlesource.com/c/v8/v8/+/3211575 +v8_scriptormodule_legacy_lifetime = true + +# we use this api +v8_enable_javascript_promise_hooks = true + enable_cdm_host_verification = false proprietary_codecs = true ffmpeg_branding = "Chrome" enable_basic_printing = true + +# Removes DLLs from the build, which are only meant to be used for Chromium development. +# See https://github.com/electron/electron/pull/17985 angle_enable_vulkan_validation_layers = false +dawn_enable_vulkan_validation_layers = false + +# This breaks native node modules +libcxx_abi_unstable = false + +# These are disabled because they cause the zip manifest to differ between +# testing and release builds. +# See https://chromium-review.googlesource.com/c/chromium/src/+/2774898. +enable_pseudolocales = false is_cfi = false -# TODO: Remove this and update CI to contain 10.14 SDK once -# crbug.com/986701 is fixed. -mac_sdk_min = "10.13" +# Make application name configurable at runtime for cookie crypto +allow_runtime_configurable_key_storage = true + +enable_cet_shadow_stack = false diff --git a/build/args/debug.gn b/build/args/debug.gn deleted file mode 100644 index 1eac72207926c..0000000000000 --- a/build/args/debug.gn +++ /dev/null @@ -1,10 +0,0 @@ -import("all.gn") -is_debug = true -is_component_build = true - -# This may be guarded behind is_chrome_branded alongside -# proprietary_codecs https://webrtc-review.googlesource.com/c/src/+/36321, -# explicitly override here to build OpenH264 encoder/FFmpeg decoder. -# The initialization of the decoder depends on whether ffmpeg has -# been built with H.264 support. -rtc_use_h264 = proprietary_codecs diff --git a/build/args/native_tests.gn b/build/args/native_tests.gn index 2559bbb3859db..416b9556cc19d 100644 --- a/build/args/native_tests.gn +++ b/build/args/native_tests.gn @@ -5,4 +5,3 @@ is_debug = false is_component_build = false is_component_ffmpeg = false symbol_level = 1 -use_jumbo_build = true diff --git a/build/asar.gni b/build/asar.gni index 4df8ea34dd9ba..3e11845bd59bb 100644 --- a/build/asar.gni +++ b/build/asar.gni @@ -57,4 +57,42 @@ template("asar") { rebase_path(outputs[0]), ] } + + node_action(target_name + "_header_hash") { + invoker_out = invoker.outputs + + deps = [ ":" + invoker.target_name ] + sources = invoker.outputs + + script = "//electron/script/gn-asar-hash.js" + outputs = [ "$target_gen_dir/asar_hashes/$target_name.hash" ] + + args = [ + rebase_path(invoker_out[0]), + rebase_path(outputs[0]), + ] + } +} + +template("asar_hashed_info_plist") { + node_action(target_name) { + assert(defined(invoker.plist_file), + "Need plist_file to add hashed assets to") + assert(defined(invoker.keys), "Need keys to replace with asset hash") + assert(defined(invoker.hash_targets), "Need hash_targets to read hash from") + + deps = invoker.hash_targets + + script = "//electron/script/gn-plist-but-with-hashes.js" + inputs = [ invoker.plist_file ] + outputs = [ "$target_gen_dir/hashed_plists/$target_name.plist" ] + hash_files = [] + foreach(hash_target, invoker.hash_targets) { + hash_files += get_target_outputs(hash_target) + } + args = [ + rebase_path(invoker.plist_file), + rebase_path(outputs[0]), + ] + invoker.keys + rebase_path(hash_files) + } } diff --git a/build/dump_syms.py b/build/dump_syms.py index a606cb8789e1b..68cea6394bcd1 100644 --- a/build/dump_syms.py +++ b/build/dump_syms.py @@ -39,7 +39,7 @@ def main(dump_syms, binary, out_dir, stamp_file, dsym_file=None): args += ["-g", dsym_file] args += [binary] - symbol_data = subprocess.check_output(args) + symbol_data = subprocess.check_output(args).decode(sys.stdout.encoding) symbol_path = os.path.join(out_dir, get_symbol_path(symbol_data)) mkdir_p(os.path.dirname(symbol_path)) diff --git a/build/extract_symbols.gni b/build/extract_symbols.gni index cd3a9f2cfd0a3..2f98aa466ba15 100644 --- a/build/extract_symbols.gni +++ b/build/extract_symbols.gni @@ -24,7 +24,11 @@ template("extract_symbols") { assert(defined(invoker.binary), "Need binary to dump") assert(defined(invoker.symbol_dir), "Need directory for symbol output") - dump_syms_label = "//third_party/breakpad:dump_syms($host_toolchain)" + if (host_os == "win" && target_cpu == "x86") { + dump_syms_label = "//third_party/breakpad:dump_syms(//build/toolchain/win:win_clang_x64)" + } else { + dump_syms_label = "//third_party/breakpad:dump_syms($host_toolchain)" + } dump_syms_binary = get_label_info(dump_syms_label, "root_out_dir") + "/dump_syms$_host_executable_suffix" @@ -34,9 +38,7 @@ template("extract_symbols") { dump_syms_binary, ] stamp_file = "${target_gen_dir}/${target_name}.stamp" - outputs = [ - stamp_file, - ] + outputs = [ stamp_file ] args = [ "./" + rebase_path(dump_syms_binary, root_build_dir), rebase_path(invoker.binary, root_build_dir), diff --git a/build/fake_v8_context_snapshot_generator.py b/build/fake_v8_context_snapshot_generator.py new file mode 100644 index 0000000000000..2309b22484bac --- /dev/null +++ b/build/fake_v8_context_snapshot_generator.py @@ -0,0 +1,8 @@ +import os +import shutil +import sys + +if os.path.exists(sys.argv[2]): + os.remove(sys.argv[2]) + +shutil.copy(sys.argv[1], sys.argv[2]) diff --git a/build/fuses/build.py b/build/fuses/build.py new file mode 100755 index 0000000000000..c82e6cfba02af --- /dev/null +++ b/build/fuses/build.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +from collections import OrderedDict +import json +import os +import sys + +dir_path = os.path.dirname(os.path.realpath(__file__)) + +SENTINEL = "dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX" + +TEMPLATE_H = """ +#ifndef ELECTRON_FUSES_H_ +#define ELECTRON_FUSES_H_ + +#if defined(WIN32) +#define FUSE_EXPORT __declspec(dllexport) +#else +#define FUSE_EXPORT __attribute__((visibility("default"))) +#endif + +namespace electron { + +namespace fuses { + +extern const volatile char kFuseWire[]; + +{getters} + +} // namespace fuses + +} // namespace electron + +#endif // ELECTRON_FUSES_H_ +""" + +TEMPLATE_CC = """ +#include "electron/fuses.h" + +namespace electron { + +namespace fuses { + +const volatile char kFuseWire[] = { /* sentinel */ {sentinel}, /* fuse_version */ {fuse_version}, /* fuse_wire_length */ {fuse_wire_length}, /* fuse_wire */ {initial_config}}; + +{getters} + +} + +} +""" + +with open(os.path.join(dir_path, "fuses.json5"), 'r') as f: + fuse_defaults = json.loads(''.join(line for line in f.readlines() if not line.strip()[0] == "/"), object_pairs_hook=OrderedDict) + +fuse_version = fuse_defaults['_version'] +del fuse_defaults['_version'] +del fuse_defaults['_schema'] +del fuse_defaults['_comment'] + +if fuse_version >= pow(2, 8): + raise Exception("Fuse version can not exceed one byte in size") + +fuses = fuse_defaults.keys() + +initial_config = "" +getters_h = "" +getters_cc = "" +index = len(SENTINEL) + 1 +for fuse in fuses: + index += 1 + initial_config += fuse_defaults[fuse] + name = ''.join(word.title() for word in fuse.split('_')) + getters_h += "FUSE_EXPORT bool Is{name}Enabled();\n".replace("{name}", name) + getters_cc += """ +bool Is{name}Enabled() { + return kFuseWire[{index}] == '1'; +} +""".replace("{name}", name).replace("{index}", str(index)) + +def c_hex(n): + s = hex(n)[2:] + return "0x" + s.rjust(2, '0') + +def hex_arr(s): + arr = [] + for char in s: + arr.append(c_hex(ord(char))) + return ",".join(arr) + +header = TEMPLATE_H.replace("{getters}", getters_h.strip()) +impl = TEMPLATE_CC.replace("{sentinel}", hex_arr(SENTINEL)) +impl = impl.replace("{fuse_version}", c_hex(fuse_version)) +impl = impl.replace("{fuse_wire_length}", c_hex(len(fuses))) +impl = impl.replace("{initial_config}", hex_arr(initial_config)) +impl = impl.replace("{getters}", getters_cc.strip()) + +with open(sys.argv[1], 'w') as f: + f.write(header) + +with open(sys.argv[2], 'w') as f: + f.write(impl) diff --git a/build/fuses/fuses.json5 b/build/fuses/fuses.json5 new file mode 100644 index 0000000000000..f4984aa2a17b4 --- /dev/null +++ b/build/fuses/fuses.json5 @@ -0,0 +1,11 @@ +{ + "_comment": "Modifying the fuse schema in any breaking way should result in the _version prop being incremented. NEVER remove a fuse or change its meaning, instead mark it as removed with 'r'", + "_schema": "0 == off, 1 == on, r == removed fuse", + "_version": 1, + "run_as_node": "1", + "cookie_encryption": "0", + "node_options": "1", + "node_cli_inspect": "1", + "embedded_asar_integrity_validation": "0", + "only_load_app_from_asar": "0" +} diff --git a/build/generate_node_defines.py b/build/generate_node_defines.py new file mode 100755 index 0000000000000..f2004abb9fe8e --- /dev/null +++ b/build/generate_node_defines.py @@ -0,0 +1,34 @@ +import os +import re +import sys + +DEFINE_EXTRACT_REGEX = re.compile('^ *# *define (\w*)', re.MULTILINE) + +def main(outDir, headers): + defines = [] + for filename in headers: + with open(filename, 'r') as f: + content = f.read() + defines += read_defines(content) + + push_and_undef = '' + for define in defines: + push_and_undef += '#pragma push_macro("%s")\n' % define + push_and_undef += '#undef %s\n' % define + with open(os.path.join(outDir, 'push_and_undef_node_defines.h'), 'w') as o: + o.write(push_and_undef) + + pop = '' + for define in defines: + pop += '#pragma pop_macro("%s")\n' % define + with open(os.path.join(outDir, 'pop_node_defines.h'), 'w') as o: + o.write(pop) + +def read_defines(content): + defines = [] + for match in DEFINE_EXTRACT_REGEX.finditer(content): + defines.append(match.group(1)) + return defines + +if __name__ == '__main__': + main(sys.argv[1], sys.argv[2:]) diff --git a/build/install-build-deps.sh b/build/install-build-deps.sh deleted file mode 100755 index 2a1266e9f7bbf..0000000000000 --- a/build/install-build-deps.sh +++ /dev/null @@ -1,653 +0,0 @@ -#!/bin/bash -e -# Copyright (c) 2012 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. -# Script to install everything needed to build chromium (well, ideally, anyway) -# See https://chromium.googlesource.com/chromium/src/+/master/docs/linux_build_instructions.md -usage() { - echo "Usage: $0 [--options]" - echo "Options:" - echo "--[no-]syms: enable or disable installation of debugging symbols" - echo "--lib32: enable installation of 32-bit libraries, e.g. for V8 snapshot" - echo "--[no-]arm: enable or disable installation of arm cross toolchain" - echo "--[no-]chromeos-fonts: enable or disable installation of Chrome OS"\ - "fonts" - echo "--[no-]nacl: enable or disable installation of prerequisites for"\ - "building standalone NaCl and all its toolchains" - echo "--[no-]backwards-compatible: enable or disable installation of packages - that are no longer currently needed and have been removed from this - script. Useful for bisection." - echo "--no-prompt: silently select standard options/defaults" - echo "--quick-check: quickly try to determine if dependencies are installed" - echo " (this avoids interactive prompts and sudo commands," - echo " so might not be 100% accurate)" - echo "--unsupported: attempt installation even on unsupported systems" - echo "Script will prompt interactively if options not given." - exit 1 -} -# Checks whether a particular package is available in the repos. -# USAGE: $ package_exists -package_exists() { - # 'apt-cache search' takes a regex string, so eg. the +'s in packages like - # "libstdc++" need to be escaped. - local escaped="$(echo $1 | sed 's/[\~\+\.\:-]/\\&/g')" - [ ! -z "$(apt-cache search --names-only "${escaped}" | \ - awk '$1 == "'$1'" { print $1; }')" ] -} -# These default to on because (some) bots need them and it keeps things -# simple for the bot setup if all bots just run the script in its default -# mode. Developers who don't want stuff they don't need installed on their -# own workstations can pass --no-arm --no-nacl when running the script. -do_inst_arm=1 -do_inst_nacl=1 -while [ "$1" != "" ] -do - case "$1" in - --syms) do_inst_syms=1;; - --no-syms) do_inst_syms=0;; - --lib32) do_inst_lib32=1;; - --arm) do_inst_arm=1;; - --no-arm) do_inst_arm=0;; - --chromeos-fonts) do_inst_chromeos_fonts=1;; - --no-chromeos-fonts) do_inst_chromeos_fonts=0;; - --nacl) do_inst_nacl=1;; - --no-nacl) do_inst_nacl=0;; - --backwards-compatible) do_inst_backwards_compatible=1;; - --no-backwards-compatible) do_inst_backwards_compatible=0;; - --add-cross-tool-repo) add_cross_tool_repo=1;; - --no-prompt) do_default=1 - do_quietly="-qq --assume-yes" - ;; - --quick-check) do_quick_check=1;; - --unsupported) do_unsupported=1;; - *) usage;; - esac - shift -done -if [ "$do_inst_arm" = "1" ]; then - do_inst_lib32=1 -fi -# Check for lsb_release command in $PATH -if ! which lsb_release > /dev/null; then - echo "ERROR: lsb_release not found in \$PATH" >&2 - exit 1; -fi -distro_codename=$(lsb_release --codename --short) -distro_id=$(lsb_release --id --short) -supported_codenames="(trusty|xenial|artful|bionic)" -supported_ids="(Debian)" -if [ 0 -eq "${do_unsupported-0}" ] && [ 0 -eq "${do_quick_check-0}" ] ; then - if [[ ! $distro_codename =~ $supported_codenames && - ! $distro_id =~ $supported_ids ]]; then - echo -e "ERROR: The only supported distros are\n" \ - "\tUbuntu 14.04 LTS (trusty)\n" \ - "\tUbuntu 16.04 LTS (xenial)\n" \ - "\tUbuntu 17.10 (artful)\n" \ - "\tUbuntu 18.04 LTS (bionic)\n" \ - "\tDebian 8 (jessie) or later" >&2 - exit 1 - fi - if ! uname -m | egrep -q "i686|x86_64"; then - echo "Only x86 architectures are currently supported" >&2 - exit - fi -fi -if [ "x$(id -u)" != x0 ] && [ 0 -eq "${do_quick_check-0}" ]; then - echo "Running as non-root user." - echo "You might have to enter your password one or more times for 'sudo'." - echo -fi -# Packages needed for chromeos only -chromeos_dev_list="libbluetooth-dev libxkbcommon-dev" -if package_exists realpath; then - chromeos_dev_list="${chromeos_dev_list} realpath" -fi -# Packages needed for development -dev_list="\ - binutils - bison - bzip2 - cdbs - curl - dbus-x11 - dpkg-dev - elfutils - devscripts - fakeroot - flex - g++ - git-core - git-svn - gperf - libappindicator3-dev - libasound2-dev - libatspi2.0-dev - libbrlapi-dev - libbz2-dev - libcairo2-dev - libcap-dev - libcups2-dev - libcurl4-gnutls-dev - libdrm-dev - libelf-dev - libffi-dev - libgbm-dev - libglib2.0-dev - libglu1-mesa-dev - libgnome-keyring-dev - libgtk-3-dev - libkrb5-dev - libnspr4-dev - libnss3-dev - libpam0g-dev - libpci-dev - libpulse-dev - libsctp-dev - libspeechd-dev - libsqlite3-dev - libssl-dev - libudev-dev - libwww-perl - libxslt1-dev - libxss-dev - libxt-dev - libxtst-dev - locales - openbox - p7zip - patch - perl - pkg-config - python - python-cherrypy3 - python-crypto - python-dev - python-numpy - python-opencv - python-openssl - python-psutil - python-yaml - rpm - ruby - subversion - uuid-dev - wdiff - x11-utils - xcompmgr - xz-utils - zip - $chromeos_dev_list -" -# 64-bit systems need a minimum set of 32-bit compat packages for the pre-built -# NaCl binaries. -if file -L /sbin/init | grep -q 'ELF 64-bit'; then - dev_list="${dev_list} libc6-i386 lib32gcc1 lib32stdc++6" -fi -# Run-time libraries required by chromeos only -chromeos_lib_list="libpulse0 libbz2-1.0" -# List of required run-time libraries -common_lib_list="\ - libappindicator3-1 - libasound2 - libatk1.0-0 - libatspi2.0-0 - libc6 - libcairo2 - libcap2 - libcups2 - libexpat1 - libffi6 - libfontconfig1 - libfreetype6 - libglib2.0-0 - libgnome-keyring0 - libgtk-3-0 - libpam0g - libpango1.0-0 - libpci3 - libpcre3 - libpixman-1-0 - libspeechd2 - libstdc++6 - libsqlite3-0 - libuuid1 - libwayland-egl1-mesa - libx11-6 - libx11-xcb1 - libxau6 - libxcb1 - libxcomposite1 - libxcursor1 - libxdamage1 - libxdmcp6 - libxext6 - libxfixes3 - libxi6 - libxinerama1 - libxrandr2 - libxrender1 - libxtst6 - zlib1g -" -# Full list of required run-time libraries -lib_list="\ - $common_lib_list - $chromeos_lib_list -" -# 32-bit libraries needed e.g. to compile V8 snapshot for Android or armhf -lib32_list="linux-libc-dev:i386 libpci3:i386" -# 32-bit libraries needed for a 32-bit build -lib32_list="$lib32_list libx11-xcb1:i386" -# Packages that have been removed from this script. Regardless of configuration -# or options passed to this script, whenever a package is removed, it should be -# added here. -backwards_compatible_list="\ - 7za - fonts-indic - fonts-ipafont - fonts-stix - fonts-thai-tlwg - fonts-tlwg-garuda - language-pack-da - language-pack-fr - language-pack-he - language-pack-zh-hant - libappindicator-dev - libappindicator1 - libappindicator3-1:i386 - libexif-dev - libexif12 - libexif12:i386 - libgbm-dev - libgl1-mesa-dev - libgl1-mesa-glx:i386 - libgles2-mesa-dev - libgtk2.0-0 - libgtk2.0-0:i386 - libgtk2.0-dev - mesa-common-dev - msttcorefonts - ttf-dejavu-core - ttf-indic-fonts - ttf-kochi-gothic - ttf-kochi-mincho - ttf-mscorefonts-installer - xfonts-mathml -" -case $distro_codename in - trusty) - backwards_compatible_list+=" \ - libgbm-dev-lts-trusty - libgl1-mesa-dev-lts-trusty - libgl1-mesa-glx-lts-trusty:i386 - libgles2-mesa-dev-lts-trusty - mesa-common-dev-lts-trusty" - ;; - xenial) - backwards_compatible_list+=" \ - libgbm-dev-lts-xenial - libgl1-mesa-dev-lts-xenial - libgl1-mesa-glx-lts-xenial:i386 - libgles2-mesa-dev-lts-xenial - mesa-common-dev-lts-xenial" - ;; -esac -# arm cross toolchain packages needed to build chrome on armhf -EM_REPO="deb http://emdebian.org/tools/debian/ jessie main" -EM_SOURCE=$(cat </dev/null); then - arm_list+=" ${GPP_ARM_PACKAGE}" - else - if [ "${add_cross_tool_repo}" = "1" ]; then - gpg --keyserver pgp.mit.edu --recv-keys ${EM_ARCHIVE_KEY_FINGER} - gpg -a --export ${EM_ARCHIVE_KEY_FINGER} | sudo apt-key add - - if ! grep "^${EM_REPO}" "${CROSSTOOLS_LIST}" &>/dev/null; then - echo "${EM_SOURCE}" | sudo tee -a "${CROSSTOOLS_LIST}" >/dev/null - fi - arm_list+=" ${GPP_ARM_PACKAGE}" - else - echo "The Debian Cross-toolchains repository is necessary to" - echo "cross-compile Chromium for arm." - echo "Rerun with --add-deb-cross-tool-repo to have it added for you." - fi - fi - fi - ;; - # All necessary ARM packages are available on the default repos on - # Debian 9 and later. - *) - arm_list="libc6-dev-armhf-cross - linux-libc-dev-armhf-cross - ${GPP_ARM_PACKAGE}" - ;; -esac -# Work around for dependency issue Ubuntu/Trusty: http://crbug.com/435056 -case $distro_codename in - trusty) - arm_list+=" g++-4.8-multilib-arm-linux-gnueabihf - gcc-4.8-multilib-arm-linux-gnueabihf" - ;; - xenial|artful|bionic) - arm_list+=" g++-5-multilib-arm-linux-gnueabihf - gcc-5-multilib-arm-linux-gnueabihf - gcc-arm-linux-gnueabihf" - ;; -esac -# Packages to build NaCl, its toolchains, and its ports. -naclports_list="ant autoconf bison cmake gawk intltool xutils-dev xsltproc" -nacl_list="\ - g++-mingw-w64-i686 - lib32z1-dev - libasound2:i386 - libcap2:i386 - libelf-dev:i386 - libfontconfig1:i386 - libglib2.0-0:i386 - libgpm2:i386 - libgtk-3-0:i386 - libncurses5:i386 - lib32ncurses5-dev - libnss3:i386 - libpango1.0-0:i386 - libssl-dev:i386 - libtinfo-dev - libtinfo-dev:i386 - libtool - libuuid1:i386 - libxcomposite1:i386 - libxcursor1:i386 - libxdamage1:i386 - libxi6:i386 - libxrandr2:i386 - libxss1:i386 - libxtst6:i386 - texinfo - xvfb - ${naclports_list} -" -if package_exists libssl1.1; then - nacl_list="${nacl_list} libssl1.1:i386" -elif package_exists libssl1.0.2; then - nacl_list="${nacl_list} libssl1.0.2:i386" -else - nacl_list="${nacl_list} libssl1.0.0:i386" -fi -# Some package names have changed over time -if package_exists libpng16-16; then - lib_list="${lib_list} libpng16-16" -else - lib_list="${lib_list} libpng12-0" -fi -if package_exists libnspr4; then - lib_list="${lib_list} libnspr4 libnss3" -else - lib_list="${lib_list} libnspr4-0d libnss3-1d" -fi -if package_exists libjpeg-dev; then - dev_list="${dev_list} libjpeg-dev" -else - dev_list="${dev_list} libjpeg62-dev" -fi -if package_exists libudev1; then - dev_list="${dev_list} libudev1" - nacl_list="${nacl_list} libudev1:i386" -else - dev_list="${dev_list} libudev0" - nacl_list="${nacl_list} libudev0:i386" -fi -if package_exists libbrlapi0.6; then - dev_list="${dev_list} libbrlapi0.6" -else - dev_list="${dev_list} libbrlapi0.5" -fi -if package_exists apache2.2-bin; then - dev_list="${dev_list} apache2.2-bin" -else - dev_list="${dev_list} apache2-bin" -fi -if package_exists libav-tools; then - dev_list="${dev_list} libav-tools" -fi -if package_exists php7.2-cgi; then - dev_list="${dev_list} php7.2-cgi libapache2-mod-php7.2" -elif package_exists php7.1-cgi; then - dev_list="${dev_list} php7.1-cgi libapache2-mod-php7.1" -elif package_exists php7.0-cgi; then - dev_list="${dev_list} php7.0-cgi libapache2-mod-php7.0" -else - dev_list="${dev_list} php5-cgi libapache2-mod-php5" -fi -# Some packages are only needed if the distribution actually supports -# installing them. -if package_exists appmenu-gtk; then - lib_list="$lib_list appmenu-gtk" -fi -# Cross-toolchain strip is needed for building the sysroots. -if package_exists binutils-arm-linux-gnueabihf; then - dev_list="${dev_list} binutils-arm-linux-gnueabihf" -fi -if package_exists binutils-aarch64-linux-gnu; then - dev_list="${dev_list} binutils-aarch64-linux-gnu" -fi -if package_exists binutils-mipsel-linux-gnu; then - dev_list="${dev_list} binutils-mipsel-linux-gnu" -fi -if package_exists binutils-mips64el-linux-gnuabi64; then - dev_list="${dev_list} binutils-mips64el-linux-gnuabi64" -fi -# When cross building for arm/Android on 64-bit systems the host binaries -# that are part of v8 need to be compiled with -m32 which means -# that basic multilib support is needed. -if file -L /sbin/init | grep -q 'ELF 64-bit'; then - # gcc-multilib conflicts with the arm cross compiler (at least in trusty) but - # g++-X.Y-multilib gives us the 32-bit support that we need. Find out the - # appropriate value of X and Y by seeing what version the current - # distribution's g++-multilib package depends on. - multilib_package=$(apt-cache depends g++-multilib --important | \ - grep -E --color=never --only-matching '\bg\+\+-[0-9.]+-multilib\b') - lib32_list="$lib32_list $multilib_package" -fi -if [ "$do_inst_syms" = "1" ]; then - echo "Including debugging symbols." - # Debian is in the process of transitioning to automatic debug packages, which - # have the -dbgsym suffix (https://wiki.debian.org/AutomaticDebugPackages). - # Untransitioned packages have the -dbg suffix. And on some systems, neither - # will be available, so exclude the ones that are missing. - dbg_package_name() { - if package_exists "$1-dbgsym"; then - echo "$1-dbgsym" - elif package_exists "$1-dbg"; then - echo "$1-dbg" - fi - } - for package in "${common_lib_list}"; do - dbg_list="$dbg_list $(dbg_package_name ${package})" - done - # Debugging symbols packages not following common naming scheme - if [ "$(dbg_package_name libstdc++6)" == "" ]; then - if package_exists libstdc++6-8-dbg; then - dbg_list="${dbg_list} libstdc++6-8-dbg" - elif package_exists libstdc++6-7-dbg; then - dbg_list="${dbg_list} libstdc++6-7-dbg" - elif package_exists libstdc++6-6-dbg; then - dbg_list="${dbg_list} libstdc++6-6-dbg" - elif package_exists libstdc++6-5-dbg; then - dbg_list="${dbg_list} libstdc++6-5-dbg" - elif package_exists libstdc++6-4.9-dbg; then - dbg_list="${dbg_list} libstdc++6-4.9-dbg" - elif package_exists libstdc++6-4.8-dbg; then - dbg_list="${dbg_list} libstdc++6-4.8-dbg" - elif package_exists libstdc++6-4.7-dbg; then - dbg_list="${dbg_list} libstdc++6-4.7-dbg" - elif package_exists libstdc++6-4.6-dbg; then - dbg_list="${dbg_list} libstdc++6-4.6-dbg" - fi - fi - if [ "$(dbg_package_name libatk1.0-0)" == "" ]; then - dbg_list="$dbg_list $(dbg_package_name libatk1.0)" - fi - if [ "$(dbg_package_name libpango1.0-0)" == "" ]; then - dbg_list="$dbg_list $(dbg_package_name libpango1.0-dev)" - fi -else - echo "Skipping debugging symbols." - dbg_list= -fi -if [ "$do_inst_lib32" = "1" ]; then - echo "Including 32-bit libraries." -else - echo "Skipping 32-bit libraries." - lib32_list= -fi -if [ "$do_inst_arm" = "1" ]; then - echo "Including ARM cross toolchain." -else - echo "Skipping ARM cross toolchain." - arm_list= -fi -if [ "$do_inst_nacl" = "1" ]; then - echo "Including NaCl, NaCl toolchain, NaCl ports dependencies." -else - echo "Skipping NaCl, NaCl toolchain, NaCl ports dependencies." - nacl_list= -fi -filtered_backwards_compatible_list= -if [ "$do_inst_backwards_compatible" = "1" ]; then - echo "Including backwards compatible packages." - for package in ${backwards_compatible_list}; do - if package_exists ${package}; then - filtered_backwards_compatible_list+=" ${package}" - fi - done -fi -# The `sort -r -s -t: -k2` sorts all the :i386 packages to the front, to avoid -# confusing dpkg-query (crbug.com/446172). -packages="$( - echo "${dev_list} ${lib_list} ${dbg_list} ${lib32_list} ${arm_list}" \ - "${nacl_list}" ${filtered_backwards_compatible_list} | tr " " "\n" | \ - sort -u | sort -r -s -t: -k2 | tr "\n" " " -)" -if [ 1 -eq "${do_quick_check-0}" ] ; then - if ! missing_packages="$(dpkg-query -W -f ' ' ${packages} 2>&1)"; then - # Distinguish between packages that actually aren't available to the - # system (i.e. not in any repo) and packages that just aren't known to - # dpkg (i.e. managed by apt). - missing_packages="$(echo "${missing_packages}" | awk '{print $NF}')" - not_installed="" - unknown="" - for p in ${missing_packages}; do - if apt-cache show ${p} > /dev/null 2>&1; then - not_installed="${p}\n${not_installed}" - else - unknown="${p}\n${unknown}" - fi - done - if [ -n "${not_installed}" ]; then - echo "WARNING: The following packages are not installed:" - echo -e "${not_installed}" | sed -e "s/^/ /" - fi - if [ -n "${unknown}" ]; then - echo "WARNING: The following packages are unknown to your system" - echo "(maybe missing a repo or need to 'sudo apt-get update'):" - echo -e "${unknown}" | sed -e "s/^/ /" - fi - exit 1 - fi - exit 0 -fi -if [ "$do_inst_lib32" = "1" ] || [ "$do_inst_nacl" = "1" ]; then - sudo dpkg --add-architecture i386 -fi -sudo apt-get update -# We initially run "apt-get" with the --reinstall option and parse its output. -# This way, we can find all the packages that need to be newly installed -# without accidentally promoting any packages from "auto" to "manual". -# We then re-run "apt-get" with just the list of missing packages. -echo "Finding missing packages..." -# Intentionally leaving $packages unquoted so it's more readable. -echo "Packages required: " $packages -echo -new_list_cmd="sudo apt-get install --reinstall $(echo $packages)" -if new_list="$(yes n | LANGUAGE=en LANG=C $new_list_cmd)"; then - # We probably never hit this following line. - echo "No missing packages, and the packages are up to date." -elif [ $? -eq 1 ]; then - # We expect apt-get to have exit status of 1. - # This indicates that we cancelled the install with "yes n|". - new_list=$(echo "$new_list" | - sed -e '1,/The following NEW packages will be installed:/d;s/^ //;t;d') - new_list=$(echo "$new_list" | sed 's/ *$//') - if [ -z "$new_list" ] ; then - echo "No missing packages, and the packages are up to date." - else - echo "Installing missing packages: $new_list." - sudo apt-get install ${do_quietly-} ${new_list} - fi - echo -else - # An apt-get exit status of 100 indicates that a real error has occurred. - # I am intentionally leaving out the '"'s around new_list_cmd, - # as this makes it easier to cut and paste the output - echo "The following command failed: " ${new_list_cmd} - echo - echo "It produces the following output:" - yes n | $new_list_cmd || true - echo - echo "You will have to install the above packages yourself." - echo - exit 100 -fi -# Install the Chrome OS default fonts. This must go after running -# apt-get, since install-chromeos-fonts depends on curl. -if [ "$do_inst_chromeos_fonts" != "0" ]; then - echo - echo "Installing Chrome OS fonts." - dir=`echo $0 | sed -r -e 's/\/[^/]+$//'` - if ! sudo $dir/linux/install-chromeos-fonts.py; then - echo "ERROR: The installation of the Chrome OS default fonts failed." - if [ `stat -f -c %T $dir` == "nfs" ]; then - echo "The reason is that your repo is installed on a remote file system." - else - echo "This is expected if your repo is installed on a remote file system." - fi - echo "It is recommended to install your repo on a local file system." - echo "You can skip the installation of the Chrome OS default founts with" - echo "the command line option: --no-chromeos-fonts." - exit 1 - fi -else - echo "Skipping installation of Chrome OS fonts." -fi -echo "Installing locales." -CHROMIUM_LOCALES="da_DK.UTF-8 fr_FR.UTF-8 he_IL.UTF-8 zh_TW.UTF-8" -LOCALE_GEN=/etc/locale.gen -if [ -e ${LOCALE_GEN} ]; then - OLD_LOCALE_GEN="$(cat /etc/locale.gen)" - for CHROMIUM_LOCALE in ${CHROMIUM_LOCALES}; do - sudo sed -i "s/^# ${CHROMIUM_LOCALE}/${CHROMIUM_LOCALE}/" ${LOCALE_GEN} - done - # Regenerating locales can take a while, so only do it if we need to. - if (echo "${OLD_LOCALE_GEN}" | cmp -s ${LOCALE_GEN}); then - echo "Locales already up-to-date." - else - sudo locale-gen - fi -else - for CHROMIUM_LOCALE in ${CHROMIUM_LOCALES}; do - sudo locale-gen ${CHROMIUM_LOCALE} - done -fi diff --git a/tools/js2c.py b/build/js2c.py similarity index 96% rename from tools/js2c.py rename to build/js2c.py index 0f7178baf0e08..7024e7391d5bf 100755 --- a/tools/js2c.py +++ b/build/js2c.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import subprocess diff --git a/build/mac/make_locale_dirs.py b/build/mac/make_locale_dirs.py index a75d0735ad127..21b220f7d913b 100644 --- a/build/mac/make_locale_dirs.py +++ b/build/mac/make_locale_dirs.py @@ -8,6 +8,7 @@ # require any direct Cocoa locale support. import os +import errno import sys @@ -16,7 +17,7 @@ def main(args): try: os.makedirs(dirname) except OSError as e: - if e.errno == os.errno.EEXIST: + if e.errno == errno.EEXIST: # It's OK if it already exists pass else: diff --git a/build/npm-run.py b/build/npm-run.py index cf5d2dbd83f29..49a6abac65d07 100644 --- a/build/npm-run.py +++ b/build/npm-run.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function import os import subprocess @@ -15,5 +15,6 @@ try: subprocess.check_output(args, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - print("NPM script '" + sys.argv[2] + "' failed with code '" + str(e.returncode) + "':\n" + e.output) + error_msg = "NPM script '{}' failed with code '{}':\n".format(sys.argv[2], e.returncode) + print(error_msg + e.output.decode('utf8')) sys.exit(e.returncode) diff --git a/build/npm.gni b/build/npm.gni index a1987d095adf4..c2a0d1725af75 100644 --- a/build/npm.gni +++ b/build/npm.gni @@ -5,14 +5,12 @@ template("npm_action") { action("npm_pre_flight_" + target_name) { inputs = [ - "package.json", - "yarn.lock", + "//electron/package.json", + "//electron/yarn.lock", ] script = "//electron/build/npm-run.py" - outputs = [ - "$target_gen_dir/npm_pre_stamps/" + target_name + ".stamp", - ] + outputs = [ "$target_gen_dir/npm_pre_stamps/" + target_name + ".stamp" ] args = [ "--silent", diff --git a/build/profile_toolchain.py b/build/profile_toolchain.py new file mode 100755 index 0000000000000..4251315e0a16a --- /dev/null +++ b/build/profile_toolchain.py @@ -0,0 +1,126 @@ +from __future__ import unicode_literals + +import contextlib +import sys +import os +import optparse +import json +import re +import subprocess + +sys.path.append("%s/../../build" % os.path.dirname(os.path.realpath(__file__))) + +import find_depot_tools +from vs_toolchain import \ + SetEnvironmentAndGetRuntimeDllDirs, \ + SetEnvironmentAndGetSDKDir, \ + NormalizePath + +sys.path.append("%s/win_toolchain" % find_depot_tools.add_depot_tools_to_path()) + +from get_toolchain_if_necessary import CalculateHash + + +@contextlib.contextmanager +def cwd(directory): + curdir = os.getcwd() + try: + os.chdir(directory) + yield + finally: + os.chdir(curdir) + + +def calculate_hash(root): + with cwd(root): + return CalculateHash('.', None) + +def windows_installed_software(): + powershell_command = [ + "Get-CimInstance", + "-Namespace", + "root\cimv2", + "-Class", + "Win32_product", + "|", + "Select", + "vendor,", + "description,", + "@{l='install_location';e='InstallLocation'},", + "@{l='install_date';e='InstallDate'},", + "@{l='install_date_2';e='InstallDate2'},", + "caption,", + "version,", + "name,", + "@{l='sku_number';e='SKUNumber'}", + "|", + "ConvertTo-Json", + ] + + proc = subprocess.Popen( + ["powershell.exe", "-Command", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + stdout, _ = proc.communicate(" ".join(powershell_command).encode("utf-8")) + + if proc.returncode != 0: + raise RuntimeError("Failed to get list of installed software") + + # On AppVeyor there's other output related to PSReadline, + # so grab only the JSON output and ignore everything else + json_match = re.match( + r".*(\[.*{.*}.*\]).*", stdout.decode("utf-8"), re.DOTALL + ) + + if not json_match: + raise RuntimeError( + "Couldn't find JSON output for list of installed software" + ) + + # Filter out missing keys + return list( + map( + lambda info: {k: info[k] for k in info if info[k]}, + json.loads(json_match.group(1)), + ) + ) + + +def windows_profile(): + runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs() + win_sdk_dir = SetEnvironmentAndGetSDKDir() + path = NormalizePath(os.environ['GYP_MSVS_OVERRIDE_PATH']) + + # since current windows executable are symbols path dependant, + # profile the current directory too + return { + 'pwd': os.getcwd(), + 'installed_software': windows_installed_software(), + 'sdks': [ + {'name': 'vs', 'path': path, 'hash': calculate_hash(path)}, + { + 'name': 'wsdk', + 'path': win_sdk_dir, + 'hash': calculate_hash(win_sdk_dir), + }, + ], + 'runtime_lib_dirs': runtime_dll_dirs, + } + + +def main(options): + if sys.platform == 'win32': + with open(options.output_json, 'w') as f: + json.dump(windows_profile(), f) + else: + raise OSError("Unsupported OS") + + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option('--output-json', metavar='FILE', default='profile.json', + help='write information about toolchain to FILE') + opts, args = parser.parse_args() + sys.exit(main(opts)) diff --git a/build/rules.gni b/build/rules.gni new file mode 100644 index 0000000000000..c6c383829b1d0 --- /dev/null +++ b/build/rules.gni @@ -0,0 +1,94 @@ +import("//build/config/mac/mac_sdk.gni") + +# Template to compile .xib and .storyboard files. +# (copied from src/build/config/ios/rules.gni) +# +# Arguments +# +# sources: +# list of string, sources to compile +# +# ibtool_flags: +# (optional) list of string, additional flags to pass to the ibtool +template("compile_ib_files") { + action_foreach(target_name) { + forward_variables_from(invoker, + [ + "testonly", + "visibility", + ]) + assert(defined(invoker.sources), + "sources must be specified for $target_name") + assert(defined(invoker.output_extension), + "output_extension must be specified for $target_name") + + ibtool_flags = [] + if (defined(invoker.ibtool_flags)) { + ibtool_flags = invoker.ibtool_flags + } + + _output_extension = invoker.output_extension + + script = "//build/config/ios/compile_ib_files.py" + sources = invoker.sources + outputs = [ + "$target_gen_dir/$target_name/{{source_name_part}}.$_output_extension", + ] + args = [ + "--input", + "{{source}}", + "--output", + rebase_path( + "$target_gen_dir/$target_name/{{source_name_part}}.$_output_extension", + root_build_dir), + ] + args += ibtool_flags + } +} + +# Template is copied here from Chromium but was removed in +# https://chromium-review.googlesource.com/c/chromium/src/+/1637981 +# Template to compile and package Mac XIB files as bundle data. +# Arguments +# sources: +# list of string, sources to comiple +# output_path: +# (optional) string, the path to use for the outputs list in the +# bundle_data step. If unspecified, defaults to bundle_resources_dir. +template("mac_xib_bundle_data") { + _target_name = target_name + _compile_target_name = _target_name + "_compile_ibtool" + + compile_ib_files(_compile_target_name) { + forward_variables_from(invoker, [ "testonly" ]) + visibility = [ ":$_target_name" ] + sources = invoker.sources + output_extension = "nib" + ibtool_flags = [ + "--minimum-deployment-target", + mac_deployment_target, + + # TODO(rsesek): Enable this once all the bots are on Xcode 7+. + # "--target-device", + # "mac", + ] + } + + bundle_data(_target_name) { + forward_variables_from(invoker, + [ + "testonly", + "visibility", + ]) + + public_deps = [ ":$_compile_target_name" ] + sources = get_target_outputs(":$_compile_target_name") + + _output_path = "{{bundle_resources_dir}}" + if (defined(invoker.output_path)) { + _output_path = invoker.output_path + } + + outputs = [ "$_output_path/{{source_file_part}}" ] + } +} diff --git a/build/run-in-dir.py b/build/run-in-dir.py index 18b0dc1074d1a..25fcb21a03b29 100644 --- a/build/run-in-dir.py +++ b/build/run-in-dir.py @@ -1,6 +1,5 @@ import sys import os -import subprocess def main(argv): cwd = argv[1] diff --git a/build/strip_framework.py b/build/strip_framework.py new file mode 100755 index 0000000000000..73cf424dde488 --- /dev/null +++ b/build/strip_framework.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import os +import subprocess +import sys + +source = sys.argv[1] +dest = sys.argv[2] + +# Ensure any existing framework is removed +subprocess.check_output(["rm", "-rf", dest]) + +subprocess.check_output(["cp", "-a", source, dest]) + +# Strip headers, we do not need to ship them +subprocess.check_output(["rm", "-r", os.path.join(dest, "Headers")]) +subprocess.check_output( + ["rm", "-r", os.path.join(dest, "Versions", "Current", "Headers")] +) diff --git a/build/templated_file.gni b/build/templated_file.gni index b75c49ce2aac7..e4d35211d3e6e 100644 --- a/build/templated_file.gni +++ b/build/templated_file.gni @@ -15,12 +15,8 @@ template("templated_file") { "inputs", "outputs", ]) - inputs = [ - invoker.template, - ] - outputs = [ - invoker.output, - ] + inputs = [ invoker.template ] + outputs = [ invoker.output ] script = "//electron/build/generate-template.py" args = [ rebase_path(invoker.template), diff --git a/build/tsc.gni b/build/tsc.gni index 339cb245ca700..ec24c694aef63 100644 --- a/build/tsc.gni +++ b/build/tsc.gni @@ -26,19 +26,12 @@ template("typescript_build") { "//electron/typings/internal-electron.d.ts", ] - type_roots = "node_modules/@types,typings" - if (defined(invoker.type_root)) { - type_roots += "," + invoker.type_root - } - base_out_path = invoker.output_gen_dir + "/electron/" args = [ "-p", rebase_path(invoker.tsconfig), "--outDir", rebase_path("$base_out_path" + invoker.output_dir_name), - "--typeRoots", - type_roots, ] outputs = [] diff --git a/build/webpack/get-outputs.js b/build/webpack/get-outputs.js deleted file mode 100644 index fca601225be2e..0000000000000 --- a/build/webpack/get-outputs.js +++ /dev/null @@ -1,2 +0,0 @@ -process.env.PRINT_WEBPACK_GRAPH = true -require('./run-compiler') diff --git a/build/webpack/run-compiler.js b/build/webpack/run-compiler.js deleted file mode 100644 index 0b3b7735743a7..0000000000000 --- a/build/webpack/run-compiler.js +++ /dev/null @@ -1,22 +0,0 @@ -const path = require('path') -const webpack = require('webpack') - -const configPath = process.argv[2] -const outPath = path.resolve(process.argv[3]) -const config = require(configPath) -config.output = { - path: path.dirname(outPath), - filename: path.basename(outPath) -} - -webpack(config, (err, stats) => { - if (err) { - console.error(err) - process.exit(1) - } else if (stats.hasErrors()) { - console.error(stats.toString('normal')) - process.exit(1) - } else { - process.exit(0) - } -}) diff --git a/build/webpack/webpack.config.asar.js b/build/webpack/webpack.config.asar.js new file mode 100644 index 0000000000000..83443f467cad2 --- /dev/null +++ b/build/webpack/webpack.config.asar.js @@ -0,0 +1,5 @@ +module.exports = require('./webpack.config.base')({ + target: 'asar', + alwaysHasNode: true, + targetDeletesNodeGlobals: true +}); diff --git a/build/webpack/webpack.config.base.js b/build/webpack/webpack.config.base.js index 0adf1f7ec681a..b2393462afc74 100644 --- a/build/webpack/webpack.config.base.js +++ b/build/webpack/webpack.config.base.js @@ -1,22 +1,19 @@ -const fs = require('fs') -const path = require('path') -const webpack = require('webpack') +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const TerserPlugin = require('terser-webpack-plugin'); +const WrapperPlugin = require('wrapper-webpack-plugin'); -const electronRoot = path.resolve(__dirname, '../..') - -const onlyPrintingGraph = !!process.env.PRINT_WEBPACK_GRAPH +const electronRoot = path.resolve(__dirname, '../..'); class AccessDependenciesPlugin { - apply(compiler) { - // Only hook into webpack when we are printing the dependency graph - if (!onlyPrintingGraph) return - + apply (compiler) { compiler.hooks.compilation.tap('AccessDependenciesPlugin', compilation => { compilation.hooks.finishModules.tap('AccessDependenciesPlugin', modules => { - const filePaths = modules.map(m => m.resource).filter(p => p).map(p => path.relative(electronRoot, p)) - console.info(JSON.stringify(filePaths)) - }) - }) + const filePaths = modules.map(m => m.resource).filter(p => p).map(p => path.relative(electronRoot, p)); + console.info(JSON.stringify(filePaths)); + }); + }); } } @@ -24,57 +21,152 @@ module.exports = ({ alwaysHasNode, loadElectronFromAlternateTarget, targetDeletesNodeGlobals, - target + target, + wrapInitWithProfilingTimeout, + wrapInitWithTryCatch }) => { - let entry = path.resolve(electronRoot, 'lib', target, 'init.ts') + let entry = path.resolve(electronRoot, 'lib', target, 'init.ts'); if (!fs.existsSync(entry)) { - entry = path.resolve(electronRoot, 'lib', target, 'init.js') + entry = path.resolve(electronRoot, 'lib', target, 'init.js'); } - return ({ - mode: 'development', - devtool: 'inline-source-map', - entry, - target: alwaysHasNode ? 'node' : 'web', - output: { - filename: `${target}.bundle.js` - }, - resolve: { - alias: { - '@electron/internal': path.resolve(electronRoot, 'lib'), - 'electron': path.resolve(electronRoot, 'lib', loadElectronFromAlternateTarget || target, 'api', 'exports', 'electron.js'), - // Force timers to resolve to our dependency that doens't use window.postMessage - 'timers': path.resolve(electronRoot, 'node_modules', 'timers-browserify', 'main.js') - }, - extensions: ['.ts', '.js'] - }, - module: { - rules: [{ - test: /\.ts$/, - loader: 'ts-loader', - options: { - configFile: path.resolve(electronRoot, 'tsconfig.electron.json'), - transpileOnly: onlyPrintingGraph, - ignoreDiagnostics: [6059] + const electronAPIFile = path.resolve(electronRoot, 'lib', loadElectronFromAlternateTarget || target, 'api', 'exports', 'electron.ts'); + + return (env = {}, argv = {}) => { + const onlyPrintingGraph = !!env.PRINT_WEBPACK_GRAPH; + const outputFilename = argv['output-filename'] || `${target}.bundle.js`; + + const defines = { + BUILDFLAG: onlyPrintingGraph ? '(a => a)' : '' + }; + + if (env.buildflags) { + const flagFile = fs.readFileSync(env.buildflags, 'utf8'); + for (const line of flagFile.split(/(\r\n|\r|\n)/g)) { + const flagMatch = line.match(/#define BUILDFLAG_INTERNAL_(.+?)\(\) \(([01])\)/); + if (flagMatch) { + const [, flagName, flagValue] = flagMatch; + defines[flagName] = JSON.stringify(Boolean(parseInt(flagValue, 10))); } - }] - }, - node: { - __dirname: false, - __filename: false, - // We provide our own "timers" import above, any usage of setImmediate inside - // one of our renderer bundles should import it from the 'timers' package - setImmediate: false, - }, - plugins: [ - new AccessDependenciesPlugin(), - ...(targetDeletesNodeGlobals ? [ - new webpack.ProvidePlugin({ - process: ['@electron/internal/renderer/webpack-provider', 'process'], - global: ['@electron/internal/renderer/webpack-provider', '_global'], - Buffer: ['@electron/internal/renderer/webpack-provider', 'Buffer'], - }) - ] : []) - ] - }) -} \ No newline at end of file + } + } + + const ignoredModules = []; + + if (defines.ENABLE_DESKTOP_CAPTURER === 'false') { + ignoredModules.push( + '@electron/internal/browser/desktop-capturer', + '@electron/internal/browser/api/desktop-capturer', + '@electron/internal/renderer/api/desktop-capturer' + ); + } + + if (defines.ENABLE_VIEWS_API === 'false') { + ignoredModules.push( + '@electron/internal/browser/api/views/image-view.js' + ); + } + + const plugins = []; + + if (onlyPrintingGraph) { + plugins.push(new AccessDependenciesPlugin()); + } + + if (targetDeletesNodeGlobals) { + plugins.push(new webpack.ProvidePlugin({ + process: ['@electron/internal/common/webpack-provider', 'process'], + global: ['@electron/internal/common/webpack-provider', '_global'], + Buffer: ['@electron/internal/common/webpack-provider', 'Buffer'] + })); + } + + plugins.push(new webpack.ProvidePlugin({ + Promise: ['@electron/internal/common/webpack-globals-provider', 'Promise'] + })); + + plugins.push(new webpack.DefinePlugin(defines)); + + if (wrapInitWithProfilingTimeout) { + plugins.push(new WrapperPlugin({ + header: 'function ___electron_webpack_init__() {', + footer: ` +}; +if ((globalThis.process || binding.process).argv.includes("--profile-electron-init")) { + setTimeout(___electron_webpack_init__, 0); +} else { + ___electron_webpack_init__(); +}` + })); + } + + if (wrapInitWithTryCatch) { + plugins.push(new WrapperPlugin({ + header: 'try {', + footer: ` +} catch (err) { + console.error('Electron ${outputFilename} script failed to run'); + console.error(err); +}` + })); + } + + return { + mode: 'development', + devtool: false, + entry, + target: alwaysHasNode ? 'node' : 'web', + output: { + filename: outputFilename + }, + resolve: { + alias: { + '@electron/internal': path.resolve(electronRoot, 'lib'), + electron$: electronAPIFile, + 'electron/main$': electronAPIFile, + 'electron/renderer$': electronAPIFile, + 'electron/common$': electronAPIFile, + // Force timers to resolve to our dependency that doesn't use window.postMessage + timers: path.resolve(electronRoot, 'node_modules', 'timers-browserify', 'main.js') + }, + extensions: ['.ts', '.js'] + }, + module: { + rules: [{ + test: (moduleName) => !onlyPrintingGraph && ignoredModules.includes(moduleName), + loader: 'null-loader' + }, { + test: /\.ts$/, + loader: 'ts-loader', + options: { + configFile: path.resolve(electronRoot, 'tsconfig.electron.json'), + transpileOnly: onlyPrintingGraph, + ignoreDiagnostics: [ + // File '{0}' is not under 'rootDir' '{1}'. + 6059 + ] + } + }] + }, + node: { + __dirname: false, + __filename: false, + // We provide our own "timers" import above, any usage of setImmediate inside + // one of our renderer bundles should import it from the 'timers' package + setImmediate: false + }, + optimization: { + minimize: env.mode === 'production', + minimizer: [ + new TerserPlugin({ + terserOptions: { + keep_classnames: true, + keep_fnames: true + } + }) + ] + }, + plugins + }; + }; +}; diff --git a/build/webpack/webpack.config.browser.js b/build/webpack/webpack.config.browser.js index 24fec49106e78..c01b97fcf8e36 100644 --- a/build/webpack/webpack.config.browser.js +++ b/build/webpack/webpack.config.browser.js @@ -1,4 +1,4 @@ module.exports = require('./webpack.config.base')({ target: 'browser', alwaysHasNode: true -}) +}); diff --git a/build/webpack/webpack.config.content_script.js b/build/webpack/webpack.config.content_script.js deleted file mode 100644 index aaf39fbc0703b..0000000000000 --- a/build/webpack/webpack.config.content_script.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = require('./webpack.config.base')({ - target: 'content_script', - alwaysHasNode: false -}) diff --git a/build/webpack/webpack.config.isolated_renderer.js b/build/webpack/webpack.config.isolated_renderer.js index 28b9e940fa971..627498c255e2e 100644 --- a/build/webpack/webpack.config.isolated_renderer.js +++ b/build/webpack/webpack.config.isolated_renderer.js @@ -1,4 +1,5 @@ module.exports = require('./webpack.config.base')({ target: 'isolated_renderer', - alwaysHasNode: false -}) + alwaysHasNode: false, + wrapInitWithTryCatch: true +}); diff --git a/build/webpack/webpack.config.renderer.js b/build/webpack/webpack.config.renderer.js index 391254d142402..ada961852e157 100644 --- a/build/webpack/webpack.config.renderer.js +++ b/build/webpack/webpack.config.renderer.js @@ -1,5 +1,7 @@ module.exports = require('./webpack.config.base')({ target: 'renderer', alwaysHasNode: true, - targetDeletesNodeGlobals: true -}) + targetDeletesNodeGlobals: true, + wrapInitWithProfilingTimeout: true, + wrapInitWithTryCatch: true +}); diff --git a/build/webpack/webpack.config.sandboxed_renderer.js b/build/webpack/webpack.config.sandboxed_renderer.js index 0ca65254ddfce..67e6a06640fe9 100644 --- a/build/webpack/webpack.config.sandboxed_renderer.js +++ b/build/webpack/webpack.config.sandboxed_renderer.js @@ -1,4 +1,6 @@ module.exports = require('./webpack.config.base')({ target: 'sandboxed_renderer', - alwaysHasNode: false -}) + alwaysHasNode: false, + wrapInitWithProfilingTimeout: true, + wrapInitWithTryCatch: true +}); diff --git a/build/webpack/webpack.config.worker.js b/build/webpack/webpack.config.worker.js index 7fc167b54f281..acf5d1d6b021d 100644 --- a/build/webpack/webpack.config.worker.js +++ b/build/webpack/webpack.config.worker.js @@ -2,5 +2,6 @@ module.exports = require('./webpack.config.base')({ target: 'worker', loadElectronFromAlternateTarget: 'renderer', alwaysHasNode: true, - targetDeletesNodeGlobals: true -}) + targetDeletesNodeGlobals: true, + wrapInitWithTryCatch: true +}); diff --git a/build/webpack/webpack.gni b/build/webpack/webpack.gni index d8c6ff6990594..bc9cd5f439f24 100644 --- a/build/webpack/webpack.gni +++ b/build/webpack/webpack.gni @@ -22,13 +22,22 @@ template("webpack_build") { "//electron/typings/internal-electron.d.ts", ] + invoker.inputs + mode = "development" + if (is_official_build) { + mode = "production" + } + args = [ + "--config", rebase_path(invoker.config_file), - rebase_path(invoker.out_file), + "--output-filename=" + get_path_info(invoker.out_file, "file"), + "--output-path=" + rebase_path(get_path_info(invoker.out_file, "dir")), + "--env.buildflags=" + + rebase_path("$target_gen_dir/buildflags/buildflags.h"), + "--env.mode=" + mode, ] + deps += [ "//electron/buildflags" ] - outputs = [ - invoker.out_file, - ] + outputs = [ invoker.out_file ] } } diff --git a/build/zip.py b/build/zip.py index ff9c888d9143c..bfaf27c08b24b 100644 --- a/build/zip.py +++ b/build/zip.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function import os import subprocess @@ -9,17 +9,32 @@ '.pdb', '.mojom.js', '.mojom-lite.js', + '.info', + '.m.js' ] PATHS_TO_SKIP = [ - 'angledata', #Skipping because it is an output of //ui/gl that we don't need - './libVkICD_mock_', #Skipping because these are outputs that we don't need - './VkICD_mock_', #Skipping because these are outputs that we don't need - + # Skip because it is an output of //ui/gl that we don't need. + 'angledata', + # Skip because these are outputs that we don't need. + './libVkICD_mock_', + # Skip because these are outputs that we don't need. + './VkICD_mock_', + # Skip because its an output of create_bundle from + # //build/config/mac/rules.gni that we don't need + 'Electron.dSYM', + # Refs https://chromium-review.googlesource.com/c/angle/angle/+/2425197. + # Remove this when Angle themselves remove the file: + # https://issuetracker.google.com/issues/168736059 + 'gen/angle/angle_commit.h', # //chrome/browser:resources depends on this via # //chrome/browser/resources/ssl/ssl_error_assistant, but we don't need to # ship it. 'pyproto', + # Skip because these are outputs that we don't need. + 'resources/inspector', + 'gen/third_party/devtools-frontend/src', + 'gen/ui/webui' ] def skip_path(dep, dist_zip, target_cpu): @@ -32,7 +47,12 @@ def skip_path(dep, dist_zip, target_cpu): should_skip = ( any(dep.startswith(path) for path in PATHS_TO_SKIP) or any(dep.endswith(ext) for ext in EXTENSIONS_TO_SKIP) or - ('arm' in target_cpu and dist_zip == 'mksnapshot.zip' and dep == 'snapshot_blob.bin')) + ( + "arm" in target_cpu + and dist_zip == "mksnapshot.zip" + and dep == "snapshot_blob.bin" + ) + ) if should_skip: print("Skipping {}".format(dep)) return should_skip @@ -46,28 +66,46 @@ def execute(argv): raise e def main(argv): - dist_zip, runtime_deps, target_cpu, target_os = argv + dist_zip, runtime_deps, target_cpu, _, flatten_val, flatten_relative_to = argv + should_flatten = flatten_val == "true" dist_files = set() with open(runtime_deps) as f: for dep in f.readlines(): dep = dep.strip() - dist_files.add(dep) - if sys.platform == 'darwin': + if not skip_path(dep, dist_zip, target_cpu): + dist_files.add(dep) + if sys.platform == 'darwin' and not should_flatten: execute(['zip', '-r', '-y', dist_zip] + list(dist_files)) else: - with zipfile.ZipFile(dist_zip, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as z: + with zipfile.ZipFile( + dist_zip, 'w', zipfile.ZIP_DEFLATED, allowZip64=True + ) as z: for dep in dist_files: - if skip_path(dep, dist_zip, target_cpu): - continue if os.path.isdir(dep): - for root, dirs, files in os.walk(dep): - for file in files: - z.write(os.path.join(root, file)) + for root, _, files in os.walk(dep): + for filename in files: + z.write(os.path.join(root, filename)) else: basename = os.path.basename(dep) dirname = os.path.dirname(dep) - arcname = os.path.join(dirname, 'chrome-sandbox') if basename == 'chrome_sandbox' else dep - z.write(dep, arcname) + arcname = ( + os.path.join(dirname, 'chrome-sandbox') + if basename == 'chrome_sandbox' + else dep + ) + name_to_write = arcname + if should_flatten: + if flatten_relative_to: + if name_to_write.startswith(flatten_relative_to): + name_to_write = name_to_write[len(flatten_relative_to):] + else: + name_to_write = os.path.basename(arcname) + else: + name_to_write = os.path.basename(arcname) + z.write( + dep, + name_to_write, + ) if __name__ == '__main__': sys.exit(main(sys.argv[1:])) diff --git a/build/zip_libcxx.py b/build/zip_libcxx.py new file mode 100644 index 0000000000000..daa94fc8fb8a5 --- /dev/null +++ b/build/zip_libcxx.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +from __future__ import print_function +import os +import subprocess +import sys +import zipfile + +def execute(argv): + try: + output = subprocess.check_output(argv, stderr=subprocess.STDOUT) + return output + except subprocess.CalledProcessError as e: + print(e.output) + raise e + +def get_object_files(base_path, archive_name): + archive_file = os.path.join(base_path, archive_name) + output = execute(['nm', '-g', archive_file]).decode('ascii') + object_files = set() + lines = output.split("\n") + for line in lines: + if line.startswith(base_path): + object_file = line.split(":")[0] + object_files.add(object_file) + if line.startswith('nm: '): + object_file = line.split(":")[1].lstrip() + object_files.add(object_file) + return list(object_files) + [archive_file] + +def main(argv): + dist_zip, = argv + out_dir = os.path.dirname(dist_zip) + base_path_libcxx = os.path.join(out_dir, 'obj/buildtools/third_party/libc++') + base_path_libcxxabi = os.path.join(out_dir, 'obj/buildtools/third_party/libc++abi') + object_files_libcxx = get_object_files(base_path_libcxx, 'libc++.a') + object_files_libcxxabi = get_object_files(base_path_libcxxabi, 'libc++abi.a') + with zipfile.ZipFile( + dist_zip, 'w', zipfile.ZIP_DEFLATED, allowZip64=True + ) as z: + object_files_libcxx.sort() + for object_file in object_files_libcxx: + z.write(object_file, os.path.relpath(object_file, base_path_libcxx)) + for object_file in object_files_libcxxabi: + z.write(object_file, os.path.relpath(object_file, base_path_libcxxabi)) + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) \ No newline at end of file diff --git a/buildflags/BUILD.gn b/buildflags/BUILD.gn index 359a0609415fc..9cbb8227f5f52 100644 --- a/buildflags/BUILD.gn +++ b/buildflags/BUILD.gn @@ -12,12 +12,14 @@ buildflag_header("buildflags") { "ENABLE_DESKTOP_CAPTURER=$enable_desktop_capturer", "ENABLE_RUN_AS_NODE=$enable_run_as_node", "ENABLE_OSR=$enable_osr", - "ENABLE_VIEW_API=$enable_view_api", - "ENABLE_PEPPER_FLASH=$enable_pepper_flash", + "ENABLE_VIEWS_API=$enable_views_api", "ENABLE_PDF_VIEWER=$enable_pdf_viewer", "ENABLE_TTS=$enable_tts", "ENABLE_COLOR_CHOOSER=$enable_color_chooser", "ENABLE_ELECTRON_EXTENSIONS=$enable_electron_extensions", + "ENABLE_BUILTIN_SPELLCHECKER=$enable_builtin_spellchecker", + "ENABLE_PICTURE_IN_PICTURE=$enable_picture_in_picture", + "ENABLE_WIN_DARK_MODE_WINDOW_UI=$enable_win_dark_mode_window_ui", "OVERRIDE_LOCATION_PROVIDER=$enable_fake_location_provider", ] } diff --git a/buildflags/buildflags.gni b/buildflags/buildflags.gni index 86c25bdca4224..5adc739ef7bb1 100644 --- a/buildflags/buildflags.gni +++ b/buildflags/buildflags.gni @@ -10,23 +10,28 @@ declare_args() { enable_osr = true - enable_view_api = false + enable_views_api = true - enable_pdf_viewer = false + enable_pdf_viewer = true enable_tts = true enable_color_chooser = true + enable_picture_in_picture = true + # Provide a fake location provider for mocking # the geolocation responses. Disable it if you # need to test with chromium's location provider. # Should not be enabled for release build. enable_fake_location_provider = !is_official_build - # Enable flash plugin support. - enable_pepper_flash = true - # Enable Chrome extensions support. - enable_electron_extensions = false + enable_electron_extensions = true + + # Enable Spellchecker support + enable_builtin_spellchecker = true + + # Undocumented Windows dark mode API + enable_win_dark_mode_window_ui = false } diff --git a/chromium_src/BUILD.gn b/chromium_src/BUILD.gn index 08ec59522b136..b49dde05fb9ed 100644 --- a/chromium_src/BUILD.gn +++ b/chromium_src/BUILD.gn @@ -2,7 +2,9 @@ # Use of this source code is governed by the MIT license that can be # found in the LICENSE file. +import("//build/config/ozone.gni") import("//build/config/ui.gni") +import("//components/spellcheck/spellcheck_build_features.gni") import("//electron/buildflags/buildflags.gni") import("//printing/buildflags/buildflags.gni") import("//third_party/widevine/cdm/widevine.gni") @@ -11,24 +13,27 @@ import("//third_party/widevine/cdm/widevine.gni") static_library("chrome") { visibility = [ "//electron:electron_lib" ] sources = [ + "//chrome/browser/accessibility/accessibility_ui.cc", + "//chrome/browser/accessibility/accessibility_ui.h", + "//chrome/browser/app_mode/app_mode_utils.cc", + "//chrome/browser/app_mode/app_mode_utils.h", + "//chrome/browser/browser_features.cc", + "//chrome/browser/browser_features.h", "//chrome/browser/browser_process.cc", "//chrome/browser/browser_process.h", "//chrome/browser/devtools/devtools_contents_resizing_strategy.cc", "//chrome/browser/devtools/devtools_contents_resizing_strategy.h", "//chrome/browser/devtools/devtools_embedder_message_dispatcher.cc", "//chrome/browser/devtools/devtools_embedder_message_dispatcher.h", + "//chrome/browser/devtools/devtools_eye_dropper.cc", + "//chrome/browser/devtools/devtools_eye_dropper.h", "//chrome/browser/devtools/devtools_file_system_indexer.cc", "//chrome/browser/devtools/devtools_file_system_indexer.h", + "//chrome/browser/devtools/devtools_settings.h", "//chrome/browser/extensions/global_shortcut_listener.cc", "//chrome/browser/extensions/global_shortcut_listener.h", - "//chrome/browser/extensions/global_shortcut_listener_mac.h", - "//chrome/browser/extensions/global_shortcut_listener_mac.mm", - "//chrome/browser/extensions/global_shortcut_listener_win.cc", - "//chrome/browser/extensions/global_shortcut_listener_win.h", "//chrome/browser/icon_loader.cc", "//chrome/browser/icon_loader.h", - "//chrome/browser/icon_loader_mac.mm", - "//chrome/browser/icon_loader_win.cc", "//chrome/browser/icon_manager.cc", "//chrome/browser/icon_manager.h", "//chrome/browser/net/chrome_mojo_proxy_resolver_factory.cc", @@ -37,37 +42,136 @@ static_library("chrome") { "//chrome/browser/net/proxy_config_monitor.h", "//chrome/browser/net/proxy_service_factory.cc", "//chrome/browser/net/proxy_service_factory.h", - "//chrome/browser/ssl/security_state_tab_helper.cc", - "//chrome/browser/ssl/security_state_tab_helper.h", - "//chrome/browser/ui/autofill/popup_view_common.cc", - "//chrome/browser/ui/autofill/popup_view_common.h", - "//chrome/browser/win/chrome_process_finder.cc", - "//chrome/browser/win/chrome_process_finder.h", + "//chrome/browser/platform_util.cc", + "//chrome/browser/platform_util.h", + "//chrome/browser/predictors/preconnect_manager.cc", + "//chrome/browser/predictors/preconnect_manager.h", + "//chrome/browser/predictors/predictors_features.cc", + "//chrome/browser/predictors/predictors_features.h", + "//chrome/browser/predictors/proxy_lookup_client_impl.cc", + "//chrome/browser/predictors/proxy_lookup_client_impl.h", + "//chrome/browser/predictors/resolve_host_client_impl.cc", + "//chrome/browser/predictors/resolve_host_client_impl.h", + "//chrome/browser/process_singleton.h", + "//chrome/browser/process_singleton_internal.cc", + "//chrome/browser/process_singleton_internal.h", + "//chrome/browser/ui/exclusive_access/exclusive_access_bubble_type.cc", + "//chrome/browser/ui/exclusive_access/exclusive_access_bubble_type.h", + "//chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc", + "//chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h", + "//chrome/browser/ui/exclusive_access/exclusive_access_manager.cc", + "//chrome/browser/ui/exclusive_access/exclusive_access_manager.h", + "//chrome/browser/ui/exclusive_access/fullscreen_controller.cc", + "//chrome/browser/ui/exclusive_access/fullscreen_controller.h", + "//chrome/browser/ui/exclusive_access/fullscreen_within_tab_helper.cc", + "//chrome/browser/ui/exclusive_access/fullscreen_within_tab_helper.h", + "//chrome/browser/ui/exclusive_access/keyboard_lock_controller.cc", + "//chrome/browser/ui/exclusive_access/keyboard_lock_controller.h", + "//chrome/browser/ui/exclusive_access/mouse_lock_controller.cc", + "//chrome/browser/ui/exclusive_access/mouse_lock_controller.h", + "//chrome/browser/ui/views/eye_dropper/eye_dropper.cc", + "//chrome/browser/ui/views/eye_dropper/eye_dropper.h", + "//chrome/browser/ui/views/eye_dropper/eye_dropper_view.cc", + "//chrome/browser/ui/views/eye_dropper/eye_dropper_view.h", "//extensions/browser/app_window/size_constraints.cc", "//extensions/browser/app_window/size_constraints.h", ] + + if (is_posix) { + sources += [ "//chrome/browser/process_singleton_posix.cc" ] + } + + if (is_mac) { + sources += [ + "//chrome/browser/extensions/global_shortcut_listener_mac.h", + "//chrome/browser/extensions/global_shortcut_listener_mac.mm", + "//chrome/browser/icon_loader_mac.mm", + "//chrome/browser/media/webrtc/system_media_capture_permissions_mac.h", + "//chrome/browser/media/webrtc/system_media_capture_permissions_mac.mm", + "//chrome/browser/media/webrtc/window_icon_util_mac.mm", + "//chrome/browser/process_singleton_mac.mm", + "//chrome/browser/ui/views/eye_dropper/eye_dropper_view_mac.h", + "//chrome/browser/ui/views/eye_dropper/eye_dropper_view_mac.mm", + ] + } + + if (is_win) { + sources += [ + "//chrome/browser/extensions/global_shortcut_listener_win.cc", + "//chrome/browser/extensions/global_shortcut_listener_win.h", + "//chrome/browser/icon_loader_win.cc", + "//chrome/browser/media/webrtc/window_icon_util_win.cc", + "//chrome/browser/process_singleton_win.cc", + "//chrome/browser/ui/frame/window_frame_util.h", + "//chrome/browser/ui/view_ids.h", + "//chrome/browser/win/chrome_process_finder.cc", + "//chrome/browser/win/chrome_process_finder.h", + "//chrome/browser/win/titlebar_config.h", + "//chrome/child/v8_crashpad_support_win.cc", + "//chrome/child/v8_crashpad_support_win.h", + ] + } + + if (is_linux) { + sources += [ "//chrome/browser/media/webrtc/window_icon_util_ozone.cc" ] + } + + if (use_aura) { + sources += [ + "//chrome/browser/platform_util_aura.cc", + "//chrome/browser/ui/views/eye_dropper/eye_dropper_view_aura.cc", + ] + } + public_deps = [ + "//chrome/browser:dev_ui_browser_resources", "//chrome/common", "//chrome/common:version_header", "//components/keyed_service/content", + "//components/paint_preview/buildflags", "//components/proxy_config", - "//components/security_state/content", + "//components/services/language_detection/public/mojom", "//content/public/browser", + "//services/strings", ] + deps = [ - "//components/feature_engagement:buildflags", + "//chrome/browser:resource_prefetch_predictor_proto", + "//components/optimization_guide/proto:optimization_guide_proto", ] if (is_linux) { sources += [ "//chrome/browser/icon_loader_auralinux.cc" ] + if (use_ozone) { + deps += [ "//ui/ozone" ] + sources += [ + "//chrome/browser/extensions/global_shortcut_listener_ozone.cc", + "//chrome/browser/extensions/global_shortcut_listener_ozone.h", + ] + } + sources += [ + "//chrome/browser/ui/views/status_icons/concat_menu_model.cc", + "//chrome/browser/ui/views/status_icons/concat_menu_model.h", + "//chrome/browser/ui/views/status_icons/status_icon_linux_dbus.cc", + "//chrome/browser/ui/views/status_icons/status_icon_linux_dbus.h", + ] + public_deps += [ + "//components/dbus/menu", + "//components/dbus/thread_linux", + ] + } + + if (is_win) { sources += [ - "//chrome/browser/extensions/global_shortcut_listener_x11.cc", - "//chrome/browser/extensions/global_shortcut_listener_x11.h", + "//chrome/browser/win/icon_reader_service.cc", + "//chrome/browser/win/icon_reader_service.h", ] + public_deps += [ "//chrome/services/util_win:lib" ] } if (enable_desktop_capturer) { sources += [ + "//chrome/browser/media/webrtc/desktop_media_list.cc", "//chrome/browser/media/webrtc/desktop_media_list.h", "//chrome/browser/media/webrtc/desktop_media_list_base.cc", "//chrome/browser/media/webrtc/desktop_media_list_base.h", @@ -79,60 +183,6 @@ static_library("chrome") { deps += [ "//ui/snapshot" ] } - if (enable_color_chooser) { - sources += [ - "//chrome/browser/platform_util.cc", - "//chrome/browser/platform_util.h", - "//chrome/browser/ui/browser_dialogs.h", - "//chrome/browser/ui/color_chooser.h", - ] - - if (use_aura) { - sources += [ - "//chrome/browser/platform_util_aura.cc", - "//chrome/browser/ui/views/color_chooser_aura.cc", - "//chrome/browser/ui/views/color_chooser_aura.h", - ] - deps += [ "//components/feature_engagement" ] - } - - if (is_mac) { - sources += [ - "//chrome/browser/media/webrtc/window_icon_util_mac.mm", - "//chrome/browser/ui/cocoa/color_chooser_mac.h", - "//chrome/browser/ui/cocoa/color_chooser_mac.mm", - ] - deps += [ - "//components/remote_cocoa/app_shim", - "//components/remote_cocoa/browser", - ] - } - - if (is_win) { - sources += [ - "//chrome/browser/media/webrtc/window_icon_util_win.cc", - "//chrome/browser/ui/views/color_chooser_dialog.cc", - "//chrome/browser/ui/views/color_chooser_dialog.h", - "//chrome/browser/ui/views/color_chooser_win.cc", - ] - } - - if (is_linux) { - sources += [ "//chrome/browser/media/webrtc/window_icon_util_x11.cc" ] - } - } - - if (enable_tts) { - sources += [ - "//chrome/browser/speech/tts_controller_delegate_impl.cc", - "//chrome/browser/speech/tts_controller_delegate_impl.h", - "//chrome/browser/speech/tts_message_filter.cc", - "//chrome/browser/speech/tts_message_filter.h", - "//chrome/renderer/tts_dispatcher.cc", - "//chrome/renderer/tts_dispatcher.h", - ] - } - if (enable_widevine) { sources += [ "//chrome/renderer/media/chrome_key_systems.cc", @@ -145,31 +195,43 @@ static_library("chrome") { if (enable_basic_printing) { sources += [ + "//chrome/browser/bad_message.cc", + "//chrome/browser/bad_message.h", "//chrome/browser/printing/print_job.cc", "//chrome/browser/printing/print_job.h", "//chrome/browser/printing/print_job_manager.cc", "//chrome/browser/printing/print_job_manager.h", "//chrome/browser/printing/print_job_worker.cc", "//chrome/browser/printing/print_job_worker.h", + "//chrome/browser/printing/print_job_worker_oop.cc", + "//chrome/browser/printing/print_job_worker_oop.h", "//chrome/browser/printing/print_view_manager_base.cc", "//chrome/browser/printing/print_view_manager_base.h", - "//chrome/browser/printing/print_view_manager_basic.cc", - "//chrome/browser/printing/print_view_manager_basic.h", "//chrome/browser/printing/printer_query.cc", "//chrome/browser/printing/printer_query.h", - "//chrome/browser/printing/printing_message_filter.cc", - "//chrome/browser/printing/printing_message_filter.h", + "//chrome/browser/printing/printing_service.cc", + "//chrome/browser/printing/printing_service.h", ] + + if (enable_oop_printing) { + sources += [ + "//chrome/browser/printing/print_backend_service_manager.cc", + "//chrome/browser/printing/print_backend_service_manager.h", + ] + } + public_deps += [ "//chrome/services/printing:lib", "//components/printing/browser", "//components/printing/renderer", - "//components/services/pdf_compositor/public/cpp:factory", - "//components/services/pdf_compositor/public/mojom", + "//components/services/print_compositor", + "//components/services/print_compositor/public/cpp", + "//components/services/print_compositor/public/mojom", + "//printing/backend", ] + deps += [ "//components/printing/common", - "//components/services/pdf_compositor", "//printing", ] @@ -177,9 +239,186 @@ static_library("chrome") { sources += [ "//chrome/browser/printing/pdf_to_emf_converter.cc", "//chrome/browser/printing/pdf_to_emf_converter.h", - "//chrome/utility/printing_handler.cc", - "//chrome/utility/printing_handler.h", ] } } + + if (enable_picture_in_picture) { + sources += [ + "//chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc", + "//chrome/browser/picture_in_picture/picture_in_picture_window_manager.h", + "//chrome/browser/ui/views/overlay/back_to_tab_image_button.cc", + "//chrome/browser/ui/views/overlay/back_to_tab_image_button.h", + "//chrome/browser/ui/views/overlay/back_to_tab_label_button.cc", + "//chrome/browser/ui/views/overlay/close_image_button.cc", + "//chrome/browser/ui/views/overlay/close_image_button.h", + "//chrome/browser/ui/views/overlay/constants.h", + "//chrome/browser/ui/views/overlay/document_overlay_window_views.cc", + "//chrome/browser/ui/views/overlay/document_overlay_window_views.h", + "//chrome/browser/ui/views/overlay/hang_up_button.cc", + "//chrome/browser/ui/views/overlay/hang_up_button.h", + "//chrome/browser/ui/views/overlay/overlay_window_image_button.cc", + "//chrome/browser/ui/views/overlay/overlay_window_image_button.h", + "//chrome/browser/ui/views/overlay/overlay_window_views.cc", + "//chrome/browser/ui/views/overlay/overlay_window_views.h", + "//chrome/browser/ui/views/overlay/playback_image_button.cc", + "//chrome/browser/ui/views/overlay/playback_image_button.h", + "//chrome/browser/ui/views/overlay/resize_handle_button.cc", + "//chrome/browser/ui/views/overlay/resize_handle_button.h", + "//chrome/browser/ui/views/overlay/skip_ad_label_button.cc", + "//chrome/browser/ui/views/overlay/skip_ad_label_button.h", + "//chrome/browser/ui/views/overlay/toggle_camera_button.cc", + "//chrome/browser/ui/views/overlay/toggle_camera_button.h", + "//chrome/browser/ui/views/overlay/toggle_microphone_button.cc", + "//chrome/browser/ui/views/overlay/toggle_microphone_button.h", + "//chrome/browser/ui/views/overlay/track_image_button.cc", + "//chrome/browser/ui/views/overlay/track_image_button.h", + "//chrome/browser/ui/views/overlay/video_overlay_window_views.cc", + "//chrome/browser/ui/views/overlay/video_overlay_window_views.h", + ] + + deps += [ + "//chrome/app/vector_icons", + "//components/vector_icons:vector_icons", + "//ui/views/controls/webview", + ] + } + + if (enable_electron_extensions) { + sources += [ + "//chrome/browser/extensions/chrome_url_request_util.cc", + "//chrome/browser/extensions/chrome_url_request_util.h", + "//chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc", + "//chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.h", + "//chrome/renderer/extensions/extension_hooks_delegate.cc", + "//chrome/renderer/extensions/extension_hooks_delegate.h", + "//chrome/renderer/extensions/tabs_hooks_delegate.cc", + "//chrome/renderer/extensions/tabs_hooks_delegate.h", + ] + + if (enable_pdf_viewer) { + sources += [ + "//chrome/browser/pdf/chrome_pdf_stream_delegate.cc", + "//chrome/browser/pdf/chrome_pdf_stream_delegate.h", + "//chrome/browser/pdf/pdf_extension_util.cc", + "//chrome/browser/pdf/pdf_extension_util.h", + "//chrome/browser/pdf/pdf_frame_util.cc", + "//chrome/browser/pdf/pdf_frame_util.h", + "//chrome/browser/plugins/pdf_iframe_navigation_throttle.cc", + "//chrome/browser/plugins/pdf_iframe_navigation_throttle.h", + "//chrome/renderer/pepper/chrome_pdf_print_client.cc", + "//chrome/renderer/pepper/chrome_pdf_print_client.h", + ] + deps += [ + "//components/pdf/browser", + "//components/pdf/renderer", + ] + } + } + + if (!is_mas_build) { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump.h" ] + if (is_mac) { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump_mac.cc" ] + } else if (is_win) { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump_win.cc" ] + } else { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump.cc" ] + } + } +} + +source_set("plugins") { + sources = [] + deps = [] + frameworks = [] + + # browser side + sources += [ + "//chrome/browser/renderer_host/pepper/chrome_browser_pepper_host_factory.cc", + "//chrome/browser/renderer_host/pepper/chrome_browser_pepper_host_factory.h", + "//chrome/browser/renderer_host/pepper/pepper_isolated_file_system_message_filter.cc", + "//chrome/browser/renderer_host/pepper/pepper_isolated_file_system_message_filter.h", + ] + + # renderer side + sources += [ + "//chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc", + "//chrome/renderer/pepper/chrome_renderer_pepper_host_factory.h", + "//chrome/renderer/pepper/pepper_flash_font_file_host.cc", + "//chrome/renderer/pepper/pepper_flash_font_file_host.h", + "//chrome/renderer/pepper/pepper_shared_memory_message_filter.cc", + "//chrome/renderer/pepper/pepper_shared_memory_message_filter.h", + ] + + deps += [ + "//components/strings", + "//media:media_buildflags", + "//ppapi/buildflags", + "//ppapi/host", + "//ppapi/proxy", + "//ppapi/proxy:ipc", + "//ppapi/shared_impl", + "//services/device/public/mojom", + "//skia", + "//storage/browser", + ] +} + +# This source set is just so we don't have to depend on all of //chrome/browser +# You may have to add new files here during the upgrade if //chrome/browser/spellchecker +# gets more files +source_set("chrome_spellchecker") { + sources = [] + deps = [] + libs = [] + public_deps = [] + + if (enable_builtin_spellchecker) { + sources += [ + "//chrome/browser/spellchecker/spell_check_host_chrome_impl.cc", + "//chrome/browser/spellchecker/spell_check_host_chrome_impl.h", + "//chrome/browser/spellchecker/spellcheck_custom_dictionary.cc", + "//chrome/browser/spellchecker/spellcheck_custom_dictionary.h", + "//chrome/browser/spellchecker/spellcheck_factory.cc", + "//chrome/browser/spellchecker/spellcheck_factory.h", + "//chrome/browser/spellchecker/spellcheck_hunspell_dictionary.cc", + "//chrome/browser/spellchecker/spellcheck_hunspell_dictionary.h", + "//chrome/browser/spellchecker/spellcheck_language_blocklist_policy_handler.cc", + "//chrome/browser/spellchecker/spellcheck_language_blocklist_policy_handler.h", + "//chrome/browser/spellchecker/spellcheck_language_policy_handler.cc", + "//chrome/browser/spellchecker/spellcheck_language_policy_handler.h", + "//chrome/browser/spellchecker/spellcheck_service.cc", + "//chrome/browser/spellchecker/spellcheck_service.h", + ] + + if (has_spellcheck_panel) { + sources += [ + "//chrome/browser/spellchecker/spell_check_panel_host_impl.cc", + "//chrome/browser/spellchecker/spell_check_panel_host_impl.h", + ] + } + + if (use_browser_spellchecker) { + sources += [ + "//chrome/browser/spellchecker/spelling_request.cc", + "//chrome/browser/spellchecker/spelling_request.h", + ] + } + + deps += [ + "//base:base_static", + "//components/language/core/browser", + "//components/spellcheck:buildflags", + "//components/sync", + ] + + public_deps += [ "//chrome/common:constants" ] + } + + public_deps += [ + "//components/spellcheck/browser", + "//components/spellcheck/common", + "//components/spellcheck/renderer", + ] } diff --git a/chromium_src/chrome/browser/process_singleton.h b/chromium_src/chrome/browser/process_singleton.h deleted file mode 100644 index 2e1436846421a..0000000000000 --- a/chromium_src/chrome/browser/process_singleton.h +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef CHROME_BROWSER_PROCESS_SINGLETON_H_ -#define CHROME_BROWSER_PROCESS_SINGLETON_H_ - -#if defined(OS_WIN) -#include -#endif // defined(OS_WIN) - -#include -#include - -#include "base/callback.h" -#include "base/command_line.h" -#include "base/files/file_path.h" -#include "base/logging.h" -#include "base/memory/ref_counted.h" -#include "base/process/process.h" -#include "base/sequence_checker.h" -#include "ui/gfx/native_widget_types.h" - -#if defined(OS_POSIX) && !defined(OS_ANDROID) -#include "base/files/scoped_temp_dir.h" -#endif - -#if defined(OS_WIN) -#include "base/win/message_window.h" -#endif // defined(OS_WIN) - -namespace base { -class CommandLine; -} - -// ProcessSingleton ---------------------------------------------------------- -// -// This class allows different browser processes to communicate with -// each other. It is named according to the user data directory, so -// we can be sure that no more than one copy of the application can be -// running at once with a given data directory. -// -// Implementation notes: -// - the Windows implementation uses an invisible global message window; -// - the Linux implementation uses a Unix domain socket in the user data dir. - -class ProcessSingleton { - public: - enum NotifyResult { - PROCESS_NONE, - PROCESS_NOTIFIED, - PROFILE_IN_USE, - LOCK_ERROR, - }; - - // Implement this callback to handle notifications from other processes. The - // callback will receive the command line and directory with which the other - // Chrome process was launched. Return true if the command line will be - // handled within the current browser instance or false if the remote process - // should handle it (i.e., because the current process is shutting down). - using NotificationCallback = base::RepeatingCallback; - - ProcessSingleton(const base::FilePath& user_data_dir, - const NotificationCallback& notification_callback); - ~ProcessSingleton(); - - // Notify another process, if available. Otherwise sets ourselves as the - // singleton instance. Returns PROCESS_NONE if we became the singleton - // instance. Callers are guaranteed to either have notified an existing - // process or have grabbed the singleton (unless the profile is locked by an - // unreachable process). - // TODO(brettw): Make the implementation of this method non-platform-specific - // by making Linux re-use the Windows implementation. - NotifyResult NotifyOtherProcessOrCreate(); - void StartListeningOnSocket(); - void OnBrowserReady(); - - // Sets ourself up as the singleton instance. Returns true on success. If - // false is returned, we are not the singleton instance and the caller must - // exit. - // NOTE: Most callers should generally prefer NotifyOtherProcessOrCreate() to - // this method, only callers for whom failure is preferred to notifying - // another process should call this directly. - bool Create(); - - // Clear any lock state during shutdown. - void Cleanup(); - -#if defined(OS_POSIX) && !defined(OS_ANDROID) - static void DisablePromptForTesting(); -#endif -#if defined(OS_WIN) - // Called to query whether to kill a hung browser process that has visible - // windows. Return true to allow killing the hung process. - using ShouldKillRemoteProcessCallback = base::RepeatingCallback; - void OverrideShouldKillRemoteProcessCallbackForTesting( - const ShouldKillRemoteProcessCallback& display_dialog_callback); -#endif - - protected: - // Notify another process, if available. - // Returns true if another process was found and notified, false if we should - // continue with the current process. - // On Windows, Create() has to be called before this. - NotifyResult NotifyOtherProcess(); - -#if defined(OS_POSIX) && !defined(OS_ANDROID) - // Exposed for testing. We use a timeout on Linux, and in tests we want - // this timeout to be short. - NotifyResult NotifyOtherProcessWithTimeout( - const base::CommandLine& command_line, - int retry_attempts, - const base::TimeDelta& timeout, - bool kill_unresponsive); - NotifyResult NotifyOtherProcessWithTimeoutOrCreate( - const base::CommandLine& command_line, - int retry_attempts, - const base::TimeDelta& timeout); - void OverrideCurrentPidForTesting(base::ProcessId pid); - void OverrideKillCallbackForTesting( - const base::RepeatingCallback& callback); -#endif - - private: - NotificationCallback notification_callback_; // Handler for notifications. - -#if defined(OS_WIN) - HWND remote_window_; // The HWND_MESSAGE of another browser. - base::win::MessageWindow window_; // The message-only window. - bool is_virtualized_; // Stuck inside Microsoft Softricity VM environment. - HANDLE lock_file_; - base::FilePath user_data_dir_; - ShouldKillRemoteProcessCallback should_kill_remote_process_callback_; -#elif defined(OS_POSIX) && !defined(OS_ANDROID) - // Start listening to the socket. - void StartListening(int sock); - - // Return true if the given pid is one of our child processes. - // Assumes that the current pid is the root of all pids of the current - // instance. - bool IsSameChromeInstance(pid_t pid); - - // Extract the process's pid from a symbol link path and if it is on - // the same host, kill the process, unlink the lock file and return true. - // If the process is part of the same chrome instance, unlink the lock file - // and return true without killing it. - // If the process is on a different host, return false. - bool KillProcessByLockPath(); - - // Default function to kill a process, overridable by tests. - void KillProcess(int pid); - - // Allow overriding for tests. - base::ProcessId current_pid_; - - // Function to call when the other process is hung and needs to be killed. - // Allows overriding for tests. - base::Callback kill_callback_; - - // Path in file system to the socket. - base::FilePath socket_path_; - - // Path in file system to the lock. - base::FilePath lock_path_; - - // Path in file system to the cookie file. - base::FilePath cookie_path_; - - // Temporary directory to hold the socket. - base::ScopedTempDir socket_dir_; - - // Helper class for linux specific messages. LinuxWatcher is ref counted - // because it posts messages between threads. - class LinuxWatcher; - scoped_refptr watcher_; - int sock_; - bool listen_on_ready_ = false; -#endif - - SEQUENCE_CHECKER(sequence_checker_); - - DISALLOW_COPY_AND_ASSIGN(ProcessSingleton); -}; - -#endif // CHROME_BROWSER_PROCESS_SINGLETON_H_ diff --git a/chromium_src/chrome/browser/process_singleton_posix.cc b/chromium_src/chrome/browser/process_singleton_posix.cc deleted file mode 100644 index d3d4924db1e8a..0000000000000 --- a/chromium_src/chrome/browser/process_singleton_posix.cc +++ /dev/null @@ -1,1107 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// On Linux, when the user tries to launch a second copy of chrome, we check -// for a socket in the user's profile directory. If the socket file is open we -// send a message to the first chrome browser process with the current -// directory and second process command line flags. The second process then -// exits. -// -// Because many networked filesystem implementations do not support unix domain -// sockets, we create the socket in a temporary directory and create a symlink -// in the profile. This temporary directory is no longer bound to the profile, -// and may disappear across a reboot or login to a separate session. To bind -// them, we store a unique cookie in the profile directory, which must also be -// present in the remote directory to connect. The cookie is checked both before -// and after the connection. /tmp is sticky, and different Chrome sessions use -// different cookies. Thus, a matching cookie before and after means the -// connection was to a directory with a valid cookie. -// -// We also have a lock file, which is a symlink to a non-existent destination. -// The destination is a string containing the hostname and process id of -// chrome's browser process, eg. "SingletonLock -> example.com-9156". When the -// first copy of chrome exits it will delete the lock file on shutdown, so that -// a different instance on a different host may then use the profile directory. -// -// If writing to the socket fails, the hostname in the lock is checked to see if -// another instance is running a different host using a shared filesystem (nfs, -// etc.) If the hostname differs an error is displayed and the second process -// exits. Otherwise the first process (if any) is killed and the second process -// starts as normal. -// -// When the second process sends the current directory and command line flags to -// the first process, it waits for an ACK message back from the first process -// for a certain time. If there is no ACK message back in time, then the first -// process will be considered as hung for some reason. The second process then -// retrieves the process id from the symbol link and kills it by sending -// SIGKILL. Then the second process starts as normal. - -#include "chrome/browser/process_singleton.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include "shell/browser/browser.h" -#include "shell/common/atom_command_line.h" - -#include "base/base_paths.h" -#include "base/bind.h" -#include "base/command_line.h" -#include "base/files/file_descriptor_watcher_posix.h" -#include "base/files/file_path.h" -#include "base/files/file_util.h" -#include "base/location.h" -#include "base/logging.h" -#include "base/macros.h" -#include "base/memory/ref_counted.h" -#include "base/message_loop/message_loop.h" -#include "base/metrics/histogram_macros.h" -#include "base/path_service.h" -#include "base/posix/eintr_wrapper.h" -#include "base/posix/safe_strerror.h" -#include "base/rand_util.h" -#include "base/sequenced_task_runner_helpers.h" -#include "base/single_thread_task_runner.h" -#include "base/stl_util.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/string_split.h" -#include "base/strings/string_util.h" -#include "base/strings/stringprintf.h" -#include "base/strings/sys_string_conversions.h" -#include "base/strings/utf_string_conversions.h" -#include "base/task/post_task.h" -#include "base/threading/platform_thread.h" -#include "base/threading/thread_restrictions.h" -#include "base/threading/thread_task_runner_handle.h" -#include "base/time/time.h" -#include "base/timer/timer.h" -#include "build/build_config.h" -#include "content/public/browser/browser_task_traits.h" -#include "content/public/browser/browser_thread.h" -#include "net/base/network_interfaces.h" -#include "ui/base/l10n/l10n_util.h" - -#if defined(TOOLKIT_VIEWS) && defined(OS_LINUX) && !defined(OS_CHROMEOS) -#include "ui/views/linux_ui/linux_ui.h" -#endif - -using content::BrowserThread; - -namespace { - -// Timeout for the current browser process to respond. 20 seconds should be -// enough. -const int kTimeoutInSeconds = 20; -// Number of retries to notify the browser. 20 retries over 20 seconds = 1 try -// per second. -const int kRetryAttempts = 20; -static bool g_disable_prompt; -const char kStartToken[] = "START"; -const char kACKToken[] = "ACK"; -const char kShutdownToken[] = "SHUTDOWN"; -const char kTokenDelimiter = '\0'; -const int kMaxMessageLength = 32 * 1024; -const int kMaxACKMessageLength = base::size(kShutdownToken) - 1; - -const char kLockDelimiter = '-'; - -const base::FilePath::CharType kSingletonCookieFilename[] = - FILE_PATH_LITERAL("SingletonCookie"); - -const base::FilePath::CharType kSingletonLockFilename[] = - FILE_PATH_LITERAL("SingletonLock"); -const base::FilePath::CharType kSingletonSocketFilename[] = - FILE_PATH_LITERAL("SS"); - -// Set the close-on-exec bit on a file descriptor. -// Returns 0 on success, -1 on failure. -int SetCloseOnExec(int fd) { - int flags = fcntl(fd, F_GETFD, 0); - if (-1 == flags) - return flags; - if (flags & FD_CLOEXEC) - return 0; - return fcntl(fd, F_SETFD, flags | FD_CLOEXEC); -} - -// Close a socket and check return value. -void CloseSocket(int fd) { - int rv = IGNORE_EINTR(close(fd)); - DCHECK_EQ(0, rv) << "Error closing socket: " << base::safe_strerror(errno); -} - -// Write a message to a socket fd. -bool WriteToSocket(int fd, const char* message, size_t length) { - DCHECK(message); - DCHECK(length); - size_t bytes_written = 0; - do { - ssize_t rv = HANDLE_EINTR( - write(fd, message + bytes_written, length - bytes_written)); - if (rv < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - // The socket shouldn't block, we're sending so little data. Just give - // up here, since NotifyOtherProcess() doesn't have an asynchronous api. - LOG(ERROR) << "ProcessSingleton would block on write(), so it gave up."; - return false; - } - PLOG(ERROR) << "write() failed"; - return false; - } - bytes_written += rv; - } while (bytes_written < length); - - return true; -} - -struct timeval TimeDeltaToTimeVal(const base::TimeDelta& delta) { - struct timeval result; - result.tv_sec = delta.InSeconds(); - result.tv_usec = delta.InMicroseconds() % base::Time::kMicrosecondsPerSecond; - return result; -} - -// Wait a socket for read for a certain timeout. -// Returns -1 if error occurred, 0 if timeout reached, > 0 if the socket is -// ready for read. -int WaitSocketForRead(int fd, const base::TimeDelta& timeout) { - fd_set read_fds; - struct timeval tv = TimeDeltaToTimeVal(timeout); - - FD_ZERO(&read_fds); - FD_SET(fd, &read_fds); - - return HANDLE_EINTR(select(fd + 1, &read_fds, NULL, NULL, &tv)); -} - -// Read a message from a socket fd, with an optional timeout. -// If |timeout| <= 0 then read immediately. -// Return number of bytes actually read, or -1 on error. -ssize_t ReadFromSocket(int fd, - char* buf, - size_t bufsize, - const base::TimeDelta& timeout) { - if (timeout > base::TimeDelta()) { - int rv = WaitSocketForRead(fd, timeout); - if (rv <= 0) - return rv; - } - - size_t bytes_read = 0; - do { - ssize_t rv = HANDLE_EINTR(read(fd, buf + bytes_read, bufsize - bytes_read)); - if (rv < 0) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { - PLOG(ERROR) << "read() failed"; - return rv; - } else { - // It would block, so we just return what has been read. - return bytes_read; - } - } else if (!rv) { - // No more data to read. - return bytes_read; - } else { - bytes_read += rv; - } - } while (bytes_read < bufsize); - - return bytes_read; -} - -// Set up a sockaddr appropriate for messaging. -void SetupSockAddr(const std::string& path, struct sockaddr_un* addr) { - addr->sun_family = AF_UNIX; - CHECK(path.length() < base::size(addr->sun_path)) - << "Socket path too long: " << path; - base::strlcpy(addr->sun_path, path.c_str(), base::size(addr->sun_path)); -} - -// Set up a socket appropriate for messaging. -int SetupSocketOnly() { - int sock = socket(PF_UNIX, SOCK_STREAM, 0); - PCHECK(sock >= 0) << "socket() failed"; - - DCHECK(base::SetNonBlocking(sock)) << "Failed to make non-blocking socket."; - int rv = SetCloseOnExec(sock); - DCHECK_EQ(0, rv) << "Failed to set CLOEXEC on socket."; - - return sock; -} - -// Set up a socket and sockaddr appropriate for messaging. -void SetupSocket(const std::string& path, int* sock, struct sockaddr_un* addr) { - *sock = SetupSocketOnly(); - SetupSockAddr(path, addr); -} - -// Read a symbolic link, return empty string if given path is not a symbol link. -base::FilePath ReadLink(const base::FilePath& path) { - base::FilePath target; - if (!base::ReadSymbolicLink(path, &target)) { - // The only errno that should occur is ENOENT. - if (errno != 0 && errno != ENOENT) - PLOG(ERROR) << "readlink(" << path.value() << ") failed"; - } - return target; -} - -// Unlink a path. Return true on success. -bool UnlinkPath(const base::FilePath& path) { - int rv = unlink(path.value().c_str()); - if (rv < 0 && errno != ENOENT) - PLOG(ERROR) << "Failed to unlink " << path.value(); - - return rv == 0; -} - -// Create a symlink. Returns true on success. -bool SymlinkPath(const base::FilePath& target, const base::FilePath& path) { - if (!base::CreateSymbolicLink(target, path)) { - // Double check the value in case symlink suceeded but we got an incorrect - // failure due to NFS packet loss & retry. - int saved_errno = errno; - if (ReadLink(path) != target) { - // If we failed to create the lock, most likely another instance won the - // startup race. - errno = saved_errno; - PLOG(ERROR) << "Failed to create " << path.value(); - return false; - } - } - return true; -} - -// Extract the hostname and pid from the lock symlink. -// Returns true if the lock existed. -bool ParseLockPath(const base::FilePath& path, - std::string* hostname, - int* pid) { - std::string real_path = ReadLink(path).value(); - if (real_path.empty()) - return false; - - std::string::size_type pos = real_path.rfind(kLockDelimiter); - - // If the path is not a symbolic link, or doesn't contain what we expect, - // bail. - if (pos == std::string::npos) { - *hostname = ""; - *pid = -1; - return true; - } - - *hostname = real_path.substr(0, pos); - - const std::string& pid_str = real_path.substr(pos + 1); - if (!base::StringToInt(pid_str, pid)) - *pid = -1; - - return true; -} - -// Returns true if the user opted to unlock the profile. -bool DisplayProfileInUseError(const base::FilePath& lock_path, - const std::string& hostname, - int pid) { - return true; -} - -bool IsChromeProcess(pid_t pid) { - base::FilePath other_chrome_path(base::GetProcessExecutablePath(pid)); - - auto* command_line = base::CommandLine::ForCurrentProcess(); - base::FilePath exec_path(command_line->GetProgram()); - base::PathService::Get(base::FILE_EXE, &exec_path); - - return (!other_chrome_path.empty() && - other_chrome_path.BaseName() == exec_path.BaseName()); -} - -// A helper class to hold onto a socket. -class ScopedSocket { - public: - ScopedSocket() : fd_(-1) { Reset(); } - ~ScopedSocket() { Close(); } - int fd() { return fd_; } - void Reset() { - Close(); - fd_ = SetupSocketOnly(); - } - void Close() { - if (fd_ >= 0) - CloseSocket(fd_); - fd_ = -1; - } - - private: - int fd_; -}; - -// Returns a random string for uniquifying profile connections. -std::string GenerateCookie() { - return base::NumberToString(base::RandUint64()); -} - -bool CheckCookie(const base::FilePath& path, const base::FilePath& cookie) { - return (cookie == ReadLink(path)); -} - -bool IsAppSandboxed() { -#if defined(OS_MACOSX) - // NB: There is no sane API for this, we have to just guess by - // reading tea leaves - base::FilePath home_dir; - if (!base::PathService::Get(base::DIR_HOME, &home_dir)) { - return false; - } - - return home_dir.value().find("Library/Containers") != std::string::npos; -#else - return false; -#endif // defined(OS_MACOSX) -} - -bool ConnectSocket(ScopedSocket* socket, - const base::FilePath& socket_path, - const base::FilePath& cookie_path) { - base::FilePath socket_target; - if (base::ReadSymbolicLink(socket_path, &socket_target)) { - // It's a symlink. Read the cookie. - base::FilePath cookie = ReadLink(cookie_path); - if (cookie.empty()) - return false; - base::FilePath remote_cookie = - socket_target.DirName().Append(kSingletonCookieFilename); - // Verify the cookie before connecting. - if (!CheckCookie(remote_cookie, cookie)) - return false; - // Now we know the directory was (at that point) created by the profile - // owner. Try to connect. - sockaddr_un addr; - SetupSockAddr(socket_target.value(), &addr); - int ret = HANDLE_EINTR(connect( - socket->fd(), reinterpret_cast(&addr), sizeof(addr))); - if (ret != 0) - return false; - // Check the cookie again. We only link in /tmp, which is sticky, so, if the - // directory is still correct, it must have been correct in-between when we - // connected. POSIX, sadly, lacks a connectat(). - if (!CheckCookie(remote_cookie, cookie)) { - socket->Reset(); - return false; - } - // Success! - return true; - } else if (errno == EINVAL) { - // It exists, but is not a symlink (or some other error we detect - // later). Just connect to it directly; this is an older version of Chrome. - sockaddr_un addr; - SetupSockAddr(socket_path.value(), &addr); - int ret = HANDLE_EINTR(connect( - socket->fd(), reinterpret_cast(&addr), sizeof(addr))); - return (ret == 0); - } else { - // File is missing, or other error. - if (errno != ENOENT) - PLOG(ERROR) << "readlink failed"; - return false; - } -} - -#if defined(OS_MACOSX) -bool ReplaceOldSingletonLock(const base::FilePath& symlink_content, - const base::FilePath& lock_path) { - // Try taking an flock(2) on the file. Failure means the lock is taken so we - // should quit. - base::ScopedFD lock_fd(HANDLE_EINTR( - open(lock_path.value().c_str(), O_RDWR | O_CREAT | O_SYMLINK, 0644))); - if (!lock_fd.is_valid()) { - PLOG(ERROR) << "Could not open singleton lock"; - return false; - } - - int rc = HANDLE_EINTR(flock(lock_fd.get(), LOCK_EX | LOCK_NB)); - if (rc == -1) { - if (errno == EWOULDBLOCK) { - LOG(ERROR) << "Singleton lock held by old process."; - } else { - PLOG(ERROR) << "Error locking singleton lock"; - } - return false; - } - - // Successfully taking the lock means we can replace it with the a new symlink - // lock. We never flock() the lock file from now on. I.e. we assume that an - // old version of Chrome will not run with the same user data dir after this - // version has run. - if (!base::DeleteFile(lock_path, false)) { - PLOG(ERROR) << "Could not delete old singleton lock."; - return false; - } - - return SymlinkPath(symlink_content, lock_path); -} -#endif // defined(OS_MACOSX) - -} // namespace - -/////////////////////////////////////////////////////////////////////////////// -// ProcessSingleton::LinuxWatcher -// A helper class for a Linux specific implementation of the process singleton. -// This class sets up a listener on the singleton socket and handles parsing -// messages that come in on the singleton socket. -class ProcessSingleton::LinuxWatcher - : public base::RefCountedThreadSafe { - public: - // A helper class to read message from an established socket. - class SocketReader { - public: - SocketReader(ProcessSingleton::LinuxWatcher* parent, - scoped_refptr ui_task_runner, - int fd) - : parent_(parent), - ui_task_runner_(ui_task_runner), - fd_(fd), - bytes_read_(0) { - DCHECK_CURRENTLY_ON(BrowserThread::IO); - // Wait for reads. - fd_watch_controller_ = base::FileDescriptorWatcher::WatchReadable( - fd, base::BindRepeating(&SocketReader::OnSocketCanReadWithoutBlocking, - base::Unretained(this))); - // If we haven't completed in a reasonable amount of time, give up. - timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(kTimeoutInSeconds), - this, &SocketReader::CleanupAndDeleteSelf); - } - - ~SocketReader() { CloseSocket(fd_); } - - // Finish handling the incoming message by optionally sending back an ACK - // message and removing this SocketReader. - void FinishWithACK(const char* message, size_t length); - - private: - void OnSocketCanReadWithoutBlocking(); - - void CleanupAndDeleteSelf() { - DCHECK_CURRENTLY_ON(BrowserThread::IO); - - parent_->RemoveSocketReader(this); - // We're deleted beyond this point. - } - - // Controls watching |fd_|. - std::unique_ptr - fd_watch_controller_; - - // The ProcessSingleton::LinuxWatcher that owns us. - ProcessSingleton::LinuxWatcher* const parent_; - - // A reference to the UI task runner. - scoped_refptr ui_task_runner_; - - // The file descriptor we're reading. - const int fd_; - - // Store the message in this buffer. - char buf_[kMaxMessageLength]; - - // Tracks the number of bytes we've read in case we're getting partial - // reads. - size_t bytes_read_; - - base::OneShotTimer timer_; - - DISALLOW_COPY_AND_ASSIGN(SocketReader); - }; - - // We expect to only be constructed on the UI thread. - explicit LinuxWatcher(ProcessSingleton* parent) - : ui_task_runner_(base::ThreadTaskRunnerHandle::Get()), parent_(parent) {} - - // Start listening for connections on the socket. This method should be - // called from the IO thread. - void StartListening(int socket); - - // This method determines if we should use the same process and if we should, - // opens a new browser tab. This runs on the UI thread. - // |reader| is for sending back ACK message. - void HandleMessage(const std::string& current_dir, - const std::vector& argv, - SocketReader* reader); - - private: - friend struct BrowserThread::DeleteOnThread; - friend class base::DeleteHelper; - - ~LinuxWatcher() { DCHECK_CURRENTLY_ON(BrowserThread::IO); } - - void OnSocketCanReadWithoutBlocking(int socket); - - // Removes and deletes the SocketReader. - void RemoveSocketReader(SocketReader* reader); - - std::unique_ptr socket_watcher_; - - // A reference to the UI message loop (i.e., the message loop we were - // constructed on). - scoped_refptr ui_task_runner_; - - // The ProcessSingleton that owns us. - ProcessSingleton* const parent_; - - std::set> readers_; - - DISALLOW_COPY_AND_ASSIGN(LinuxWatcher); -}; - -void ProcessSingleton::LinuxWatcher::OnSocketCanReadWithoutBlocking( - int socket) { - DCHECK_CURRENTLY_ON(BrowserThread::IO); - // Accepting incoming client. - sockaddr_un from; - socklen_t from_len = sizeof(from); - int connection_socket = HANDLE_EINTR( - accept(socket, reinterpret_cast(&from), &from_len)); - if (-1 == connection_socket) { - PLOG(ERROR) << "accept() failed"; - return; - } - DCHECK(base::SetNonBlocking(connection_socket)) - << "Failed to make non-blocking socket."; - readers_.insert( - std::make_unique(this, ui_task_runner_, connection_socket)); -} - -void ProcessSingleton::LinuxWatcher::StartListening(int socket) { - DCHECK_CURRENTLY_ON(BrowserThread::IO); - // Watch for client connections on this socket. - socket_watcher_ = base::FileDescriptorWatcher::WatchReadable( - socket, base::BindRepeating(&LinuxWatcher::OnSocketCanReadWithoutBlocking, - base::Unretained(this), socket)); -} - -void ProcessSingleton::LinuxWatcher::HandleMessage( - const std::string& current_dir, - const std::vector& argv, - SocketReader* reader) { - DCHECK(ui_task_runner_->BelongsToCurrentThread()); - DCHECK(reader); - - if (parent_->notification_callback_.Run(argv, base::FilePath(current_dir))) { - // Send back "ACK" message to prevent the client process from starting up. - reader->FinishWithACK(kACKToken, base::size(kACKToken) - 1); - } else { - LOG(WARNING) << "Not handling interprocess notification as browser" - " is shutting down"; - // Send back "SHUTDOWN" message, so that the client process can start up - // without killing this process. - reader->FinishWithACK(kShutdownToken, base::size(kShutdownToken) - 1); - return; - } -} - -void ProcessSingleton::LinuxWatcher::RemoveSocketReader(SocketReader* reader) { - DCHECK_CURRENTLY_ON(BrowserThread::IO); - DCHECK(reader); - auto it = std::find_if(readers_.begin(), readers_.end(), - [reader](const std::unique_ptr& ptr) { - return ptr.get() == reader; - }); - readers_.erase(it); -} - -/////////////////////////////////////////////////////////////////////////////// -// ProcessSingleton::LinuxWatcher::SocketReader -// - -void ProcessSingleton::LinuxWatcher::SocketReader:: - OnSocketCanReadWithoutBlocking() { - DCHECK_CURRENTLY_ON(BrowserThread::IO); - while (bytes_read_ < sizeof(buf_)) { - ssize_t rv = - HANDLE_EINTR(read(fd_, buf_ + bytes_read_, sizeof(buf_) - bytes_read_)); - if (rv < 0) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { - PLOG(ERROR) << "read() failed"; - CloseSocket(fd_); - return; - } else { - // It would block, so we just return and continue to watch for the next - // opportunity to read. - return; - } - } else if (!rv) { - // No more data to read. It's time to process the message. - break; - } else { - bytes_read_ += rv; - } - } - - // Validate the message. The shortest message is kStartToken\0x\0x - const size_t kMinMessageLength = base::size(kStartToken) + 4; - if (bytes_read_ < kMinMessageLength) { - buf_[bytes_read_] = 0; - LOG(ERROR) << "Invalid socket message (wrong length):" << buf_; - CleanupAndDeleteSelf(); - return; - } - - std::string str(buf_, bytes_read_); - std::vector tokens = - base::SplitString(str, std::string(1, kTokenDelimiter), - base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); - - if (tokens.size() < 3 || tokens[0] != kStartToken) { - LOG(ERROR) << "Wrong message format: " << str; - CleanupAndDeleteSelf(); - return; - } - - // Stop the expiration timer to prevent this SocketReader object from being - // terminated unexpectly. - timer_.Stop(); - - std::string current_dir = tokens[1]; - // Remove the first two tokens. The remaining tokens should be the command - // line argv array. - tokens.erase(tokens.begin()); - tokens.erase(tokens.begin()); - - // Return to the UI thread to handle opening a new browser tab. - ui_task_runner_->PostTask( - FROM_HERE, base::BindOnce(&ProcessSingleton::LinuxWatcher::HandleMessage, - parent_, current_dir, tokens, this)); - fd_watch_controller_.reset(); - - // LinuxWatcher::HandleMessage() is in charge of destroying this SocketReader - // object by invoking SocketReader::FinishWithACK(). -} - -void ProcessSingleton::LinuxWatcher::SocketReader::FinishWithACK( - const char* message, - size_t length) { - if (message && length) { - // Not necessary to care about the return value. - WriteToSocket(fd_, message, length); - } - - if (shutdown(fd_, SHUT_WR) < 0) - PLOG(ERROR) << "shutdown() failed"; - - base::PostTaskWithTraits( - FROM_HERE, {BrowserThread::IO}, - base::BindOnce(&ProcessSingleton::LinuxWatcher::RemoveSocketReader, - parent_, this)); - // We will be deleted once the posted RemoveSocketReader task runs. -} - -/////////////////////////////////////////////////////////////////////////////// -// ProcessSingleton -// -ProcessSingleton::ProcessSingleton( - const base::FilePath& user_data_dir, - const NotificationCallback& notification_callback) - : notification_callback_(notification_callback), - current_pid_(base::GetCurrentProcId()) { - // The user_data_dir may have not been created yet. - base::ThreadRestrictions::ScopedAllowIO allow_io; - base::CreateDirectoryAndGetError(user_data_dir, nullptr); - - socket_path_ = user_data_dir.Append(kSingletonSocketFilename); - lock_path_ = user_data_dir.Append(kSingletonLockFilename); - cookie_path_ = user_data_dir.Append(kSingletonCookieFilename); - - kill_callback_ = base::BindRepeating(&ProcessSingleton::KillProcess, - base::Unretained(this)); -} - -ProcessSingleton::~ProcessSingleton() { - DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - // Manually free resources with IO explicitly allowed. - base::ThreadRestrictions::ScopedAllowIO allow_io; - watcher_ = nullptr; - ignore_result(socket_dir_.Delete()); -} - -ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() { - return NotifyOtherProcessWithTimeout( - *base::CommandLine::ForCurrentProcess(), kRetryAttempts, - base::TimeDelta::FromSeconds(kTimeoutInSeconds), true); -} - -ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout( - const base::CommandLine& cmd_line, - int retry_attempts, - const base::TimeDelta& timeout, - bool kill_unresponsive) { - DCHECK_GE(retry_attempts, 0); - DCHECK_GE(timeout.InMicroseconds(), 0); - - base::TimeDelta sleep_interval = timeout / retry_attempts; - - ScopedSocket socket; - for (int retries = 0; retries <= retry_attempts; ++retries) { - // Try to connect to the socket. - if (ConnectSocket(&socket, socket_path_, cookie_path_)) - break; - - // If we're in a race with another process, they may be in Create() and have - // created the lock but not attached to the socket. So we check if the - // process with the pid from the lockfile is currently running and is a - // chrome browser. If so, we loop and try again for |timeout|. - - std::string hostname; - int pid; - if (!ParseLockPath(lock_path_, &hostname, &pid)) { - // No lockfile exists. - return PROCESS_NONE; - } - - if (hostname.empty()) { - // Invalid lockfile. - UnlinkPath(lock_path_); - return PROCESS_NONE; - } - - if (hostname != net::GetHostName() && !IsChromeProcess(pid)) { - // Locked by process on another host. If the user selected to unlock - // the profile, try to continue; otherwise quit. - if (DisplayProfileInUseError(lock_path_, hostname, pid)) { - UnlinkPath(lock_path_); - return PROCESS_NONE; - } - return PROFILE_IN_USE; - } - - if (!IsChromeProcess(pid)) { - // Orphaned lockfile (no process with pid, or non-chrome process.) - UnlinkPath(lock_path_); - return PROCESS_NONE; - } - - if (IsSameChromeInstance(pid)) { - // Orphaned lockfile (pid is part of same chrome instance we are, even - // though we haven't tried to create a lockfile yet). - UnlinkPath(lock_path_); - return PROCESS_NONE; - } - - if (retries == retry_attempts) { - // Retries failed. Kill the unresponsive chrome process and continue. - if (!kill_unresponsive || !KillProcessByLockPath()) - return PROFILE_IN_USE; - return PROCESS_NONE; - } - - base::PlatformThread::Sleep(sleep_interval); - } - - timeval socket_timeout = TimeDeltaToTimeVal(timeout); - setsockopt(socket.fd(), SOL_SOCKET, SO_SNDTIMEO, &socket_timeout, - sizeof(socket_timeout)); - - // Found another process, prepare our command line - // format is "START\0\0\0...\0". - std::string to_send(kStartToken); - to_send.push_back(kTokenDelimiter); - - base::FilePath current_dir; - if (!base::PathService::Get(base::DIR_CURRENT, ¤t_dir)) - return PROCESS_NONE; - to_send.append(current_dir.value()); - - const std::vector& argv = electron::AtomCommandLine::argv(); - for (std::vector::const_iterator it = argv.begin(); - it != argv.end(); ++it) { - to_send.push_back(kTokenDelimiter); - to_send.append(*it); - } - - // Send the message - if (!WriteToSocket(socket.fd(), to_send.data(), to_send.length())) { - // Try to kill the other process, because it might have been dead. - if (!kill_unresponsive || !KillProcessByLockPath()) - return PROFILE_IN_USE; - return PROCESS_NONE; - } - - if (shutdown(socket.fd(), SHUT_WR) < 0) - PLOG(ERROR) << "shutdown() failed"; - - // Read ACK message from the other process. It might be blocked for a certain - // timeout, to make sure the other process has enough time to return ACK. - char buf[kMaxACKMessageLength + 1]; - ssize_t len = ReadFromSocket(socket.fd(), buf, kMaxACKMessageLength, timeout); - - // Failed to read ACK, the other process might have been frozen. - if (len <= 0) { - if (!kill_unresponsive || !KillProcessByLockPath()) - return PROFILE_IN_USE; - return PROCESS_NONE; - } - - buf[len] = '\0'; - if (strncmp(buf, kShutdownToken, base::size(kShutdownToken) - 1) == 0) { - // The other process is shutting down, it's safe to start a new process. - return PROCESS_NONE; - } else if (strncmp(buf, kACKToken, base::size(kACKToken) - 1) == 0) { -#if defined(TOOLKIT_VIEWS) && defined(OS_LINUX) && !defined(OS_CHROMEOS) - // Likely NULL in unit tests. - views::LinuxUI* linux_ui = views::LinuxUI::instance(); - if (linux_ui) - linux_ui->NotifyWindowManagerStartupComplete(); -#endif - - // Assume the other process is handling the request. - return PROCESS_NOTIFIED; - } - - NOTREACHED() << "The other process returned unknown message: " << buf; - return PROCESS_NOTIFIED; -} - -ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate() { - return NotifyOtherProcessWithTimeoutOrCreate( - *base::CommandLine::ForCurrentProcess(), kRetryAttempts, - base::TimeDelta::FromSeconds(kTimeoutInSeconds)); -} - -void ProcessSingleton::StartListeningOnSocket() { - watcher_ = new LinuxWatcher(this); - base::PostTaskWithTraits( - FROM_HERE, {BrowserThread::IO}, - base::BindOnce(&ProcessSingleton::LinuxWatcher::StartListening, watcher_, - sock_)); -} - -void ProcessSingleton::OnBrowserReady() { - if (listen_on_ready_) { - StartListeningOnSocket(); - listen_on_ready_ = false; - } -} - -ProcessSingleton::NotifyResult -ProcessSingleton::NotifyOtherProcessWithTimeoutOrCreate( - const base::CommandLine& command_line, - int retry_attempts, - const base::TimeDelta& timeout) { - const base::TimeTicks begin_ticks = base::TimeTicks::Now(); - NotifyResult result = NotifyOtherProcessWithTimeout( - command_line, retry_attempts, timeout, true); - if (result != PROCESS_NONE) { - if (result == PROCESS_NOTIFIED) { - UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToNotify", - base::TimeTicks::Now() - begin_ticks); - } else { - UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToFailure", - base::TimeTicks::Now() - begin_ticks); - } - return result; - } - - if (Create()) { - UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToCreate", - base::TimeTicks::Now() - begin_ticks); - return PROCESS_NONE; - } - - // If the Create() failed, try again to notify. (It could be that another - // instance was starting at the same time and managed to grab the lock before - // we did.) - // This time, we don't want to kill anything if we aren't successful, since we - // aren't going to try to take over the lock ourselves. - result = NotifyOtherProcessWithTimeout(command_line, retry_attempts, timeout, - false); - - if (result == PROCESS_NOTIFIED) { - UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToNotify", - base::TimeTicks::Now() - begin_ticks); - } else { - UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToFailure", - base::TimeTicks::Now() - begin_ticks); - } - - if (result != PROCESS_NONE) - return result; - - return LOCK_ERROR; -} - -void ProcessSingleton::OverrideCurrentPidForTesting(base::ProcessId pid) { - current_pid_ = pid; -} - -void ProcessSingleton::OverrideKillCallbackForTesting( - const base::RepeatingCallback& callback) { - kill_callback_ = callback; -} - -void ProcessSingleton::DisablePromptForTesting() { - g_disable_prompt = true; -} - -bool ProcessSingleton::Create() { - base::ThreadRestrictions::ScopedAllowIO allow_io; - int sock; - sockaddr_un addr; - - // The symlink lock is pointed to the hostname and process id, so other - // processes can find it out. - base::FilePath symlink_content(base::StringPrintf( - "%s%c%u", net::GetHostName().c_str(), kLockDelimiter, current_pid_)); - - // Create symbol link before binding the socket, to ensure only one instance - // can have the socket open. - if (!SymlinkPath(symlink_content, lock_path_)) { - // TODO(jackhou): Remove this case once this code is stable on Mac. - // http://crbug.com/367612 -#if defined(OS_MACOSX) - // On Mac, an existing non-symlink lock file means the lock could be held by - // the old process singleton code. If we can successfully replace the lock, - // continue as normal. - if (base::IsLink(lock_path_) || - !ReplaceOldSingletonLock(symlink_content, lock_path_)) { - return false; - } -#else - // If we failed to create the lock, most likely another instance won the - // startup race. - return false; -#endif - } - - if (IsAppSandboxed()) { - // For sandboxed applications, the tmp dir could be too long to fit - // addr->sun_path, so we need to make it as short as possible. - base::FilePath tmp_dir; - if (!base::GetTempDir(&tmp_dir)) { - LOG(ERROR) << "Failed to get temporary directory."; - return false; - } - if (!socket_dir_.Set(tmp_dir.Append("S"))) { - LOG(ERROR) << "Failed to set socket directory."; - return false; - } - } else { - // Create the socket file somewhere in /tmp which is usually mounted as a - // normal filesystem. Some network filesystems (notably AFS) are screwy and - // do not support Unix domain sockets. - if (!socket_dir_.CreateUniqueTempDir()) { - LOG(ERROR) << "Failed to create socket directory."; - return false; - } - } - - // Check that the directory was created with the correct permissions. - int dir_mode = 0; - CHECK(base::GetPosixFilePermissions(socket_dir_.GetPath(), &dir_mode) && - dir_mode == base::FILE_PERMISSION_USER_MASK) - << "Temp directory mode is not 700: " << std::oct << dir_mode; - - // Setup the socket symlink and the two cookies. - base::FilePath socket_target_path = - socket_dir_.GetPath().Append(kSingletonSocketFilename); - base::FilePath cookie(GenerateCookie()); - base::FilePath remote_cookie_path = - socket_dir_.GetPath().Append(kSingletonCookieFilename); - UnlinkPath(socket_path_); - UnlinkPath(cookie_path_); - if (!SymlinkPath(socket_target_path, socket_path_) || - !SymlinkPath(cookie, cookie_path_) || - !SymlinkPath(cookie, remote_cookie_path)) { - // We've already locked things, so we can't have lost the startup race, - // but something doesn't like us. - LOG(ERROR) << "Failed to create symlinks."; - if (!socket_dir_.Delete()) - LOG(ERROR) << "Encountered a problem when deleting socket directory."; - return false; - } - - SetupSocket(socket_target_path.value(), &sock, &addr); - - if (bind(sock, reinterpret_cast(&addr), sizeof(addr)) < 0) { - PLOG(ERROR) << "Failed to bind() " << socket_target_path.value(); - CloseSocket(sock); - return false; - } - - if (listen(sock, 5) < 0) - NOTREACHED() << "listen failed: " << base::safe_strerror(errno); - - sock_ = sock; - - if (BrowserThread::IsThreadInitialized(BrowserThread::IO)) { - StartListeningOnSocket(); - } else { - listen_on_ready_ = true; - } - - return true; -} - -void ProcessSingleton::Cleanup() { - UnlinkPath(socket_path_); - UnlinkPath(cookie_path_); - UnlinkPath(lock_path_); -} - -bool ProcessSingleton::IsSameChromeInstance(pid_t pid) { - pid_t cur_pid = current_pid_; - while (pid != cur_pid) { - pid = base::GetParentProcessId(pid); - if (pid < 0) - return false; - if (!IsChromeProcess(pid)) - return false; - } - return true; -} - -bool ProcessSingleton::KillProcessByLockPath() { - std::string hostname; - int pid; - ParseLockPath(lock_path_, &hostname, &pid); - - if (!hostname.empty() && hostname != net::GetHostName()) { - return DisplayProfileInUseError(lock_path_, hostname, pid); - } - UnlinkPath(lock_path_); - - if (IsSameChromeInstance(pid)) - return true; - - if (pid > 0) { - kill_callback_.Run(pid); - return true; - } - - LOG(ERROR) << "Failed to extract pid from path: " << lock_path_.value(); - return true; -} - -void ProcessSingleton::KillProcess(int pid) { - // TODO(james.su@gmail.com): Is SIGKILL ok? - int rv = kill(static_cast(pid), SIGKILL); - // ESRCH = No Such Process (can happen if the other process is already in - // progress of shutting down and finishes before we try to kill it). - DCHECK(rv == 0 || errno == ESRCH) - << "Error killing process: " << base::safe_strerror(errno); -} diff --git a/chromium_src/chrome/browser/process_singleton_win.cc b/chromium_src/chrome/browser/process_singleton_win.cc deleted file mode 100644 index 8763901842197..0000000000000 --- a/chromium_src/chrome/browser/process_singleton_win.cc +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "chrome/browser/process_singleton.h" - -#include - -#include "base/base_paths.h" -#include "base/bind.h" -#include "base/command_line.h" -#include "base/files/file_path.h" -#include "base/files/file_util.h" -#include "base/process/process.h" -#include "base/process/process_info.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/stringprintf.h" -#include "base/strings/utf_string_conversions.h" -#include "base/time/time.h" -#include "base/win/registry.h" -#include "base/win/scoped_handle.h" -#include "base/win/windows_version.h" -#include "chrome/browser/win/chrome_process_finder.h" -#include "content/public/common/result_codes.h" -#include "net/base/escape.h" -#include "ui/base/l10n/l10n_util.h" -#include "ui/gfx/win/hwnd_util.h" - -namespace { - -const char kLockfile[] = "lockfile"; - -// A helper class that acquires the given |mutex| while the AutoLockMutex is in -// scope. -class AutoLockMutex { - public: - explicit AutoLockMutex(HANDLE mutex) : mutex_(mutex) { - DWORD result = ::WaitForSingleObject(mutex_, INFINITE); - DPCHECK(result == WAIT_OBJECT_0) << "Result = " << result; - } - - ~AutoLockMutex() { - BOOL released = ::ReleaseMutex(mutex_); - DPCHECK(released); - } - - private: - HANDLE mutex_; - DISALLOW_COPY_AND_ASSIGN(AutoLockMutex); -}; - -// A helper class that releases the given |mutex| while the AutoUnlockMutex is -// in scope and immediately re-acquires it when going out of scope. -class AutoUnlockMutex { - public: - explicit AutoUnlockMutex(HANDLE mutex) : mutex_(mutex) { - BOOL released = ::ReleaseMutex(mutex_); - DPCHECK(released); - } - - ~AutoUnlockMutex() { - DWORD result = ::WaitForSingleObject(mutex_, INFINITE); - DPCHECK(result == WAIT_OBJECT_0) << "Result = " << result; - } - - private: - HANDLE mutex_; - DISALLOW_COPY_AND_ASSIGN(AutoUnlockMutex); -}; - -// Checks the visibility of the enumerated window and signals once a visible -// window has been found. -BOOL CALLBACK BrowserWindowEnumeration(HWND window, LPARAM param) { - bool* result = reinterpret_cast(param); - *result = ::IsWindowVisible(window) != 0; - // Stops enumeration if a visible window has been found. - return !*result; -} - -bool ParseCommandLine(const COPYDATASTRUCT* cds, - base::CommandLine::StringVector* parsed_command_line, - base::FilePath* current_directory) { - // We should have enough room for the shortest command (min_message_size) - // and also be a multiple of wchar_t bytes. The shortest command - // possible is L"START\0\0" (empty current directory and command line). - static const int min_message_size = 7; - if (cds->cbData < min_message_size * sizeof(wchar_t) || - cds->cbData % sizeof(wchar_t) != 0) { - LOG(WARNING) << "Invalid WM_COPYDATA, length = " << cds->cbData; - return false; - } - - // We split the string into 4 parts on NULLs. - DCHECK(cds->lpData); - const std::wstring msg(static_cast(cds->lpData), - cds->cbData / sizeof(wchar_t)); - const std::wstring::size_type first_null = msg.find_first_of(L'\0'); - if (first_null == 0 || first_null == std::wstring::npos) { - // no NULL byte, don't know what to do - LOG(WARNING) << "Invalid WM_COPYDATA, length = " << msg.length() - << ", first null = " << first_null; - return false; - } - - // Decode the command, which is everything until the first NULL. - if (msg.substr(0, first_null) == L"START") { - // Another instance is starting parse the command line & do what it would - // have done. - VLOG(1) << "Handling STARTUP request from another process"; - const std::wstring::size_type second_null = - msg.find_first_of(L'\0', first_null + 1); - if (second_null == std::wstring::npos || first_null == msg.length() - 1 || - second_null == msg.length()) { - LOG(WARNING) << "Invalid format for start command, we need a string in 4 " - "parts separated by NULLs"; - return false; - } - - // Get current directory. - *current_directory = - base::FilePath(msg.substr(first_null + 1, second_null - first_null)); - - const std::wstring::size_type third_null = - msg.find_first_of(L'\0', second_null + 1); - if (third_null == std::wstring::npos || third_null == msg.length()) { - LOG(WARNING) << "Invalid format for start command, we need a string in 4 " - "parts separated by NULLs"; - } - - // Get command line. - const std::wstring cmd_line = - msg.substr(second_null + 1, third_null - second_null); - *parsed_command_line = base::CommandLine::FromString(cmd_line).argv(); - return true; - } - return false; -} - -bool ProcessLaunchNotification( - const ProcessSingleton::NotificationCallback& notification_callback, - UINT message, - WPARAM wparam, - LPARAM lparam, - LRESULT* result) { - if (message != WM_COPYDATA) - return false; - - // Handle the WM_COPYDATA message from another process. - const COPYDATASTRUCT* cds = reinterpret_cast(lparam); - - base::CommandLine::StringVector parsed_command_line; - base::FilePath current_directory; - if (!ParseCommandLine(cds, &parsed_command_line, ¤t_directory)) { - *result = TRUE; - return true; - } - - *result = notification_callback.Run(parsed_command_line, current_directory) - ? TRUE - : FALSE; - return true; -} - -bool TerminateAppWithError() { - // TODO: This is called when the secondary process can't ping the primary - // process. Need to find out what to do here. - return false; -} - -} // namespace - -ProcessSingleton::ProcessSingleton( - const base::FilePath& user_data_dir, - const NotificationCallback& notification_callback) - : notification_callback_(notification_callback), - is_virtualized_(false), - lock_file_(INVALID_HANDLE_VALUE), - user_data_dir_(user_data_dir), - should_kill_remote_process_callback_( - base::BindRepeating(&TerminateAppWithError)) { - // The user_data_dir may have not been created yet. - base::CreateDirectoryAndGetError(user_data_dir, nullptr); -} - -ProcessSingleton::~ProcessSingleton() { - DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - if (lock_file_ != INVALID_HANDLE_VALUE) - ::CloseHandle(lock_file_); -} - -// Code roughly based on Mozilla. -ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() { - if (is_virtualized_) - return PROCESS_NOTIFIED; // We already spawned the process in this case. - if (lock_file_ == INVALID_HANDLE_VALUE && !remote_window_) { - return LOCK_ERROR; - } else if (!remote_window_) { - return PROCESS_NONE; - } - - switch (chrome::AttemptToNotifyRunningChrome(remote_window_, false)) { - case chrome::NOTIFY_SUCCESS: - return PROCESS_NOTIFIED; - case chrome::NOTIFY_FAILED: - remote_window_ = NULL; - return PROCESS_NONE; - case chrome::NOTIFY_WINDOW_HUNG: - // Fall through and potentially terminate the hung browser. - break; - } - - DWORD process_id = 0; - DWORD thread_id = ::GetWindowThreadProcessId(remote_window_, &process_id); - if (!thread_id || !process_id) { - remote_window_ = NULL; - return PROCESS_NONE; - } - base::Process process = base::Process::Open(process_id); - - // The window is hung. Scan for every window to find a visible one. - bool visible_window = false; - ::EnumThreadWindows(thread_id, &BrowserWindowEnumeration, - reinterpret_cast(&visible_window)); - - // If there is a visible browser window, ask the user before killing it. - if (visible_window && !should_kill_remote_process_callback_.Run()) { - // The user denied. Quit silently. - return PROCESS_NOTIFIED; - } - - // Time to take action. Kill the browser process. - process.Terminate(content::RESULT_CODE_HUNG, true); - remote_window_ = NULL; - return PROCESS_NONE; -} - -ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate() { - ProcessSingleton::NotifyResult result = PROCESS_NONE; - if (!Create()) { - result = NotifyOtherProcess(); - if (result == PROCESS_NONE) - result = PROFILE_IN_USE; - } - return result; -} - -void ProcessSingleton::StartListeningOnSocket() {} -void ProcessSingleton::OnBrowserReady() {} - -// Look for a Chrome instance that uses the same profile directory. If there -// isn't one, create a message window with its title set to the profile -// directory path. -bool ProcessSingleton::Create() { - static const wchar_t kMutexName[] = L"Local\\AtomProcessSingletonStartup!"; - - remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_); - if (!remote_window_) { - // Make sure we will be the one and only process creating the window. - // We use a named Mutex since we are protecting against multi-process - // access. As documented, it's clearer to NOT request ownership on creation - // since it isn't guaranteed we will get it. It is better to create it - // without ownership and explicitly get the ownership afterward. - base::win::ScopedHandle only_me(::CreateMutex(NULL, FALSE, kMutexName)); - if (!only_me.IsValid()) { - DPLOG(FATAL) << "CreateMutex failed"; - return false; - } - - AutoLockMutex auto_lock_only_me(only_me.Get()); - - // We now own the mutex so we are the only process that can create the - // window at this time, but we must still check if someone created it - // between the time where we looked for it above and the time the mutex - // was given to us. - remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_); - if (!remote_window_) { - // We have to make sure there is no Chrome instance running on another - // machine that uses the same profile. - base::FilePath lock_file_path = user_data_dir_.AppendASCII(kLockfile); - lock_file_ = - ::CreateFile(lock_file_path.value().c_str(), GENERIC_WRITE, - FILE_SHARE_READ, NULL, CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, NULL); - DWORD error = ::GetLastError(); - LOG_IF(WARNING, lock_file_ != INVALID_HANDLE_VALUE && - error == ERROR_ALREADY_EXISTS) - << "Lock file exists but is writable."; - LOG_IF(ERROR, lock_file_ == INVALID_HANDLE_VALUE) - << "Lock file can not be created! Error code: " << error; - - if (lock_file_ != INVALID_HANDLE_VALUE) { - // Set the window's title to the path of our user data directory so - // other Chrome instances can decide if they should forward to us. - bool result = - window_.CreateNamed(base::BindRepeating(&ProcessLaunchNotification, - notification_callback_), - user_data_dir_.value()); - - // NB: Ensure that if the primary app gets started as elevated - // admin inadvertently, secondary windows running not as elevated - // will still be able to send messages - ::ChangeWindowMessageFilterEx(window_.hwnd(), WM_COPYDATA, MSGFLT_ALLOW, - NULL); - CHECK(result && window_.hwnd()); - } - } - } - - return window_.hwnd() != NULL; -} - -void ProcessSingleton::Cleanup() {} - -void ProcessSingleton::OverrideShouldKillRemoteProcessCallbackForTesting( - const ShouldKillRemoteProcessCallback& display_dialog_callback) { - should_kill_remote_process_callback_ = display_dialog_callback; -} diff --git a/chromium_src/chrome/browser/ui/views/frame/global_menu_bar_registrar_x11.h b/chromium_src/chrome/browser/ui/views/frame/global_menu_bar_registrar_x11.h deleted file mode 100644 index 8ffb6a3e8b11e..0000000000000 --- a/chromium_src/chrome/browser/ui/views/frame/global_menu_bar_registrar_x11.h +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2013 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef CHROME_BROWSER_UI_VIEWS_FRAME_GLOBAL_MENU_BAR_REGISTRAR_X11_H_ -#define CHROME_BROWSER_UI_VIEWS_FRAME_GLOBAL_MENU_BAR_REGISTRAR_X11_H_ - -#include - -#include - -#include "base/memory/ref_counted.h" -#include "base/memory/singleton.h" -#include "ui/base/glib/glib_signal.h" - -// Advertises our menu bars to Unity. -// -// GlobalMenuBarX11 is responsible for managing the DbusmenuServer for each -// XID. We need a separate object to own the dbus channel to -// com.canonical.AppMenu.Registrar and to register/unregister the mapping -// between a XID and the DbusmenuServer instance we are offering. -class GlobalMenuBarRegistrarX11 { - public: - static GlobalMenuBarRegistrarX11* GetInstance(); - - void OnWindowMapped(unsigned long xid); - void OnWindowUnmapped(unsigned long xid); - - private: - friend struct base::DefaultSingletonTraits; - - GlobalMenuBarRegistrarX11(); - ~GlobalMenuBarRegistrarX11(); - - // Sends the actual message. - void RegisterXID(unsigned long xid); - void UnregisterXID(unsigned long xid); - - CHROMEG_CALLBACK_1(GlobalMenuBarRegistrarX11, - void, - OnProxyCreated, - GObject*, - GAsyncResult*); - CHROMEG_CALLBACK_1(GlobalMenuBarRegistrarX11, - void, - OnNameOwnerChanged, - GObject*, - GParamSpec*); - - GDBusProxy* registrar_proxy_; - - // Window XIDs which want to be registered, but haven't yet been because - // we're waiting for the proxy to become available. - std::set live_xids_; - - DISALLOW_COPY_AND_ASSIGN(GlobalMenuBarRegistrarX11); -}; - -#endif // CHROME_BROWSER_UI_VIEWS_FRAME_GLOBAL_MENU_BAR_REGISTRAR_X11_H_ diff --git a/components/pepper_flash/BUILD.gn b/components/pepper_flash/BUILD.gn deleted file mode 100644 index ebf8e6b9b1fa6..0000000000000 --- a/components/pepper_flash/BUILD.gn +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2018 GitHub, Inc. -# Use of this source code is governed by the MIT license that can be -# found in the LICENSE file. - -component("pepper_flash") { - visibility = [ "//electron:electron_lib" ] - defines = [ "IS_PEPPER_FLASH_IMPL" ] - sources = [ - "//chrome/browser/renderer_host/pepper/chrome_browser_pepper_host_factory.cc", - "//chrome/browser/renderer_host/pepper/chrome_browser_pepper_host_factory.h", - "//chrome/browser/renderer_host/pepper/pepper_broker_message_filter.cc", - "//chrome/browser/renderer_host/pepper/pepper_broker_message_filter.h", - "//chrome/browser/renderer_host/pepper/pepper_flash_browser_host.cc", - "//chrome/browser/renderer_host/pepper/pepper_flash_browser_host.h", - "//chrome/browser/renderer_host/pepper/pepper_flash_clipboard_message_filter.cc", - "//chrome/browser/renderer_host/pepper/pepper_flash_clipboard_message_filter.h", - "//chrome/browser/renderer_host/pepper/pepper_flash_drm_host.cc", - "//chrome/browser/renderer_host/pepper/pepper_flash_drm_host.h", - "//chrome/browser/renderer_host/pepper/pepper_isolated_file_system_message_filter.cc", - "//chrome/browser/renderer_host/pepper/pepper_isolated_file_system_message_filter.h", - "//chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc", - "//chrome/renderer/pepper/chrome_renderer_pepper_host_factory.h", - "//chrome/renderer/pepper/pepper_flash_drm_renderer_host.cc", - "//chrome/renderer/pepper/pepper_flash_drm_renderer_host.h", - "//chrome/renderer/pepper/pepper_flash_font_file_host.cc", - "//chrome/renderer/pepper/pepper_flash_font_file_host.h", - "//chrome/renderer/pepper/pepper_flash_fullscreen_host.cc", - "//chrome/renderer/pepper/pepper_flash_fullscreen_host.h", - "//chrome/renderer/pepper/pepper_flash_menu_host.cc", - "//chrome/renderer/pepper/pepper_flash_menu_host.h", - "//chrome/renderer/pepper/pepper_flash_renderer_host.cc", - "//chrome/renderer/pepper/pepper_flash_renderer_host.h", - "//chrome/renderer/pepper/pepper_helper.cc", - "//chrome/renderer/pepper/pepper_helper.h", - "//chrome/renderer/pepper/pepper_shared_memory_message_filter.cc", - "//chrome/renderer/pepper/pepper_shared_memory_message_filter.h", - ] - deps = [ - "//content/public/browser", - "//content/public/renderer", - "//media:media_buildflags", - "//ppapi/host", - "//ppapi/proxy", - "//ppapi/proxy:ipc", - "//ppapi/shared_impl", - "//services/device/public/mojom", - "//skia", - "//third_party/adobe/flash:flapper_version_h", - "//ui/base", - "//ui/base/clipboard", - ] - if (is_mac) { - sources += [ - "//chrome/browser/renderer_host/pepper/monitor_finder_mac.h", - "//chrome/browser/renderer_host/pepper/monitor_finder_mac.mm", - ] - } - if (is_linux) { - deps += [ "//components/services/font/public/cpp" ] - } -} diff --git a/default_app/default_app.ts b/default_app/default_app.ts index d517aeb351256..44bc2ebd9affc 100644 --- a/default_app/default_app.ts +++ b/default_app/default_app.ts @@ -1,102 +1,102 @@ -import { app, dialog, BrowserWindow, shell, ipcMain } from 'electron' -import * as path from 'path' +import { shell } from 'electron/common'; +import { app, dialog, BrowserWindow, ipcMain } from 'electron/main'; +import * as path from 'path'; +import * as url from 'url'; -let mainWindow: BrowserWindow | null = null +let mainWindow: BrowserWindow | null = null; // Quit when all windows are closed. app.on('window-all-closed', () => { - app.quit() -}) + app.quit(); +}); function decorateURL (url: string) { // safely add `?utm_source=default_app - const parsedUrl = new URL(url) - parsedUrl.searchParams.append('utm_source', 'default_app') - return parsedUrl.toString() + const parsedUrl = new URL(url); + parsedUrl.searchParams.append('utm_source', 'default_app'); + return parsedUrl.toString(); } // Find the shortest path to the electron binary -const absoluteElectronPath = process.execPath -const relativeElectronPath = path.relative(process.cwd(), absoluteElectronPath) +const absoluteElectronPath = process.execPath; +const relativeElectronPath = path.relative(process.cwd(), absoluteElectronPath); const electronPath = absoluteElectronPath.length < relativeElectronPath.length ? absoluteElectronPath - : relativeElectronPath + : relativeElectronPath; -const indexPath = path.resolve(app.getAppPath(), 'index.html') +const indexPath = path.resolve(app.getAppPath(), 'index.html'); function isTrustedSender (webContents: Electron.WebContents) { if (webContents !== (mainWindow && mainWindow.webContents)) { - return false + return false; } - const parsedUrl = new URL(webContents.getURL()) - const urlPath = process.platform === 'win32' - // Strip the prefixed "/" that occurs on windows - ? path.resolve(parsedUrl.pathname.substr(1)) - : parsedUrl.pathname - return parsedUrl.protocol === 'file:' && urlPath === indexPath + try { + return url.fileURLToPath(webContents.getURL()) === indexPath; + } catch { + return false; + } } ipcMain.handle('bootstrap', (event) => { - return isTrustedSender(event.sender) ? electronPath : null -}) + return isTrustedSender(event.sender) ? electronPath : null; +}); -async function createWindow () { - await app.whenReady() +async function createWindow (backgroundColor?: string) { + await app.whenReady(); const options: Electron.BrowserWindowConstructorOptions = { - width: 900, - height: 600, + width: 960, + height: 620, autoHideMenuBar: true, - backgroundColor: '#FFFFFF', + backgroundColor, webPreferences: { preload: path.resolve(__dirname, 'preload.js'), contextIsolation: true, - sandbox: true, - enableRemoteModule: false + sandbox: true }, useContentSize: true, show: false - } + }; if (process.platform === 'linux') { - options.icon = path.join(__dirname, 'icon.png') + options.icon = path.join(__dirname, 'icon.png'); } - mainWindow = new BrowserWindow(options) - mainWindow.on('ready-to-show', () => mainWindow!.show()) + mainWindow = new BrowserWindow(options); + mainWindow.on('ready-to-show', () => mainWindow!.show()); mainWindow.webContents.on('new-window', (event, url) => { - event.preventDefault() - shell.openExternal(decorateURL(url)) - }) + event.preventDefault(); + shell.openExternal(decorateURL(url)); + }); mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, done) => { - const parsedUrl = new URL(webContents.getURL()) + const parsedUrl = new URL(webContents.getURL()); const options: Electron.MessageBoxOptions = { title: 'Permission Request', message: `Allow '${parsedUrl.origin}' to access '${permission}'?`, buttons: ['OK', 'Cancel'], cancelId: 1 - } + }; dialog.showMessageBox(mainWindow!, options).then(({ response }) => { - done(response === 0) - }) - }) + done(response === 0); + }); + }); - return mainWindow + return mainWindow; } export const loadURL = async (appUrl: string) => { - mainWindow = await createWindow() - mainWindow.loadURL(appUrl) - mainWindow.focus() -} + mainWindow = await createWindow(); + mainWindow.loadURL(appUrl); + mainWindow.focus(); +}; export const loadFile = async (appPath: string) => { - mainWindow = await createWindow() - mainWindow.loadFile(appPath) - mainWindow.focus() -} + mainWindow = await createWindow(appPath === 'index.html' ? '#2f3241' : undefined); + mainWindow.loadFile(appPath); + mainWindow.focus(); +}; diff --git a/default_app/index.html b/default_app/index.html index 68edcf8180618..5ac92e4fb8f17 100644 --- a/default_app/index.html +++ b/default_app/index.html @@ -2,10 +2,10 @@ Electron - + + - @@ -54,36 +54,39 @@ + \ No newline at end of file diff --git a/default_app/index.ts b/default_app/index.ts deleted file mode 100644 index 6b3fdd084f0b4..0000000000000 --- a/default_app/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -async function getOcticonSvg (name: string) { - try { - const response = await fetch(`octicon/${name}.svg`) - const div = document.createElement('div') - div.innerHTML = await response.text() - return div - } catch { - return null - } -} - -async function loadSVG (element: HTMLSpanElement) { - for (const cssClass of element.classList) { - if (cssClass.startsWith('octicon-')) { - const icon = await getOcticonSvg(cssClass.substr(8)) - if (icon) { - for (const elemClass of element.classList) { - icon.classList.add(elemClass) - } - element.before(icon) - element.remove() - break - } - } - } -} - -for (const element of document.querySelectorAll('.octicon')) { - loadSVG(element) -} diff --git a/default_app/main.ts b/default_app/main.ts index fba9562165fe9..f2834624f7430 100644 --- a/default_app/main.ts +++ b/default_app/main.ts @@ -1,8 +1,9 @@ -import { app, dialog } from 'electron' +import * as electron from 'electron/main'; -import * as fs from 'fs' -import * as path from 'path' -import * as url from 'url' +import * as fs from 'fs'; +import * as path from 'path'; +import * as url from 'url'; +const { app, dialog } = electron; type DefaultAppOptions = { file: null | string; @@ -14,10 +15,10 @@ type DefaultAppOptions = { modules: string[]; } -const Module = require('module') +const Module = require('module'); // Parse command line options. -const argv = process.argv.slice(1) +const argv = process.argv.slice(1); const option: DefaultAppOptions = { file: null, @@ -27,50 +28,50 @@ const option: DefaultAppOptions = { interactive: false, abi: false, modules: [] -} +}; -let nextArgIsRequire = false +let nextArgIsRequire = false; for (const arg of argv) { if (nextArgIsRequire) { - option.modules.push(arg) - nextArgIsRequire = false - continue + option.modules.push(arg); + nextArgIsRequire = false; + continue; } else if (arg === '--version' || arg === '-v') { - option.version = true - break + option.version = true; + break; } else if (arg.match(/^--app=/)) { - option.file = arg.split('=')[1] - break + option.file = arg.split('=')[1]; + break; } else if (arg === '--interactive' || arg === '-i' || arg === '-repl') { - option.interactive = true + option.interactive = true; } else if (arg === '--test-type=webdriver') { - option.webdriver = true + option.webdriver = true; } else if (arg === '--require' || arg === '-r') { - nextArgIsRequire = true - continue + nextArgIsRequire = true; + continue; } else if (arg === '--abi' || arg === '-a') { - option.abi = true - continue + option.abi = true; + continue; } else if (arg === '--no-help') { - option.noHelp = true - continue + option.noHelp = true; + continue; } else if (arg[0] === '-') { - continue + continue; } else { - option.file = arg - break + option.file = arg; + break; } } if (nextArgIsRequire) { - console.error('Invalid Usage: --require [file]\n\n"file" is required') - process.exit(1) + console.error('Invalid Usage: --require [file]\n\n"file" is required'); + process.exit(1); } // Set up preload modules if (option.modules.length > 0) { - Module._preloadModules(option.modules) + Module._preloadModules(option.modules); } function loadApplicationPackage (packagePath: string) { @@ -79,102 +80,179 @@ function loadApplicationPackage (packagePath: string) { configurable: false, enumerable: true, value: true - }) + }); try { // Override app name and version. - packagePath = path.resolve(packagePath) - const packageJsonPath = path.join(packagePath, 'package.json') - let appPath + packagePath = path.resolve(packagePath); + const packageJsonPath = path.join(packagePath, 'package.json'); + let appPath; if (fs.existsSync(packageJsonPath)) { - let packageJson + let packageJson; try { - packageJson = require(packageJsonPath) + packageJson = require(packageJsonPath); } catch (e) { - showErrorMessage(`Unable to parse ${packageJsonPath}\n\n${e.message}`) - return + showErrorMessage(`Unable to parse ${packageJsonPath}\n\n${(e as Error).message}`); + return; } if (packageJson.version) { - app.setVersion(packageJson.version) + app.setVersion(packageJson.version); } if (packageJson.productName) { - app.name = packageJson.productName + app.name = packageJson.productName; } else if (packageJson.name) { - app.name = packageJson.name + app.name = packageJson.name; } - appPath = packagePath + appPath = packagePath; } try { - const filePath = Module._resolveFilename(packagePath, module, true) - app._setDefaultAppPaths(appPath || path.dirname(filePath)) + const filePath = Module._resolveFilename(packagePath, module, true); + app.setAppPath(appPath || path.dirname(filePath)); } catch (e) { - showErrorMessage(`Unable to find Electron app at ${packagePath}\n\n${e.message}`) - return + showErrorMessage(`Unable to find Electron app at ${packagePath}\n\n${(e as Error).message}`); + return; } // Run the app. - Module._load(packagePath, module, true) + Module._load(packagePath, module, true); } catch (e) { - console.error('App threw an error during load') - console.error(e.stack || e) - throw e + console.error('App threw an error during load'); + console.error((e as Error).stack || e); + throw e; } } function showErrorMessage (message: string) { - app.focus() - dialog.showErrorBox('Error launching app', message) - process.exit(1) + app.focus(); + dialog.showErrorBox('Error launching app', message); + process.exit(1); } async function loadApplicationByURL (appUrl: string) { - const { loadURL } = await import('./default_app') - loadURL(appUrl) + const { loadURL } = await import('./default_app'); + loadURL(appUrl); } async function loadApplicationByFile (appPath: string) { - const { loadFile } = await import('./default_app') - loadFile(appPath) + const { loadFile } = await import('./default_app'); + loadFile(appPath); } function startRepl () { if (process.platform === 'win32') { - console.error('Electron REPL not currently supported on Windows') - process.exit(1) + console.error('Electron REPL not currently supported on Windows'); + process.exit(1); } - // prevent quitting - app.on('window-all-closed', () => {}) + // Prevent quitting. + app.on('window-all-closed', () => {}); + + const GREEN = '32'; + const colorize = (color: string, s: string) => `\x1b[${color}m${s}\x1b[0m`; + const electronVersion = colorize(GREEN, `v${process.versions.electron}`); + const nodeVersion = colorize(GREEN, `v${process.versions.node}`); + + console.info(` + Welcome to the Electron.js REPL \\[._.]/ + + You can access all Electron.js modules here as well as Node.js modules. + Using: Node.js ${nodeVersion} and Electron.js ${electronVersion} + `); + + const { REPLServer } = require('repl'); + const repl = new REPLServer({ + prompt: '> ' + }).on('exit', () => { + process.exit(0); + }); - const repl = require('repl') - repl.start('> ').on('exit', () => { - process.exit(0) - }) + function defineBuiltin (context: any, name: string, getter: Function) { + const setReal = (val: any) => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete context[name]; + context[name] = val; + }; + + Object.defineProperty(context, name, { + get: () => { + const lib = getter(); + + delete context[name]; + Object.defineProperty(context, name, { + get: () => lib, + set: setReal, + configurable: true, + enumerable: false + }); + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false + }); + } + + defineBuiltin(repl.context, 'electron', () => electron); + for (const api of Object.keys(electron) as (keyof typeof electron)[]) { + defineBuiltin(repl.context, api, () => electron[api]); + } + + // Copied from node/lib/repl.js. For better DX, we don't want to + // show e.g 'contentTracing' at a higher priority than 'const', so + // we only trigger custom tab-completion when no common words are + // potentially matches. + const commonWords = [ + 'async', 'await', 'break', 'case', 'catch', 'const', 'continue', + 'debugger', 'default', 'delete', 'do', 'else', 'export', 'false', + 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', + 'new', 'null', 'return', 'switch', 'this', 'throw', 'true', 'try', + 'typeof', 'var', 'void', 'while', 'with', 'yield' + ]; + + const electronBuiltins = [...Object.keys(electron), 'original-fs', 'electron']; + + const defaultComplete = repl.completer; + repl.completer = (line: string, callback: Function) => { + const lastSpace = line.lastIndexOf(' '); + const currentSymbol = line.substring(lastSpace + 1, repl.cursor); + + const filterFn = (c: string) => c.startsWith(currentSymbol); + const ignores = commonWords.filter(filterFn); + const hits = electronBuiltins.filter(filterFn); + + if (!ignores.length && hits.length) { + callback(null, [hits, currentSymbol]); + } else { + defaultComplete.apply(repl, [line, callback]); + } + }; } // Start the specified app if there is one specified in command line, otherwise // start the default app. if (option.file && !option.webdriver) { - const file = option.file - const protocol = url.parse(file).protocol - const extension = path.extname(file) + const file = option.file; + const protocol = url.parse(file).protocol; + const extension = path.extname(file); if (protocol === 'http:' || protocol === 'https:' || protocol === 'file:' || protocol === 'chrome:') { - loadApplicationByURL(file) + loadApplicationByURL(file); } else if (extension === '.html' || extension === '.htm') { - loadApplicationByFile(path.resolve(file)) + loadApplicationByFile(path.resolve(file)); } else { - loadApplicationPackage(file) + loadApplicationPackage(file); } } else if (option.version) { - console.log('v' + process.versions.electron) - process.exit(0) + console.log('v' + process.versions.electron); + process.exit(0); } else if (option.abi) { - console.log(process.versions.modules) - process.exit(0) + console.log(process.versions.modules); + process.exit(0); } else if (option.interactive) { - startRepl() + startRepl(); } else { if (!option.noHelp) { const welcomeMessage = ` @@ -192,10 +270,10 @@ Options: -i, --interactive Open a REPL to the main process. -r, --require Module to preload (option can be repeated). -v, --version Print the version. - -a, --abi Print the Node ABI version.` + -a, --abi Print the Node ABI version.`; - console.log(welcomeMessage) + console.log(welcomeMessage); } - loadApplicationByFile('index.html') + loadApplicationByFile('index.html'); } diff --git a/default_app/preload.ts b/default_app/preload.ts index a5546da8f9a32..aedaa6e6b7aed 100644 --- a/default_app/preload.ts +++ b/default_app/preload.ts @@ -1,20 +1,58 @@ -import { ipcRenderer } from 'electron' +import { ipcRenderer, contextBridge } from 'electron/renderer'; + +const policy = window.trustedTypes.createPolicy('electron-default-app', { + // we trust the SVG contents + createHTML: input => input +}); + +async function getOcticonSvg (name: string) { + try { + const response = await fetch(`octicon/${name}.svg`); + const div = document.createElement('div'); + div.innerHTML = policy.createHTML(await response.text()); + return div; + } catch { + return null; + } +} + +async function loadSVG (element: HTMLSpanElement) { + for (const cssClass of element.classList) { + if (cssClass.startsWith('octicon-')) { + const icon = await getOcticonSvg(cssClass.substr(8)); + if (icon) { + for (const elemClass of element.classList) { + icon.classList.add(elemClass); + } + element.before(icon); + element.remove(); + break; + } + } + } +} async function initialize () { - const electronPath = await ipcRenderer.invoke('bootstrap') + const electronPath = await ipcRenderer.invoke('bootstrap'); function replaceText (selector: string, text: string) { - const element = document.querySelector(selector) + const element = document.querySelector(selector); if (element) { - element.innerText = text + element.innerText = text; } } - replaceText('.electron-version', `Electron v${process.versions.electron}`) - replaceText('.chrome-version', `Chromium v${process.versions.chrome}`) - replaceText('.node-version', `Node v${process.versions.node}`) - replaceText('.v8-version', `v8 v${process.versions.v8}`) - replaceText('.command-example', `${electronPath} path-to-app`) + replaceText('.electron-version', `Electron v${process.versions.electron}`); + replaceText('.chrome-version', `Chromium v${process.versions.chrome}`); + replaceText('.node-version', `Node v${process.versions.node}`); + replaceText('.v8-version', `v8 v${process.versions.v8}`); + replaceText('.command-example', `${electronPath} path-to-app`); + + for (const element of document.querySelectorAll('.octicon')) { + loadSVG(element); + } } -document.addEventListener('DOMContentLoaded', initialize) +contextBridge.exposeInMainWorld('electronDefaultApp', { + initialize +}); diff --git a/default_app/styles.css b/default_app/styles.css index 0839e520368d2..4f37de3509b14 100644 --- a/default_app/styles.css +++ b/default_app/styles.css @@ -8,7 +8,7 @@ body { } .container { - margin: 15px 30px 30px 30px; + margin: 15px 30px; background-color: #2f3241; flex: 1; display: flex; diff --git a/docs-translations/README.md b/docs-translations/README.md deleted file mode 100644 index 3b7676af53de5..0000000000000 --- a/docs-translations/README.md +++ /dev/null @@ -1,33 +0,0 @@ -## Docs Translations - -This directory once contained unstructured translations of Electron's -documentation, but has been deprecated in favor of a new translation process -using [Crowdin], a GitHub-friendly platform for collaborative translation. - -For more details, visit the [electron/i18n] repo. - -## Contributing - -If you're interested in helping translate Electron's docs, visit -[Crowdin] and log in with your GitHub account. And thanks! - -## Offline Docs - -If you miss having access to Electron's raw markdown files in your preferred -language, don't fret! You can still get raw docs, they're just in a -different place now. See [electron/i18n/tree/master/content] - -To more easily view and browse offline docs in your language, clone the repo and use [vmd], -an Electron-based GitHub-styled markdown viewer: - -```sh -npm i -g vmd -git clone https://github.com/electron/i18n -vmd electron-i18n/content/zh-CN -``` - -[crowdin.com/project/electron]: https://crowdin.com/project/electron -[Crowdin]: https://crowdin.com/project/electron -[electron/i18n]: https://github.com/electron/i18n#readme -[electron/i18n/tree/master/content]: https://github.com/electron/i18n/tree/master/content -[vmd]: http://ghub.io/vmd diff --git a/docs/README.md b/docs/README.md index 08e6be4c1cfad..af7460e6b528b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,27 +18,14 @@ an issue: ## Guides and Tutorials -* [About Electron](tutorial/about.md) -* [Setting up the Development Environment](tutorial/development-environment.md) - * [Setting up macOS](tutorial/development-environment.md#setting-up-macos) - * [Setting up Windows](tutorial/development-environment.md#setting-up-windows) - * [Setting up Linux](tutorial/development-environment.md#setting-up-linux) - * [Choosing an Editor](tutorial/development-environment.md#a-good-editor) -* [Creating your First App](tutorial/first-app.md) - * [Installing Electron](tutorial/first-app.md#installing-electron) - * [Electron Development in a Nutshell](tutorial/first-app.md#electron-development-in-a-nutshell) - * [Running Your App](tutorial/first-app.md#running-your-app) -* [Boilerplates and CLIs](tutorial/boilerplates-and-clis.md) - * [Boilerplate vs CLI](tutorial/boilerplates-and-clis.md#boilerplate-vs-cli) - * [electron-forge](tutorial/boilerplates-and-clis.md#electron-forge) - * [electron-builder](tutorial/boilerplates-and-clis.md#electron-builder) - * [electron-react-boilerplate](tutorial/boilerplates-and-clis.md#electron-react-boilerplate) - * [Other Tools and Boilerplates](tutorial/boilerplates-and-clis.md#other-tools-and-boilerplates) -* [Application Architecture](tutorial/application-architecture.md) - * [Main and Renderer Processes](tutorial/application-architecture.md#main-and-renderer-processes) - * [Using Electron's APIs](tutorial/application-architecture.md#using-electron-apis) - * [Using Node.js APIs](tutorial/application-architecture.md#using-nodejs-apis) - * [Using Native Node.js Modules](tutorial/using-native-node-modules.md) +### Getting started + +* [Introduction](tutorial/introduction.md) +* [Quick Start](tutorial/quick-start.md) +* [Process Model](tutorial/process-model.md) + +### Learning the basics + * Adding Features to Your App * [Notifications](tutorial/notifications.md) * [Recent Documents](tutorial/recent-documents.md) @@ -51,33 +38,38 @@ an issue: * [Represented File for macOS BrowserWindows](tutorial/represented-file.md) * [Native File Drag & Drop](tutorial/native-file-drag-drop.md) * [Offscreen Rendering](tutorial/offscreen-rendering.md) - * [Supporting macOS Dark Mode](tutorial/mojave-dark-mode-guide.md) + * [Dark Mode](tutorial/dark-mode.md) + * [Web embeds in Electron](tutorial/web-embeds.md) +* [Boilerplates and CLIs](tutorial/boilerplates-and-clis.md) + * [Boilerplate vs CLI](tutorial/boilerplates-and-clis.md#boilerplate-vs-cli) + * [electron-forge](tutorial/boilerplates-and-clis.md#electron-forge) + * [electron-builder](tutorial/boilerplates-and-clis.md#electron-builder) + * [electron-react-boilerplate](tutorial/boilerplates-and-clis.md#electron-react-boilerplate) + * [Other Tools and Boilerplates](tutorial/boilerplates-and-clis.md#other-tools-and-boilerplates) + +### Advanced steps + +* Application Architecture + * [Using Native Node.js Modules](tutorial/using-native-node-modules.md) + * [Performance Strategies](tutorial/performance.md) + * [Security Strategies](tutorial/security.md) + * [Process Sandboxing](tutorial/sandbox.md) * [Accessibility](tutorial/accessibility.md) - * [Spectron](tutorial/accessibility.md#spectron) - * [Devtron](tutorial/accessibility.md#devtron) - * [Enabling Accessibility](tutorial/accessibility.md#enabling-accessibility) + * [Manually Enabling Accessibility Features](tutorial/accessibility.md#manually-enabling-accessibility-features) * [Testing and Debugging](tutorial/application-debugging.md) * [Debugging the Main Process](tutorial/debugging-main-process.md) - * [Debugging the Main Process with Visual Studio Code](tutorial/debugging-main-process-vscode.md) - * [Using Selenium and WebDriver](tutorial/using-selenium-and-webdriver.md) + * [Debugging with Visual Studio Code](tutorial/debugging-vscode.md) * [Testing on Headless CI Systems (Travis, Jenkins)](tutorial/testing-on-headless-ci.md) * [DevTools Extension](tutorial/devtools-extension.md) - * [Automated Testing with a Custom Driver](tutorial/automated-testing-with-a-custom-driver.md) + * [Automated Testing](tutorial/automated-testing.md) + * [REPL](tutorial/repl.md) * [Distribution](tutorial/application-distribution.md) * [Supported Platforms](tutorial/support.md#supported-platforms) * [Code Signing](tutorial/code-signing.md) * [Mac App Store](tutorial/mac-app-store-submission-guide.md) * [Windows Store](tutorial/windows-store-guide.md) * [Snapcraft](tutorial/snapcraft.md) -* [Security](tutorial/security.md) - * [Reporting Security Issues](tutorial/security.md#reporting-security-issues) - * [Chromium Security Issues and Upgrades](tutorial/security.md#chromium-security-issues-and-upgrades) - * [Electron Security Warnings](tutorial/security.md#electron-security-warnings) - * [Security Checklist](tutorial/security.md#checklist-security-recommendations) * [Updates](tutorial/updates.md) - * [Deploying an Update Server](tutorial/updates.md#deploying-an-update-server) - * [Implementing Updates in Your App](tutorial/updates.md#implementing-updates-in-your-app) - * [Applying Updates](tutorial/updates.md#applying-updates) * [Getting Support](tutorial/support.md) ## Detailed Tutorials @@ -91,14 +83,7 @@ These individual tutorials expand on topics discussed in the guide above. * Electron Releases & Developer Feedback * [Versioning Policy](tutorial/electron-versioning.md) * [Release Timelines](tutorial/electron-timelines.md) - * [App Feedback Program](tutorial/app-feedback-program.md) -* [Packaging App Source Code with asar](tutorial/application-packaging.md) - * [Generating asar Archives](tutorial/application-packaging.md#generating-asar-archives) - * [Using asar Archives](tutorial/application-packaging.md#using-asar-archives) - * [Limitations](tutorial/application-packaging.md#limitations-of-the-node-api) - * [Adding Unpacked Files to asar Archives](tutorial/application-packaging.md#adding-unpacked-files-to-asar-archives) * [Testing Widevine CDM](tutorial/testing-widevine-cdm.md) -* [Using Pepper Flash Plugin](tutorial/using-pepper-flash-plugin.md) --- @@ -108,16 +93,16 @@ These individual tutorials expand on topics discussed in the guide above. * [Synopsis](api/synopsis.md) * [Process Object](api/process.md) -* [Supported Chrome Command Line Switches](api/chrome-command-line-switches.md) +* [Supported Command Line Switches](api/command-line-switches.md) * [Environment Variables](api/environment-variables.md) -* [Breaking API Changes](api/breaking-changes.md) +* [Chrome Extensions Support](api/extensions.md) +* [Breaking API Changes](breaking-changes.md) ### Custom DOM Elements: * [`File` Object](api/file-object.md) * [`` Tag](api/webview-tag.md) * [`window.open` Function](api/window-open.md) -* [`BrowserWindowProxy` Object](api/browser-window-proxy.md) ### Modules for the Main Process: @@ -132,29 +117,35 @@ These individual tutorials expand on topics discussed in the guide above. * [ipcMain](api/ipc-main.md) * [Menu](api/menu.md) * [MenuItem](api/menu-item.md) +* [MessageChannelMain](api/message-channel-main.md) +* [MessagePortMain](api/message-port-main.md) * [net](api/net.md) * [netLog](api/net-log.md) +* [nativeTheme](api/native-theme.md) +* [Notification](api/notification.md) * [powerMonitor](api/power-monitor.md) * [powerSaveBlocker](api/power-save-blocker.md) * [protocol](api/protocol.md) * [screen](api/screen.md) * [session](api/session.md) +* [ShareMenu](api/share-menu.md) * [systemPreferences](api/system-preferences.md) * [TouchBar](api/touch-bar.md) * [Tray](api/tray.md) * [webContents](api/web-contents.md) +* [webFrameMain](api/web-frame-main.md) ### Modules for the Renderer Process (Web Page): -* [desktopCapturer](api/desktop-capturer.md) +* [contextBridge](api/context-bridge.md) * [ipcRenderer](api/ipc-renderer.md) -* [remote](api/remote.md) * [webFrame](api/web-frame.md) ### Modules for Both Processes: * [clipboard](api/clipboard.md) * [crashReporter](api/crash-reporter.md) +* [desktopCapturer](api/desktop-capturer.md) * [nativeImage](api/native-image.md) * [shell](api/shell.md) diff --git a/docs/api/accelerator.md b/docs/api/accelerator.md index 7bbccd4bc54ee..b6f7d475dabdf 100644 --- a/docs/api/accelerator.md +++ b/docs/api/accelerator.md @@ -2,7 +2,7 @@ > Define keyboard shortcuts. -Accelerators are Strings that can contain multiple modifiers and a single key code, +Accelerators are strings that can contain multiple modifiers and a single key code, combined by the `+` character, and are used to define keyboard shortcuts throughout your application. @@ -18,7 +18,7 @@ method, i.e. ```javascript const { app, globalShortcut } = require('electron') -app.on('ready', () => { +app.whenReady().then(() => { // Register a 'CommandOrControl+Y' shortcut listener. globalShortcut.register('CommandOrControl+Y', () => { // Do stuff when Y and either Command/Control is pressed. @@ -35,7 +35,7 @@ Linux and Windows to define some accelerators. Use `Alt` instead of `Option`. The `Option` key only exists on macOS, whereas the `Alt` key is available on all platforms. -The `Super` key is mapped to the `Windows` key on Windows and Linux and +The `Super` (or `Meta`) key is mapped to the `Windows` key on Windows and Linux and `Cmd` on macOS. ## Available modifiers @@ -48,13 +48,14 @@ The `Super` key is mapped to the `Windows` key on Windows and Linux and * `AltGr` * `Shift` * `Super` +* `Meta` ## Available key codes * `0` to `9` * `A` to `Z` * `F1` to `F24` -* Punctuations like `~`, `!`, `@`, `#`, `$`, etc. +* Punctuation like `~`, `!`, `@`, `#`, `$`, etc. * `Plus` * `Space` * `Tab` diff --git a/docs/api/app.md b/docs/api/app.md old mode 100644 new mode 100755 index 5759178bf40e5..8ccc9db200208 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -32,12 +32,15 @@ In most cases, you should do everything in the `ready` event handler. Returns: -* `launchInfo` Object _macOS_ +* `event` Event +* `launchInfo` Record | [NotificationResponse](structures/notification-response.md) _macOS_ -Emitted when Electron has finished initializing. On macOS, `launchInfo` holds -the `userInfo` of the `NSUserNotification` that was used to open the application, -if it was launched from Notification Center. You can call `app.isReady()` to -check if this event has already fired. +Emitted once, when Electron has finished initializing. On macOS, `launchInfo` +holds the `userInfo` of the [`NSUserNotification`](https://developer.apple.com/documentation/foundation/nsusernotification) +or information from [`UNNotificationResponse`](https://developer.apple.com/documentation/usernotifications/unnotificationresponse) +that was used to open the application, if it was launched from Notification Center. +You can also call `app.isReady()` to check if this event has already fired and `app.whenReady()` +to get a Promise that is fulfilled when Electron is initialized. ### Event: 'window-all-closed' @@ -74,7 +77,7 @@ Returns: * `event` Event Emitted when all windows have been closed and the application will quit. -Calling `event.preventDefault()` will prevent the default behaviour, which is +Calling `event.preventDefault()` will prevent the default behavior, which is terminating the application. See the description of the `window-all-closed` event for the differences between @@ -100,7 +103,7 @@ to a shutdown/restart of the system or a user logout. Returns: * `event` Event -* `path` String +* `path` string Emitted when the user wants to open a file with the application. The `open-file` event is usually emitted when the application is already open and the OS wants @@ -119,7 +122,7 @@ filepath. Returns: * `event` Event -* `url` String +* `url` string Emitted when the user wants to open a URL with the application. Your application's `Info.plist` file must define the URL scheme within the `CFBundleURLTypes` key, and @@ -132,22 +135,34 @@ You should call `event.preventDefault()` if you want to handle this event. Returns: * `event` Event -* `hasVisibleWindows` Boolean +* `hasVisibleWindows` boolean Emitted when the application is activated. Various actions can trigger this event, such as launching the application for the first time, attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon. +### Event: 'did-become-active' _macOS_ + +Returns: + +* `event` Event + +Emitted when mac application become active. Difference from `activate` event is +that `did-become-active` is emitted every time the app becomes active, not only +when Dock icon is clicked or application is re-launched. + ### Event: 'continue-activity' _macOS_ Returns: * `event` Event -* `type` String - A string identifying the activity. Maps to +* `type` string - A string identifying the activity. Maps to [`NSUserActivity.activityType`][activity-type]. -* `userInfo` Object - Contains app-specific state stored by the activity on +* `userInfo` unknown - Contains app-specific state stored by the activity on another device. +* `details` Object + * `webpageURL` string (optional) - A string identifying the URL of the webpage accessed by the activity on another device, if available. Emitted during [Handoff][handoff] when an activity from a different device wants to be resumed. You should call `event.preventDefault()` if you want to handle @@ -163,7 +178,7 @@ Supported activity types are specified in the app's `Info.plist` under the Returns: * `event` Event -* `type` String - A string identifying the activity. Maps to +* `type` string - A string identifying the activity. Maps to [`NSUserActivity.activityType`][activity-type]. Emitted during [Handoff][handoff] before an activity from a different device wants @@ -175,9 +190,9 @@ this event. Returns: * `event` Event -* `type` String - A string identifying the activity. Maps to +* `type` string - A string identifying the activity. Maps to [`NSUserActivity.activityType`][activity-type]. -* `error` String - A string with the error's localized description. +* `error` string - A string with the error's localized description. Emitted during [Handoff][handoff] when an activity from a different device fails to be resumed. @@ -187,9 +202,9 @@ fails to be resumed. Returns: * `event` Event -* `type` String - A string identifying the activity. Maps to +* `type` string - A string identifying the activity. Maps to [`NSUserActivity.activityType`][activity-type]. -* `userInfo` Object - Contains app-specific state stored by the activity. +* `userInfo` unknown - Contains app-specific state stored by the activity. Emitted during [Handoff][handoff] after an activity from this device was successfully resumed on another one. @@ -199,11 +214,11 @@ resumed on another one. Returns: * `event` Event -* `type` String - A string identifying the activity. Maps to +* `type` string - A string identifying the activity. Maps to [`NSUserActivity.activityType`][activity-type]. -* `userInfo` Object - Contains app-specific state stored by the activity. +* `userInfo` unknown - Contains app-specific state stored by the activity. -Emitted when [Handoff][handoff] is about to be resumed on another device. If you need to update the state to be transferred, you should call `event.preventDefault()` immediately, construct a new `userInfo` dictionary and call `app.updateCurrentActiviy()` in a timely manner. Otherwise, the operation will fail and `continue-activity-error` will be called. +Emitted when [Handoff][handoff] is about to be resumed on another device. If you need to update the state to be transferred, you should call `event.preventDefault()` immediately, construct a new `userInfo` dictionary and call `app.updateCurrentActivity()` in a timely manner. Otherwise, the operation will fail and `continue-activity-error` will be called. ### Event: 'new-window-for-tab' _macOS_ @@ -257,11 +272,12 @@ Returns: * `event` Event * `webContents` [WebContents](web-contents.md) -* `url` String -* `error` String - The error code +* `url` string +* `error` string - The error code * `certificate` [Certificate](structures/certificate.md) * `callback` Function - * `isTrusted` Boolean - Whether to consider the certificate as trusted + * `isTrusted` boolean - Whether to consider the certificate as trusted +* `isMainFrame` boolean Emitted when failed to verify the `certificate` for `url`, to trust the certificate you should prevent the default behavior with @@ -314,19 +330,17 @@ Returns: * `event` Event * `webContents` [WebContents](web-contents.md) -* `request` Object - * `method` String +* `authenticationResponseDetails` Object * `url` URL - * `referrer` URL * `authInfo` Object - * `isProxy` Boolean - * `scheme` String - * `host` String + * `isProxy` boolean + * `scheme` string + * `host` string * `port` Integer - * `realm` String + * `realm` string * `callback` Function - * `username` String - * `password` String + * `username` string (optional) + * `password` string (optional) Emitted when `webContents` wants to do basic auth. @@ -337,41 +351,108 @@ should prevent the default behavior with `event.preventDefault()` and call ```javascript const { app } = require('electron') -app.on('login', (event, webContents, request, authInfo, callback) => { +app.on('login', (event, webContents, details, authInfo, callback) => { event.preventDefault() callback('username', 'secret') }) ``` +If `callback` is called without a username or password, the authentication +request will be cancelled and the authentication error will be returned to the +page. + ### Event: 'gpu-info-update' Emitted whenever there is a GPU info update. -### Event: 'gpu-process-crashed' +### Event: 'gpu-process-crashed' _Deprecated_ Returns: * `event` Event -* `killed` Boolean +* `killed` boolean Emitted when the GPU process crashes or is killed. -### Event: 'renderer-process-crashed' +**Deprecated:** This event is superceded by the `child-process-gone` event +which contains more information about why the child process disappeared. It +isn't always because it crashed. The `killed` boolean can be replaced by +checking `reason === 'killed'` when you switch to that event. + +### Event: 'renderer-process-crashed' _Deprecated_ Returns: * `event` Event * `webContents` [WebContents](web-contents.md) -* `killed` Boolean +* `killed` boolean Emitted when the renderer process of `webContents` crashes or is killed. +**Deprecated:** This event is superceded by the `render-process-gone` event +which contains more information about why the render process disappeared. It +isn't always because it crashed. The `killed` boolean can be replaced by +checking `reason === 'killed'` when you switch to that event. + +### Event: 'render-process-gone' + +Returns: + +* `event` Event +* `webContents` [WebContents](web-contents.md) +* `details` Object + * `reason` string - The reason the render process is gone. Possible values: + * `clean-exit` - Process exited with an exit code of zero + * `abnormal-exit` - Process exited with a non-zero exit code + * `killed` - Process was sent a SIGTERM or otherwise killed externally + * `crashed` - Process crashed + * `oom` - Process ran out of memory + * `launch-failed` - Process never successfully launched + * `integrity-failure` - Windows code integrity checks failed + * `exitCode` Integer - The exit code of the process, unless `reason` is + `launch-failed`, in which case `exitCode` will be a platform-specific + launch failure error code. + +Emitted when the renderer process unexpectedly disappears. This is normally +because it was crashed or killed. + +### Event: 'child-process-gone' + +Returns: + +* `event` Event +* `details` Object + * `type` string - Process type. One of the following values: + * `Utility` + * `Zygote` + * `Sandbox helper` + * `GPU` + * `Pepper Plugin` + * `Pepper Plugin Broker` + * `Unknown` + * `reason` string - The reason the child process is gone. Possible values: + * `clean-exit` - Process exited with an exit code of zero + * `abnormal-exit` - Process exited with a non-zero exit code + * `killed` - Process was sent a SIGTERM or otherwise killed externally + * `crashed` - Process crashed + * `oom` - Process ran out of memory + * `launch-failed` - Process never successfully launched + * `integrity-failure` - Windows code integrity checks failed + * `exitCode` number - The exit code for the process + (e.g. status from waitpid if on posix, from GetExitCodeProcess on Windows). + * `serviceName` string (optional) - The non-localized name of the process. + * `name` string (optional) - The name of the process. + Examples for utility: `Audio Service`, `Content Decryption Module Service`, `Network Service`, `Video Capture`, etc. + +Emitted when the child process unexpectedly disappears. This is normally +because it was crashed or killed. It does not include renderer processes. + ### Event: 'accessibility-support-changed' _macOS_ _Windows_ Returns: * `event` Event -* `accessibilitySupportEnabled` Boolean - `true` when Chrome's accessibility +* `accessibilitySupportEnabled` boolean - `true` when Chrome's accessibility support is enabled, `false` otherwise. Emitted when Chrome's accessibility support changes. This event fires when @@ -390,7 +471,7 @@ Emitted when Electron has created a new `session`. ```javascript const { app } = require('electron') -app.on('session-created', (event, session) => { +app.on('session-created', (session) => { console.log(session) }) ``` @@ -400,8 +481,9 @@ app.on('session-created', (event, session) => { Returns: * `event` Event -* `argv` String[] - An array of the second instance's command line arguments -* `workingDirectory` String - The second instance's working directory +* `argv` string[] - An array of the second instance's command line arguments +* `workingDirectory` string - The second instance's working directory +* `additionalData` unknown - A JSON object of additional data passed from the second instance This event will be emitted inside the primary instance of your application when a second instance has been executed and calls `app.requestSingleInstanceLock()`. @@ -411,92 +493,14 @@ and `workingDirectory` is its current working directory. Usually applications respond to this by making their primary window focused and non-minimized. +**Note:** If the second instance is started by a different user than the first, the `argv` array will not include the arguments. + This event is guaranteed to be emitted after the `ready` event of `app` gets emitted. **Note:** Extra command line arguments might be added by Chromium, such as `--original-process-start-time`. -### Event: 'desktop-capturer-get-sources' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) - -Emitted when `desktopCapturer.getSources()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will make it return empty sources. - -### Event: 'remote-require' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) -* `moduleName` String - -Emitted when `remote.require()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will prevent the module from being returned. -Custom value can be returned by setting `event.returnValue`. - -### Event: 'remote-get-global' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) -* `globalName` String - -Emitted when `remote.getGlobal()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will prevent the global from being returned. -Custom value can be returned by setting `event.returnValue`. - -### Event: 'remote-get-builtin' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) -* `moduleName` String - -Emitted when `remote.getBuiltin()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will prevent the module from being returned. -Custom value can be returned by setting `event.returnValue`. - -### Event: 'remote-get-current-window' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) - -Emitted when `remote.getCurrentWindow()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will prevent the object from being returned. -Custom value can be returned by setting `event.returnValue`. - -### Event: 'remote-get-current-web-contents' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) - -Emitted when `remote.getCurrentWebContents()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will prevent the object from being returned. -Custom value can be returned by setting `event.returnValue`. - -### Event: 'remote-get-guest-web-contents' - -Returns: - -* `event` Event -* `webContents` [WebContents](web-contents.md) -* `guestWebContents` [WebContents](web-contents.md) - -Emitted when `.getWebContents()` is called in the renderer process of `webContents`. -Calling `event.preventDefault()` will prevent the object from being returned. -Custom value can be returned by setting `event.returnValue`. - ## Methods The `app` object has the following methods: @@ -526,8 +530,8 @@ and `will-quit` events will not be emitted. ### `app.relaunch([options])` * `options` Object (optional) - * `args` String[] (optional) - * `execPath` String (optional) + * `args` string[] (optional) + * `execPath` string (optional) Relaunches the app when current instance exits. @@ -554,7 +558,8 @@ app.exit(0) ### `app.isReady()` -Returns `Boolean` - `true` if Electron has finished initializing, `false` otherwise. +Returns `boolean` - `true` if Electron has finished initializing, `false` otherwise. +See also `app.whenReady()`. ### `app.whenReady()` @@ -562,11 +567,17 @@ Returns `Promise` - fulfilled when Electron is initialized. May be used as a convenient alternative to checking `app.isReady()` and subscribing to the `ready` event if the app is not ready yet. -### `app.focus()` +### `app.focus([options])` + +* `options` Object (optional) + * `steal` boolean _macOS_ - Make the receiver the active app even if another app is + currently active. On Linux, focuses on the first visible window. On macOS, makes the application the active app. On Windows, focuses on the application's first window. +You should seek to use the `steal` option as sparingly as possible. + ### `app.hide()` _macOS_ Hides all application windows without minimizing them. @@ -578,19 +589,19 @@ them. ### `app.setAppLogsPath([path])` -* `path` String (optional) - A custom path for your logs. Must be absolute. +* `path` string (optional) - A custom path for your logs. Must be absolute. Sets or creates a directory your app's logs which can then be manipulated with `app.getPath()` or `app.setPath(pathName, newPath)`. -Calling `app.setAppLogsPath()` without a `path` parameter will result in this directory being set to `/Library/Logs/YourAppName` on _macOS_, and inside the `userData` directory on _Linux_ and _Windows_. +Calling `app.setAppLogsPath()` without a `path` parameter will result in this directory being set to `~/Library/Logs/YourAppName` on _macOS_, and inside the `userData` directory on _Linux_ and _Windows_. ### `app.getAppPath()` -Returns `String` - The current application directory. +Returns `string` - The current application directory. ### `app.getPath(name)` -* `name` String - You can request the following paths by the name: +* `name` string - You can request the following paths by the name: * `home` User's home directory. * `appData` Per-user application data directory, which by default points to: * `%APPDATA%` on Windows @@ -608,17 +619,20 @@ Returns `String` - The current application directory. * `music` Directory for a user's music. * `pictures` Directory for a user's pictures. * `videos` Directory for a user's videos. + * `recent` Directory for the user's recent files (Windows only). * `logs` Directory for your app's log folder. - * `pepperFlashSystemPlugin` Full path to the system version of the Pepper Flash plugin. + * `crashDumps` Directory where crash dumps are stored. -Returns `String` - A path to a special directory or file associated with `name`. On +Returns `string` - A path to a special directory or file associated with `name`. On failure, an `Error` is thrown. +If `app.getPath('logs')` is called without called `app.setAppLogsPath()` being called first, a default log directory will be created equivalent to calling `app.setAppLogsPath()` without a `path` parameter. + ### `app.getFileIcon(path[, options])` -* `path` String +* `path` string * `options` Object (optional) - * `size` String + * `size` string * `small` - 16x16 * `normal` - 32x32 * `large` - 48x48 on _Linux_, 32x32 on _Windows_, unsupported on _macOS_. @@ -636,8 +650,8 @@ On _Linux_ and _macOS_, icons depend on the application associated with file mim ### `app.setPath(name, path)` -* `name` String -* `path` String +* `name` string +* `path` string Overrides the `path` to a special directory or file associated with `name`. If the path specifies a directory that does not exist, an `Error` is thrown. @@ -651,13 +665,13 @@ directory. If you want to change this location, you have to override the ### `app.getVersion()` -Returns `String` - The version of the loaded application. If no version is found in the +Returns `string` - The version of the loaded application. If no version is found in the application's `package.json` file, the version of the current bundle or executable is returned. ### `app.getName()` -Returns `String` - The current application's name, which is the name in the application's +Returns `string` - The current application's name, which is the name in the application's `package.json` file. Usually the `name` field of `package.json` is a short lowercase name, according @@ -665,21 +679,20 @@ to the npm modules spec. You should usually also specify a `productName` field, which is your application's full capitalized name, and which will be preferred over `name` by Electron. -**[Deprecated](modernization/property-updates.md)** - ### `app.setName(name)` -* `name` String +* `name` string Overrides the current application's name. -**[Deprecated](modernization/property-updates.md)** +**Note:** This function overrides the name used internally by Electron; it does not affect the name that the OS uses. ### `app.getLocale()` -Returns `String` - The current application locale. Possible return values are documented [here](locales.md). +Returns `string` - The current application locale, fetched using Chromium's `l10n_util` library. +Possible return values are documented [here](https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc). -To set the locale, you'll want to use a command line switch at app startup, which may be found [here](https://github.com/electron/electron/blob/master/docs/api/chrome-command-line-switches.md). +To set the locale, you'll want to use a command line switch at app startup, which may be found [here](command-line-switches.md). **Note:** When distributing your packaged app, you have to also ship the `locales` folder. @@ -688,13 +701,13 @@ To set the locale, you'll want to use a command line switch at app startup, whic ### `app.getLocaleCountryCode()` -Returns `String` - User operating system's locale two-letter [ISO 3166](https://www.iso.org/iso-3166-country-codes.html) country code. The value is taken from native OS APIs. +Returns `string` - User operating system's locale two-letter [ISO 3166](https://www.iso.org/iso-3166-country-codes.html) country code. The value is taken from native OS APIs. **Note:** When unable to detect locale country code, it returns empty string. ### `app.addRecentDocument(path)` _macOS_ _Windows_ -* `path` String +* `path` string Adds `path` to the recent documents list. @@ -707,56 +720,54 @@ Clears the recent documents list. ### `app.setAsDefaultProtocolClient(protocol[, path, args])` -* `protocol` String - The name of your protocol, without `://`. If you want your - app to handle `electron://` links, call this method with `electron` as the - parameter. -* `path` String (optional) _Windows_ - Defaults to `process.execPath` -* `args` String[] (optional) _Windows_ - Defaults to an empty array - -Returns `Boolean` - Whether the call succeeded. +* `protocol` string - The name of your protocol, without `://`. For example, + if you want your app to handle `electron://` links, call this method with + `electron` as the parameter. +* `path` string (optional) _Windows_ - The path to the Electron executable. + Defaults to `process.execPath` +* `args` string[] (optional) _Windows_ - Arguments passed to the executable. + Defaults to an empty array -This method sets the current executable as the default handler for a protocol -(aka URI scheme). It allows you to integrate your app deeper into the operating -system. Once registered, all links with `your-protocol://` will be opened with -the current executable. The whole link, including protocol, will be passed to -your application as a parameter. +Returns `boolean` - Whether the call succeeded. -On Windows, you can provide optional parameters path, the path to your executable, -and args, an array of arguments to be passed to your executable when it launches. +Sets the current executable as the default handler for a protocol (aka URI +scheme). It allows you to integrate your app deeper into the operating system. +Once registered, all links with `your-protocol://` will be opened with the +current executable. The whole link, including protocol, will be passed to your +application as a parameter. **Note:** On macOS, you can only register protocols that have been added to -your app's `info.plist`, which can not be modified at runtime. You can however -change the file with a simple text editor or script during build time. -Please refer to [Apple's documentation][CFBundleURLTypes] for details. +your app's `info.plist`, which cannot be modified at runtime. However, you can +change the file during build time via [Electron Forge][electron-forge], +[Electron Packager][electron-packager], or by editing `info.plist` with a text +editor. Please refer to [Apple's documentation][CFBundleURLTypes] for details. **Note:** In a Windows Store environment (when packaged as an `appx`) this API will return `true` for all calls but the registry key it sets won't be accessible by other applications. In order to register your Windows Store application as a default protocol handler you must [declare the protocol in your manifest](https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-uap-protocol). -The API uses the Windows Registry and LSSetDefaultHandlerForURLScheme internally. +The API uses the Windows Registry and `LSSetDefaultHandlerForURLScheme` internally. ### `app.removeAsDefaultProtocolClient(protocol[, path, args])` _macOS_ _Windows_ -* `protocol` String - The name of your protocol, without `://`. -* `path` String (optional) _Windows_ - Defaults to `process.execPath` -* `args` String[] (optional) _Windows_ - Defaults to an empty array +* `protocol` string - The name of your protocol, without `://`. +* `path` string (optional) _Windows_ - Defaults to `process.execPath` +* `args` string[] (optional) _Windows_ - Defaults to an empty array -Returns `Boolean` - Whether the call succeeded. +Returns `boolean` - Whether the call succeeded. This method checks if the current executable as the default handler for a protocol (aka URI scheme). If so, it will remove the app as the default handler. ### `app.isDefaultProtocolClient(protocol[, path, args])` -* `protocol` String - The name of your protocol, without `://`. -* `path` String (optional) _Windows_ - Defaults to `process.execPath` -* `args` String[] (optional) _Windows_ - Defaults to an empty array +* `protocol` string - The name of your protocol, without `://`. +* `path` string (optional) _Windows_ - Defaults to `process.execPath` +* `args` string[] (optional) _Windows_ - Defaults to an empty array -Returns `Boolean` - -This method checks if the current executable is the default handler for a protocol -(aka URI scheme). If so, it will return true. Otherwise, it will return false. +Returns `boolean` - Whether the current executable is the default handler for a +protocol (aka URI scheme). **Note:** On macOS, you can use this method to check if the app has been registered as the default protocol handler for a protocol. You can also verify @@ -764,7 +775,37 @@ this by checking `~/Library/Preferences/com.apple.LaunchServices.plist` on the macOS machine. Please refer to [Apple's documentation][LSCopyDefaultHandlerForURLScheme] for details. -The API uses the Windows Registry and LSCopyDefaultHandlerForURLScheme internally. +The API uses the Windows Registry and `LSCopyDefaultHandlerForURLScheme` internally. + +### `app.getApplicationNameForProtocol(url)` + +* `url` string - a URL with the protocol name to check. Unlike the other + methods in this family, this accepts an entire URL, including `://` at a + minimum (e.g. `https://`). + +Returns `string` - Name of the application handling the protocol, or an empty + string if there is no handler. For instance, if Electron is the default + handler of the URL, this could be `Electron` on Windows and Mac. However, + don't rely on the precise format which is not guaranteed to remain unchanged. + Expect a different format on Linux, possibly with a `.desktop` suffix. + +This method returns the application name of the default handler for the protocol +(aka URI scheme) of a URL. + +### `app.getApplicationInfoForProtocol(url)` _macOS_ _Windows_ + +* `url` string - a URL with the protocol name to check. Unlike the other + methods in this family, this accepts an entire URL, including `://` at a + minimum (e.g. `https://`). + +Returns `Promise` - Resolve with an object containing the following: + +* `icon` NativeImage - the display icon of the app handling the protocol. +* `path` string - installation path of the app handling the protocol. +* `name` string - display name of the app handling the protocol. + +This method returns a promise that contains the application name, icon and path of the default handler for the protocol +(aka URI scheme) of a URL. ### `app.setUserTasks(tasks)` _Windows_ @@ -774,7 +815,7 @@ Adds `tasks` to the [Tasks][tasks] category of the Jump List on Windows. `tasks` is an array of [`Task`](structures/task.md) objects. -Returns `Boolean` - Whether the call succeeded. +Returns `boolean` - Whether the call succeeded. **Note:** If you'd like to customize the Jump List even more use `app.setJumpList(categories)` instead. @@ -796,6 +837,8 @@ Returns `Object`: * `categories` [JumpListCategory[]](structures/jump-list-category.md) | `null` - Array of `JumpListCategory` objects. +Returns `string` + Sets or removes a custom Jump List for the application, and returns one of the following strings: @@ -825,6 +868,10 @@ re-add a removed item to a custom category earlier than that will result in the entire custom category being omitted from the Jump List. The list of removed items can be obtained using `app.getJumpListSettings()`. +**Note:** The maximum length of a Jump List item's `description` property is +260 characters. Beyond this limit, the item will not be added to the Jump +List, nor will it be displayed. + Here's a very simple example of creating a custom Jump List: ```javascript @@ -885,9 +932,11 @@ app.setJumpList([ ]) ``` -### `app.requestSingleInstanceLock()` +### `app.requestSingleInstanceLock([additionalData])` -Returns `Boolean` +* `additionalData` Record (optional) - A JSON object containing additional data to send to the first instance. + +Returns `boolean` The return value of this method indicates whether or not this instance of your application successfully obtained the lock. If it failed to obtain the lock, @@ -912,12 +961,16 @@ starts: const { app } = require('electron') let myWindow = null -const gotTheLock = app.requestSingleInstanceLock() +const additionalData = { myKey: 'myValue' } +const gotTheLock = app.requestSingleInstanceLock(additionalData) if (!gotTheLock) { app.quit() } else { - app.on('second-instance', (event, commandLine, workingDirectory) => { + app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => { + // Print out data received from the second instance. + console.log(additionalData) + // Someone tried to run a second instance, we should focus our window. if (myWindow) { if (myWindow.isMinimized()) myWindow.restore() @@ -926,14 +979,15 @@ if (!gotTheLock) { }) // Create myWindow, load the rest of the app, etc... - app.on('ready', () => { + app.whenReady().then(() => { + myWindow = createWindow() }) } ``` ### `app.hasSingleInstanceLock()` -Returns `Boolean` +Returns `boolean` This method returns whether or not this instance of your app is currently holding the single instance lock. You can request the lock with @@ -947,10 +1001,10 @@ allow multiple instances of the application to once again run side by side. ### `app.setUserActivity(type, userInfo[, webpageURL])` _macOS_ -* `type` String - Uniquely identifies the activity. Maps to +* `type` string - Uniquely identifies the activity. Maps to [`NSUserActivity.activityType`][activity-type]. -* `userInfo` Object - App-specific state to store for use by another device. -* `webpageURL` String (optional) - The webpage to load in a browser if no suitable app is +* `userInfo` any - App-specific state to store for use by another device. +* `webpageURL` string (optional) - The webpage to load in a browser if no suitable app is installed on the resuming device. The scheme must be `http` or `https`. Creates an `NSUserActivity` and sets it as the current activity. The activity @@ -958,7 +1012,7 @@ is eligible for [Handoff][handoff] to another device afterward. ### `app.getCurrentActivityType()` _macOS_ -Returns `String` - The type of the currently running activity. +Returns `string` - The type of the currently running activity. ### `app.invalidateCurrentActivity()` _macOS_ @@ -970,30 +1024,97 @@ Marks the current [Handoff][handoff] user activity as inactive without invalidat ### `app.updateCurrentActivity(type, userInfo)` _macOS_ -* `type` String - Uniquely identifies the activity. Maps to +* `type` string - Uniquely identifies the activity. Maps to [`NSUserActivity.activityType`][activity-type]. -* `userInfo` Object - App-specific state to store for use by another device. +* `userInfo` any - App-specific state to store for use by another device. Updates the current activity if its type matches `type`, merging the entries from `userInfo` into its current `userInfo` dictionary. ### `app.setAppUserModelId(id)` _Windows_ -* `id` String +* `id` string Changes the [Application User Model ID][app-user-model-id] to `id`. +### `app.setActivationPolicy(policy)` _macOS_ + +* `policy` string - Can be 'regular', 'accessory', or 'prohibited'. + +Sets the activation policy for a given app. + +Activation policy types: + +* 'regular' - The application is an ordinary app that appears in the Dock and may have a user interface. +* 'accessory' - The application doesn’t appear in the Dock and doesn’t have a menu bar, but it may be activated programmatically or by clicking on one of its windows. +* 'prohibited' - The application doesn’t appear in the Dock and may not create windows or be activated. + ### `app.importCertificate(options, callback)` _Linux_ * `options` Object - * `certificate` String - Path for the pkcs12 file. - * `password` String - Passphrase for the certificate. + * `certificate` string - Path for the pkcs12 file. + * `password` string - Passphrase for the certificate. * `callback` Function * `result` Integer - Result of import. Imports the certificate in pkcs12 format into the platform certificate store. `callback` is called with the `result` of import operation, a value of `0` -indicates success while any other value indicates failure according to Chromium [net_error_list](https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h). +indicates success while any other value indicates failure according to Chromium [net_error_list](https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h). + +### `app.configureHostResolver(options)` + +* `options` Object + * `enableBuiltInResolver` boolean (optional) - Whether the built-in host + resolver is used in preference to getaddrinfo. When enabled, the built-in + resolver will attempt to use the system's DNS settings to do DNS lookups + itself. Enabled by default on macOS, disabled by default on Windows and + Linux. + * `secureDnsMode` string (optional) - Can be "off", "automatic" or "secure". + Configures the DNS-over-HTTP mode. When "off", no DoH lookups will be + performed. When "automatic", DoH lookups will be performed first if DoH is + available, and insecure DNS lookups will be performed as a fallback. When + "secure", only DoH lookups will be performed. Defaults to "automatic". + * `secureDnsServers` string[] (optional) - A list of DNS-over-HTTP + server templates. See [RFC8484 § 3][] for details on the template format. + Most servers support the POST method; the template for such servers is + simply a URI. Note that for [some DNS providers][doh-providers], the + resolver will automatically upgrade to DoH unless DoH is explicitly + disabled, even if there are no DoH servers provided in this list. + * `enableAdditionalDnsQueryTypes` boolean (optional) - Controls whether additional DNS + query types, e.g. HTTPS (DNS type 65) will be allowed besides the + traditional A and AAAA queries when a request is being made via insecure + DNS. Has no effect on Secure DNS which always allows additional types. + Defaults to true. + +Configures host resolution (DNS and DNS-over-HTTPS). By default, the following +resolvers will be used, in order: + +1. DNS-over-HTTPS, if the [DNS provider supports it][doh-providers], then +2. the built-in resolver (enabled on macOS only by default), then +3. the system's resolver (e.g. `getaddrinfo`). + +This can be configured to either restrict usage of non-encrypted DNS +(`secureDnsMode: "secure"`), or disable DNS-over-HTTPS (`secureDnsMode: +"off"`). It is also possible to enable or disable the built-in resolver. + +To disable insecure DNS, you can specify a `secureDnsMode` of `"secure"`. If you do +so, you should make sure to provide a list of DNS-over-HTTPS servers to use, in +case the user's DNS configuration does not include a provider that supports +DoH. + +```js +app.configureHostResolver({ + secureDnsMode: 'secure', + secureDnsServers: [ + 'https://cloudflare-dns.com/dns-query' + ] +}) +``` + +This API must be called after the `ready` event is emitted. + +[doh-providers]: https://source.chromium.org/chromium/chromium/src/+/main:net/dns/public/doh_provider_entry.cc;l=31?q=%22DohProviderEntry::GetList()%22&ss=chromium%2Fchromium%2Fsrc +[RFC8484 § 3]: https://datatracker.ietf.org/doc/html/rfc8484#section-3 ### `app.disableHardwareAcceleration()` @@ -1005,7 +1126,7 @@ This method can only be called before app is ready. By default, Chromium disables 3D APIs (e.g. WebGL) until restart on a per domain basis if the GPU processes crashes too frequently. This function -disables that behaviour. +disables that behavior. This method can only be called before app is ready. @@ -1021,7 +1142,7 @@ Returns [`GPUFeatureStatus`](structures/gpu-feature-status.md) - The Graphics Fe ### `app.getGPUInfo(infoType)` -* `infoType` String - Can be `basic` or `complete`. +* `infoType` string - Can be `basic` or `complete`. Returns `Promise` @@ -1030,9 +1151,12 @@ For `infoType` equal to `complete`: For `infoType` equal to `basic`: Promise is fulfilled with `Object` containing fewer attributes than when requested with `complete`. Here's an example of basic response: + ```js -{ auxAttributes: - { amdSwitchable: true, +{ + auxAttributes: + { + amdSwitchable: true, canSupportThreadedTextureMailbox: false, directComposition: false, directRendering: true, @@ -1045,48 +1169,46 @@ For `infoType` equal to `basic`: sandboxed: false, softwareRendering: false, supportsOverlays: false, - videoDecodeAcceleratorFlags: 0 }, -gpuDevice: - [ { active: true, deviceId: 26657, vendorId: 4098 }, - { active: false, deviceId: 3366, vendorId: 32902 } ], -machineModelName: 'MacBookPro', -machineModelVersion: '11.5' } + videoDecodeAcceleratorFlags: 0 + }, + gpuDevice: + [{ active: true, deviceId: 26657, vendorId: 4098 }, + { active: false, deviceId: 3366, vendorId: 32902 }], + machineModelName: 'MacBookPro', + machineModelVersion: '11.5' +} ``` Using `basic` should be preferred if only basic information like `vendorId` or `driverId` is needed. -### `app.setBadgeCount(count)` _Linux_ _macOS_ +### `app.setBadgeCount([count])` _Linux_ _macOS_ -* `count` Integer +* `count` Integer (optional) - If a value is provided, set the badge to the provided value otherwise, on macOS, display a plain white dot (e.g. unknown number of notifications). On Linux, if a value is not provided the badge will not display. -Returns `Boolean` - Whether the call succeeded. +Returns `boolean` - Whether the call succeeded. Sets the counter badge for current app. Setting the count to `0` will hide the badge. On macOS, it shows on the dock icon. On Linux, it only works for Unity launcher. -**Note:** Unity launcher requires the existence of a `.desktop` file to work, -for more information please read [Desktop Environment Integration][unity-requirement]. - -**[Deprecated](modernization/property-updates.md)** +**Note:** Unity launcher requires a `.desktop` file to work. For more information, +please read the [Unity integration documentation][unity-requirement]. ### `app.getBadgeCount()` _Linux_ _macOS_ Returns `Integer` - The current value displayed in the counter badge. -**[Deprecated](modernization/property-updates.md)** - ### `app.isUnityRunning()` _Linux_ -Returns `Boolean` - Whether the current desktop environment is Unity launcher. +Returns `boolean` - Whether the current desktop environment is Unity launcher. ### `app.getLoginItemSettings([options])` _macOS_ _Windows_ * `options` Object (optional) - * `path` String (optional) _Windows_ - The executable path to compare against. + * `path` string (optional) _Windows_ - The executable path to compare against. Defaults to `process.execPath`. - * `args` String[] (optional) _Windows_ - The command-line arguments to compare + * `args` string[] (optional) _Windows_ - The command-line arguments to compare against. Defaults to an empty array. If you provided `path` and `args` options to `app.setLoginItemSettings`, then you @@ -1094,34 +1216,43 @@ need to pass the same arguments here for `openAtLogin` to be set correctly. Returns `Object`: -* `openAtLogin` Boolean - `true` if the app is set to open at login. -* `openAsHidden` Boolean _macOS_ - `true` if the app is set to open as hidden at login. +* `openAtLogin` boolean - `true` if the app is set to open at login. +* `openAsHidden` boolean _macOS_ - `true` if the app is set to open as hidden at login. This setting is not available on [MAS builds][mas-builds]. -* `wasOpenedAtLogin` Boolean _macOS_ - `true` if the app was opened at login +* `wasOpenedAtLogin` boolean _macOS_ - `true` if the app was opened at login automatically. This setting is not available on [MAS builds][mas-builds]. -* `wasOpenedAsHidden` Boolean _macOS_ - `true` if the app was opened as a hidden login +* `wasOpenedAsHidden` boolean _macOS_ - `true` if the app was opened as a hidden login item. This indicates that the app should not open any windows at startup. This setting is not available on [MAS builds][mas-builds]. -* `restoreState` Boolean _macOS_ - `true` if the app was opened as a login item that +* `restoreState` boolean _macOS_ - `true` if the app was opened as a login item that should restore the state from the previous session. This indicates that the app should restore the windows that were open the last time the app was closed. This setting is not available on [MAS builds][mas-builds]. +* `executableWillLaunchAtLogin` boolean _Windows_ - `true` if app is set to open at login and its run key is not deactivated. This differs from `openAtLogin` as it ignores the `args` option, this property will be true if the given executable would be launched at login with **any** arguments. +* `launchItems` Object[] _Windows_ + * `name` string _Windows_ - name value of a registry entry. + * `path` string _Windows_ - The executable to an app that corresponds to a registry entry. + * `args` string[] _Windows_ - the command-line arguments to pass to the executable. + * `scope` string _Windows_ - one of `user` or `machine`. Indicates whether the registry entry is under `HKEY_CURRENT USER` or `HKEY_LOCAL_MACHINE`. + * `enabled` boolean _Windows_ - `true` if the app registry key is startup approved and therefore shows as `enabled` in Task Manager and Windows settings. ### `app.setLoginItemSettings(settings)` _macOS_ _Windows_ * `settings` Object - * `openAtLogin` Boolean (optional) - `true` to open the app at login, `false` to remove + * `openAtLogin` boolean (optional) - `true` to open the app at login, `false` to remove the app as a login item. Defaults to `false`. - * `openAsHidden` Boolean (optional) _macOS_ - `true` to open the app as hidden. Defaults to + * `openAsHidden` boolean (optional) _macOS_ - `true` to open the app as hidden. Defaults to `false`. The user can edit this setting from the System Preferences so `app.getLoginItemSettings().wasOpenedAsHidden` should be checked when the app is opened to know the current value. This setting is not available on [MAS builds][mas-builds]. - * `path` String (optional) _Windows_ - The executable to launch at login. + * `path` string (optional) _Windows_ - The executable to launch at login. Defaults to `process.execPath`. - * `args` String[] (optional) _Windows_ - The command-line arguments to pass to + * `args` string[] (optional) _Windows_ - The command-line arguments to pass to the executable. Defaults to an empty array. Take care to wrap paths in quotes. - + * `enabled` boolean (optional) _Windows_ - `true` will change the startup approved registry key and `enable / disable` the App in Task Manager and Windows Settings. + Defaults to `true`. + * `name` string (optional) _Windows_ - value name to write into registry. Defaults to the app's AppUserModelId(). Set the app's login item settings. To work with Electron's `autoUpdater` on Windows, which uses [Squirrel][Squirrel-Windows], @@ -1145,17 +1276,15 @@ app.setLoginItemSettings({ ### `app.isAccessibilitySupportEnabled()` _macOS_ _Windows_ -Returns `Boolean` - `true` if Chrome's accessibility support is enabled, +Returns `boolean` - `true` if Chrome's accessibility support is enabled, `false` otherwise. This API will return `true` if the use of assistive technologies, such as screen readers, has been detected. See https://www.chromium.org/developers/design-documents/accessibility for more details. -**[Deprecated](modernization/property-updates.md)** - ### `app.setAccessibilitySupportEnabled(enabled)` _macOS_ _Windows_ -* `enabled` Boolean - Enable or disable [accessibility tree](https://developers.google.com/web/fundamentals/accessibility/semantics-builtin/the-accessibility-tree) rendering +* `enabled` boolean - Enable or disable [accessibility tree](https://developers.google.com/web/fundamentals/accessibility/semantics-builtin/the-accessibility-tree) rendering Manually enables Chrome's accessibility support, allowing to expose accessibility switch to users in application settings. See [Chromium's accessibility docs](https://www.chromium.org/developers/design-documents/accessibility) for more details. Disabled by default. @@ -1164,30 +1293,29 @@ This API must be called after the `ready` event is emitted. **Note:** Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default. -**[Deprecated](modernization/property-updates.md)** - -### `app.showAboutPanel()` _macOS_ _Linux_ +### `app.showAboutPanel()` Show the app's about panel options. These options can be overridden with `app.setAboutPanelOptions(options)`. -### `app.setAboutPanelOptions(options)` _macOS_ _Linux_ +### `app.setAboutPanelOptions(options)` * `options` Object - * `applicationName` String (optional) - The app's name. - * `applicationVersion` String (optional) - The app's version. - * `copyright` String (optional) - Copyright information. - * `version` String (optional) _macOS_ - The app's build version number. - * `credits` String (optional) _macOS_ - Credit information. - * `authors` String[] (optional) _Linux_ - List of app authors. - * `website` String (optional) _Linux_ - The app's website. - * `iconPath` String (optional) _Linux_ - Path to the app's icon. Will be shown as 64x64 pixels while retaining aspect ratio. - -Set the about panel options. This will override the values defined in the app's -`.plist` file on MacOS. See the [Apple docs][about-panel-options] for more details. On Linux, values must be set in order to be shown; there are no defaults. + * `applicationName` string (optional) - The app's name. + * `applicationVersion` string (optional) - The app's version. + * `copyright` string (optional) - Copyright information. + * `version` string (optional) _macOS_ - The app's build version number. + * `credits` string (optional) _macOS_ _Windows_ - Credit information. + * `authors` string[] (optional) _Linux_ - List of app authors. + * `website` string (optional) _Linux_ - The app's website. + * `iconPath` string (optional) _Linux_ _Windows_ - Path to the app's icon in a JPEG or PNG file format. On Linux, will be shown as 64x64 pixels while retaining aspect ratio. + +Set the about panel options. This will override the values defined in the app's `.plist` file on macOS. See the [Apple docs][about-panel-options] for more details. On Linux, values must be set in order to be shown; there are no defaults. + +If you do not set `credits` but still wish to surface them in your app, AppKit will look for a file named "Credits.html", "Credits.rtf", and "Credits.rtfd", in that order, in the bundle returned by the NSBundle class method main. The first file found is used, and if none is found, the info area is left blank. See Apple [documentation](https://developer.apple.com/documentation/appkit/nsaboutpaneloptioncredits?language=objc) for more information. ### `app.isEmojiPanelSupported()` -Returns `Boolean` - whether or not the current OS version allows for native emoji pickers. +Returns `boolean` - whether or not the current OS version allows for native emoji pickers. ### `app.showEmojiPanel()` _macOS_ _Windows_ @@ -1195,7 +1323,7 @@ Show the platform's native emoji picker. ### `app.startAccessingSecurityScopedResource(bookmarkData)` _mas_ -* `bookmarkData` String - The base64 encoded security scoped bookmark data returned by the `dialog.showOpenDialog` or `dialog.showSaveDialog` methods. +* `bookmarkData` string - The base64 encoded security scoped bookmark data returned by the `dialog.showOpenDialog` or `dialog.showSaveDialog` methods. Returns `Function` - This function **must** be called once you have finished accessing the security scoped file. If you do not remember to stop accessing the bookmark, [kernel resources will be leaked](https://developer.apple.com/reference/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc) and your app will lose its ability to reach outside the sandbox completely, until your app is restarted. @@ -1210,24 +1338,24 @@ stopAccessingSecurityScopedResource() Start accessing a security scoped resource. With this method Electron applications that are packaged for the Mac App Store may reach outside their sandbox to access files chosen by the user. See [Apple's documentation](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) for a description of how this system works. -### `app.enableSandbox()` _Experimental_ +### `app.enableSandbox()` -Enables full sandbox mode on the app. +Enables full sandbox mode on the app. This means that all renderers will be launched sandboxed, regardless of the value of the `sandbox` flag in WebPreferences. This method can only be called before app is ready. ### `app.isInApplicationsFolder()` _macOS_ -Returns `Boolean` - Whether the application is currently running from the +Returns `boolean` - Whether the application is currently running from the systems Application folder. Use in combination with `app.moveToApplicationsFolder()` ### `app.moveToApplicationsFolder([options])` _macOS_ * `options` Object (optional) - * `conflictHandler` Function (optional) - A handler for potential conflict in move failure. - * `conflictType` String - The type of move conflict encountered by the handler; can be `exists` or `existsAndRunning`, where `exists` means that an app of the same name is present in the Applications directory and `existsAndRunning` means both that it exists and that it's presently running. + * `conflictHandler` Function\ (optional) - A handler for potential conflict in move failure. + * `conflictType` string - The type of move conflict encountered by the handler; can be `exists` or `existsAndRunning`, where `exists` means that an app of the same name is present in the Applications directory and `existsAndRunning` means both that it exists and that it's presently running. -Returns `Boolean` - Whether the move was successful. Please note that if +Returns `boolean` - Whether the move was successful. Please note that if the move is successful, your application will quit and relaunch. No confirmation dialog will be presented by default. If you wish to allow @@ -1240,7 +1368,7 @@ method returns false. If we fail to perform the copy, then this method will throw an error. The message in the error should be informative and tell you exactly what went wrong. -By default, if an app of the same name as the one being moved exists in the Applications directory and is _not_ running, the existing app will be trashed and the active app moved into its place. If it _is_ running, the pre-existing running app will assume focus and the the previously active app will quit itself. This behavior can be changed by providing the optional conflict handler, where the boolean returned by the handler determines whether or not the move conflict is resolved with default behavior. i.e. returning `false` will ensure no further action is taken, returning `true` will result in the default behavior and the method continuing. +By default, if an app of the same name as the one being moved exists in the Applications directory and is _not_ running, the existing app will be trashed and the active app moved into its place. If it _is_ running, the pre-existing running app will assume focus and the previously active app will quit itself. This behavior can be changed by providing the optional conflict handler, where the boolean returned by the handler determines whether or not the move conflict is resolved with default behavior. i.e. returning `false` will ensure no further action is taken, returning `true` will result in the default behavior and the method continuing. For example: @@ -1261,11 +1389,30 @@ app.moveToApplicationsFolder({ Would mean that if an app already exists in the user directory, if the user chooses to 'Continue Move' then the function would continue with its default behavior and the existing app will be trashed and the active app moved into its place. +### `app.isSecureKeyboardEntryEnabled()` _macOS_ + +Returns `boolean` - whether `Secure Keyboard Entry` is enabled. + +By default this API will return `false`. + +### `app.setSecureKeyboardEntryEnabled(enabled)` _macOS_ + +* `enabled` boolean - Enable or disable `Secure Keyboard Entry` + +Set the `Secure Keyboard Entry` is enabled in your application. + +By using this API, important information such as password and other sensitive information can be prevented from being intercepted by other processes. + +See [Apple's documentation](https://developer.apple.com/library/archive/technotes/tn2150/_index.html) for more +details. + +**Note:** Enable `Secure Keyboard Entry` only when it is needed and disable it when it is no longer needed. + ## Properties ### `app.accessibilitySupportEnabled` _macOS_ _Windows_ -A `Boolean` property that's `true` if Chrome's accessibility support is enabled, `false` otherwise. This property will be `true` if the use of assistive technologies, such as screen readers, has been detected. Setting this property to `true` manually enables Chrome's accessibility support, allowing developers to expose accessibility switch to users in application settings. +A `boolean` property that's `true` if Chrome's accessibility support is enabled, `false` otherwise. This property will be `true` if the use of assistive technologies, such as screen readers, has been detected. Setting this property to `true` manually enables Chrome's accessibility support, allowing developers to expose accessibility switch to users in application settings. See [Chromium's accessibility docs](https://www.chromium.org/developers/design-documents/accessibility) for more details. Disabled by default. @@ -1284,8 +1431,11 @@ An `Integer` property that returns the badge count for current app. Setting the On macOS, setting this with any nonzero integer shows on the dock icon. On Linux, this property only works for Unity launcher. -**Note:** Unity launcher requires the existence of a `.desktop` file to work, -for more information please read [Desktop Environment Integration][unity-requirement]. +**Note:** Unity launcher requires a `.desktop` file to work. For more information, +please read the [Unity integration documentation][unity-requirement]. + +**Note:** On macOS, you need to ensure that your application has the permission +to display notifications for this property to take effect. ### `app.commandLine` _Readonly_ @@ -1294,21 +1444,23 @@ command line arguments that Chromium uses. ### `app.dock` _macOS_ _Readonly_ -A [`Dock`](./dock.md) object that allows you to perform actions on your app icon in the user's +A [`Dock`](./dock.md) `| undefined` object that allows you to perform actions on your app icon in the user's dock on macOS. ### `app.isPackaged` _Readonly_ -A `Boolean` property that returns `true` if the app is packaged, `false` otherwise. For many apps, this property can be used to distinguish development and production environments. +A `boolean` property that returns `true` if the app is packaged, `false` otherwise. For many apps, this property can be used to distinguish development and production environments. [dock-menu]:https://developer.apple.com/macos/human-interface-guidelines/menus/dock-menus/ [tasks]:https://msdn.microsoft.com/en-us/library/windows/desktop/dd378460(v=vs.85).aspx#tasks [app-user-model-id]: https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx +[electron-forge]: https://www.electronforge.io/ +[electron-packager]: https://github.com/electron/electron-packager [CFBundleURLTypes]: https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/TP40009249-102207-TPXREF115 [LSCopyDefaultHandlerForURLScheme]: https://developer.apple.com/library/mac/documentation/Carbon/Reference/LaunchServicesReference/#//apple_ref/c/func/LSCopyDefaultHandlerForURLScheme [handoff]: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/Handoff/HandoffFundamentals/HandoffFundamentals.html [activity-type]: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSUserActivity_Class/index.html#//apple_ref/occ/instp/NSUserActivity/activityType -[unity-requirement]: ../tutorial/desktop-environment-integration.md#unity-launcher +[unity-requirement]: https://help.ubuntu.com/community/UnityLaunchersAndDesktopFiles#Adding_shortcuts_to_a_launcher [mas-builds]: ../tutorial/mac-app-store-submission-guide.md [Squirrel-Windows]: https://github.com/Squirrel/Squirrel.Windows [JumpListBeginListMSDN]: https://msdn.microsoft.com/en-us/library/windows/desktop/dd378398(v=vs.85).aspx @@ -1316,7 +1468,7 @@ A `Boolean` property that returns `true` if the app is packaged, `false` otherw ### `app.name` -A `String` property that indicates the current application's name, which is the name in the application's `package.json` file. +A `string` property that indicates the current application's name, which is the name in the application's `package.json` file. Usually the `name` field of `package.json` is a short lowercase name, according to the npm modules spec. You should usually also specify a `productName` @@ -1325,22 +1477,33 @@ preferred over `name` by Electron. ### `app.userAgentFallback` -A `String` which is the user agent string Electron will use as a global fallback. +A `string` which is the user agent string Electron will use as a global fallback. This is the user agent that will be used when no user agent is set at the `webContents` or `session` level. It is useful for ensuring that your entire app has the same user agent. Set to a custom value as early as possible in your app's initialization to ensure that your overridden value is used. -### `app.allowRendererProcessReuse` +### `app.runningUnderRosettaTranslation` _macOS_ _Readonly_ _Deprecated_ + +A `boolean` which when `true` indicates that the app is currently running +under the [Rosetta Translator Environment](https://en.wikipedia.org/wiki/Rosetta_(software)). + +You can use this property to prompt users to download the arm64 version of +your application when they are running the x64 version under Rosetta +incorrectly. + +**Deprecated:** This property is superceded by the `runningUnderARM64Translation` +property which detects when the app is being translated to ARM64 in both macOS +and Windows. + +### `app.runningUnderARM64Translation` _Readonly_ _macOS_ _Windows_ -A `Boolean` which when `true` disables the overrides that Electron has in place -to ensure renderer processes are restarted on every navigation. The current -default value for this property is `false`. +A `boolean` which when `true` indicates that the app is currently running under +an ARM64 translator (like the macOS +[Rosetta Translator Environment](https://en.wikipedia.org/wiki/Rosetta_(software)) +or Windows [WOW](https://en.wikipedia.org/wiki/Windows_on_Windows)). -The intention is for these overrides to become disabled by default and then at -some point in the future this property will be removed. This property impacts -which native modules you can use in the renderer process. For more information -on the direction Electron is going with renderer process restarts and usage of -native modules in the renderer process please check out this -[Tracking Issue](https://github.com/electron/electron/issues/18397). +You can use this property to prompt users to download the arm64 version of +your application when they are running the x64 version under Rosetta +incorrectly. diff --git a/docs/api/auto-updater.md b/docs/api/auto-updater.md index 0d121097e81d0..d181dfaccb0ec 100644 --- a/docs/api/auto-updater.md +++ b/docs/api/auto-updater.md @@ -43,7 +43,7 @@ The installer generated with Squirrel will create a shortcut icon with an same ID for your app with `app.setAppUserModelId` API, otherwise Windows will not be able to pin your app properly in task bar. -Unlike Squirrel.Mac, Windows can host updates on S3 or any other static file host. +Like Squirrel.Mac, Windows can host updates on S3 or any other static file host. You can read the documents of [Squirrel.Windows][squirrel-windows] to get more details about how Squirrel.Windows works. @@ -77,10 +77,10 @@ Emitted when there is no available update. Returns: * `event` Event -* `releaseNotes` String -* `releaseName` String +* `releaseNotes` string +* `releaseName` string * `releaseDate` Date -* `updateURL` String +* `updateURL` string Emitted when an update has been downloaded. @@ -102,22 +102,25 @@ The `autoUpdater` object has the following methods: ### `autoUpdater.setFeedURL(options)` * `options` Object - * `url` String - * `headers` Object (optional) _macOS_ - HTTP request headers. - * `serverType` String (optional) _macOS_ - Either `json` or `default`, see the [Squirrel.Mac][squirrel-mac] + * `url` string + * `headers` Record (optional) _macOS_ - HTTP request headers. + * `serverType` string (optional) _macOS_ - Can be `json` or `default`, see the [Squirrel.Mac][squirrel-mac] README for more information. Sets the `url` and initialize the auto updater. ### `autoUpdater.getFeedURL()` -Returns `String` - The current update feed URL. +Returns `string` - The current update feed URL. ### `autoUpdater.checkForUpdates()` Asks the server whether there is an update. You must call `setFeedURL` before using this API. +**Note:** If an update is available it will be downloaded automatically. +Calling `autoUpdater.checkForUpdates()` twice will download the update two times. + ### `autoUpdater.quitAndInstall()` Restarts the app and installs the update after it has been downloaded. It diff --git a/docs/api/breaking-changes-ns.md b/docs/api/breaking-changes-ns.md deleted file mode 100644 index 1914c1f43f449..0000000000000 --- a/docs/api/breaking-changes-ns.md +++ /dev/null @@ -1,61 +0,0 @@ -# Breaking changes (NetworkService) (Draft) - -This document describes changes to Electron APIs after migrating network code -to NetworkService API. - -We don't currently have an estimate of when we will enable `NetworkService` by -default in Electron, but as Chromium is already removing non-`NetworkService` -code, we might switch before Electron 10. - -The content of this document should be moved to `breaking-changes.md` once we have -determined when to enable `NetworkService` in Electron. - -## Planned Breaking API Changes - -### `protocol.unregisterProtocol` -### `protocol.uninterceptProtocol` - -The APIs are now synchronous and the optional callback is no longer needed. - -```javascript -// Deprecated -protocol.unregisterProtocol(scheme, () => { /* ... */ }) -// Replace with -protocol.unregisterProtocol(scheme) -``` - -### `protocol.registerFileProtocol` -### `protocol.registerBufferProtocol` -### `protocol.registerStringProtocol` -### `protocol.registerHttpProtocol` -### `protocol.registerStreamProtocol` -### `protocol.interceptFileProtocol` -### `protocol.interceptStringProtocol` -### `protocol.interceptBufferProtocol` -### `protocol.interceptHttpProtocol` -### `protocol.interceptStreamProtocol` - -The APIs are now synchronous and the optional callback is no longer needed. - -```javascript -// Deprecated -protocol.registerFileProtocol(scheme, handler, () => { /* ... */ }) -// Replace with -protocol.registerFileProtocol(scheme, handler) -``` - -The registered or intercepted protocol does not have effect on current page -until navigation happens. - -### `protocol.isProtocolHandled` - -This API is deprecated and users should use `protocol.isProtocolRegistered` -and `protocol.isProtocolIntercepted` instead. - -```javascript -// Deprecated -protocol.isProtocolHandled(scheme).then(() => { /* ... */ }) -// Replace with -const isRegistered = protocol.isProtocolRegistered(scheme) -const isIntercepted = protocol.isProtocolIntercepted(scheme) -``` diff --git a/docs/api/breaking-changes.md b/docs/api/breaking-changes.md deleted file mode 100644 index 57c0afedffb16..0000000000000 --- a/docs/api/breaking-changes.md +++ /dev/null @@ -1,548 +0,0 @@ -# Breaking Changes - -Breaking changes will be documented here, and deprecation warnings added to JS code where possible, at least [one major version](../tutorial/electron-versioning.md#semver) before the change is made. - -## `FIXME` comments - -The `FIXME` string is used in code comments to denote things that should be fixed for future releases. See https://github.com/electron/electron/search?q=fixme - -## Planned Breaking API Changes (7.0) - -### Node Headers URL - -This is the URL specified as `disturl` in a `.npmrc` file or as the `--dist-url` -command line flag when building native Node modules. Both will be supported for -the foreseeable future but it is recommended that you switch. - -Deprecated: https://atom.io/download/electron - -Replace with: https://electronjs.org/headers - -### `session.clearAuthCache(options)` - -The `session.clearAuthCache` API no longer accepts options for what to clear, and instead unconditionally clears the whole cache. - -```js -// Deprecated -session.clearAuthCache({ type: 'password' }) -// Replace with -session.clearAuthCache() -``` - -### `powerMonitor.querySystemIdleState` - -```js -// Removed in Electron 7.0 -powerMonitor.querySystemIdleState(threshold, callback) -// Replace with synchronous API -const idleState = getSystemIdleState(threshold) -``` - -### `powerMonitor.querySystemIdleTime` - -```js -// Removed in Electron 7.0 -powerMonitor.querySystemIdleTime(callback) -// Replace with synchronous API -const idleTime = getSystemIdleTime() -``` - -### webFrame Isolated World APIs - -```js -// Removed in Elecron 7.0 -webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp) -webFrame.setIsolatedWorldHumanReadableName(worldId, name) -webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin) -// Replace with -webFrame.setIsolatedWorldInfo( - worldId, - { - securityOrigin: 'some_origin', - name: 'human_readable_name', - csp: 'content_security_policy' - }) -``` - -### Removal of deprecated `marked` property on getBlinkMemoryInfo - -This property was removed in Chromium 77, and as such is no longer available. - -## Planned Breaking API Changes (6.0) - -### `win.setMenu(null)` - -```js -// Deprecated -win.setMenu(null) -// Replace with -win.removeMenu() -``` - -### `contentTracing.getTraceBufferUsage()` - -```js -// Deprecated -contentTracing.getTraceBufferUsage((percentage, value) => { - // do something -}) -// Replace with -contentTracing.getTraceBufferUsage().then(infoObject => { - // infoObject has percentage and value fields -}) -``` - -### `electron.screen` in renderer process - -```js -// Deprecated -require('electron').screen -// Replace with -require('electron').remote.screen -``` - -### `require` in sandboxed renderers - -```js -// Deprecated -require('child_process') -// Replace with -require('electron').remote.require('child_process') - -// Deprecated -require('fs') -// Replace with -require('electron').remote.require('fs') - -// Deprecated -require('os') -// Replace with -require('electron').remote.require('os') - -// Deprecated -require('path') -// Replace with -require('electron').remote.require('path') -``` - -### `powerMonitor.querySystemIdleState` - -```js -// Deprecated -powerMonitor.querySystemIdleState(threshold, callback) -// Replace with synchronous API -const idleState = getSystemIdleState(threshold) -``` - -### `powerMonitor.querySystemIdleTime` - -```js -// Deprecated -powerMonitor.querySystemIdleTime(callback) -// Replace with synchronous API -const idleTime = getSystemIdleTime() -``` - -### `app.enableMixedSandbox` - -```js -// Deprecated -app.enableMixedSandbox() -``` - -Mixed-sandbox mode is now enabled by default. - -### `Tray` - -Under macOS Catalina our former Tray implementation breaks. -Apple's native substitute doesn't support changing the highlighting behavior. - -```js -// Deprecated -tray.setHighlightMode(mode) -// API will be removed in v7.0 without replacement. -``` - -## Planned Breaking API Changes (5.0) - -### `new BrowserWindow({ webPreferences })` - -The following `webPreferences` option default values are deprecated in favor of the new defaults listed below. - -| Property | Deprecated Default | New Default | -|----------|--------------------|-------------| -| `contextIsolation` | `false` | `true` | -| `nodeIntegration` | `true` | `false` | -| `webviewTag` | `nodeIntegration` if set else `true` | `false` | - -E.g. Re-enabling the webviewTag - -```js -const w = new BrowserWindow({ - webPreferences: { - webviewTag: true - } -}) -``` - -### `nativeWindowOpen` - -Child windows opened with the `nativeWindowOpen` option will always have Node.js integration disabled, unless `nodeIntegrationInSubFrames` is `true. - -### Privileged Schemes Registration - -Renderer process APIs `webFrame.setRegisterURLSchemeAsPrivileged` and `webFrame.registerURLSchemeAsBypassingCSP` as well as browser process API `protocol.registerStandardSchemes` have been removed. -A new API, `protocol.registerSchemesAsPrivileged` has been added and should be used for registering custom schemes with the required privileges. Custom schemes are required to be registered before app ready. - -### webFrame Isolated World APIs - -```js -// Deprecated -webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp) -webFrame.setIsolatedWorldHumanReadableName(worldId, name) -webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin) -// Replace with -webFrame.setIsolatedWorldInfo( - worldId, - { - securityOrigin: 'some_origin', - name: 'human_readable_name', - csp: 'content_security_policy' - }) -``` - -## `webFrame.setSpellCheckProvider` -The `spellCheck` callback is now asynchronous, and `autoCorrectWord` parameter has been removed. -```js -// Deprecated -webFrame.setSpellCheckProvider('en-US', true, { - spellCheck: (text) => { - return !spellchecker.isMisspelled(text) - } -}) -// Replace with -webFrame.setSpellCheckProvider('en-US', { - spellCheck: (words, callback) => { - callback(words.filter(text => spellchecker.isMisspelled(text))) - } -}) -``` - -## Planned Breaking API Changes (4.0) - -The following list includes the breaking API changes made in Electron 4.0. - -### `app.makeSingleInstance` - -```js -// Deprecated -app.makeSingleInstance((argv, cwd) => { - /* ... */ -}) -// Replace with -app.requestSingleInstanceLock() -app.on('second-instance', (event, argv, cwd) => { - /* ... */ -}) -``` - -### `app.releaseSingleInstance` - -```js -// Deprecated -app.releaseSingleInstance() -// Replace with -app.releaseSingleInstanceLock() -``` - -### `app.getGPUInfo` - -```js -app.getGPUInfo('complete') -// Now behaves the same with `basic` on macOS -app.getGPUInfo('basic') -``` - -### `win_delay_load_hook` - -When building native modules for windows, the `win_delay_load_hook` variable in -the module's `binding.gyp` must be true (which is the default). If this hook is -not present, then the native module will fail to load on Windows, with an error -message like `Cannot find module`. See the [native module -guide](/docs/tutorial/using-native-node-modules.md) for more. - -## Breaking API Changes (3.0) - -The following list includes the breaking API changes in Electron 3.0. - -### `app` - -```js -// Deprecated -app.getAppMemoryInfo() -// Replace with -app.getAppMetrics() - -// Deprecated -const metrics = app.getAppMetrics() -const { memory } = metrics[0] // Deprecated property -``` - -### `BrowserWindow` - -```js -// Deprecated -let optionsA = { webPreferences: { blinkFeatures: '' } } -let windowA = new BrowserWindow(optionsA) -// Replace with -let optionsB = { webPreferences: { enableBlinkFeatures: '' } } -let windowB = new BrowserWindow(optionsB) - -// Deprecated -window.on('app-command', (e, cmd) => { - if (cmd === 'media-play_pause') { - // do something - } -}) -// Replace with -window.on('app-command', (e, cmd) => { - if (cmd === 'media-play-pause') { - // do something - } -}) -``` - -### `clipboard` - -```js -// Deprecated -clipboard.readRtf() -// Replace with -clipboard.readRTF() - -// Deprecated -clipboard.writeRtf() -// Replace with -clipboard.writeRTF() - -// Deprecated -clipboard.readHtml() -// Replace with -clipboard.readHTML() - -// Deprecated -clipboard.writeHtml() -// Replace with -clipboard.writeHTML() -``` - -### `crashReporter` - -```js -// Deprecated -crashReporter.start({ - companyName: 'Crashly', - submitURL: 'https://crash.server.com', - autoSubmit: true -}) -// Replace with -crashReporter.start({ - companyName: 'Crashly', - submitURL: 'https://crash.server.com', - uploadToServer: true -}) -``` - -### `nativeImage` - -```js -// Deprecated -nativeImage.createFromBuffer(buffer, 1.0) -// Replace with -nativeImage.createFromBuffer(buffer, { - scaleFactor: 1.0 -}) -``` - -### `process` - -```js -// Deprecated -const info = process.getProcessMemoryInfo() -``` - -### `screen` - -```js -// Deprecated -screen.getMenuBarHeight() -// Replace with -screen.getPrimaryDisplay().workArea -``` - -### `session` - -```js -// Deprecated -ses.setCertificateVerifyProc((hostname, certificate, callback) => { - callback(true) -}) -// Replace with -ses.setCertificateVerifyProc((request, callback) => { - callback(0) -}) -``` - -### `Tray` - -```js -// Deprecated -tray.setHighlightMode(true) -// Replace with -tray.setHighlightMode('on') - -// Deprecated -tray.setHighlightMode(false) -// Replace with -tray.setHighlightMode('off') -``` - -### `webContents` - -```js -// Deprecated -webContents.openDevTools({ detach: true }) -// Replace with -webContents.openDevTools({ mode: 'detach' }) - -// Removed -webContents.setSize(options) -// There is no replacement for this API -``` - -### `webFrame` - -```js -// Deprecated -webFrame.registerURLSchemeAsSecure('app') -// Replace with -protocol.registerStandardSchemes(['app'], { secure: true }) - -// Deprecated -webFrame.registerURLSchemeAsPrivileged('app', { secure: true }) -// Replace with -protocol.registerStandardSchemes(['app'], { secure: true }) -``` - -### `` - -```js -// Removed -webview.setAttribute('disableguestresize', '') -// There is no replacement for this API - -// Removed -webview.setAttribute('guestinstance', instanceId) -// There is no replacement for this API - -// Keyboard listeners no longer work on webview tag -webview.onkeydown = () => { /* handler */ } -webview.onkeyup = () => { /* handler */ } -``` - -### Node Headers URL - -This is the URL specified as `disturl` in a `.npmrc` file or as the `--dist-url` -command line flag when building native Node modules. - -Deprecated: https://atom.io/download/atom-shell - -Replace with: https://atom.io/download/electron - -## Breaking API Changes (2.0) - -The following list includes the breaking API changes made in Electron 2.0. - -### `BrowserWindow` - -```js -// Deprecated -let optionsA = { titleBarStyle: 'hidden-inset' } -let windowA = new BrowserWindow(optionsA) -// Replace with -let optionsB = { titleBarStyle: 'hiddenInset' } -let windowB = new BrowserWindow(optionsB) -``` - -### `menu` - -```js -// Removed -menu.popup(browserWindow, 100, 200, 2) -// Replaced with -menu.popup(browserWindow, { x: 100, y: 200, positioningItem: 2 }) -``` - -### `nativeImage` - -```js -// Removed -nativeImage.toPng() -// Replaced with -nativeImage.toPNG() - -// Removed -nativeImage.toJpeg() -// Replaced with -nativeImage.toJPEG() -``` - -### `process` - -* `process.versions.electron` and `process.version.chrome` will be made - read-only properties for consistency with the other `process.versions` - properties set by Node. - -### `webContents` - -```js -// Removed -webContents.setZoomLevelLimits(1, 2) -// Replaced with -webContents.setVisualZoomLevelLimits(1, 2) -``` - -### `webFrame` - -```js -// Removed -webFrame.setZoomLevelLimits(1, 2) -// Replaced with -webFrame.setVisualZoomLevelLimits(1, 2) -``` - -### `` - -```js -// Removed -webview.setZoomLevelLimits(1, 2) -// Replaced with -webview.setVisualZoomLevelLimits(1, 2) -``` - -### Duplicate ARM Assets - -Each Electron release includes two identical ARM builds with slightly different -filenames, like `electron-v1.7.3-linux-arm.zip` and -`electron-v1.7.3-linux-armv7l.zip`. The asset with the `v7l` prefix was added -to clarify to users which ARM version it supports, and to disambiguate it from -future armv6l and arm64 assets that may be produced. - -The file _without the prefix_ is still being published to avoid breaking any -setups that may be consuming it. Starting at 2.0, the unprefixed file will -no longer be published. - -For details, see -[6986](https://github.com/electron/electron/pull/6986) -and -[7189](https://github.com/electron/electron/pull/7189). diff --git a/docs/api/browser-view.md b/docs/api/browser-view.md index c4a38fce0af6c..ed63f4a4881db 100644 --- a/docs/api/browser-view.md +++ b/docs/api/browser-view.md @@ -1,29 +1,30 @@ -## Class: BrowserView - -> Create and control views. - -Process: [Main](../glossary.md#main-process) +# BrowserView A `BrowserView` can be used to embed additional web content into a [`BrowserWindow`](browser-window.md). It is like a child window, except that it is positioned relative to its owning window. It is meant to be an alternative to the `webview` tag. +## Class: BrowserView + +> Create and control views. + +Process: [Main](../glossary.md#main-process) + ### Example ```javascript // In the main process. -const { BrowserView, BrowserWindow } = require('electron') +const { app, BrowserView, BrowserWindow } = require('electron') -let win = new BrowserWindow({ width: 800, height: 600 }) -win.on('closed', () => { - win = null -}) +app.whenReady().then(() => { + const win = new BrowserWindow({ width: 800, height: 600 }) -let view = new BrowserView() -win.setBrowserView(view) -view.setBounds({ x: 0, y: 0, width: 300, height: 300 }) -view.webContents.loadURL('https://electronjs.org') + const view = new BrowserView() + win.setBrowserView(view) + view.setBounds({ x: 0, y: 0, width: 300, height: 300 }) + view.webContents.loadURL('https://electronjs.org') +}) ``` ### `new BrowserView([options])` _Experimental_ @@ -31,25 +32,6 @@ view.webContents.loadURL('https://electronjs.org') * `options` Object (optional) * `webPreferences` Object (optional) - See [BrowserWindow](browser-window.md). -### Static Methods - -#### `BrowserView.getAllViews()` - -Returns `BrowserView[]` - An array of all opened BrowserViews. - -#### `BrowserView.fromWebContents(webContents)` - -* `webContents` [WebContents](web-contents.md) - -Returns `BrowserView | null` - The BrowserView that owns the given `webContents` -or `null` if the contents are not owned by a BrowserView. - -#### `BrowserView.fromId(id)` - -* `id` Integer - -Returns `BrowserView` - The view with the given `id`. - ### Instance Properties Objects created with `new BrowserView` have the following properties: @@ -58,34 +40,20 @@ Objects created with `new BrowserView` have the following properties: A [`WebContents`](web-contents.md) object owned by this view. -#### `view.id` _Experimental_ - -A `Integer` representing the unique ID of the view. - ### Instance Methods Objects created with `new BrowserView` have the following instance methods: -#### `view.destroy()` - -Force closing the view, the `unload` and `beforeunload` events won't be emitted -for the web page. After you're done with a view, call this function in order to -free memory and other resources as soon as possible. - -#### `view.isDestroyed()` - -Returns `Boolean` - Whether the view is destroyed. - #### `view.setAutoResize(options)` _Experimental_ * `options` Object - * `width` Boolean (optional) - If `true`, the view's width will grow and shrink together + * `width` boolean (optional) - If `true`, the view's width will grow and shrink together with the window. `false` by default. - * `height` Boolean (optional) - If `true`, the view's height will grow and shrink + * `height` boolean (optional) - If `true`, the view's height will grow and shrink together with the window. `false` by default. - * `horizontal` Boolean (optional) - If `true`, the view's x position and width will grow + * `horizontal` boolean (optional) - If `true`, the view's x position and width will grow and shrink proportionally with the window. `false` by default. - * `vertical` Boolean (optional) - If `true`, the view's y position and height will grow + * `vertical` boolean (optional) - If `true`, the view's y position and height will grow and shrink proportionally with the window. `false` by default. #### `view.setBounds(bounds)` _Experimental_ @@ -102,5 +70,31 @@ The `bounds` of this BrowserView instance as `Object`. #### `view.setBackgroundColor(color)` _Experimental_ -* `color` String - Color in `#aarrggbb` or `#argb` form. The alpha channel is - optional. +* `color` string - Color in Hex, RGB, ARGB, HSL, HSLA or named CSS color format. The alpha channel is + optional for the hex type. + +Examples of valid `color` values: + +* Hex + * #fff (RGB) + * #ffff (ARGB) + * #ffffff (RRGGBB) + * #ffffffff (AARRGGBB) +* RGB + * rgb\(([\d]+),\s*([\d]+),\s*([\d]+)\) + * e.g. rgb(255, 255, 255) +* RGBA + * rgba\(([\d]+),\s*([\d]+),\s*([\d]+),\s*([\d.]+)\) + * e.g. rgba(255, 255, 255, 1.0) +* HSL + * hsl\((-?[\d.]+),\s*([\d.]+)%,\s*([\d.]+)%\) + * e.g. hsl(200, 20%, 50%) +* HSLA + * hsla\((-?[\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\) + * e.g. hsla(200, 20%, 50%, 0.5) +* Color name + * Options are listed in [SkParseColor.cpp](https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/utils/SkParseColor.cpp;l=11-152;drc=eea4bf52cb0d55e2a39c828b017c80a5ee054148) + * Similar to CSS Color Module Level 3 keywords, but case-sensitive. + * e.g. `blueviolet` or `red` + +**Note:** Hex format with alpha takes `AARRGGBB` or `ARGB`, _not_ `RRGGBBA` or `RGA`. diff --git a/docs/api/browser-window-proxy.md b/docs/api/browser-window-proxy.md deleted file mode 100644 index 33a1022317d99..0000000000000 --- a/docs/api/browser-window-proxy.md +++ /dev/null @@ -1,53 +0,0 @@ -## Class: BrowserWindowProxy - -> Manipulate the child browser window - -Process: [Renderer](../glossary.md#renderer-process) - -The `BrowserWindowProxy` object is returned from `window.open` and provides -limited functionality with the child window. - -### Instance Methods - -The `BrowserWindowProxy` object has the following instance methods: - -#### `win.blur()` - -Removes focus from the child window. - -#### `win.close()` - -Forcefully closes the child window without calling its unload event. - -#### `win.eval(code)` - -* `code` String - -Evaluates the code in the child window. - -#### `win.focus()` - -Focuses the child window (brings the window to front). - -#### `win.print()` - -Invokes the print dialog on the child window. - -#### `win.postMessage(message, targetOrigin)` - -* `message` any -* `targetOrigin` String - -Sends a message to the child window with the specified origin or `*` for no -origin preference. - -In addition to these methods, the child window implements `window.opener` object -with no properties and a single method. - -### Instance Properties - -The `BrowserWindowProxy` object has the following instance properties: - -#### `win.closed` - -A `Boolean` that is set to true after the child window gets closed. diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 3fcac5ab91953..f00d9f2e1a54e 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -8,32 +8,28 @@ Process: [Main](../glossary.md#main-process) // In the main process. const { BrowserWindow } = require('electron') -// Or use `remote` from the renderer process. -// const { BrowserWindow } = require('electron').remote - -let win = new BrowserWindow({ width: 800, height: 600 }) -win.on('closed', () => { - win = null -}) +const win = new BrowserWindow({ width: 800, height: 600 }) // Load a remote URL win.loadURL('https://github.com') // Or load a local HTML file -win.loadURL(`file://${__dirname}/app/index.html`) +win.loadFile('index.html') ``` -## Frameless window +## Window customization -To create a window without chrome, or a transparent window in arbitrary shape, -you can use the [Frameless Window](frameless-window.md) API. +The `BrowserWindow` class exposes various ways to modify the look and behavior of +your app's windows. For more details, see the [Window Customization](../tutorial/window-customization.md) +tutorial. -## Showing window gracefully +## Showing the window gracefully -When loading a page in the window directly, users may see the page load incrementally, which is not a good experience for a native app. To make the window display -without visual flash, there are two solutions for different situations. +When loading a page in the window directly, users may see the page load incrementally, +which is not a good experience for a native app. To make the window display +without a visual flash, there are two solutions for different situations. -## Using `ready-to-show` event +### Using the `ready-to-show` event While loading the page, the `ready-to-show` event will be emitted when the renderer process has rendered the page for the first time if the window has not been shown yet. Showing @@ -41,7 +37,7 @@ the window after this event will have no visual flash: ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ show: false }) +const win = new BrowserWindow({ show: false }) win.once('ready-to-show', () => { win.show() }) @@ -51,7 +47,10 @@ This event is usually emitted after the `did-finish-load` event, but for pages with many remote resources, it may be emitted before the `did-finish-load` event. -## Setting `backgroundColor` +Please note that using this event implies that the renderer will be considered "visible" and +paint even though `show` is false. This event will never fire if you use `paintWhenInitiallyHidden: false` + +### Setting the `backgroundColor` property For a complex app, the `ready-to-show` event could be emitted too late, making the app feel slow. In this case, it is recommended to show the window @@ -60,13 +59,25 @@ immediately, and use a `backgroundColor` close to your app's background: ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ backgroundColor: '#2e2c29' }) +const win = new BrowserWindow({ backgroundColor: '#2e2c29' }) win.loadURL('https://github.com') ``` Note that even for apps that use `ready-to-show` event, it is still recommended to set `backgroundColor` to make app feel more native. +Some examples of valid `backgroundColor` values include: + +```js +const win = new BrowserWindow() +win.setBackgroundColor('hsl(230, 100%, 50%)') +win.setBackgroundColor('rgb(255, 145, 145)') +win.setBackgroundColor('#ff00a3') +win.setBackgroundColor('blueviolet') +``` + +For more information about these color types see valid options in [win.setBackgroundColor](browser-window.md#winsetbackgroundcolorbackgroundcolor). + ## Parent and child windows By using `parent` option, you can create child windows: @@ -74,8 +85,8 @@ By using `parent` option, you can create child windows: ```javascript const { BrowserWindow } = require('electron') -let top = new BrowserWindow() -let child = new BrowserWindow({ parent: top }) +const top = new BrowserWindow() +const child = new BrowserWindow({ parent: top }) child.show() top.show() ``` @@ -90,7 +101,7 @@ window, you have to set both `parent` and `modal` options: ```javascript const { BrowserWindow } = require('electron') -let child = new BrowserWindow({ parent: top, modal: true, show: false }) +const child = new BrowserWindow({ parent: top, modal: true, show: false }) child.loadURL('https://github.com') child.once('ready-to-show', () => { child.show() @@ -144,216 +155,203 @@ It creates a new `BrowserWindow` with native properties as set by the `options`. Default is to center the window. * `y` Integer (optional) - (**required** if x is used) Window's top offset from screen. Default is to center the window. - * `useContentSize` Boolean (optional) - The `width` and `height` would be used as web + * `useContentSize` boolean (optional) - The `width` and `height` would be used as web page's size, which means the actual window's size will include window frame's size and be slightly larger. Default is `false`. - * `center` Boolean (optional) - Show window in the center of the screen. + * `center` boolean (optional) - Show window in the center of the screen. * `minWidth` Integer (optional) - Window's minimum width. Default is `0`. * `minHeight` Integer (optional) - Window's minimum height. Default is `0`. * `maxWidth` Integer (optional) - Window's maximum width. Default is no limit. * `maxHeight` Integer (optional) - Window's maximum height. Default is no limit. - * `resizable` Boolean (optional) - Whether window is resizable. Default is `true`. - * `movable` Boolean (optional) - Whether window is movable. This is not implemented + * `resizable` boolean (optional) - Whether window is resizable. Default is `true`. + * `movable` boolean (optional) - Whether window is movable. This is not implemented on Linux. Default is `true`. - * `minimizable` Boolean (optional) - Whether window is minimizable. This is not + * `minimizable` boolean (optional) - Whether window is minimizable. This is not implemented on Linux. Default is `true`. - * `maximizable` Boolean (optional) - Whether window is maximizable. This is not + * `maximizable` boolean (optional) - Whether window is maximizable. This is not implemented on Linux. Default is `true`. - * `closable` Boolean (optional) - Whether window is closable. This is not implemented + * `closable` boolean (optional) - Whether window is closable. This is not implemented on Linux. Default is `true`. - * `focusable` Boolean (optional) - Whether the window can be focused. Default is + * `focusable` boolean (optional) - Whether the window can be focused. Default is `true`. On Windows setting `focusable: false` also implies setting `skipTaskbar: true`. On Linux setting `focusable: false` makes the window stop interacting with wm, so the window will always stay on top in all workspaces. - * `alwaysOnTop` Boolean (optional) - Whether the window should always stay on top of + * `alwaysOnTop` boolean (optional) - Whether the window should always stay on top of other windows. Default is `false`. - * `fullscreen` Boolean (optional) - Whether the window should show in fullscreen. When + * `fullscreen` boolean (optional) - Whether the window should show in fullscreen. When explicitly set to `false` the fullscreen button will be hidden or disabled on macOS. Default is `false`. - * `fullscreenable` Boolean (optional) - Whether the window can be put into fullscreen + * `fullscreenable` boolean (optional) - Whether the window can be put into fullscreen mode. On macOS, also whether the maximize/zoom button should toggle full screen mode or maximize window. Default is `true`. - * `simpleFullscreen` Boolean (optional) - Use pre-Lion fullscreen on macOS. Default is `false`. - * `skipTaskbar` Boolean (optional) - Whether to show the window in taskbar. Default is + * `simpleFullscreen` boolean (optional) - Use pre-Lion fullscreen on macOS. Default is `false`. + * `skipTaskbar` boolean (optional) - Whether to show the window in taskbar. Default is `false`. - * `kiosk` Boolean (optional) - The kiosk mode. Default is `false`. - * `title` String (optional) - Default window title. Default is `"Electron"`. If the HTML tag `` is defined in the HTML file loaded by `loadURL()`, this property will be ignored. - * `icon` ([NativeImage](native-image.md) | String) (optional) - The window icon. On Windows it is + * `kiosk` boolean (optional) - Whether the window is in kiosk mode. Default is `false`. + * `title` string (optional) - Default window title. Default is `"Electron"`. If the HTML tag `<title>` is defined in the HTML file loaded by `loadURL()`, this property will be ignored. + * `icon` ([NativeImage](native-image.md) | string) (optional) - The window icon. On Windows it is recommended to use `ICO` icons to get best visual effects, you can also leave it undefined so the executable's icon will be used. - * `show` Boolean (optional) - Whether window should be shown when created. Default is + * `show` boolean (optional) - Whether window should be shown when created. Default is `true`. - * `frame` Boolean (optional) - Specify `false` to create a - [Frameless Window](frameless-window.md). Default is `true`. + * `paintWhenInitiallyHidden` boolean (optional) - Whether the renderer should be active when `show` is `false` and it has just been created. In order for `document.visibilityState` to work correctly on first load with `show: false` you should set this to `false`. Setting this to `false` will cause the `ready-to-show` event to not fire. Default is `true`. + * `frame` boolean (optional) - Specify `false` to create a + [frameless window](../tutorial/window-customization.md#create-frameless-windows). Default is `true`. * `parent` BrowserWindow (optional) - Specify parent window. Default is `null`. - * `modal` Boolean (optional) - Whether this is a modal window. This only works when the + * `modal` boolean (optional) - Whether this is a modal window. This only works when the window is a child window. Default is `false`. - * `acceptFirstMouse` Boolean (optional) - Whether the web view accepts a single - mouse-down event that simultaneously activates the window. Default is - `false`. - * `disableAutoHideCursor` Boolean (optional) - Whether to hide cursor when typing. + * `acceptFirstMouse` boolean (optional) - Whether clicking an inactive window will also + click through to the web contents. Default is `false` on macOS. This option is not + configurable on other platforms. + * `disableAutoHideCursor` boolean (optional) - Whether to hide cursor when typing. Default is `false`. - * `autoHideMenuBar` Boolean (optional) - Auto hide the menu bar unless the `Alt` + * `autoHideMenuBar` boolean (optional) - Auto hide the menu bar unless the `Alt` key is pressed. Default is `false`. - * `enableLargerThanScreen` Boolean (optional) - Enable the window to be resized larger + * `enableLargerThanScreen` boolean (optional) - Enable the window to be resized larger than screen. Only relevant for macOS, as other OSes allow larger-than-screen windows by default. Default is `false`. - * `backgroundColor` String (optional) - Window's background color as a hexadecimal value, - like `#66CD00` or `#FFF` or `#80FFFFFF` (alpha in #AARRGGBB format is supported if - `transparent` is set to `true`). Default is `#FFF` (white). - * `hasShadow` Boolean (optional) - Whether window should have a shadow. This is only - implemented on macOS. Default is `true`. - * `opacity` Number (optional) - Set the initial opacity of the window, between 0.0 (fully + * `backgroundColor` string (optional) - The window's background color in Hex, RGB, RGBA, HSL, HSLA or named CSS color format. Alpha in #AARRGGBB format is supported if `transparent` is set to `true`. Default is `#FFF` (white). See [win.setBackgroundColor](browser-window.md#winsetbackgroundcolorbackgroundcolor) for more information. + * `hasShadow` boolean (optional) - Whether window should have a shadow. Default is `true`. + * `opacity` number (optional) - Set the initial opacity of the window, between 0.0 (fully transparent) and 1.0 (fully opaque). This is only implemented on Windows and macOS. - * `darkTheme` Boolean (optional) - Forces using dark theme for the window, only works on + * `darkTheme` boolean (optional) - Forces using dark theme for the window, only works on some GTK+3 desktop environments. Default is `false`. - * `transparent` Boolean (optional) - Makes the window [transparent](frameless-window.md#transparent-window). + * `transparent` boolean (optional) - Makes the window [transparent](../tutorial/window-customization.md#create-transparent-windows). Default is `false`. On Windows, does not work unless the window is frameless. - * `type` String (optional) - The type of window, default is normal window. See more about + * `type` string (optional) - The type of window, default is normal window. See more about this below. - * `titleBarStyle` String (optional) - The style of window title bar. + * `visualEffectState` string (optional) - Specify how the material appearance should reflect window activity state on macOS. Must be used with the `vibrancy` property. Possible values are: + * `followWindow` - The backdrop should automatically appear active when the window is active, and inactive when it is not. This is the default. + * `active` - The backdrop should always appear active. + * `inactive` - The backdrop should always appear inactive. + * `titleBarStyle` string (optional) _macOS_ _Windows_ - The style of window title bar. Default is `default`. Possible values are: - * `default` - Results in the standard gray opaque Mac title - bar. - * `hidden` - Results in a hidden title bar and a full size content window, yet - the title bar still has the standard window controls ("traffic lights") in - the top left. - * `hiddenInset` - Results in a hidden title bar with an alternative look + * `default` - Results in the standard title bar for macOS or Windows respectively. + * `hidden` - Results in a hidden title bar and a full size content window. On macOS, the window still has the standard window controls (“traffic lights”) in the top left. On Windows, when combined with `titleBarOverlay: true` it will activate the Window Controls Overlay (see `titleBarOverlay` for more information), otherwise no window controls will be shown. + * `hiddenInset` - Only on macOS, results in a hidden title bar with an alternative look where the traffic light buttons are slightly more inset from the window edge. - * `customButtonsOnHover` Boolean (optional) - Draw custom close, - and minimize buttons on macOS frameless windows. These buttons will not display - unless hovered over in the top left of the window. These custom buttons prevent - issues with mouse events that occur with the standard window toolbar buttons. - **Note:** This option is currently experimental. - * `fullscreenWindowTitle` Boolean (optional) - Shows the title in the - title bar in full screen mode on macOS for all `titleBarStyle` options. + * `customButtonsOnHover` - Only on macOS, results in a hidden title bar and a full size + content window, the traffic light buttons will display when being hovered + over in the top left of the window. **Note:** This option is currently + experimental. + * `trafficLightPosition` [Point](structures/point.md) (optional) - Set a + custom position for the traffic light buttons in frameless windows. + * `roundedCorners` boolean (optional) - Whether frameless window should have + rounded corners on macOS. Default is `true`. + * `fullscreenWindowTitle` boolean (optional) _Deprecated_ - Shows the title in + the title bar in full screen mode on macOS for `hiddenInset` titleBarStyle. Default is `false`. - * `thickFrame` Boolean (optional) - Use `WS_THICKFRAME` style for frameless windows on + * `thickFrame` boolean (optional) - Use `WS_THICKFRAME` style for frameless windows on Windows, which adds standard window frame. Setting it to `false` will remove window shadow and window animations. Default is `true`. - * `vibrancy` String (optional) - Add a type of vibrancy effect to the window, only on + * `vibrancy` string (optional) - Add a type of vibrancy effect to the window, only on macOS. Can be `appearance-based`, `light`, `dark`, `titlebar`, `selection`, - `menu`, `popover`, `sidebar`, `medium-light`, `ultra-dark`, `header`, `sheet`, `window`, `hud`, `fullscreen-ui`, `tooltip`, `content`, `under-window`, or `under-page`. Please note that using `frame: false` in combination with a vibrancy value requires that you use a non-default `titleBarStyle` as well. Also note that `appearance-based`, `light`, `dark`, `medium-light`, and `ultra-dark` have been deprecated and will be removed in an upcoming version of macOS. - * `zoomToPageWidth` Boolean (optional) - Controls the behavior on macOS when + `menu`, `popover`, `sidebar`, `medium-light`, `ultra-dark`, `header`, `sheet`, `window`, `hud`, `fullscreen-ui`, `tooltip`, `content`, `under-window`, or `under-page`. Please note that `appearance-based`, `light`, `dark`, `medium-light`, and `ultra-dark` are deprecated and have been removed in macOS Catalina (10.15). + * `zoomToPageWidth` boolean (optional) - Controls the behavior on macOS when option-clicking the green stoplight button on the toolbar or by clicking the Window > Zoom menu item. If `true`, the window will grow to the preferred width of the web page when zoomed, `false` will cause it to zoom to the width of the screen. This will also affect the behavior when calling `maximize()` directly. Default is `false`. - * `tabbingIdentifier` String (optional) - Tab group name, allows opening the + * `tabbingIdentifier` string (optional) - Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. This also adds a native new tab button to your window's tab bar and allows your `app` and window to receive the `new-window-for-tab` event. * `webPreferences` Object (optional) - Settings of web page's features. - * `devTools` Boolean (optional) - Whether to enable DevTools. If it is set to `false`, can not use `BrowserWindow.webContents.openDevTools()` to open DevTools. Default is `true`. - * `nodeIntegration` Boolean (optional) - Whether node integration is enabled. + * `devTools` boolean (optional) - Whether to enable DevTools. If it is set to `false`, can not use `BrowserWindow.webContents.openDevTools()` to open DevTools. Default is `true`. + * `nodeIntegration` boolean (optional) - Whether node integration is enabled. Default is `false`. - * `nodeIntegrationInWorker` Boolean (optional) - Whether node integration is + * `nodeIntegrationInWorker` boolean (optional) - Whether node integration is enabled in web workers. Default is `false`. More about this can be found in [Multithreading](../tutorial/multithreading.md). - * `nodeIntegrationInSubFrames` Boolean (optional) - Experimental option for + * `nodeIntegrationInSubFrames` boolean (optional) - Experimental option for enabling Node.js support in sub-frames such as iframes and child windows. All your preloads will load for every iframe, you can use `process.isMainFrame` to determine if you are in the main frame or not. - * `preload` String (optional) - Specifies a script that will be loaded before other + * `preload` string (optional) - Specifies a script that will be loaded before other scripts run in the page. This script will always have access to node APIs no matter whether node integration is turned on or off. The value should be the absolute file path to the script. When node integration is turned off, the preload script can reintroduce Node global symbols back to the global scope. See example - [here](process.md#event-loaded). - * `sandbox` Boolean (optional) - If set, this will sandbox the renderer + [here](context-bridge.md#exposing-node-global-symbols). + * `sandbox` boolean (optional) - If set, this will sandbox the renderer associated with the window, making it compatible with the Chromium OS-level sandbox and disabling the Node.js engine. This is not the same as the `nodeIntegration` option and the APIs available to the preload script - are more limited. Read more about the option [here](sandbox-option.md). - **Note:** This option is currently experimental and may change or be - removed in future Electron releases. - * `enableRemoteModule` Boolean (optional) - Whether to enable the [`remote`](remote.md) module. - Default is `true`. + are more limited. Read more about the option [here](../tutorial/sandbox.md). * `session` [Session](session.md#class-session) (optional) - Sets the session used by the page. Instead of passing the Session object directly, you can also choose to use the `partition` option instead, which accepts a partition string. When both `session` and `partition` are provided, `session` will be preferred. Default is the default session. - * `partition` String (optional) - Sets the session used by the page according to the + * `partition` string (optional) - Sets the session used by the page according to the session's partition string. If `partition` starts with `persist:`, the page will use a persistent session available to all pages in the app with the same `partition`. If there is no `persist:` prefix, the page will use an in-memory session. By assigning the same `partition`, multiple pages can share the same session. Default is the default session. - * `affinity` String (optional) - When specified, web pages with the same - `affinity` will run in the same renderer process. Note that due to reusing - the renderer process, certain `webPreferences` options will also be shared - between the web pages even when you specified different values for them, - including but not limited to `preload`, `sandbox` and `nodeIntegration`. - So it is suggested to use exact same `webPreferences` for web pages with - the same `affinity`. _This property is experimental_ - * `zoomFactor` Number (optional) - The default zoom factor of the page, `3.0` represents + * `zoomFactor` number (optional) - The default zoom factor of the page, `3.0` represents `300%`. Default is `1.0`. - * `javascript` Boolean (optional) - Enables JavaScript support. Default is `true`. - * `webSecurity` Boolean (optional) - When `false`, it will disable the + * `javascript` boolean (optional) - Enables JavaScript support. Default is `true`. + * `webSecurity` boolean (optional) - When `false`, it will disable the same-origin policy (usually using testing websites by people), and set `allowRunningInsecureContent` to `true` if this options has not been set by user. Default is `true`. - * `allowRunningInsecureContent` Boolean (optional) - Allow an https page to run + * `allowRunningInsecureContent` boolean (optional) - Allow an https page to run JavaScript, CSS or plugins from http URLs. Default is `false`. - * `images` Boolean (optional) - Enables image support. Default is `true`. - * `textAreasAreResizable` Boolean (optional) - Make TextArea elements resizable. Default + * `images` boolean (optional) - Enables image support. Default is `true`. + * `imageAnimationPolicy` string (optional) - Specifies how to run image animations (E.g. GIFs). Can be `animate`, `animateOnce` or `noAnimation`. Default is `animate`. + * `textAreasAreResizable` boolean (optional) - Make TextArea elements resizable. Default is `true`. - * `webgl` Boolean (optional) - Enables WebGL support. Default is `true`. - * `plugins` Boolean (optional) - Whether plugins should be enabled. Default is `false`. - * `experimentalFeatures` Boolean (optional) - Enables Chromium's experimental features. + * `webgl` boolean (optional) - Enables WebGL support. Default is `true`. + * `plugins` boolean (optional) - Whether plugins should be enabled. Default is `false`. + * `experimentalFeatures` boolean (optional) - Enables Chromium's experimental features. Default is `false`. - * `scrollBounce` Boolean (optional) - Enables scroll bounce (rubber banding) effect on + * `scrollBounce` boolean (optional) - Enables scroll bounce (rubber banding) effect on macOS. Default is `false`. - * `enableBlinkFeatures` String (optional) - A list of feature strings separated by `,`, like + * `enableBlinkFeatures` string (optional) - A list of feature strings separated by `,`, like `CSSVariables,KeyboardEventKey` to enable. The full list of supported feature strings can be found in the [RuntimeEnabledFeatures.json5][runtime-enabled-features] file. - * `disableBlinkFeatures` String (optional) - A list of feature strings separated by `,`, + * `disableBlinkFeatures` string (optional) - A list of feature strings separated by `,`, like `CSSVariables,KeyboardEventKey` to disable. The full list of supported feature strings can be found in the [RuntimeEnabledFeatures.json5][runtime-enabled-features] file. * `defaultFontFamily` Object (optional) - Sets the default font for the font-family. - * `standard` String (optional) - Defaults to `Times New Roman`. - * `serif` String (optional) - Defaults to `Times New Roman`. - * `sansSerif` String (optional) - Defaults to `Arial`. - * `monospace` String (optional) - Defaults to `Courier New`. - * `cursive` String (optional) - Defaults to `Script`. - * `fantasy` String (optional) - Defaults to `Impact`. + * `standard` string (optional) - Defaults to `Times New Roman`. + * `serif` string (optional) - Defaults to `Times New Roman`. + * `sansSerif` string (optional) - Defaults to `Arial`. + * `monospace` string (optional) - Defaults to `Courier New`. + * `cursive` string (optional) - Defaults to `Script`. + * `fantasy` string (optional) - Defaults to `Impact`. * `defaultFontSize` Integer (optional) - Defaults to `16`. * `defaultMonospaceFontSize` Integer (optional) - Defaults to `13`. * `minimumFontSize` Integer (optional) - Defaults to `0`. - * `defaultEncoding` String (optional) - Defaults to `ISO-8859-1`. - * `backgroundThrottling` Boolean (optional) - Whether to throttle animations and timers + * `defaultEncoding` string (optional) - Defaults to `ISO-8859-1`. + * `backgroundThrottling` boolean (optional) - Whether to throttle animations and timers when the page becomes background. This also affects the [Page Visibility API](#page-visibility). Defaults to `true`. - * `offscreen` Boolean (optional) - Whether to enable offscreen rendering for the browser + * `offscreen` boolean (optional) - Whether to enable offscreen rendering for the browser window. Defaults to `false`. See the [offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for more details. - * `contextIsolation` Boolean (optional) - Whether to run Electron APIs and + * `contextIsolation` boolean (optional) - Whether to run Electron APIs and the specified `preload` script in a separate JavaScript context. Defaults - to `false`. The context that the `preload` script runs in will still - have full access to the `document` and `window` globals but it will use - its own set of JavaScript builtins (`Array`, `Object`, `JSON`, etc.) - and will be isolated from any changes made to the global environment - by the loaded page. The Electron API will only be available in the - `preload` script and not the loaded page. This option should be used when - loading potentially untrusted remote content to ensure the loaded content - cannot tamper with the `preload` script and any Electron APIs being used. - This option uses the same technique used by [Chrome Content Scripts][chrome-content-scripts]. - You can access this context in the dev tools by selecting the - 'Electron Isolated Context' entry in the combo box at the top of the - Console tab. - * `nativeWindowOpen` Boolean (optional) - Whether to use native - `window.open()`. Defaults to `false`. Child windows will always have node - integration disabled unless `nodeIntegrationInSubFrames` is true. **Note:** This option is currently - experimental. - * `webviewTag` Boolean (optional) - Whether to enable the [`<webview>` tag](webview-tag.md). + to `true`. The context that the `preload` script runs in will only have + access to its own dedicated `document` and `window` globals, as well as + its own set of JavaScript builtins (`Array`, `Object`, `JSON`, etc.), + which are all invisible to the loaded content. The Electron API will only + be available in the `preload` script and not the loaded page. This option + should be used when loading potentially untrusted remote content to ensure + the loaded content cannot tamper with the `preload` script and any + Electron APIs being used. This option uses the same technique used by + [Chrome Content Scripts][chrome-content-scripts]. You can access this + context in the dev tools by selecting the 'Electron Isolated Context' + entry in the combo box at the top of the Console tab. + * `webviewTag` boolean (optional) - Whether to enable the [`<webview>` tag](webview-tag.md). Defaults to `false`. **Note:** The `preload` script configured for the `<webview>` will have node integration enabled when it is executed so you should ensure remote/untrusted content @@ -361,24 +359,49 @@ It creates a new `BrowserWindow` with native properties as set by the `options`. script. You can use the `will-attach-webview` event on [webContents](web-contents.md) to strip away the `preload` script and to validate or alter the `<webview>`'s initial settings. - * `additionalArguments` String[] (optional) - A list of strings that will be appended + * `additionalArguments` string[] (optional) - A list of strings that will be appended to `process.argv` in the renderer process of this app. Useful for passing small bits of data down to renderer process preload scripts. - * `safeDialogs` Boolean (optional) - Whether to enable browser style + * `safeDialogs` boolean (optional) - Whether to enable browser style consecutive dialog protection. Default is `false`. - * `safeDialogsMessage` String (optional) - The message to display when + * `safeDialogsMessage` string (optional) - The message to display when consecutive dialog protection is triggered. If not defined the default message would be used, note that currently the default message is in English and not localized. - * `navigateOnDragDrop` Boolean (optional) - Whether dragging and dropping a + * `disableDialogs` boolean (optional) - Whether to disable dialogs + completely. Overrides `safeDialogs`. Default is `false`. + * `navigateOnDragDrop` boolean (optional) - Whether dragging and dropping a file or link onto the page causes a navigation. Default is `false`. - * `autoplayPolicy` String (optional) - Autoplay policy to apply to + * `autoplayPolicy` string (optional) - Autoplay policy to apply to content in the window, can be `no-user-gesture-required`, `user-gesture-required`, `document-user-activation-required`. Defaults to `no-user-gesture-required`. - * `disableHtmlFullscreenWindowResize` Boolean (optional) - Whether to + * `disableHtmlFullscreenWindowResize` boolean (optional) - Whether to prevent the window from resizing when entering HTML Fullscreen. Default is `false`. + * `accessibleTitle` string (optional) - An alternative title string provided only + to accessibility tools such as screen readers. This string is not directly + visible to users. + * `spellcheck` boolean (optional) - Whether to enable the builtin spellchecker. + Default is `true`. + * `enableWebSQL` boolean (optional) - Whether to enable the [WebSQL api](https://www.w3.org/TR/webdatabase/). + Default is `true`. + * `v8CacheOptions` string (optional) - Enforces the v8 code caching policy + used by blink. Accepted values are + * `none` - Disables code caching + * `code` - Heuristic based code caching + * `bypassHeatCheck` - Bypass code caching heuristics but with lazy compilation + * `bypassHeatCheckAndEagerCompile` - Same as above except compilation is eager. + Default policy is `code`. + * `enablePreferredSizeMode` boolean (optional) - Whether to enable + preferred size mode. The preferred size is the minimum size needed to + contain the layout of the document—without requiring scrolling. Enabling + this will cause the `preferred-size-changed` event to be emitted on the + `WebContents` when the preferred size changes. Default is `false`. + * `titleBarOverlay` Object | Boolean (optional) - When using a frameless window in conjunction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`. + * `color` String (optional) _Windows_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color. + * `symbolColor` String (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color. + * `height` Integer (optional) _macOS_ _Windows_ - The height of the title bar and Window Controls Overlay in pixels. Default is system height. When setting minimum or maximum window size with `minWidth`/`maxWidth`/ `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from @@ -411,8 +434,8 @@ labeled as such. Returns: * `event` Event -* `title` String -* `explicitSet` Boolean +* `title` string +* `explicitSet` boolean Emitted when the document changed its title, calling `event.preventDefault()` will prevent the native window's title from changing. @@ -441,9 +464,10 @@ window.onbeforeunload = (e) => { // a non-void value will silently cancel the close. // It is recommended to use the dialog API to let the user confirm closing the // application. - e.returnValue = false // equivalent to `return false` but not recommended + e.returnValue = false } ``` + _**Note**: There is a subtle difference between the behaviors of `window.onbeforeunload = handler` and `window.addEventListener('beforeunload', handler)`. It is recommended to always set the `event.returnValue` explicitly, instead of only returning a value, as the former works more consistently within Electron._ #### Event: 'closed' @@ -485,6 +509,9 @@ Emitted when the window is hidden. Emitted when the web page has been rendered (while not being shown) and window can be displayed without a visual flash. +Please note that using this event implies that the renderer will be considered "visible" and +paint even though `show` is false. This event will never fire if you use `paintWhenInitiallyHidden: false` + #### Event: 'maximize' Emitted when window is maximized. @@ -506,37 +533,52 @@ Emitted when the window is restored from a minimized state. Returns: * `event` Event -* `newBounds` [`Rectangle`](structures/rectangle.md) - Size the window is being resized to. +* `newBounds` [Rectangle](structures/rectangle.md) - Size the window is being resized to. +* `details` Object + * `edge` (string) - The edge of the window being dragged for resizing. Can be `bottom`, `left`, `right`, `top-left`, `top-right`, `bottom-left` or `bottom-right`. Emitted before the window is resized. Calling `event.preventDefault()` will prevent the window from being resized. Note that this is only emitted when the window is being resized manually. Resizing the window with `setBounds`/`setSize` will not emit this event. +The possible values and behaviors of the `edge` option are platform dependent. Possible values are: + +* On Windows, possible values are `bottom`, `top`, `left`, `right`, `top-left`, `top-right`, `bottom-left`, `bottom-right`. +* On macOS, possible values are `bottom` and `right`. + * The value `bottom` is used to denote vertical resizing. + * The value `right` is used to denote horizontal resizing. + #### Event: 'resize' Emitted after the window has been resized. -#### Event: 'will-move' _Windows_ +#### Event: 'resized' _macOS_ _Windows_ + +Emitted once when the window has finished being resized. + +This is usually emitted when the window has been resized manually. On macOS, resizing the window with `setBounds`/`setSize` and setting the `animate` parameter to `true` will also emit this event once resizing has finished. + +#### Event: 'will-move' _macOS_ _Windows_ Returns: * `event` Event -* `newBounds` [`Rectangle`](structures/rectangle.md) - Location the window is being moved to. +* `newBounds` [Rectangle](structures/rectangle.md) - Location the window is being moved to. -Emitted before the window is moved. Calling `event.preventDefault()` will prevent the window from being moved. +Emitted before the window is moved. On Windows, calling `event.preventDefault()` will prevent the window from being moved. -Note that this is only emitted when the window is being resized manually. Resizing the window with `setBounds`/`setSize` will not emit this event. +Note that this is only emitted when the window is being moved manually. Moving the window with `setPosition`/`setBounds`/`center` will not emit this event. #### Event: 'move' Emitted when the window is being moved to a new position. -__Note__: On macOS this event is an alias of `moved`. - -#### Event: 'moved' _macOS_ +#### Event: 'moved' _macOS_ _Windows_ Emitted once when the window is moved to a new position. +__Note__: On macOS this event is an alias of `move`. + #### Event: 'enter-full-screen' Emitted when the window enters a full-screen state. @@ -558,7 +600,7 @@ Emitted when the window leaves a full-screen state triggered by HTML API. Returns: * `event` Event -* `isAlwaysOnTop` Boolean +* `isAlwaysOnTop` boolean Emitted when the window is set or unset to show always on top of other windows. @@ -567,7 +609,7 @@ Emitted when the window is set or unset to show always on top of other windows. Returns: * `event` Event -* `command` String +* `command` string Emitted when an [App Command](https://msdn.microsoft.com/en-us/library/windows/desktop/ms646275(v=vs.85).aspx) is invoked. These are typically related to keyboard media keys or browser @@ -579,7 +621,7 @@ e.g. `APPCOMMAND_BROWSER_BACKWARD` is emitted as `browser-backward`. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() win.on('app-command', (e, cmd) => { // Navigate the window back when the user hits their mouse back button if (cmd === 'browser-backward' && win.webContents.canGoBack()) { @@ -610,10 +652,16 @@ Emitted when scroll wheel event phase filed upon reaching the edge of element. Returns: * `event` Event -* `direction` String +* `direction` string Emitted on 3-finger swipe. Possible directions are `up`, `right`, `down`, `left`. +The method underlying this event is built to handle older macOS-style trackpad swiping, +where the content on the screen doesn't move with the swipe. Most macOS trackpads are not +configured to allow this kind of swiping anymore, so in order for it to emit properly the +'Swipe between pages' preference in `System Preferences > Trackpad > More Gestures` must be +set to 'Swipe with two or three fingers'. + #### Event: 'rotate-gesture' _macOS_ Returns: @@ -639,6 +687,20 @@ Emitted when the window has closed a sheet. Emitted when the native new tab button is clicked. +#### Event: 'system-context-menu' _Windows_ + +Returns: + +* `event` Event +* `point` [Point](structures/point.md) - The screen coordinates the context menu was triggered at + +Emitted when the system context menu is triggered on the window, this is +normally only triggered when the user right clicks on the non-client area +of your window. This is the window titlebar or any area you have declared +as `-webkit-app-region: drag` in a frameless window. + +Calling `event.preventDefault()` will prevent the menu from being displayed. + ### Static Methods The `BrowserWindow` class has the following static methods: @@ -655,7 +717,8 @@ Returns `BrowserWindow | null` - The window that is focused in this application, * `webContents` [WebContents](web-contents.md) -Returns `BrowserWindow` - The window that owns the given `webContents`. +Returns `BrowserWindow | null` - The window that owns the given `webContents` +or `null` if the contents are not owned by a window. #### `BrowserWindow.fromBrowserView(browserView)` @@ -667,144 +730,123 @@ Returns `BrowserWindow | null` - The window that owns the given `browserView`. I * `id` Integer -Returns `BrowserWindow` - The window with the given `id`. +Returns `BrowserWindow | null` - The window with the given `id`. -#### `BrowserWindow.addExtension(path)` +### Instance Properties -* `path` String +Objects created with `new BrowserWindow` have the following properties: -Adds Chrome extension located at `path`, and returns extension's name. +```javascript +const { BrowserWindow } = require('electron') +// In this example `win` is our instance +const win = new BrowserWindow({ width: 800, height: 600 }) +win.loadURL('https://github.com') +``` -The method will also not return if the extension's manifest is missing or incomplete. +#### `win.webContents` _Readonly_ -**Note:** This API cannot be called before the `ready` event of the `app` module -is emitted. +A `WebContents` object this window owns. All web page related events and +operations will be done via it. -#### `BrowserWindow.removeExtension(name)` +See the [`webContents` documentation](web-contents.md) for its methods and +events. -* `name` String +#### `win.id` _Readonly_ -Remove a Chrome extension by name. +A `Integer` property representing the unique ID of the window. Each ID is unique among all `BrowserWindow` instances of the entire Electron application. -**Note:** This API cannot be called before the `ready` event of the `app` module -is emitted. +#### `win.autoHideMenuBar` -#### `BrowserWindow.getExtensions()` +A `boolean` property that determines whether the window menu bar should hide itself automatically. Once set, the menu bar will only show when users press the single `Alt` key. -Returns `Object` - The keys are the extension names and each value is -an Object containing `name` and `version` properties. +If the menu bar is already visible, setting this property to `true` won't +hide it immediately. -**Note:** This API cannot be called before the `ready` event of the `app` module -is emitted. +#### `win.simpleFullScreen` -#### `BrowserWindow.addDevToolsExtension(path)` +A `boolean` property that determines whether the window is in simple (pre-Lion) fullscreen mode. -* `path` String +#### `win.fullScreen` -Adds DevTools extension located at `path`, and returns extension's name. +A `boolean` property that determines whether the window is in fullscreen mode. -The extension will be remembered so you only need to call this API once, this -API is not for programming use. If you try to add an extension that has already -been loaded, this method will not return and instead log a warning to the -console. +#### `win.focusable` _Windows_ _macOS_ -The method will also not return if the extension's manifest is missing or incomplete. +A `boolean` property that determines whether the window is focusable. -**Note:** This API cannot be called before the `ready` event of the `app` module -is emitted. +#### `win.visibleOnAllWorkspaces` -#### `BrowserWindow.removeDevToolsExtension(name)` +A `boolean` property that determines whether the window is visible on all workspaces. -* `name` String +**Note:** Always returns false on Windows. -Remove a DevTools extension by name. +#### `win.shadow` -**Note:** This API cannot be called before the `ready` event of the `app` module -is emitted. +A `boolean` property that determines whether the window has a shadow. -#### `BrowserWindow.getDevToolsExtensions()` +#### `win.menuBarVisible` _Windows_ _Linux_ -Returns `Object` - The keys are the extension names and each value is -an Object containing `name` and `version` properties. +A `boolean` property that determines whether the menu bar should be visible. -To check if a DevTools extension is installed you can run the following: +**Note:** If the menu bar is auto-hide, users can still bring up the menu bar by pressing the single `Alt` key. -```javascript -const { BrowserWindow } = require('electron') +#### `win.kiosk` -let installed = BrowserWindow.getDevToolsExtensions().hasOwnProperty('devtron') -console.log(installed) -``` +A `boolean` property that determines whether the window is in kiosk mode. -**Note:** This API cannot be called before the `ready` event of the `app` module -is emitted. +#### `win.documentEdited` _macOS_ -### Instance Properties +A `boolean` property that specifies whether the window’s document has been edited. -Objects created with `new BrowserWindow` have the following properties: +The icon in title bar will become gray when set to `true`. -```javascript -const { BrowserWindow } = require('electron') -// In this example `win` is our instance -let win = new BrowserWindow({ width: 800, height: 600 }) -win.loadURL('https://github.com') -``` +#### `win.representedFilename` _macOS_ -#### `win.webContents` _Readonly_ +A `string` property that determines the pathname of the file the window represents, +and the icon of the file will show in window's title bar. -A `WebContents` object this window owns. All web page related events and -operations will be done via it. +#### `win.title` -See the [`webContents` documentation](web-contents.md) for its methods and -events. - -#### `win.id` _Readonly_ - -A `Integer` property representing the unique ID of the window. - -#### `win.autoHideMenuBar` - -A `Boolean` property that determines whether the window menu bar should hide itself automatically. Once set, the menu bar will only show when users press the single `Alt` key. +A `string` property that determines the title of the native window. -If the menu bar is already visible, setting this property to `true` won't -hide it immediately. +**Note:** The title of the web page can be different from the title of the native window. #### `win.minimizable` -A `Boolean` property that determines whether the window can be manually minimized by user. +A `boolean` property that determines whether the window can be manually minimized by user. On Linux the setter is a no-op, although the getter returns `true`. #### `win.maximizable` -A `Boolean` property that determines whether the window can be manually maximized by user. +A `boolean` property that determines whether the window can be manually maximized by user. On Linux the setter is a no-op, although the getter returns `true`. #### `win.fullScreenable` -A `Boolean` property that determines whether the maximize/zoom window button toggles fullscreen mode or +A `boolean` property that determines whether the maximize/zoom window button toggles fullscreen mode or maximizes the window. #### `win.resizable` -A `Boolean` property that determines whether the window can be manually resized by user. +A `boolean` property that determines whether the window can be manually resized by user. #### `win.closable` -A `Boolean` property that determines whether the window can be manually closed by user. +A `boolean` property that determines whether the window can be manually closed by user. On Linux the setter is a no-op, although the getter returns `true`. #### `win.movable` -A `Boolean` property that determines Whether the window can be moved by user. +A `boolean` property that determines Whether the window can be moved by user. On Linux the setter is a no-op, although the getter returns `true`. #### `win.excludedFromShownWindowsMenu` _macOS_ -A `Boolean` property that determines whether the window is excluded from the application’s Windows menu. `false` by default. +A `boolean` property that determines whether the window is excluded from the application’s Windows menu. `false` by default. ```js const win = new BrowserWindow({ height: 600, width: 600 }) @@ -821,6 +863,12 @@ const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) ``` +#### `win.accessibleTitle` + +A `string` property that defines an alternative title provided only to +accessibility tools such as screen readers. This string is not directly +visible to users. + ### Instance Methods Objects created with `new BrowserWindow` have the following instance methods: @@ -850,11 +898,11 @@ Removes focus from the window. #### `win.isFocused()` -Returns `Boolean` - Whether the window is focused. +Returns `boolean` - Whether the window is focused. #### `win.isDestroyed()` -Returns `Boolean` - Whether the window is destroyed. +Returns `boolean` - Whether the window is destroyed. #### `win.show()` @@ -870,11 +918,11 @@ Hides the window. #### `win.isVisible()` -Returns `Boolean` - Whether the window is visible to the user. +Returns `boolean` - Whether the window is visible to the user. #### `win.isModal()` -Returns `Boolean` - Whether current window is a modal window. +Returns `boolean` - Whether current window is a modal window. #### `win.maximize()` @@ -887,7 +935,7 @@ Unmaximizes the window. #### `win.isMaximized()` -Returns `Boolean` - Whether the window is maximized. +Returns `boolean` - Whether the window is maximized. #### `win.minimize()` @@ -900,39 +948,39 @@ Restores the window from minimized state to its previous state. #### `win.isMinimized()` -Returns `Boolean` - Whether the window is minimized. +Returns `boolean` - Whether the window is minimized. #### `win.setFullScreen(flag)` -* `flag` Boolean +* `flag` boolean Sets whether the window should be in fullscreen mode. #### `win.isFullScreen()` -Returns `Boolean` - Whether the window is in fullscreen mode. +Returns `boolean` - Whether the window is in fullscreen mode. #### `win.setSimpleFullScreen(flag)` _macOS_ -* `flag` Boolean +* `flag` boolean Enters or leaves simple fullscreen mode. -Simple fullscreen mode emulates the native fullscreen behavior found in versions of Mac OS X prior to Lion (10.7). +Simple fullscreen mode emulates the native fullscreen behavior found in versions of macOS prior to Lion (10.7). #### `win.isSimpleFullScreen()` _macOS_ -Returns `Boolean` - Whether the window is in simple (pre-Lion) fullscreen mode. +Returns `boolean` - Whether the window is in simple (pre-Lion) fullscreen mode. #### `win.isNormal()` -Returns `Boolean` - Whether the window is in normal state (not maximized, not minimized, not in fullscreen mode). +Returns `boolean` - Whether the window is in normal state (not maximized, not minimized, not in fullscreen mode). -#### `win.setAspectRatio(aspectRatio[, extraSize])` _macOS_ +#### `win.setAspectRatio(aspectRatio[, extraSize])` * `aspectRatio` Float - The aspect ratio to maintain for some portion of the content view. -* `extraSize` [Size](structures/size.md) (optional) - The extra size not to be included while +* `extraSize` [Size](structures/size.md) (optional) _macOS_ - The extra size not to be included while maintaining the aspect ratio. This will make a window maintain an aspect ratio. The extra size allows a @@ -945,28 +993,49 @@ Perhaps there are 15 pixels of controls on the left edge, 25 pixels of controls on the right edge and 50 pixels of controls below the player. In order to maintain a 16:9 aspect ratio (standard aspect ratio for HD @1920x1080) within the player itself we would call this function with arguments of 16/9 and -[ 40, 50 ]. The second argument doesn't care where the extra width and height +{ width: 40, height: 50 }. The second argument doesn't care where the extra width and height are within the content view--only that they exist. Sum any extra width and height areas you have within the overall content view. -Calling this function with a value of `0` will remove any previously set aspect -ratios. +The aspect ratio is not respected when window is resized programmatically with +APIs like `win.setSize`. #### `win.setBackgroundColor(backgroundColor)` -* `backgroundColor` String - Window's background color as a hexadecimal value, - like `#66CD00` or `#FFF` or `#80FFFFFF` (alpha is supported if `transparent` - is `true`). Default is `#FFF` (white). - -Sets the background color of the window. See [Setting -`backgroundColor`](#setting-backgroundcolor). +* `backgroundColor` string - Color in Hex, RGB, RGBA, HSL, HSLA or named CSS color format. The alpha channel is optional for the hex type. + +Examples of valid `backgroundColor` values: + +* Hex + * #fff (shorthand RGB) + * #ffff (shorthand ARGB) + * #ffffff (RGB) + * #ffffffff (ARGB) +* RGB + * rgb\(([\d]+),\s*([\d]+),\s*([\d]+)\) + * e.g. rgb(255, 255, 255) +* RGBA + * rgba\(([\d]+),\s*([\d]+),\s*([\d]+),\s*([\d.]+)\) + * e.g. rgba(255, 255, 255, 1.0) +* HSL + * hsl\((-?[\d.]+),\s*([\d.]+)%,\s*([\d.]+)%\) + * e.g. hsl(200, 20%, 50%) +* HSLA + * hsla\((-?[\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\) + * e.g. hsla(200, 20%, 50%, 0.5) +* Color name + * Options are listed in [SkParseColor.cpp](https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/utils/SkParseColor.cpp;l=11-152;drc=eea4bf52cb0d55e2a39c828b017c80a5ee054148) + * Similar to CSS Color Module Level 3 keywords, but case-sensitive. + * e.g. `blueviolet` or `red` + +Sets the background color of the window. See [Setting `backgroundColor`](#setting-the-backgroundcolor-property). #### `win.previewFile(path[, displayName])` _macOS_ -* `path` String - The absolute path to the file to preview with QuickLook. This +* `path` string - The absolute path to the file to preview with QuickLook. This is important as Quick Look uses the file name and file extension on the path to determine the content type of the file to open. -* `displayName` String (optional) - The name of the file to display on the +* `displayName` string (optional) - The name of the file to display on the Quick Look modal view. This is purely visual and does not affect the content type of the file. Defaults to `path`. @@ -979,7 +1048,7 @@ Closes the currently open [Quick Look][quick-look] panel. #### `win.setBounds(bounds[, animate])` * `bounds` Partial<[Rectangle](structures/rectangle.md)> -* `animate` Boolean (optional) _macOS_ +* `animate` boolean (optional) _macOS_ Resizes and moves the window to the supplied bounds. Any properties that are not supplied will default to their current values. @@ -1001,10 +1070,18 @@ console.log(win.getBounds()) Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `Object`. +#### `win.getBackgroundColor()` + +Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format. + +See [Setting `backgroundColor`](#setting-the-backgroundcolor-property). + +**Note:** The alpha value is _not_ returned alongside the red, green, and blue values. + #### `win.setContentBounds(bounds[, animate])` * `bounds` [Rectangle](structures/rectangle.md) -* `animate` Boolean (optional) _macOS_ +* `animate` boolean (optional) _macOS_ Resizes and moves the window's client area (e.g. the web page) to the supplied bounds. @@ -1021,19 +1098,19 @@ Returns [`Rectangle`](structures/rectangle.md) - Contains the window bounds of t #### `win.setEnabled(enable)` -* `enable` Boolean +* `enable` boolean Disable or enable the window. #### `win.isEnabled()` -Returns Boolean - whether the window is enabled. +Returns `boolean` - whether the window is enabled. #### `win.setSize(width, height[, animate])` * `width` Integer * `height` Integer -* `animate` Boolean (optional) _macOS_ +* `animate` boolean (optional) _macOS_ Resizes the window to `width` and `height`. If `width` or `height` are below any set minimum size constraints the window will snap to its minimum size. @@ -1045,7 +1122,7 @@ Returns `Integer[]` - Contains the window's width and height. * `width` Integer * `height` Integer -* `animate` Boolean (optional) _macOS_ +* `animate` boolean (optional) _macOS_ Resizes the window's client area (e.g. the web page) to `width` and `height`. @@ -1077,104 +1154,76 @@ Returns `Integer[]` - Contains the window's maximum width and height. #### `win.setResizable(resizable)` -* `resizable` Boolean - -Sets whether the window can be manually resized by user. +* `resizable` boolean -**[Deprecated](modernization/property-updates.md)** +Sets whether the window can be manually resized by the user. #### `win.isResizable()` -Returns `Boolean` - Whether the window can be manually resized by user. - -**[Deprecated](modernization/property-updates.md)** +Returns `boolean` - Whether the window can be manually resized by the user. #### `win.setMovable(movable)` _macOS_ _Windows_ -* `movable` Boolean +* `movable` boolean Sets whether the window can be moved by user. On Linux does nothing. -**[Deprecated](modernization/property-updates.md)** - #### `win.isMovable()` _macOS_ _Windows_ -Returns `Boolean` - Whether the window can be moved by user. +Returns `boolean` - Whether the window can be moved by user. On Linux always returns `true`. -**[Deprecated](modernization/property-updates.md)** - #### `win.setMinimizable(minimizable)` _macOS_ _Windows_ -* `minimizable` Boolean - -Sets whether the window can be manually minimized by user. On Linux does -nothing. +* `minimizable` boolean -**[Deprecated](modernization/property-updates.md)** +Sets whether the window can be manually minimized by user. On Linux does nothing. #### `win.isMinimizable()` _macOS_ _Windows_ -Returns `Boolean` - Whether the window can be manually minimized by user +Returns `boolean` - Whether the window can be manually minimized by the user. On Linux always returns `true`. -**[Deprecated](modernization/property-updates.md)** - #### `win.setMaximizable(maximizable)` _macOS_ _Windows_ -* `maximizable` Boolean - -Sets whether the window can be manually maximized by user. On Linux does -nothing. +* `maximizable` boolean -**[Deprecated](modernization/property-updates.md)** +Sets whether the window can be manually maximized by user. On Linux does nothing. #### `win.isMaximizable()` _macOS_ _Windows_ -Returns `Boolean` - Whether the window can be manually maximized by user. +Returns `boolean` - Whether the window can be manually maximized by user. On Linux always returns `true`. -**[Deprecated](modernization/property-updates.md)** - #### `win.setFullScreenable(fullscreenable)` -* `fullscreenable` Boolean - -Sets whether the maximize/zoom window button toggles fullscreen mode or -maximizes the window. +* `fullscreenable` boolean -**[Deprecated](modernization/property-updates.md)** +Sets whether the maximize/zoom window button toggles fullscreen mode or maximizes the window. #### `win.isFullScreenable()` -Returns `Boolean` - Whether the maximize/zoom window button toggles fullscreen mode or -maximizes the window. - -**[Deprecated](modernization/property-updates.md)** +Returns `boolean` - Whether the maximize/zoom window button toggles fullscreen mode or maximizes the window. #### `win.setClosable(closable)` _macOS_ _Windows_ -* `closable` Boolean +* `closable` boolean Sets whether the window can be manually closed by user. On Linux does nothing. -**[Deprecated](modernization/property-updates.md)** - #### `win.isClosable()` _macOS_ _Windows_ -Returns `Boolean` - Whether the window can be manually closed by user. +Returns `boolean` - Whether the window can be manually closed by user. On Linux always returns `true`. -**[Deprecated](modernization/property-updates.md)** - #### `win.setAlwaysOnTop(flag[, level][, relativeLevel])` -* `flag` Boolean -* `level` String (optional) _macOS_ _Windows_ - Values include `normal`, +* `flag` boolean +* `level` string (optional) _macOS_ _Windows_ - Values include `normal`, `floating`, `torn-off-menu`, `modal-panel`, `main-menu`, `status`, `pop-up-menu`, `screen-saver`, and ~~`dock`~~ (Deprecated). The default is `floating` when `flag` is true. The `level` is reset to `normal` when the @@ -1192,7 +1241,15 @@ can not be focused on. #### `win.isAlwaysOnTop()` -Returns `Boolean` - Whether the window is always on top of other windows. +Returns `boolean` - Whether the window is always on top of other windows. + +#### `win.moveAbove(mediaSourceId)` + +* `mediaSourceId` string - Window id in the format of DesktopCapturerSource's id. For example "window:1869:0". + +Moves window above the source window in the sense of z-order. If the +`mediaSourceId` is not of type window or if the window does not exist then +this method throws an error. #### `win.moveTop()` @@ -1206,7 +1263,7 @@ Moves window to the center of the screen. * `x` Integer * `y` Integer -* `animate` Boolean (optional) _macOS_ +* `animate` boolean (optional) _macOS_ Moves window to `x` and `y`. @@ -1216,13 +1273,13 @@ Returns `Integer[]` - Contains the window's current position. #### `win.setTitle(title)` -* `title` String +* `title` string Changes the title of native window to `title`. #### `win.getTitle()` -Returns `String` - The title of the native window. +Returns `string` - The title of the native window. **Note:** The title of the web page can be different from the title of the native window. @@ -1238,33 +1295,53 @@ a HTML-rendered toolbar. For example: ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() -let toolbarRect = document.getElementById('toolbar').getBoundingClientRect() +const toolbarRect = document.getElementById('toolbar').getBoundingClientRect() win.setSheetOffset(toolbarRect.height) ``` #### `win.flashFrame(flag)` -* `flag` Boolean +* `flag` boolean Starts or stops flashing the window to attract user's attention. #### `win.setSkipTaskbar(skip)` -* `skip` Boolean +* `skip` boolean Makes the window not show in the taskbar. #### `win.setKiosk(flag)` -* `flag` Boolean +* `flag` boolean -Enters or leaves the kiosk mode. +Enters or leaves kiosk mode. #### `win.isKiosk()` -Returns `Boolean` - Whether the window is in kiosk mode. +Returns `boolean` - Whether the window is in kiosk mode. + +#### `win.isTabletMode()` _Windows_ + +Returns `boolean` - Whether the window is in Windows 10 tablet mode. + +Since Windows 10 users can [use their PC as tablet](https://support.microsoft.com/en-us/help/17210/windows-10-use-your-pc-like-a-tablet), +under this mode apps can choose to optimize their UI for tablets, such as +enlarging the titlebar and hiding titlebar buttons. + +This API returns whether the window is in tablet mode, and the `resize` event +can be be used to listen to changes to tablet mode. + +#### `win.getMediaSourceId()` + +Returns `string` - Window id in the format of DesktopCapturerSource's id. For example "window:1324:0". + +More precisely the format is `window:id:other_id` where `id` is `HWND` on +Windows, `CGWindowID` (`uint64_t`) on macOS and `Window` (`unsigned long`) on +Linux. `other_id` is used to identify web contents (tabs) so within the same +top level window. #### `win.getNativeWindowHandle()` @@ -1277,6 +1354,8 @@ The native type of the handle is `HWND` on Windows, `NSView*` on macOS, and * `message` Integer * `callback` Function + * `wParam` any - The `wParam` provided to the WndProc + * `lParam` any - The `lParam` provided to the WndProc Hooks a windows message. The `callback` is called when the message is received in the WndProc. @@ -1285,7 +1364,7 @@ the message is received in the WndProc. * `message` Integer -Returns `Boolean` - `true` or `false` depending on whether the message is hooked. +Returns `boolean` - `true` or `false` depending on whether the message is hooked. #### `win.unhookWindowMessage(message)` _Windows_ @@ -1299,25 +1378,25 @@ Unhooks all of the window messages. #### `win.setRepresentedFilename(filename)` _macOS_ -* `filename` String +* `filename` string Sets the pathname of the file the window represents, and the icon of the file will show in window's title bar. #### `win.getRepresentedFilename()` _macOS_ -Returns `String` - The pathname of the file the window represents. +Returns `string` - The pathname of the file the window represents. #### `win.setDocumentEdited(edited)` _macOS_ -* `edited` Boolean +* `edited` boolean Specifies whether the window’s document has been edited, and the icon in title bar will become gray when set to `true`. #### `win.isDocumentEdited()` _macOS_ -Returns `Boolean` - Whether the window's document has been edited. +Returns `boolean` - Whether the window's document has been edited. #### `win.focusOnWebView()` @@ -1329,17 +1408,17 @@ Returns `Boolean` - Whether the window's document has been edited. Returns `Promise<NativeImage>` - Resolves with a [NativeImage](native-image.md) -Captures a snapshot of the page within `rect`. Omitting `rect` will capture the whole visible page. +Captures a snapshot of the page within `rect`. Omitting `rect` will capture the whole visible page. If the page is not visible, `rect` may be empty. #### `win.loadURL(url[, options])` -* `url` String +* `url` string * `options` Object (optional) - * `httpReferrer` (String | [Referrer](structures/referrer.md)) (optional) - An HTTP Referrer URL. - * `userAgent` String (optional) - A user agent originating the request. - * `extraHeaders` String (optional) - Extra headers separated by "\n" - * `postData` ([UploadRawData[]](structures/upload-raw-data.md) | [UploadFile[]](structures/upload-file.md) | [UploadBlob[]](structures/upload-blob.md)) (optional) - * `baseURLForDataURL` String (optional) - Base URL (with trailing path separator) for files to be loaded by the data URL. This is needed only if the specified `url` is a data URL and needs to load other files. + * `httpReferrer` (string | [Referrer](structures/referrer.md)) (optional) - An HTTP Referrer URL. + * `userAgent` string (optional) - A user agent originating the request. + * `extraHeaders` string (optional) - Extra headers separated by "\n" + * `postData` ([UploadRawData](structures/upload-raw-data.md) | [UploadFile](structures/upload-file.md))[] (optional) + * `baseURLForDataURL` string (optional) - Base URL (with trailing path separator) for files to be loaded by the data URL. This is needed only if the specified `url` is a data URL and needs to load other files. Returns `Promise<void>` - the promise will resolve when the page has finished loading (see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects @@ -1355,7 +1434,7 @@ Node's [`url.format`](https://nodejs.org/api/url.html#url_url_format_urlobject) method: ```javascript -let url = require('url').format({ +const url = require('url').format({ protocol: 'file', slashes: true, pathname: require('path').join(__dirname, 'index.html') @@ -1379,11 +1458,11 @@ win.loadURL('http://localhost:8000/post', { #### `win.loadFile(filePath[, options])` -* `filePath` String +* `filePath` string * `options` Object (optional) - * `query` Object (optional) - Passed to `url.format()`. - * `search` String (optional) - Passed to `url.format()`. - * `hash` String (optional) - Passed to `url.format()`. + * `query` Record<string, string> (optional) - Passed to `url.format()`. + * `search` string (optional) - Passed to `url.format()`. + * `hash` string (optional) - Passed to `url.format()`. Returns `Promise<void>` - the promise will resolve when the page has finished loading (see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects @@ -1411,7 +1490,7 @@ Remove the window's menu bar. * `progress` Double * `options` Object (optional) - * `mode` String _Windows_ - Mode for the progress bar. Can be `none`, `normal`, `indeterminate`, `error` or `paused`. + * `mode` string _Windows_ - Mode for the progress bar. Can be `none`, `normal`, `indeterminate`, `error` or `paused`. Sets progress value in progress bar. Valid range is [0, 1.0]. @@ -1431,35 +1510,33 @@ mode set (but with a value within the valid range), `normal` will be assumed. * `overlay` [NativeImage](native-image.md) | null - the icon to display on the bottom right corner of the taskbar icon. If this parameter is `null`, the overlay is cleared -* `description` String - a description that will be provided to Accessibility +* `description` string - a description that will be provided to Accessibility screen readers Sets a 16 x 16 pixel overlay onto the current taskbar icon, usually used to convey some sort of application status or to passively notify the user. -#### `win.setHasShadow(hasShadow)` _macOS_ - -* `hasShadow` Boolean +#### `win.setHasShadow(hasShadow)` -Sets whether the window should have a shadow. On Windows and Linux does -nothing. +* `hasShadow` boolean -#### `win.hasShadow()` _macOS_ +Sets whether the window should have a shadow. -Returns `Boolean` - Whether the window has a shadow. +#### `win.hasShadow()` -On Windows and Linux always returns -`true`. +Returns `boolean` - Whether the window has a shadow. #### `win.setOpacity(opacity)` _Windows_ _macOS_ -* `opacity` Number - between 0.0 (fully transparent) and 1.0 (fully opaque) +* `opacity` number - between 0.0 (fully transparent) and 1.0 (fully opaque) -Sets the opacity of the window. On Linux does nothing. +Sets the opacity of the window. On Linux, does nothing. Out of bound number +values are clamped to the [0, 1] range. -#### `win.getOpacity()` _Windows_ _macOS_ +#### `win.getOpacity()` -Returns `Number` - between 0.0 (fully transparent) and 1.0 (fully opaque) +Returns `number` - between 0.0 (fully transparent) and 1.0 (fully opaque). On +Linux, always returns 1. #### `win.setShape(rects)` _Windows_ _Linux_ _Experimental_ @@ -1476,10 +1553,10 @@ whatever is behind the window. * `buttons` [ThumbarButton[]](structures/thumbar-button.md) -Returns `Boolean` - Whether the buttons were added successfully +Returns `boolean` - Whether the buttons were added successfully Add a thumbnail toolbar with a specified set of buttons to the thumbnail image -of a window in a taskbar button layout. Returns a `Boolean` object indicates +of a window in a taskbar button layout. Returns a `boolean` object indicates whether the thumbnail has been added successfully. The number of buttons in thumbnail toolbar should be no greater than 7 due to @@ -1493,11 +1570,11 @@ The `buttons` is an array of `Button` objects: * `icon` [NativeImage](native-image.md) - The icon showing in thumbnail toolbar. * `click` Function - * `tooltip` String (optional) - The text of the button's tooltip. - * `flags` String[] (optional) - Control specific states and behaviors of the + * `tooltip` string (optional) - The text of the button's tooltip. + * `flags` string[] (optional) - Control specific states and behaviors of the button. By default, it is `['enabled']`. -The `flags` is an array that can include following `String`s: +The `flags` is an array that can include following `string`s: * `enabled` - The button is active and available to the user. * `disabled` - The button is disabled. It is present, but has a visual state @@ -1521,7 +1598,7 @@ the entire window by specifying an empty region: #### `win.setThumbnailToolTip(toolTip)` _Windows_ -* `toolTip` String +* `toolTip` string Sets the toolTip that is displayed when hovering over the window thumbnail in the taskbar. @@ -1529,13 +1606,13 @@ in the taskbar. #### `win.setAppDetails(options)` _Windows_ * `options` Object - * `appId` String (optional) - Window's [App User Model ID](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391569(v=vs.85).aspx). + * `appId` string (optional) - Window's [App User Model ID](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391569(v=vs.85).aspx). It has to be set, otherwise the other options will have no effect. - * `appIconPath` String (optional) - Window's [Relaunch Icon](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391573(v=vs.85).aspx). + * `appIconPath` string (optional) - Window's [Relaunch Icon](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391573(v=vs.85).aspx). * `appIconIndex` Integer (optional) - Index of the icon in `appIconPath`. Ignored when `appIconPath` is not set. Default is `0`. - * `relaunchCommand` String (optional) - Window's [Relaunch Command](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391571(v=vs.85).aspx). - * `relaunchDisplayName` String (optional) - Window's [Relaunch Display Name](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391572(v=vs.85).aspx). + * `relaunchCommand` string (optional) - Window's [Relaunch Command](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391571(v=vs.85).aspx). + * `relaunchDisplayName` string (optional) - Window's [Relaunch Display Name](https://msdn.microsoft.com/en-us/library/windows/desktop/dd391572(v=vs.85).aspx). Sets the properties for the window's taskbar button. @@ -1548,53 +1625,52 @@ Same as `webContents.showDefinitionForSelection()`. #### `win.setIcon(icon)` _Windows_ _Linux_ -* `icon` [NativeImage](native-image.md) +* `icon` [NativeImage](native-image.md) | string Changes window icon. #### `win.setWindowButtonVisibility(visible)` _macOS_ -* `visible` Boolean +* `visible` boolean Sets whether the window traffic light buttons should be visible. -This cannot be called when `titleBarStyle` is set to `customButtonsOnHover`. - #### `win.setAutoHideMenuBar(hide)` -* `hide` Boolean +* `hide` boolean Sets whether the window menu bar should hide itself automatically. Once set the menu bar will only show when users press the single `Alt` key. -If the menu bar is already visible, calling `setAutoHideMenuBar(true)` won't -hide it immediately. - -**[Deprecated](modernization/property-updates.md)** +If the menu bar is already visible, calling `setAutoHideMenuBar(true)` won't hide it immediately. #### `win.isMenuBarAutoHide()` -Returns `Boolean` - Whether menu bar automatically hides itself. - -**[Deprecated](modernization/property-updates.md)** +Returns `boolean` - Whether menu bar automatically hides itself. #### `win.setMenuBarVisibility(visible)` _Windows_ _Linux_ -* `visible` Boolean +* `visible` boolean -Sets whether the menu bar should be visible. If the menu bar is auto-hide, users -can still bring up the menu bar by pressing the single `Alt` key. +Sets whether the menu bar should be visible. If the menu bar is auto-hide, users can still bring up the menu bar by pressing the single `Alt` key. #### `win.isMenuBarVisible()` -Returns `Boolean` - Whether the menu bar is visible. +Returns `boolean` - Whether the menu bar is visible. #### `win.setVisibleOnAllWorkspaces(visible[, options])` -* `visible` Boolean +* `visible` boolean * `options` Object (optional) - * `visibleOnFullScreen` Boolean (optional) _macOS_ - Sets whether - the window should be visible above fullscreen windows + * `visibleOnFullScreen` boolean (optional) _macOS_ - Sets whether + the window should be visible above fullscreen windows. + * `skipTransformProcessType` boolean (optional) _macOS_ - Calling + setVisibleOnAllWorkspaces will by default transform the process + type between UIElementApplication and ForegroundApplication to + ensure the correct behavior. However, this will hide the window + and dock for a short time every time it is called. If your window + is already of type UIElementApplication, you can bypass this + transformation by passing true to skipTransformProcessType. Sets whether the window should be visible on all workspaces. @@ -1602,15 +1678,15 @@ Sets whether the window should be visible on all workspaces. #### `win.isVisibleOnAllWorkspaces()` -Returns `Boolean` - Whether the window is visible on all workspaces. +Returns `boolean` - Whether the window is visible on all workspaces. **Note:** This API always returns false on Windows. #### `win.setIgnoreMouseEvents(ignore[, options])` -* `ignore` Boolean +* `ignore` boolean * `options` Object (optional) - * `forward` Boolean (optional) _macOS_ _Windows_ - If true, forwards mouse move + * `forward` boolean (optional) _macOS_ _Windows_ - If true, forwards mouse move messages to Chromium, enabling mouse related events such as `mouseleave`. Only used when `ignore` is true. If `ignore` is false, forwarding is always disabled regardless of this value. @@ -1623,21 +1699,27 @@ events. #### `win.setContentProtection(enable)` _macOS_ _Windows_ -* `enable` Boolean +* `enable` boolean Prevents the window contents from being captured by other apps. On macOS it sets the NSWindow's sharingType to NSWindowSharingNone. -On Windows it calls SetWindowDisplayAffinity with `WDA_MONITOR`. +On Windows it calls SetWindowDisplayAffinity with `WDA_EXCLUDEFROMCAPTURE`. +For Windows 10 version 2004 and up the window will be removed from capture entirely, +older Windows versions behave as if `WDA_MONITOR` is applied capturing a black window. #### `win.setFocusable(focusable)` _macOS_ _Windows_ -* `focusable` Boolean +* `focusable` boolean Changes whether the window can be focused. On macOS it does not remove the focus from the window. +#### `win.isFocusable()` _macOS_ _Windows_ + +Returns whether the window can be focused. + #### `win.setParentWindow(parent)` * `parent` BrowserWindow | null @@ -1647,7 +1729,7 @@ current window into a top-level window. #### `win.getParentWindow()` -Returns `BrowserWindow` - The parent window. +Returns `BrowserWindow | null` - The parent window or `null` if there is no parent. #### `win.getChildWindows()` @@ -1655,7 +1737,7 @@ Returns `BrowserWindow[]` - All child windows. #### `win.setAutoHideCursor(autoHide)` _macOS_ -* `autoHide` Boolean +* `autoHide` boolean Controls whether to hide cursor when typing. @@ -1692,7 +1774,7 @@ Adds a window as a tab on this window, after the tab for the window instance. #### `win.setVibrancy(type)` _macOS_ -* `type` String | null - Can be `appearance-based`, `light`, `dark`, `titlebar`, +* `type` string | null - Can be `appearance-based`, `light`, `dark`, `titlebar`, `selection`, `menu`, `popover`, `sidebar`, `medium-light`, `ultra-dark`, `header`, `sheet`, `window`, `hud`, `fullscreen-ui`, `tooltip`, `content`, `under-window`, or `under-page`. See the [macOS documentation][vibrancy-docs] for more details. @@ -1702,7 +1784,18 @@ will remove the vibrancy effect on the window. Note that `appearance-based`, `light`, `dark`, `medium-light`, and `ultra-dark` have been deprecated and will be removed in an upcoming version of macOS. -#### `win.setTouchBar(touchBar)` _macOS_ _Experimental_ +#### `win.setTrafficLightPosition(position)` _macOS_ + +* `position` [Point](structures/point.md) + +Set a custom position for the traffic light buttons in frameless window. + +#### `win.getTrafficLightPosition()` _macOS_ + +Returns `Point` - The custom position for the traffic light buttons in +frameless window. + +#### `win.setTouchBar(touchBar)` _macOS_ * `touchBar` TouchBar | null @@ -1715,14 +1808,14 @@ removed in future Electron releases. #### `win.setBrowserView(browserView)` _Experimental_ -* `browserView` [BrowserView](browser-view.md) | null - Attach browserView to win. -If there is some other browserViews was attached they will be removed from +* `browserView` [BrowserView](browser-view.md) | null - Attach `browserView` to `win`. +If there are other `BrowserView`s attached, they will be removed from this window. #### `win.getBrowserView()` _Experimental_ -Returns `BrowserView | null` - an BrowserView what is attached. Returns `null` -if none is attached. Throw error if multiple BrowserViews is attached. +Returns `BrowserView | null` - The `BrowserView` attached to `win`. Returns `null` +if one is not attached. Throws an error if multiple `BrowserView`s are attached. #### `win.addBrowserView(browserView)` _Experimental_ @@ -1734,6 +1827,13 @@ Replacement API for setBrowserView supporting work with multi browser views. * `browserView` [BrowserView](browser-view.md) +#### `win.setTopBrowserView(browserView)` _Experimental_ + +* `browserView` [BrowserView](browser-view.md) + +Raises `browserView` above other `BrowserView`s attached to `win`. +Throws an error if `browserView` is not attached to `win`. + #### `win.getBrowserViews()` _Experimental_ Returns `BrowserView[]` - an array of all BrowserViews that have been attached @@ -1742,6 +1842,16 @@ with `addBrowserView` or `setBrowserView`. **Note:** The BrowserView API is currently experimental and may change or be removed in future Electron releases. +#### `win.setTitleBarOverlay(options)` _Windows_ + +* `options` Object + * `color` String (optional) _Windows_ - The CSS color of the Window Controls Overlay when enabled. + * `symbolColor` String (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled. + * `height` Integer (optional) _Windows_ - The height of the title bar and Window Controls Overlay in pixels. + +On a Window with Window Controls Overlay already enabled, this method updates +the style of the title bar overlay. + [runtime-enabled-features]: https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/runtime_enabled_features.json5?l=70 [page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API [quick-look]: https://en.wikipedia.org/wiki/Quick_Look @@ -1749,3 +1859,5 @@ removed in future Electron releases. [window-levels]: https://developer.apple.com/documentation/appkit/nswindow/level [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter +[overlay-javascript-apis]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#javascript-apis +[overlay-css-env-vars]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#css-environment-variables diff --git a/docs/api/chrome-command-line-switches.md b/docs/api/chrome-command-line-switches.md deleted file mode 100644 index 81d5f503cc71b..0000000000000 --- a/docs/api/chrome-command-line-switches.md +++ /dev/null @@ -1,194 +0,0 @@ -# Supported Chrome Command Line Switches - -> Command line switches supported by Electron. - -You can use [app.commandLine.appendSwitch][append-switch] to append them in -your app's main script before the [ready][ready] event of the [app][app] module -is emitted: - -```javascript -const { app } = require('electron') -app.commandLine.appendSwitch('remote-debugging-port', '8315') -app.commandLine.appendSwitch('host-rules', 'MAP * 127.0.0.1') - -app.on('ready', () => { - // Your code here -}) -``` - -## --ignore-connections-limit=`domains` - -Ignore the connections limit for `domains` list separated by `,`. - -## --disable-http-cache - -Disables the disk cache for HTTP requests. - -## --disable-http2 - -Disable HTTP/2 and SPDY/3.1 protocols. - -## --lang - -Set a custom locale. - -## --inspect=`port` and --inspect-brk=`port` - -Debug-related flags, see the [Debugging the Main Process][debugging-main-process] guide for details. - -## --remote-debugging-port=`port` - -Enables remote debugging over HTTP on the specified `port`. - -## --disk-cache-size=`size` - -Forces the maximum disk space to be used by the disk cache, in bytes. - -## --js-flags=`flags` - -Specifies the flags passed to the Node.js engine. It has to be passed when starting -Electron if you want to enable the `flags` in the main process. - -```sh -$ electron --js-flags="--harmony_proxies --harmony_collections" your-app -``` - -See the [Node.js documentation][node-cli] or run `node --help` in your terminal for a list of available flags. Additionally, run `node --v8-options` to see a list of flags that specifically refer to Node.js's V8 JavaScript engine. - -## --proxy-server=`address:port` - -Use a specified proxy server, which overrides the system setting. This switch -only affects requests with HTTP protocol, including HTTPS and WebSocket -requests. It is also noteworthy that not all proxy servers support HTTPS and -WebSocket requests. The proxy URL does not support username and password -authentication [per Chromium issue](https://bugs.chromium.org/p/chromium/issues/detail?id=615947). - -## --proxy-bypass-list=`hosts` - -Instructs Electron to bypass the proxy server for the given semi-colon-separated -list of hosts. This flag has an effect only if used in tandem with -`--proxy-server`. - -For example: - -```javascript -const { app } = require('electron') -app.commandLine.appendSwitch('proxy-bypass-list', '<local>;*.google.com;*foo.com;1.2.3.4:5678') -``` - -Will use the proxy server for all hosts except for local addresses (`localhost`, -`127.0.0.1` etc.), `google.com` subdomains, hosts that contain the suffix -`foo.com` and anything at `1.2.3.4:5678`. - -## --proxy-pac-url=`url` - -Uses the PAC script at the specified `url`. - -## --no-proxy-server - -Don't use a proxy server and always make direct connections. Overrides any other -proxy server flags that are passed. - -## --host-rules=`rules` - -A comma-separated list of `rules` that control how hostnames are mapped. - -For example: - -* `MAP * 127.0.0.1` Forces all hostnames to be mapped to 127.0.0.1 -* `MAP *.google.com proxy` Forces all google.com subdomains to be resolved to - "proxy". -* `MAP test.com [::1]:77` Forces "test.com" to resolve to IPv6 loopback. Will - also force the port of the resulting socket address to be 77. -* `MAP * baz, EXCLUDE www.google.com` Remaps everything to "baz", except for - "www.google.com". - -These mappings apply to the endpoint host in a net request (the TCP connect -and host resolver in a direct connection, and the `CONNECT` in an HTTP proxy -connection, and the endpoint host in a `SOCKS` proxy connection). - -## --host-resolver-rules=`rules` - -Like `--host-rules` but these `rules` only apply to the host resolver. - -## --auth-server-whitelist=`url` - -A comma-separated list of servers for which integrated authentication is enabled. - -For example: - -```sh ---auth-server-whitelist='*example.com, *foobar.com, *baz' -``` - -then any `url` ending with `example.com`, `foobar.com`, `baz` will be considered -for integrated authentication. Without `*` prefix the URL has to match exactly. - -## --auth-negotiate-delegate-whitelist=`url` - -A comma-separated list of servers for which delegation of user credentials is required. -Without `*` prefix the URL has to match exactly. - -## --ignore-certificate-errors - -Ignores certificate related errors. - -## --ppapi-flash-path=`path` - -Sets the `path` of the pepper flash plugin. - -## --ppapi-flash-version=`version` - -Sets the `version` of the pepper flash plugin. - -## --log-net-log=`path` - -Enables net log events to be saved and writes them to `path`. - -## --disable-renderer-backgrounding - -Prevents Chromium from lowering the priority of invisible pages' renderer -processes. - -This flag is global to all renderer processes, if you only want to disable -throttling in one window, you can take the hack of -[playing silent audio][play-silent-audio]. - -## --enable-logging - -Prints Chromium's logging into console. - -This switch can not be used in `app.commandLine.appendSwitch` since it is parsed -earlier than user's app is loaded, but you can set the `ELECTRON_ENABLE_LOGGING` -environment variable to achieve the same effect. - -## --v=`log_level` - -Gives the default maximal active V-logging level; 0 is the default. Normally -positive values are used for V-logging levels. - -This switch only works when `--enable-logging` is also passed. - -## --vmodule=`pattern` - -Gives the per-module maximal V-logging levels to override the value given by -`--v`. E.g. `my_module=2,foo*=3` would change the logging level for all code in -source files `my_module.*` and `foo*.*`. - -Any pattern containing a forward or backward slash will be tested against the -whole pathname and not only the module. E.g. `*/foo/bar/*=2` would change the -logging level for all code in the source files under a `foo/bar` directory. - -This switch only works when `--enable-logging` is also passed. - -## --no-sandbox - -Disables Chromium sandbox, which is now enabled by default. -Should only be used for testing. - -[app]: app.md -[append-switch]: app.md#appcommandlineappendswitchswitch-value -[ready]: app.md#event-ready -[play-silent-audio]: https://github.com/atom/atom/pull/9485/files -[debugging-main-process]: ../tutorial/debugging-main-process.md -[node-cli]: https://nodejs.org/api/cli.html diff --git a/docs/api/client-request.md b/docs/api/client-request.md index 50924d21504a2..12f1ce1157e5b 100644 --- a/docs/api/client-request.md +++ b/docs/api/client-request.md @@ -2,38 +2,53 @@ > Make HTTP/HTTPS requests. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ `ClientRequest` implements the [Writable Stream](https://nodejs.org/api/stream.html#stream_writable_streams) interface and is therefore an [EventEmitter][event-emitter]. ### `new ClientRequest(options)` -* `options` (Object | String) - If `options` is a String, it is interpreted as +* `options` (Object | string) - If `options` is a string, it is interpreted as the request URL. If it is an object, it is expected to fully specify an HTTP request via the following properties: - * `method` String (optional) - The HTTP request method. Defaults to the GET -method. - * `url` String (optional) - The request URL. Must be provided in the absolute -form with the protocol scheme specified as http or https. - * `session` Object (optional) - The [`Session`](session.md) instance with -which the request is associated. - * `partition` String (optional) - The name of the [`partition`](session.md) - with which the request is associated. Defaults to the empty string. The -`session` option prevails on `partition`. Thus if a `session` is explicitly -specified, `partition` is ignored. - * `protocol` String (optional) - The protocol scheme in the form 'scheme:'. -Currently supported values are 'http:' or 'https:'. Defaults to 'http:'. - * `host` String (optional) - The server host provided as a concatenation of -the hostname and the port number 'hostname:port'. - * `hostname` String (optional) - The server host name. + * `method` string (optional) - The HTTP request method. Defaults to the GET + method. + * `url` string (optional) - The request URL. Must be provided in the absolute + form with the protocol scheme specified as http or https. + * `session` Session (optional) - The [`Session`](session.md) instance with + which the request is associated. + * `partition` string (optional) - The name of the [`partition`](session.md) + with which the request is associated. Defaults to the empty string. The + `session` option supersedes `partition`. Thus if a `session` is explicitly + specified, `partition` is ignored. + * `credentials` string (optional) - Can be `include` or `omit`. Whether to + send [credentials](https://fetch.spec.whatwg.org/#credentials) with this + request. If set to `include`, credentials from the session associated with + the request will be used. If set to `omit`, credentials will not be sent + with the request (and the `'login'` event will not be triggered in the + event of a 401). This matches the behavior of the + [fetch](https://fetch.spec.whatwg.org/#concept-request-credentials-mode) + option of the same name. If this option is not specified, authentication + data from the session will be sent, and cookies will not be sent (unless + `useSessionCookies` is set). + * `useSessionCookies` boolean (optional) - Whether to send cookies with this + request from the provided session. If `credentials` is specified, this + option has no effect. Default is `false`. + * `protocol` string (optional) - Can be `http:` or `https:`. The protocol + scheme in the form 'scheme:'. Defaults to 'http:'. + * `host` string (optional) - The server host provided as a concatenation of + the hostname and the port number 'hostname:port'. + * `hostname` string (optional) - The server host name. * `port` Integer (optional) - The server's listening port number. - * `path` String (optional) - The path part of the request URL. - * `redirect` String (optional) - The redirect mode for this request. Should be -one of `follow`, `error` or `manual`. Defaults to `follow`. When mode is `error`, -any redirection will be aborted. When mode is `manual` the redirection will be -deferred until [`request.followRedirect`](#requestfollowredirect) is invoked. Listen for the [`redirect`](#event-redirect) event in -this mode to get more details about the redirect request. + * `path` string (optional) - The path part of the request URL. + * `redirect` string (optional) - Can be `follow`, `error` or `manual`. The + redirect mode for this request. When mode is `error`, any redirection will + be aborted. When mode is `manual` the redirection will be cancelled unless + [`request.followRedirect`](#requestfollowredirect) is invoked synchronously + during the [`redirect`](#event-redirect) event. Defaults to `follow`. + * `origin` string (optional) - The origin URL of the request. `options` properties such as `protocol`, `host`, `hostname`, `port` and `path` strictly follow the Node.js model as described in the @@ -57,34 +72,35 @@ const request = net.request({ Returns: -* `response` IncomingMessage - An object representing the HTTP response message. +* `response` [IncomingMessage](incoming-message.md) - An object representing the HTTP response message. #### Event: 'login' Returns: * `authInfo` Object - * `isProxy` Boolean - * `scheme` String - * `host` String + * `isProxy` boolean + * `scheme` string + * `host` string * `port` Integer - * `realm` String + * `realm` string * `callback` Function - * `username` String - * `password` String + * `username` string (optional) + * `password` string (optional) Emitted when an authenticating proxy is asking for user credentials. The `callback` function is expected to be called back with user credentials: -* `username` String -* `password` String +* `username` string +* `password` string ```JavaScript request.on('login', (authInfo, callback) => { callback('username', 'password') }) ``` + Providing empty credentials will cancel the request and report an authentication error on the response object: @@ -126,24 +142,26 @@ Emitted as the last event in the HTTP request-response transaction. The `close` event indicates that no more events will be emitted on either the `request` or `response` objects. - #### Event: 'redirect' Returns: * `statusCode` Integer -* `method` String -* `redirectUrl` String -* `responseHeaders` Object +* `method` string +* `redirectUrl` string +* `responseHeaders` Record<string, string[]> -Emitted when there is redirection and the mode is `manual`. Calling -[`request.followRedirect`](#requestfollowredirect) will continue with the redirection. +Emitted when the server returns a redirect response (e.g. 301 Moved +Permanently). Calling [`request.followRedirect`](#requestfollowredirect) will +continue with the redirection. If this event is handled, +[`request.followRedirect`](#requestfollowredirect) must be called +**synchronously**, otherwise the request will be cancelled. ### Instance Properties #### `request.chunkedEncoding` -A `Boolean` specifying whether the request will use HTTP chunked transfer encoding +A `boolean` specifying whether the request will use HTTP chunked transfer encoding or not. Defaults to false. The property is readable and writable, however it can be set only before the first write operation as the HTTP headers are not yet put on the wire. Trying to set the `chunkedEncoding` property after the first write @@ -157,32 +175,46 @@ internally buffered inside Electron process memory. #### `request.setHeader(name, value)` -* `name` String - An extra HTTP header name. -* `value` Object - An extra HTTP header value. +* `name` string - An extra HTTP header name. +* `value` string - An extra HTTP header value. Adds an extra HTTP header. The header name will be issued as-is without lowercasing. It can be called only before first write. Calling this method after -the first write will throw an error. If the passed value is not a `String`, its +the first write will throw an error. If the passed value is not a `string`, its `toString()` method will be called to obtain the final value. +Certain headers are restricted from being set by apps. These headers are +listed below. More information on restricted headers can be found in +[Chromium's header utils](https://source.chromium.org/chromium/chromium/src/+/master:services/network/public/cpp/header_util.cc;drc=1562cab3f1eda927938f8f4a5a91991fefde66d3;bpv=1;bpt=1;l=22). + +* `Content-Length` +* `Host` +* `Trailer` or `Te` +* `Upgrade` +* `Cookie2` +* `Keep-Alive` +* `Transfer-Encoding` + +Additionally, setting the `Connection` header to the value `upgrade` is also disallowed. + #### `request.getHeader(name)` -* `name` String - Specify an extra header name. +* `name` string - Specify an extra header name. -Returns `Object` - The value of a previously set extra header name. +Returns `string` - The value of a previously set extra header name. #### `request.removeHeader(name)` -* `name` String - Specify an extra header name. +* `name` string - Specify an extra header name. Removes a previously set extra header name. This method can be called only before first write. Trying to call it after the first write will throw an error. #### `request.write(chunk[, encoding][, callback])` -* `chunk` (String | Buffer) - A chunk of the request body's data. If it is a +* `chunk` (string | Buffer) - A chunk of the request body's data. If it is a string, it is converted into a Buffer using the specified encoding. -* `encoding` String (optional) - Used to convert string chunks into Buffer +* `encoding` string (optional) - Used to convert string chunks into Buffer objects. Defaults to 'utf-8'. * `callback` Function (optional) - Called after the write operation ends. @@ -198,8 +230,8 @@ it is not allowed to add or remove a custom header. #### `request.end([chunk][, encoding][, callback])` -* `chunk` (String | Buffer) (optional) -* `encoding` String (optional) +* `chunk` (string | Buffer) (optional) +* `encoding` string (optional) * `callback` Function (optional) Sends the last chunk of the request data. Subsequent write or end operations @@ -214,15 +246,16 @@ response object,it will emit the `aborted` event. #### `request.followRedirect()` -Continues any deferred redirection request when the redirection mode is `manual`. +Continues any pending redirection. Can only be called during a `'redirect'` +event. #### `request.getUploadProgress()` Returns `Object`: -* `active` Boolean - Whether the request is currently active. If this is false +* `active` boolean - Whether the request is currently active. If this is false no other properties will be set -* `started` Boolean - Whether the upload has started. If this is false both +* `started` boolean - Whether the upload has started. If this is false both `current` and `total` will be set to 0. * `current` Integer - The number of bytes that have been uploaded so far * `total` Integer - The number of bytes that will be uploaded this request diff --git a/docs/api/clipboard.md b/docs/api/clipboard.md index 71cf7616d87be..3d43c3715de4c 100644 --- a/docs/api/clipboard.md +++ b/docs/api/clipboard.md @@ -4,19 +4,13 @@ Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process) -The following example shows how to write a string to the clipboard: - -```javascript -const { clipboard } = require('electron') -clipboard.writeText('Example String') -``` - On Linux, there is also a `selection` clipboard. To manipulate it you need to pass `selection` to each method: ```javascript const { clipboard } = require('electron') -clipboard.writeText('Example String', 'selection') + +clipboard.writeText('Example string', 'selection') console.log(clipboard.readText('selection')) ``` @@ -28,81 +22,133 @@ The `clipboard` module has the following methods: ### `clipboard.readText([type])` -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. + +Returns `string` - The content in the clipboard as plain text. + +```js +const { clipboard } = require('electron') -Returns `String` - The content in the clipboard as plain text. +clipboard.writeText('hello i am a bit of text!') + +const text = clipboard.readText() +console.log(text) +// hello i am a bit of text!' +``` ### `clipboard.writeText(text[, type])` -* `text` String -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `text` string +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Writes the `text` into the clipboard as plain text. +```js +const { clipboard } = require('electron') + +const text = 'hello i am a bit of text!' +clipboard.writeText(text) +``` + ### `clipboard.readHTML([type])` -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. + +Returns `string` - The content in the clipboard as markup. + +```js +const { clipboard } = require('electron') -Returns `String` - The content in the clipboard as markup. +clipboard.writeHTML('<b>Hi</b>') +const html = clipboard.readHTML() + +console.log(html) +// <meta charset='utf-8'><b>Hi</b> +``` ### `clipboard.writeHTML(markup[, type])` -* `markup` String -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `markup` string +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Writes `markup` to the clipboard. +```js +const { clipboard } = require('electron') + +clipboard.writeHTML('<b>Hi</b>') +``` + ### `clipboard.readImage([type])` -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Returns [`NativeImage`](native-image.md) - The image content in the clipboard. ### `clipboard.writeImage(image[, type])` * `image` [NativeImage](native-image.md) -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Writes `image` to the clipboard. ### `clipboard.readRTF([type])` -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. -Returns `String` - The content in the clipboard as RTF. +Returns `string` - The content in the clipboard as RTF. + +```js +const { clipboard } = require('electron') + +clipboard.writeRTF('{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard\nThis is some {\\b bold} text.\\par\n}') + +const rtf = clipboard.readRTF() +console.log(rtf) +// {\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard\nThis is some {\\b bold} text.\\par\n} +``` ### `clipboard.writeRTF(text[, type])` -* `text` String -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `text` string +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Writes the `text` into the clipboard in RTF. +```js +const { clipboard } = require('electron') + +const rtf = '{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard\nThis is some {\\b bold} text.\\par\n}' +clipboard.writeRTF(rtf) +``` + ### `clipboard.readBookmark()` _macOS_ _Windows_ Returns `Object`: -* `title` String -* `url` String +* `title` string +* `url` string Returns an Object containing `title` and `url` keys representing the bookmark in the clipboard. The `title` and `url` values will be empty strings when the -bookmark is unavailable. +bookmark is unavailable. The `title` value will always be empty on Windows. ### `clipboard.writeBookmark(title, url[, type])` _macOS_ _Windows_ -* `title` String -* `url` String -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `title` string - Unused on Windows +* `url` string +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. -Writes the `title` and `url` into the clipboard as a bookmark. +Writes the `title` (macOS only) and `url` into the clipboard as a bookmark. **Note:** Most apps on Windows don't support pasting bookmarks into them so you can use `clipboard.write` to write both a bookmark and fallback text to the clipboard. ```js -clipboard.write({ +const { clipboard } = require('electron') + +clipboard.writeBookmark({ text: 'https://electronjs.org', bookmark: 'Electron Homepage' }) @@ -110,73 +156,126 @@ clipboard.write({ ### `clipboard.readFindText()` _macOS_ -Returns `String` - The text on the find pasteboard. This method uses synchronous -IPC when called from the renderer process. The cached value is reread from the -find pasteboard whenever the application is activated. +Returns `string` - The text on the find pasteboard, which is the pasteboard that holds information about the current state of the active application’s find panel. + +This method uses synchronous IPC when called from the renderer process. +The cached value is reread from the find pasteboard whenever the application is activated. ### `clipboard.writeFindText(text)` _macOS_ -* `text` String +* `text` string -Writes the `text` into the find pasteboard as plain text. This method uses -synchronous IPC when called from the renderer process. +Writes the `text` into the find pasteboard (the pasteboard that holds information about the current state of the active application’s find panel) as plain text. This method uses synchronous IPC when called from the renderer process. ### `clipboard.clear([type])` -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Clears the clipboard content. ### `clipboard.availableFormats([type])` -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. -Returns `String[]` - An array of supported formats for the clipboard `type`. +Returns `string[]` - An array of supported formats for the clipboard `type`. + +```js +const { clipboard } = require('electron') + +const formats = clipboard.availableFormats() +console.log(formats) +// [ 'text/plain', 'text/html' ] +``` ### `clipboard.has(format[, type])` _Experimental_ -* `format` String -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `format` string +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. -Returns `Boolean` - Whether the clipboard supports the specified `format`. +Returns `boolean` - Whether the clipboard supports the specified `format`. -```javascript +```js const { clipboard } = require('electron') -console.log(clipboard.has('<p>selection</p>')) + +const hasFormat = clipboard.has('public/utf8-plain-text') +console.log(hasFormat) +// 'true' or 'false' ``` ### `clipboard.read(format)` _Experimental_ -* `format` String +* `format` string -Returns `String` - Reads `format` type from the clipboard. +Returns `string` - Reads `format` type from the clipboard. + +`format` should contain valid ASCII characters and have `/` separator. +`a/c`, `a/bc` are valid formats while `/abc`, `abc/`, `a/`, `/a`, `a` +are not valid. ### `clipboard.readBuffer(format)` _Experimental_ -* `format` String +* `format` string Returns `Buffer` - Reads `format` type from the clipboard. +```js +const { clipboard } = require('electron') + +const buffer = Buffer.from('this is binary', 'utf8') +clipboard.writeBuffer('public/utf8-plain-text', buffer) + +const ret = clipboard.readBuffer('public/utf8-plain-text') + +console.log(buffer.equals(out)) +// true +``` + ### `clipboard.writeBuffer(format, buffer[, type])` _Experimental_ -* `format` String +* `format` string * `buffer` Buffer -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. Writes the `buffer` into the clipboard as `format`. +```js +const { clipboard } = require('electron') + +const buffer = Buffer.from('writeBuffer', 'utf8') +clipboard.writeBuffer('public/utf8-plain-text', buffer) +``` + ### `clipboard.write(data[, type])` * `data` Object - * `text` String (optional) - * `html` String (optional) + * `text` string (optional) + * `html` string (optional) * `image` [NativeImage](native-image.md) (optional) - * `rtf` String (optional) - * `bookmark` String (optional) - The title of the URL at `text`. -* `type` String (optional) - Can be `selection` or `clipboard`. `selection` is only available on Linux. + * `rtf` string (optional) + * `bookmark` string (optional) - The title of the URL at `text`. +* `type` string (optional) - Can be `selection` or `clipboard`; default is 'clipboard'. `selection` is only available on Linux. -```javascript +Writes `data` to the clipboard. + +```js const { clipboard } = require('electron') -clipboard.write({ text: 'test', html: '<b>test</b>' }) + +clipboard.write({ + text: 'test', + html: '<b>Hi</b>', + rtf: '{\\rtf1\\utf8 text}', + bookmark: 'a title' +}) + +console.log(clipboard.readText()) +// 'test' + +console.log(clipboard.readHTML()) +// <meta charset='utf-8'><b>Hi</b> + +console.log(clipboard.readRTF()) +// '{\\rtf1\\utf8 text}' + +console.log(clipboard.readBookmark()) +// { title: 'a title', url: 'test' } ``` -Writes `data` to the clipboard. diff --git a/docs/api/command-line-switches.md b/docs/api/command-line-switches.md new file mode 100644 index 0000000000000..f395379c063fa --- /dev/null +++ b/docs/api/command-line-switches.md @@ -0,0 +1,281 @@ +# Supported Command Line Switches + +> Command line switches supported by Electron. + +You can use [app.commandLine.appendSwitch][append-switch] to append them in +your app's main script before the [ready][ready] event of the [app][app] module +is emitted: + +```javascript +const { app } = require('electron') +app.commandLine.appendSwitch('remote-debugging-port', '8315') +app.commandLine.appendSwitch('host-rules', 'MAP * 127.0.0.1') + +app.whenReady().then(() => { + // Your code here +}) +``` + +## Electron CLI Flags + +### --auth-server-whitelist=`url` + +A comma-separated list of servers for which integrated authentication is enabled. + +For example: + +```sh +--auth-server-whitelist='*example.com, *foobar.com, *baz' +``` + +then any `url` ending with `example.com`, `foobar.com`, `baz` will be considered +for integrated authentication. Without `*` prefix the URL has to match exactly. + +### --auth-negotiate-delegate-whitelist=`url` + +A comma-separated list of servers for which delegation of user credentials is required. +Without `*` prefix the URL has to match exactly. + +### --disable-ntlm-v2 + +Disables NTLM v2 for posix platforms, no effect elsewhere. + +### --disable-http-cache + +Disables the disk cache for HTTP requests. + +### --disable-http2 + +Disable HTTP/2 and SPDY/3.1 protocols. + +### --disable-renderer-backgrounding + +Prevents Chromium from lowering the priority of invisible pages' renderer +processes. + +This flag is global to all renderer processes, if you only want to disable +throttling in one window, you can take the hack of +[playing silent audio][play-silent-audio]. + +### --disk-cache-size=`size` + +Forces the maximum disk space to be used by the disk cache, in bytes. + +### --enable-logging[=file] + +Prints Chromium's logging to stderr (or a log file). + +The `ELECTRON_ENABLE_LOGGING` environment variable has the same effect as +passing `--enable-logging`. + +Passing `--enable-logging` will result in logs being printed on stderr. +Passing `--enable-logging=file` will result in logs being saved to the file +specified by `--log-file=...`, or to `electron_debug.log` in the user-data +directory if `--log-file` is not specified. + +> **Note:** On Windows, logs from child processes cannot be sent to stderr. +> Logging to a file is the most reliable way to collect logs on Windows. + +See also `--log-file`, `--log-level`, `--v`, and `--vmodule`. + +### --force-fieldtrials=`trials` + +Field trials to be forcefully enabled or disabled. + +For example: `WebRTC-Audio-Red-For-Opus/Enabled/` + +### --host-rules=`rules` + +A comma-separated list of `rules` that control how hostnames are mapped. + +For example: + +* `MAP * 127.0.0.1` Forces all hostnames to be mapped to 127.0.0.1 +* `MAP *.google.com proxy` Forces all google.com subdomains to be resolved to + "proxy". +* `MAP test.com [::1]:77` Forces "test.com" to resolve to IPv6 loopback. Will + also force the port of the resulting socket address to be 77. +* `MAP * baz, EXCLUDE www.google.com` Remaps everything to "baz", except for + "www.google.com". + +These mappings apply to the endpoint host in a net request (the TCP connect +and host resolver in a direct connection, and the `CONNECT` in an HTTP proxy +connection, and the endpoint host in a `SOCKS` proxy connection). + +### --host-resolver-rules=`rules` + +Like `--host-rules` but these `rules` only apply to the host resolver. + +### --ignore-certificate-errors + +Ignores certificate related errors. + +### --ignore-connections-limit=`domains` + +Ignore the connections limit for `domains` list separated by `,`. + +### --js-flags=`flags` + +Specifies the flags passed to the Node.js engine. It has to be passed when starting +Electron if you want to enable the `flags` in the main process. + +```sh +$ electron --js-flags="--harmony_proxies --harmony_collections" your-app +``` + +See the [Node.js documentation][node-cli] or run `node --help` in your terminal for a list of available flags. Additionally, run `node --v8-options` to see a list of flags that specifically refer to Node.js's V8 JavaScript engine. + +### --lang + +Set a custom locale. + +### --log-file=`path` + +If `--enable-logging` is specified, logs will be written to the given path. The +parent directory must exist. + +Setting the `ELECTRON_LOG_FILE` environment variable is equivalent to passing +this flag. If both are present, the command-line switch takes precedence. + +### --log-net-log=`path` + +Enables net log events to be saved and writes them to `path`. + +### --log-level=`N` + +Sets the verbosity of logging when used together with `--enable-logging`. +`N` should be one of [Chrome's LogSeverities][severities]. + +Note that two complimentary logging mechanisms in Chromium -- `LOG()` +and `VLOG()` -- are controlled by different switches. `--log-level` +controls `LOG()` messages, while `--v` and `--vmodule` control `VLOG()` +messages. So you may want to use a combination of these three switches +depending on the granularity you want and what logging calls are made +by the code you're trying to watch. + +See [Chromium Logging source][logging] for more information on how +`LOG()` and `VLOG()` interact. Loosely speaking, `VLOG()` can be thought +of as sub-levels / per-module levels inside `LOG(INFO)` to control the +firehose of `LOG(INFO)` data. + +See also `--enable-logging`, `--log-level`, `--v`, and `--vmodule`. + +### --no-proxy-server + +Don't use a proxy server and always make direct connections. Overrides any other +proxy server flags that are passed. + +### --no-sandbox + +Disables the Chromium [sandbox](https://www.chromium.org/developers/design-documents/sandbox). +Forces renderer process and Chromium helper processes to run un-sandboxed. +Should only be used for testing. + +### --proxy-bypass-list=`hosts` + +Instructs Electron to bypass the proxy server for the given semi-colon-separated +list of hosts. This flag has an effect only if used in tandem with +`--proxy-server`. + +For example: + +```javascript +const { app } = require('electron') +app.commandLine.appendSwitch('proxy-bypass-list', '<local>;*.google.com;*foo.com;1.2.3.4:5678') +``` + +Will use the proxy server for all hosts except for local addresses (`localhost`, +`127.0.0.1` etc.), `google.com` subdomains, hosts that contain the suffix +`foo.com` and anything at `1.2.3.4:5678`. + +### --proxy-pac-url=`url` + +Uses the PAC script at the specified `url`. + +### --proxy-server=`address:port` + +Use a specified proxy server, which overrides the system setting. This switch +only affects requests with HTTP protocol, including HTTPS and WebSocket +requests. It is also noteworthy that not all proxy servers support HTTPS and +WebSocket requests. The proxy URL does not support username and password +authentication [per Chromium issue](https://bugs.chromium.org/p/chromium/issues/detail?id=615947). + +### --remote-debugging-port=`port` + +Enables remote debugging over HTTP on the specified `port`. + +### --v=`log_level` + +Gives the default maximal active V-logging level; 0 is the default. Normally +positive values are used for V-logging levels. + +This switch only works when `--enable-logging` is also passed. + +See also `--enable-logging`, `--log-level`, and `--vmodule`. + +### --vmodule=`pattern` + +Gives the per-module maximal V-logging levels to override the value given by +`--v`. E.g. `my_module=2,foo*=3` would change the logging level for all code in +source files `my_module.*` and `foo*.*`. + +Any pattern containing a forward or backward slash will be tested against the +whole pathname and not only the module. E.g. `*/foo/bar/*=2` would change the +logging level for all code in the source files under a `foo/bar` directory. + +This switch only works when `--enable-logging` is also passed. + +See also `--enable-logging`, `--log-level`, and `--v`. + +### --force_high_performance_gpu + +Force using discrete GPU when there are multiple GPUs available. + +### --force_low_power_gpu + +Force using integrated GPU when there are multiple GPUs available. + +## Node.js Flags + +Electron supports some of the [CLI flags][node-cli] supported by Node.js. + +**Note:** Passing unsupported command line switches to Electron when it is not running in `ELECTRON_RUN_AS_NODE` will have no effect. + +### --inspect-brk[=[host:]port] + +Activate inspector on host:port and break at start of user script. Default host:port is 127.0.0.1:9229. + +Aliased to `--debug-brk=[host:]port`. + +### --inspect-port=[host:]port + +Set the `host:port` to be used when the inspector is activated. Useful when activating the inspector by sending the SIGUSR1 signal. Default host is `127.0.0.1`. + +Aliased to `--debug-port=[host:]port`. + +### --inspect[=[host:]port] + +Activate inspector on `host:port`. Default is `127.0.0.1:9229`. + +V8 inspector integration allows tools such as Chrome DevTools and IDEs to debug and profile Electron instances. The tools attach to Electron instances via a TCP port and communicate using the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). + +See the [Debugging the Main Process][debugging-main-process] guide for more details. + +Aliased to `--debug[=[host:]port`. + +### --inspect-publish-uid=stderr,http + +Specify ways of the inspector web socket url exposure. + +By default inspector websocket url is available in stderr and under /json/list endpoint on http://host:port/json/list. + +[app]: app.md +[append-switch]: command-line.md#commandlineappendswitchswitch-value +[ready]: app.md#event-ready +[play-silent-audio]: https://github.com/atom/atom/pull/9485/files +[debugging-main-process]: ../tutorial/debugging-main-process.md +[logging]: https://source.chromium.org/chromium/chromium/src/+/master:base/logging.h +[node-cli]: https://nodejs.org/api/cli.html +[play-silent-audio]: https://github.com/atom/atom/pull/9485/files +[ready]: app.md#event-ready +[severities]: https://source.chromium.org/chromium/chromium/src/+/master:base/logging.h?q=logging::LogSeverity&ss=chromium diff --git a/docs/api/command-line.md b/docs/api/command-line.md index 886377e7c9ef6..9d36a03161b36 100644 --- a/docs/api/command-line.md +++ b/docs/api/command-line.md @@ -2,7 +2,8 @@ > Manipulate the command line arguments for your app that Chromium reads -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ The following example shows how to check if the `--disable-gpu` flag is set. @@ -12,15 +13,15 @@ app.commandLine.hasSwitch('disable-gpu') ``` For more information on what kinds of flags and switches you can use, check -out the [Chrome Command Line Switches](./chrome-command-line-switches.md) +out the [Command Line Switches](./command-line-switches.md) document. ### Instance Methods #### `commandLine.appendSwitch(switch[, value])` -* `switch` String - A command-line switch, without the leading `--` -* `value` String (optional) - A value for the given switch +* `switch` string - A command-line switch, without the leading `--` +* `value` string (optional) - A value for the given switch Append a switch (with optional `value`) to Chromium's command line. @@ -29,7 +30,7 @@ control Chromium's behavior. #### `commandLine.appendArgument(value)` -* `value` String - The argument to append to the command line +* `value` string - The argument to append to the command line Append an argument to Chromium's command line. The argument will be quoted correctly. Switches will precede arguments regardless of appending order. @@ -41,14 +42,23 @@ control Chromium's behavior. #### `commandLine.hasSwitch(switch)` -* `switch` String - A command-line switch +* `switch` string - A command-line switch -Returns `Boolean` - Whether the command-line switch is present. +Returns `boolean` - Whether the command-line switch is present. #### `commandLine.getSwitchValue(switch)` -* `switch` String - A command-line switch +* `switch` string - A command-line switch -Returns `String` - The command-line switch value. +Returns `string` - The command-line switch value. **Note:** When the switch is not present or has no value, it returns empty string. + +#### `commandLine.removeSwitch(switch)` + +* `switch` string - A command-line switch + +Removes the specified switch from Chromium's command line. + +**Note:** This will not affect `process.argv`. The intended usage of this function is to +control Chromium's behavior. diff --git a/docs/api/content-tracing.md b/docs/api/content-tracing.md index e9b21ce6b98e1..91e3ca43a2560 100644 --- a/docs/api/content-tracing.md +++ b/docs/api/content-tracing.md @@ -13,10 +13,10 @@ module is emitted. ```javascript const { app, contentTracing } = require('electron') -app.on('ready', () => { +app.whenReady().then(() => { (async () => { await contentTracing.startRecording({ - include_categories: ['*'] + included_categories: ['*'] }) console.log('Tracing started') await new Promise(resolve => setTimeout(resolve, 5000)) @@ -32,12 +32,15 @@ The `contentTracing` module has the following methods: ### `contentTracing.getCategories()` -Returns `Promise<String[]>` - resolves with an array of category groups once all child processes have acknowledged the `getCategories` request +Returns `Promise<string[]>` - resolves with an array of category groups once all child processes have acknowledged the `getCategories` request Get a set of category groups. The category groups can change as new code paths are reached. See also the [list of built-in tracing categories](https://chromium.googlesource.com/chromium/src/+/master/base/trace_event/builtin_categories.h). +> **NOTE:** Electron adds a non-default tracing category called `"electron"`. +> This category can be used to capture Electron-specific tracing events. + ### `contentTracing.startRecording(options)` * `options` ([TraceConfig](structures/trace-config.md) | [TraceCategoriesAndOptions](structures/trace-categories-and-options.md)) @@ -54,9 +57,9 @@ only one trace operation can be in progress at a time. ### `contentTracing.stopRecording([resultFilePath])` -* `resultFilePath` String (optional) +* `resultFilePath` string (optional) -Returns `Promise<String>` - resolves with a path to a file that contains the traced data once all child processes have acknowledged the `stopRecording` request +Returns `Promise<string>` - resolves with a path to a file that contains the traced data once all child processes have acknowledged the `stopRecording` request Stop recording on all processes. @@ -74,7 +77,10 @@ will be returned in the promise. Returns `Promise<Object>` - Resolves with an object containing the `value` and `percentage` of trace buffer maximum usage +* `value` number +* `percentage` number + Get the maximum usage across processes of trace buffer as a percentage of the full state. -[trace viewer]: https://github.com/catapult-project/catapult/blob/master/tracing +[trace viewer]: https://chromium.googlesource.com/catapult/+/HEAD/tracing/README.md diff --git a/docs/api/context-bridge.md b/docs/api/context-bridge.md new file mode 100644 index 0000000000000..c816b43ab64d8 --- /dev/null +++ b/docs/api/context-bridge.md @@ -0,0 +1,132 @@ +# contextBridge + +> Create a safe, bi-directional, synchronous bridge across isolated contexts + +Process: [Renderer](../glossary.md#renderer-process) + +An example of exposing an API to a renderer from an isolated preload script is given below: + +```javascript +// Preload (Isolated World) +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld( + 'electron', + { + doThing: () => ipcRenderer.send('do-a-thing') + } +) +``` + +```javascript +// Renderer (Main World) + +window.electron.doThing() +``` + +## Glossary + +### Main World + +The "Main World" is the JavaScript context that your main renderer code runs in. By default, the +page you load in your renderer executes code in this world. + +### Isolated World + +When `contextIsolation` is enabled in your `webPreferences` (this is the default behavior since Electron 12.0.0), your `preload` scripts run in an +"Isolated World". You can read more about context isolation and what it affects in the +[security](../tutorial/security.md#3-enable-context-isolation-for-remote-content) docs. + +## Methods + +The `contextBridge` module has the following methods: + +### `contextBridge.exposeInMainWorld(apiKey, api)` + +* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. +* `api` any - Your API, more information on what this API can be and how it works is available below. + +## Usage + +### API + +The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or an object +whose keys are strings and values are a `Function`, `string`, `number`, `Array`, `boolean`, or another nested object that meets the same conditions. + +`Function` values are proxied to the other context and all other values are **copied** and **frozen**. Any data / primitives sent in +the API become immutable and updates on either side of the bridge do not result in an update on the other side. + +An example of a complex API is shown below: + +```javascript +const { contextBridge } = require('electron') + +contextBridge.exposeInMainWorld( + 'electron', + { + doThing: () => ipcRenderer.send('do-a-thing'), + myPromises: [Promise.resolve(), Promise.reject(new Error('whoops'))], + anAsyncFunction: async () => 123, + data: { + myFlags: ['a', 'b', 'c'], + bootTime: 1234 + }, + nestedAPI: { + evenDeeper: { + youCanDoThisAsMuchAsYouWant: { + fn: () => ({ + returnData: 123 + }) + } + } + } + } +) +``` + +### API Functions + +`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This +results in some key limitations that we've outlined below. + +#### Parameter / Error / Return Type support + +Because parameters, errors and return values are **copied** when they are sent over the bridge, there are only certain types that can be used. +At a high level, if the type you want to use can be serialized and deserialized into the same object it will work. A table of type support +has been included below for completeness: + +| Type | Complexity | Parameter Support | Return Value Support | Limitations | +| ---- | ---------- | ----------------- | -------------------- | ----------- | +| `string` | Simple | ✅ | ✅ | N/A | +| `number` | Simple | ✅ | ✅ | N/A | +| `boolean` | Simple | ✅ | ✅ | N/A | +| `Object` | Complex | ✅ | ✅ | Keys must be supported using only "Simple" types in this table. Values must be supported in this table. Prototype modifications are dropped. Sending custom classes will copy values but not the prototype. | +| `Array` | Complex | ✅ | ✅ | Same limitations as the `Object` type | +| `Error` | Complex | ✅ | ✅ | Errors that are thrown are also copied, this can result in the message and stack trace of the error changing slightly due to being thrown in a different context, and any custom properties on the Error object [will be lost](https://github.com/electron/electron/issues/25596) | +| `Promise` | Complex | ✅ | ✅ | N/A +| `Function` | Complex | ✅ | ✅ | Prototype modifications are dropped. Sending classes or constructors will not work. | +| [Cloneable Types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) | Simple | ✅ | ✅ | See the linked document on cloneable types | +| `Element` | Complex | ✅ | ✅ | Prototype modifications are dropped. Sending custom elements will not work. | +| `Blob` | Complex | ✅ | ✅ | N/A | +| `Symbol` | N/A | ❌ | ❌ | Symbols cannot be copied across contexts so they are dropped | + +If the type you care about is not in the above table, it is probably not supported. + +### Exposing Node Global Symbols + +The `contextBridge` can be used by the preload script to give your renderer access to Node APIs. +The table of supported types described above also applies to Node APIs that you expose through `contextBridge`. +Please note that many Node APIs grant access to local system resources. +Be very cautious about which globals and APIs you expose to untrusted remote content. + +```javascript +const { contextBridge } = require('electron') +const crypto = require('crypto') +contextBridge.exposeInMainWorld('nodeCrypto', { + sha256sum (data) { + const hash = crypto.createHash('sha256') + hash.update(data) + return hash.digest('hex') + } +}) +``` diff --git a/docs/api/cookies.md b/docs/api/cookies.md index db876f0040253..99b02ded75e83 100644 --- a/docs/api/cookies.md +++ b/docs/api/cookies.md @@ -2,7 +2,8 @@ > Query and modify a session's cookies. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ Instances of the `Cookies` class are accessed by using `cookies` property of a `Session`. @@ -45,9 +46,11 @@ The following events are available on instances of `Cookies`: #### Event: 'changed' +Returns: + * `event` Event * `cookie` [Cookie](structures/cookie.md) - The cookie that was changed. -* `cause` String - The cause of the change with one of the following values: +* `cause` string - The cause of the change with one of the following values: * `explicit` - The cookie was changed directly by a consumer's action. * `overwrite` - The cookie was automatically removed due to an insert operation that overwrote it. @@ -55,7 +58,7 @@ The following events are available on instances of `Cookies`: * `evicted` - The cookie was automatically evicted during garbage collection. * `expired-overwrite` - The cookie was overwritten with an already-expired expiration date. -* `removed` Boolean - `true` if the cookie was removed, `false` otherwise. +* `removed` boolean - `true` if the cookie was removed, `false` otherwise. Emitted when a cookie is changed because it was added, edited, removed, or expired. @@ -67,14 +70,14 @@ The following methods are available on instances of `Cookies`: #### `cookies.get(filter)` * `filter` Object - * `url` String (optional) - Retrieves cookies which are associated with + * `url` string (optional) - Retrieves cookies which are associated with `url`. Empty implies retrieving cookies of all URLs. - * `name` String (optional) - Filters cookies by name. - * `domain` String (optional) - Retrieves cookies whose domains match or are + * `name` string (optional) - Filters cookies by name. + * `domain` string (optional) - Retrieves cookies whose domains match or are subdomains of `domains`. - * `path` String (optional) - Retrieves cookies whose path matches `path`. - * `secure` Boolean (optional) - Filters cookies by their Secure property. - * `session` Boolean (optional) - Filters out session or persistent cookies. + * `path` string (optional) - Retrieves cookies whose path matches `path`. + * `secure` boolean (optional) - Filters cookies by their Secure property. + * `session` boolean (optional) - Filters out session or persistent cookies. Returns `Promise<Cookie[]>` - A promise which resolves an array of cookie objects. @@ -84,18 +87,19 @@ the response. #### `cookies.set(details)` * `details` Object - * `url` String - The URL to associate the cookie with. The promise will be rejected if the URL is invalid. - * `name` String (optional) - The name of the cookie. Empty by default if omitted. - * `value` String (optional) - The value of the cookie. Empty by default if omitted. - * `domain` String (optional) - The domain of the cookie; this will be normalized with a preceding dot so that it's also valid for subdomains. Empty by default if omitted. - * `path` String (optional) - The path of the cookie. Empty by default if omitted. - * `secure` Boolean (optional) - Whether the cookie should be marked as Secure. Defaults to - false. - * `httpOnly` Boolean (optional) - Whether the cookie should be marked as HTTP only. + * `url` string - The URL to associate the cookie with. The promise will be rejected if the URL is invalid. + * `name` string (optional) - The name of the cookie. Empty by default if omitted. + * `value` string (optional) - The value of the cookie. Empty by default if omitted. + * `domain` string (optional) - The domain of the cookie; this will be normalized with a preceding dot so that it's also valid for subdomains. Empty by default if omitted. + * `path` string (optional) - The path of the cookie. Empty by default if omitted. + * `secure` boolean (optional) - Whether the cookie should be marked as Secure. Defaults to + false unless [Same Site=None](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure) attribute is used. + * `httpOnly` boolean (optional) - Whether the cookie should be marked as HTTP only. Defaults to false. * `expirationDate` Double (optional) - The expiration date of the cookie as the number of seconds since the UNIX epoch. If omitted then the cookie becomes a session cookie and will not be retained between sessions. + * `sameSite` string (optional) - The [Same Site](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_cookies) policy to apply to this cookie. Can be `unspecified`, `no_restriction`, `lax` or `strict`. Default is `lax`. Returns `Promise<void>` - A promise which resolves when the cookie has been set @@ -103,8 +107,8 @@ Sets a cookie with `details`. #### `cookies.remove(url, name)` -* `url` String - The URL associated with the cookie. -* `name` String - The name of cookie to remove. +* `url` string - The URL associated with the cookie. +* `name` string - The name of cookie to remove. Returns `Promise<void>` - A promise which resolves when the cookie has been removed diff --git a/docs/api/crash-reporter.md b/docs/api/crash-reporter.md index 6ba7a21ea4ba1..9ef7ecc313281 100644 --- a/docs/api/crash-reporter.md +++ b/docs/api/crash-reporter.md @@ -4,18 +4,13 @@ Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process) -The following is an example of automatically submitting a crash report to a -remote server: +The following is an example of setting up Electron to automatically submit +crash reports to a remote server: ```javascript const { crashReporter } = require('electron') -crashReporter.start({ - productName: 'YourName', - companyName: 'YourCompany', - submitURL: 'https://your-domain.com/url-to-submit', - uploadToServer: true -}) +crashReporter.start({ submitURL: 'https://your-domain.com/url-to-submit' }) ``` For setting up a server to accept and process crash reports, you can use @@ -24,17 +19,23 @@ following projects: * [socorro](https://github.com/mozilla/socorro) * [mini-breakpad-server](https://github.com/electron/mini-breakpad-server) +> **Note:** Electron uses Crashpad, not Breakpad, to collect and upload +> crashes, but for the time being, the [upload protocol is the same](https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/doc/overview_design.md#Upload-to-collection-server). + Or use a 3rd party hosted solution: * [Backtrace](https://backtrace.io/electron/) * [Sentry](https://docs.sentry.io/clients/electron) * [BugSplat](https://www.bugsplat.com/docs/platforms/electron) +* [Bugsnag](https://docs.bugsnag.com/platforms/electron/) + +Crash reports are stored temporarily before being uploaded in a directory +underneath the app's user data directory, called 'Crashpad'. You can override +this directory by calling `app.setPath('crashDumps', '/path/to/crashes')` +before starting the crash reporter. -Crash reports are saved locally in an application-specific temp directory folder. -For a `productName` of `YourName`, crash reports will be stored in a folder -named `YourName Crashes` inside the temp directory. You can customize this temp -directory location for your app by calling the `app.setPath('temp', '/my/custom/temp')` -API before starting the crash reporter. +Electron uses [crashpad](https://chromium.googlesource.com/crashpad/crashpad/+/refs/heads/main/README.md) +to monitor and report crashes. ## Methods @@ -43,40 +44,68 @@ The `crashReporter` module has the following methods: ### `crashReporter.start(options)` * `options` Object - * `companyName` String - * `submitURL` String - URL that crash reports will be sent to as POST. - * `productName` String (optional) - Defaults to `app.name`. - * `uploadToServer` Boolean (optional) - Whether crash reports should be sent to the server. Default is `true`. - * `ignoreSystemCrashHandler` Boolean (optional) - Default is `false`. - * `extra` Object (optional) - An object you can define that will be sent along with the - report. Only string properties are sent correctly. Nested objects are not - supported. When using Windows, the property names and values must be fewer than 64 characters. - * `crashesDirectory` String (optional) - Directory to store the crash reports temporarily (only used when the crash reporter is started via `process.crashReporter.start`). - -You are required to call this method before using any other `crashReporter` APIs -and in each process (main/renderer) from which you want to collect crash reports. -You can pass different options to `crashReporter.start` when calling from different processes. - -**Note** Child processes created via the `child_process` module will not have access to the Electron modules. -Therefore, to collect crash reports from them, use `process.crashReporter.start` instead. Pass the same options as above -along with an additional one called `crashesDirectory` that should point to a directory to store the crash -reports temporarily. You can test this out by calling `process.crash()` to crash the child process. - -**Note:** If you need send additional/updated `extra` parameters after your -first call `start` you can call `addExtraParameter` on macOS or call `start` -again with the new/updated `extra` parameters on Linux and Windows. - -**Note:** On macOS and windows, Electron uses a new `crashpad` client for crash collection and reporting. -If you want to enable crash reporting, initializing `crashpad` from the main process using `crashReporter.start` is required -regardless of which process you want to collect crashes from. Once initialized this way, the crashpad handler collects -crashes from all processes. You still have to call `crashReporter.start` from the renderer or child process, otherwise crashes from -them will get reported without `companyName`, `productName` or any of the `extra` information. + * `submitURL` string (optional) - URL that crash reports will be sent to as + POST. Required unless `uploadToServer` is `false`. + * `productName` string (optional) - Defaults to `app.name`. + * `companyName` string (optional) _Deprecated_ - Deprecated alias for + `{ globalExtra: { _companyName: ... } }`. + * `uploadToServer` boolean (optional) - Whether crash reports should be sent + to the server. If false, crash reports will be collected and stored in the + crashes directory, but not uploaded. Default is `true`. + * `ignoreSystemCrashHandler` boolean (optional) - If true, crashes generated + in the main process will not be forwarded to the system crash handler. + Default is `false`. + * `rateLimit` boolean (optional) _macOS_ _Windows_ - If true, limit the + number of crashes uploaded to 1/hour. Default is `false`. + * `compress` boolean (optional) - If true, crash reports will be compressed + and uploaded with `Content-Encoding: gzip`. Default is `true`. + * `extra` Record<string, string> (optional) - Extra string key/value + annotations that will be sent along with crash reports that are generated + in the main process. Only string values are supported. Crashes generated in + child processes will not contain these extra + parameters to crash reports generated from child processes, call + [`addExtraParameter`](#crashreporteraddextraparameterkey-value) from the + child process. + * `globalExtra` Record<string, string> (optional) - Extra string key/value + annotations that will be sent along with any crash reports generated in any + process. These annotations cannot be changed once the crash reporter has + been started. If a key is present in both the global extra parameters and + the process-specific extra parameters, then the global one will take + precedence. By default, `productName` and the app version are included, as + well as the Electron version. + +This method must be called before using any other `crashReporter` APIs. Once +initialized this way, the crashpad handler collects crashes from all +subsequently created processes. The crash reporter cannot be disabled once +started. + +This method should be called as early as possible in app startup, preferably +before `app.on('ready')`. If the crash reporter is not initialized at the time +a renderer process is created, then that renderer process will not be monitored +by the crash reporter. + +**Note:** You can test out the crash reporter by generating a crash using +`process.crash()`. + +**Note:** If you need to send additional/updated `extra` parameters after your +first call `start` you can call `addExtraParameter`. + +**Note:** Parameters passed in `extra`, `globalExtra` or set with +`addExtraParameter` have limits on the length of the keys and values. Key names +must be at most 39 bytes long, and values must be no longer than 127 bytes. +Keys with names longer than the maximum will be silently ignored. Key values +longer than the maximum length will be truncated. + +**Note:** This method is only available in the main process. ### `crashReporter.getLastCrashReport()` -Returns [`CrashReport`](structures/crash-report.md): +Returns [`CrashReport`](structures/crash-report.md) - The date and ID of the +last crash report. Only crash reports that have been uploaded will be returned; +even if a crash report is present on disk it will not be returned until it is +uploaded. In the case that there are no uploaded reports, `null` is returned. -Returns the date and ID of the last crash report. Only crash reports that have been uploaded will be returned; even if a crash report is present on disk it will not be returned until it is uploaded. In the case that there are no uploaded reports, `null` is returned. +**Note:** This method is only available in the main process. ### `crashReporter.getUploadedReports()` @@ -85,54 +114,96 @@ Returns [`CrashReport[]`](structures/crash-report.md): Returns all uploaded crash reports. Each report contains the date and uploaded ID. +**Note:** This method is only available in the main process. + ### `crashReporter.getUploadToServer()` -Returns `Boolean` - Whether reports should be submitted to the server. Set through +Returns `boolean` - Whether reports should be submitted to the server. Set through the `start` method or `setUploadToServer`. -**Note:** This API can only be called from the main process. +**Note:** This method is only available in the main process. ### `crashReporter.setUploadToServer(uploadToServer)` -* `uploadToServer` Boolean _macOS_ - Whether reports should be submitted to the server. +* `uploadToServer` boolean - Whether reports should be submitted to the server. This would normally be controlled by user preferences. This has no effect if called before `start` is called. -**Note:** This API can only be called from the main process. +**Note:** This method is only available in the main process. + +### `crashReporter.addExtraParameter(key, value)` -### `crashReporter.addExtraParameter(key, value)` _macOS_ _Windows_ +* `key` string - Parameter key, must be no longer than 39 bytes. +* `value` string - Parameter value, must be no longer than 127 bytes. -* `key` String - Parameter key, must be less than 64 characters long. -* `value` String - Parameter value, must be less than 64 characters long. +Set an extra parameter to be sent with the crash report. The values specified +here will be sent in addition to any values set via the `extra` option when +`start` was called. -Set an extra parameter to be sent with the crash report. The values -specified here will be sent in addition to any values set via the `extra` option when `start` was called. This API is only available on macOS and windows, if you need to add/update extra parameters on Linux after your first call to `start` you can call `start` again with the updated `extra` options. +Parameters added in this fashion (or via the `extra` parameter to +`crashReporter.start`) are specific to the calling process. Adding extra +parameters in the main process will not cause those parameters to be sent along +with crashes from renderer or other child processes. Similarly, adding extra +parameters in a renderer process will not result in those parameters being sent +with crashes that occur in other renderer processes or in the main process. -### `crashReporter.removeExtraParameter(key)` _macOS_ _Windows_ +**Note:** Parameters have limits on the length of the keys and values. Key +names must be no longer than 39 bytes, and values must be no longer than 20320 +bytes. Keys with names longer than the maximum will be silently ignored. Key +values longer than the maximum length will be truncated. -* `key` String - Parameter key, must be less than 64 characters long. +### `crashReporter.removeExtraParameter(key)` -Remove a extra parameter from the current set of parameters so that it will not be sent with the crash report. +* `key` string - Parameter key, must be no longer than 39 bytes. + +Remove an extra parameter from the current set of parameters. Future crashes +will not include this parameter. ### `crashReporter.getParameters()` -See all of the current parameters being passed to the crash reporter. +Returns `Record<string, string>` - The current 'extra' parameters of the crash reporter. + +## In Node child processes + +Since `require('electron')` is not available in Node child processes, the +following APIs are available on the `process` object in Node child processes. + +#### `process.crashReporter.start(options)` + +See [`crashReporter.start()`](#crashreporterstartoptions). + +Note that if the crash reporter is started in the main process, it will +automatically monitor child processes, so it should not be started in the child +process. Only use this method if the main process does not initialize the crash +reporter. + +#### `process.crashReporter.getParameters()` + +See [`crashReporter.getParameters()`](#crashreportergetparameters). + +#### `process.crashReporter.addExtraParameter(key, value)` + +See [`crashReporter.addExtraParameter(key, value)`](#crashreporteraddextraparameterkey-value). + +#### `process.crashReporter.removeExtraParameter(key)` + +See [`crashReporter.removeExtraParameter(key)`](#crashreporterremoveextraparameterkey). ## Crash Report Payload The crash reporter will send the following data to the `submitURL` as a `multipart/form-data` `POST`: -* `ver` String - The version of Electron. -* `platform` String - e.g. 'win32'. -* `process_type` String - e.g. 'renderer'. -* `guid` String - e.g. '5e1286fc-da97-479e-918b-6bfb0c3d1c72'. -* `_version` String - The version in `package.json`. -* `_productName` String - The product name in the `crashReporter` `options` +* `ver` string - The version of Electron. +* `platform` string - e.g. 'win32'. +* `process_type` string - e.g. 'renderer'. +* `guid` string - e.g. '5e1286fc-da97-479e-918b-6bfb0c3d1c72'. +* `_version` string - The version in `package.json`. +* `_productName` string - The product name in the `crashReporter` `options` object. -* `prod` String - Name of the underlying product. In this case Electron. -* `_companyName` String - The company name in the `crashReporter` `options` +* `prod` string - Name of the underlying product. In this case Electron. +* `_companyName` string - The company name in the `crashReporter` `options` object. * `upload_file_minidump` File - The crash report in the format of `minidump`. * All level one properties of the `extra` object in the `crashReporter` diff --git a/docs/api/debugger.md b/docs/api/debugger.md index 1a5dc8812c960..d8cf9eddc03dd 100644 --- a/docs/api/debugger.md +++ b/docs/api/debugger.md @@ -2,14 +2,15 @@ > An alternate transport for Chrome's remote debugging protocol. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ Chrome Developer Tools has a [special binding][rdp] available at JavaScript runtime that allows interacting with pages and instrumenting them. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() try { win.webContents.debugger.attach('1.1') @@ -39,7 +40,7 @@ win.webContents.debugger.sendCommand('Network.enable') Returns: * `event` Event -* `reason` String - Reason for detaching debugger. +* `reason` string - Reason for detaching debugger. Emitted when the debugging session is terminated. This happens either when `webContents` is closed or devtools is invoked for the attached `webContents`. @@ -49,9 +50,11 @@ Emitted when the debugging session is terminated. This happens either when Returns: * `event` Event -* `method` String - Method name. -* `params` Object - Event parameters defined by the 'parameters' +* `method` string - Method name. +* `params` any - Event parameters defined by the 'parameters' attribute in the remote debugging protocol. +* `sessionId` string - Unique identifier of attached debugging session, + will match the value sent from `debugger.sendCommand`. Emitted whenever the debugging target issues an instrumentation event. @@ -62,23 +65,28 @@ Emitted whenever the debugging target issues an instrumentation event. #### `debugger.attach([protocolVersion])` -* `protocolVersion` String (optional) - Requested debugging protocol version. +* `protocolVersion` string (optional) - Requested debugging protocol version. Attaches the debugger to the `webContents`. #### `debugger.isAttached()` -Returns `Boolean` - Whether a debugger is attached to the `webContents`. +Returns `boolean` - Whether a debugger is attached to the `webContents`. #### `debugger.detach()` Detaches the debugger from the `webContents`. -#### `debugger.sendCommand(method[, commandParams])` +#### `debugger.sendCommand(method[, commandParams, sessionId])` -* `method` String - Method name, should be one of the methods defined by the +* `method` string - Method name, should be one of the methods defined by the [remote debugging protocol][rdp]. -* `commandParams` Object (optional) - JSON object with request parameters. +* `commandParams` any (optional) - JSON object with request parameters. +* `sessionId` string (optional) - send command to the target with associated + debugging session id. The initial value can be obtained by sending + [Target.attachToTarget][attachToTarget] message. + +[attachToTarget]: https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-attachToTarget Returns `Promise<any>` - A promise that resolves with the response defined by the 'returns' attribute of the command description in the remote debugging protocol diff --git a/docs/api/desktop-capturer.md b/docs/api/desktop-capturer.md index f8e12d04d0788..fa4874e3c86f7 100644 --- a/docs/api/desktop-capturer.md +++ b/docs/api/desktop-capturer.md @@ -3,40 +3,49 @@ > Access information about media sources that can be used to capture audio and > video from the desktop using the [`navigator.mediaDevices.getUserMedia`] API. -Process: [Renderer](../glossary.md#renderer-process) +Process: [Main](../glossary.md#main-process) The following example shows how to capture video from a desktop window whose title is `Electron`: ```javascript -// In the renderer process. +// In the main process. const { desktopCapturer } = require('electron') desktopCapturer.getSources({ types: ['window', 'screen'] }).then(async sources => { for (const source of sources) { if (source.name === 'Electron') { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: false, - video: { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: source.id, - minWidth: 1280, - maxWidth: 1280, - minHeight: 720, - maxHeight: 720 - } - } - }) - handleStream(stream) - } catch (e) { - handleError(e) - } + mainWindow.webContents.send('SET_SOURCE', source.id) return } } }) +``` + +```javascript +// In the preload script. +const { ipcRenderer } = require('electron') + +ipcRenderer.on('SET_SOURCE', async (event, sourceId) => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sourceId, + minWidth: 1280, + maxWidth: 1280, + minHeight: 720, + maxHeight: 720 + } + } + }) + handleStream(stream) + } catch (e) { + handleError(e) + } +}) function handleStream (stream) { const video = document.querySelector('video') @@ -79,19 +88,23 @@ The `desktopCapturer` module has the following methods: ### `desktopCapturer.getSources(options)` * `options` Object - * `types` String[] - An array of Strings that lists the types of desktop sources + * `types` string[] - An array of strings that lists the types of desktop sources to be captured, available types are `screen` and `window`. * `thumbnailSize` [Size](structures/size.md) (optional) - The size that the media source thumbnail should be scaled to. Default is `150` x `150`. Set width or height to 0 when you do not need the thumbnails. This will save the processing time required for capturing the content of each window and screen. - * `fetchWindowIcons` Boolean (optional) - Set to true to enable fetching window icons. The default + * `fetchWindowIcons` boolean (optional) - Set to true to enable fetching window icons. The default value is false. When false the appIcon property of the sources return null. Same if a source has the type screen. Returns `Promise<DesktopCapturerSource[]>` - Resolves with an array of [`DesktopCapturerSource`](structures/desktop-capturer-source.md) objects, each `DesktopCapturerSource` represents a screen or an individual window that can be captured. +**Note** Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher, +which can detected by [`systemPreferences.getMediaAccessStatus`]. + [`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia +[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-windows-macos ## Caveats diff --git a/docs/api/dialog.md b/docs/api/dialog.md index 07404cf869023..bc536e15baee1 100644 --- a/docs/api/dialog.md +++ b/docs/api/dialog.md @@ -11,14 +11,6 @@ const { dialog } = require('electron') console.log(dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] })) ``` -The Dialog is opened from Electron's main thread. If you want to use the dialog -object from a renderer process, remember to access it using the remote: - -```javascript -const { dialog } = require('electron').remote -console.log(dialog) -``` - ## Methods The `dialog` module has the following methods: @@ -27,12 +19,12 @@ The `dialog` module has the following methods: * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object - * `title` String (optional) - * `defaultPath` String (optional) - * `buttonLabel` String (optional) - Custom label for the confirmation button, when + * `title` string (optional) + * `defaultPath` string (optional) + * `buttonLabel` string (optional) - Custom label for the confirmation button, when left empty the default label will be used. * `filters` [FileFilter[]](structures/file-filter.md) (optional) - * `properties` String[] (optional) - Contains which features the dialog should + * `properties` string[] (optional) - Contains which features the dialog should use. The following values are supported: * `openFile` - Allow files to be selected. * `openDirectory` - Allow directories to be selected. @@ -48,9 +40,12 @@ The `dialog` module has the following methods: their target path. * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders, as a directory instead of a file. - * `message` String (optional) _macOS_ - Message to display above input + * `dontAddToRecent` _Windows_ - Do not add the item being opened to the recent documents list. + * `message` string (optional) _macOS_ - Message to display above input boxes. - * `securityScopedBookmarks` Boolean (optional) _macOS_ _mas_ - Create [security scoped bookmarks](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. + * `securityScopedBookmarks` boolean (optional) _macOS_ _mas_ - Create [security scoped bookmarks](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. + +Returns `string[] | undefined`, the file paths chosen by the user; if the dialog is cancelled it returns `undefined`. The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal. @@ -87,12 +82,12 @@ dialog.showOpenDialogSync(mainWindow, { * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object - * `title` String (optional) - * `defaultPath` String (optional) - * `buttonLabel` String (optional) - Custom label for the confirmation button, when + * `title` string (optional) + * `defaultPath` string (optional) + * `buttonLabel` string (optional) - Custom label for the confirmation button, when left empty the default label will be used. * `filters` [FileFilter[]](structures/file-filter.md) (optional) - * `properties` String[] (optional) - Contains which features the dialog should + * `properties` string[] (optional) - Contains which features the dialog should use. The following values are supported: * `openFile` - Allow files to be selected. * `openDirectory` - Allow directories to be selected. @@ -108,15 +103,16 @@ dialog.showOpenDialogSync(mainWindow, { their target path. * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders, as a directory instead of a file. - * `message` String (optional) _macOS_ - Message to display above input + * `dontAddToRecent` _Windows_ - Do not add the item being opened to the recent documents list. + * `message` string (optional) _macOS_ - Message to display above input boxes. - * `securityScopedBookmarks` Boolean (optional) _macOS_ _mas_ - Create [security scoped bookmarks](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. + * `securityScopedBookmarks` boolean (optional) _macOS_ _mas_ - Create [security scoped bookmarks](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. Returns `Promise<Object>` - Resolve with an object containing the following: -* `canceled` Boolean - whether or not the dialog was canceled. -* `filePaths` String[] - An array of file paths chosen by the user. If the dialog is cancelled this will be an empty array. -* `bookmarks` String[] (optional) _macOS_ _mas_ - An array matching the `filePaths` array of base64 encoded strings which contains security scoped bookmark data. `securityScopedBookmarks` must be enabled for this to be populated. +* `canceled` boolean - whether or not the dialog was canceled. +* `filePaths` string[] - An array of file paths chosen by the user. If the dialog is cancelled this will be an empty array. +* `bookmarks` string[] (optional) _macOS_ _mas_ - An array matching the `filePaths` array of base64 encoded strings which contains security scoped bookmark data. `securityScopedBookmarks` must be enabled for this to be populated. (For return values, see [table here](#bookmarks-array).) The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal. @@ -158,20 +154,27 @@ dialog.showOpenDialog(mainWindow, { * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object - * `title` String (optional) - * `defaultPath` String (optional) - Absolute directory path, absolute file + * `title` string (optional) - The dialog title. Cannot be displayed on some _Linux_ desktop environments. + * `defaultPath` string (optional) - Absolute directory path, absolute file path, or file name to use by default. - * `buttonLabel` String (optional) - Custom label for the confirmation button, when + * `buttonLabel` string (optional) - Custom label for the confirmation button, when left empty the default label will be used. * `filters` [FileFilter[]](structures/file-filter.md) (optional) - * `message` String (optional) _macOS_ - Message to display above text fields. - * `nameFieldLabel` String (optional) _macOS_ - Custom label for the text + * `message` string (optional) _macOS_ - Message to display above text fields. + * `nameFieldLabel` string (optional) _macOS_ - Custom label for the text displayed in front of the filename text field. - * `showsTagField` Boolean (optional) _macOS_ - Show the tags input box, + * `showsTagField` boolean (optional) _macOS_ - Show the tags input box, defaults to `true`. - * `securityScopedBookmarks` Boolean (optional) _macOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path. + * `properties` string[] (optional) + * `showHiddenFiles` - Show hidden files in dialog. + * `createDirectory` _macOS_ - Allow creating new directories from dialog. + * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders, + as a directory instead of a file. + * `showOverwriteConfirmation` _Linux_ - Sets whether the user will be presented a confirmation dialog if the user types a file name that already exists. + * `dontAddToRecent` _Windows_ - Do not add the item being saved to the recent documents list. + * `securityScopedBookmarks` boolean (optional) _macOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path. -Returns `String | undefined`, the path of the file chosen by the user; if the dialog is cancelled it returns `undefined`. +Returns `string | undefined`, the path of the file chosen by the user; if the dialog is cancelled it returns `undefined`. The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal. @@ -182,23 +185,30 @@ The `filters` specifies an array of file types that can be displayed, see * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object - * `title` String (optional) - * `defaultPath` String (optional) - Absolute directory path, absolute file + * `title` string (optional) - The dialog title. Cannot be displayed on some _Linux_ desktop environments. + * `defaultPath` string (optional) - Absolute directory path, absolute file path, or file name to use by default. - * `buttonLabel` String (optional) - Custom label for the confirmation button, when + * `buttonLabel` string (optional) - Custom label for the confirmation button, when left empty the default label will be used. * `filters` [FileFilter[]](structures/file-filter.md) (optional) - * `message` String (optional) _macOS_ - Message to display above text fields. - * `nameFieldLabel` String (optional) _macOS_ - Custom label for the text + * `message` string (optional) _macOS_ - Message to display above text fields. + * `nameFieldLabel` string (optional) _macOS_ - Custom label for the text displayed in front of the filename text field. - * `showsTagField` Boolean (optional) _macOS_ - Show the tags input box, - defaults to `true`. - * `securityScopedBookmarks` Boolean (optional) _macOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path. + * `showsTagField` boolean (optional) _macOS_ - Show the tags input box, defaults to `true`. + * `properties` string[] (optional) + * `showHiddenFiles` - Show hidden files in dialog. + * `createDirectory` _macOS_ - Allow creating new directories from dialog. + * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders, + as a directory instead of a file. + * `showOverwriteConfirmation` _Linux_ - Sets whether the user will be presented a confirmation dialog if the user types a file name that already exists. + * `dontAddToRecent` _Windows_ - Do not add the item being saved to the recent documents list. + * `securityScopedBookmarks` boolean (optional) _macOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path. Returns `Promise<Object>` - Resolve with an object containing the following: - * `canceled` Boolean - whether or not the dialog was canceled. - * `filePath` String (optional) - If the dialog is canceled, this will be `undefined`. - * `bookmark` String (optional) _macOS_ _mas_ - Base64 encoded string which contains the security scoped bookmark data for the saved file. `securityScopedBookmarks` must be enabled for this to be present. + +* `canceled` boolean - whether or not the dialog was canceled. +* `filePath` string (optional) - If the dialog is canceled, this will be `undefined`. +* `bookmark` string (optional) _macOS_ _mas_ - Base64 encoded string which contains the security scoped bookmark data for the saved file. `securityScopedBookmarks` must be enabled for this to be present. (For return values, see [table here](#bookmarks-array).) The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal. @@ -212,32 +222,29 @@ expanding and collapsing the dialog. * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object - * `type` String (optional) - Can be `"none"`, `"info"`, `"error"`, `"question"` or + * `message` string - Content of the message box. + * `type` string (optional) - Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows, `"question"` displays the same icon as `"info"`, unless you set an icon using the `"icon"` option. On macOS, both `"warning"` and `"error"` display the same warning icon. - * `buttons` String[] (optional) - Array of texts for buttons. On Windows, an empty array + * `buttons` string[] (optional) - Array of texts for buttons. On Windows, an empty array will result in one button labeled "OK". * `defaultId` Integer (optional) - Index of the button in the buttons array which will be selected by default when the message box opens. - * `title` String (optional) - Title of the message box, some platforms will not show it. - * `message` String - Content of the message box. - * `detail` String (optional) - Extra information of the message. - * `checkboxLabel` String (optional) - If provided, the message box will - include a checkbox with the given label. - * `checkboxChecked` Boolean (optional) - Initial checked state of the - checkbox. `false` by default. - * `icon` [NativeImage](native-image.md) (optional) + * `title` string (optional) - Title of the message box, some platforms will not show it. + * `detail` string (optional) - Extra information of the message. + * `icon` ([NativeImage](native-image.md) | string) (optional) + * `textWidth` Integer (optional) _macOS_ - Custom width of the text in the message box. * `cancelId` Integer (optional) - The index of the button to be used to cancel the dialog, via the `Esc` key. By default this is assigned to the first button with "cancel" or "no" as the label. If no such labeled buttons exist and this option is not set, `0` will be used as the return value. - * `noLink` Boolean (optional) - On Windows Electron will try to figure out which one of + * `noLink` boolean (optional) - On Windows Electron will try to figure out which one of the `buttons` are common buttons (like "Cancel" or "Yes"), and show the others as command links in the dialog. This can make the dialog appear in the style of modern Windows apps. If you don't like this behavior, you can set `noLink` to `true`. - * `normalizeAccessKeys` Boolean (optional) - Normalize the keyboard access keys + * `normalizeAccessKeys` boolean (optional) - Normalize the keyboard access keys across platforms. Default is `false`. Enabling this assumes `&` is used in the button labels for the placement of the keyboard shortcut access key and labels will be converted so they work correctly on each platform, `&` @@ -252,37 +259,44 @@ Shows a message box, it will block the process until the message box is closed. It returns the index of the clicked button. The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal. +If `browserWindow` is not shown dialog will not be attached to it. In such case it will be displayed as an independent window. ### `dialog.showMessageBox([browserWindow, ]options)` * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object - * `type` String (optional) - Can be `"none"`, `"info"`, `"error"`, `"question"` or + * `message` string - Content of the message box. + * `type` string (optional) - Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows, `"question"` displays the same icon as `"info"`, unless you set an icon using the `"icon"` option. On macOS, both `"warning"` and `"error"` display the same warning icon. - * `buttons` String[] (optional) - Array of texts for buttons. On Windows, an empty array + * `buttons` string[] (optional) - Array of texts for buttons. On Windows, an empty array will result in one button labeled "OK". * `defaultId` Integer (optional) - Index of the button in the buttons array which will be selected by default when the message box opens. - * `title` String (optional) - Title of the message box, some platforms will not show it. - * `message` String - Content of the message box. - * `detail` String (optional) - Extra information of the message. - * `checkboxLabel` String (optional) - If provided, the message box will + * `signal` AbortSignal (optional) - Pass an instance of [AbortSignal][] to + optionally close the message box, the message box will behave as if it was + cancelled by the user. On macOS, `signal` does not work with message boxes + that do not have a parent window, since those message boxes run + synchronously due to platform limitations. + * `title` string (optional) - Title of the message box, some platforms will not show it. + * `detail` string (optional) - Extra information of the message. + * `checkboxLabel` string (optional) - If provided, the message box will include a checkbox with the given label. - * `checkboxChecked` Boolean (optional) - Initial checked state of the + * `checkboxChecked` boolean (optional) - Initial checked state of the checkbox. `false` by default. - * `icon` [NativeImage](native-image.md) (optional) + * `icon` ([NativeImage](native-image.md) | string) (optional) + * `textWidth` Integer (optional) _macOS_ - Custom width of the text in the message box. * `cancelId` Integer (optional) - The index of the button to be used to cancel the dialog, via the `Esc` key. By default this is assigned to the first button with "cancel" or "no" as the label. If no such labeled buttons exist and this option is not set, `0` will be used as the return value. - * `noLink` Boolean (optional) - On Windows Electron will try to figure out which one of + * `noLink` boolean (optional) - On Windows Electron will try to figure out which one of the `buttons` are common buttons (like "Cancel" or "Yes"), and show the others as command links in the dialog. This can make the dialog appear in the style of modern Windows apps. If you don't like this behavior, you can set `noLink` to `true`. - * `normalizeAccessKeys` Boolean (optional) - Normalize the keyboard access keys + * `normalizeAccessKeys` boolean (optional) - Normalize the keyboard access keys across platforms. Default is `false`. Enabling this assumes `&` is used in the button labels for the placement of the keyboard shortcut access key and labels will be converted so they work correctly on each platform, `&` @@ -292,18 +306,19 @@ The `browserWindow` argument allows the dialog to attach itself to a parent wind via `Alt-W` on Windows and Linux. Returns `Promise<Object>` - resolves with a promise containing the following properties: - * `response` Number - The index of the clicked button. - * `checkboxChecked` Boolean - The checked state of the checkbox if + +* `response` number - The index of the clicked button. +* `checkboxChecked` boolean - The checked state of the checkbox if `checkboxLabel` was set. Otherwise `false`. -Shows a message box, it will block the process until the message box is closed. +Shows a message box. The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal. ### `dialog.showErrorBox(title, content)` -* `title` String - The title to display in the error box. -* `content` String - The text content to display in the error box. +* `title` string - The title to display in the error box. +* `content` string - The text content to display in the error box. Displays a modal dialog that shows an error message. @@ -317,7 +332,7 @@ and no GUI dialog will appear. * `browserWindow` [BrowserWindow](browser-window.md) (optional) * `options` Object * `certificate` [Certificate](structures/certificate.md) - The certificate to trust/import. - * `message` String - The message to display to the user. + * `message` string - The message to display to the user. Returns `Promise<void>` - resolves when the certificate trust dialog is shown. @@ -333,6 +348,17 @@ On Windows the options are more limited, due to the Win32 APIs used: * The `browserWindow` argument is ignored since it is not possible to make this confirmation dialog modal. +## Bookmarks array + +`showOpenDialog`, `showOpenDialogSync`, `showSaveDialog`, and `showSaveDialogSync` will return a `bookmarks` array. + +| Build Type | securityScopedBookmarks boolean | Return Type | Return Value | +|------------|---------------------------------|:-----------:|--------------------------------| +| macOS mas | True | Success | `['LONGBOOKMARKSTRING']` | +| macOS mas | True | Error | `['']` (array of empty string) | +| macOS mas | False | NA | `[]` (empty array) | +| non mas | any | NA | `[]` (empty array) | + ## Sheets On macOS, dialogs are presented as sheets attached to a window if you provide @@ -341,3 +367,5 @@ window is provided. You can call `BrowserWindow.getCurrentWindow().setSheetOffset(offset)` to change the offset from the window frame where sheets are attached. + +[AbortSignal]: https://nodejs.org/api/globals.html#globals_class_abortsignal diff --git a/docs/api/dock.md b/docs/api/dock.md index df2c53e5dab63..3e798e793192e 100644 --- a/docs/api/dock.md +++ b/docs/api/dock.md @@ -2,7 +2,8 @@ > Control your app in the macOS dock -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ The following example shows how to bounce your icon on the dock. @@ -15,9 +16,11 @@ app.dock.bounce() #### `dock.bounce([type])` _macOS_ -* `type` String (optional) - Can be `critical` or `informational`. The default is +* `type` string (optional) - Can be `critical` or `informational`. The default is `informational` +Returns `Integer` - an ID representing the request. + When `critical` is passed, the dock icon will bounce until either the application becomes active or the request is canceled. @@ -25,7 +28,7 @@ When `informational` is passed, the dock icon will bounce for one second. However, the request remains active until either the application becomes active or the request is canceled. -Returns `Integer` an ID representing the request. +**Note:** This method can only be used while the app is not focused; when the app is focused it will return -1. #### `dock.cancelBounce(id)` _macOS_ @@ -35,19 +38,19 @@ Cancel the bounce of `id`. #### `dock.downloadFinished(filePath)` _macOS_ -* `filePath` String +* `filePath` string Bounces the Downloads stack if the filePath is inside the Downloads folder. #### `dock.setBadge(text)` _macOS_ -* `text` String +* `text` string Sets the string to be displayed in the dock’s badging area. #### `dock.getBadge()` _macOS_ -Returns `String` - The badge string of the dock. +Returns `string` - The badge string of the dock. #### `dock.hide()` _macOS_ @@ -59,7 +62,7 @@ Returns `Promise<void>` - Resolves when the dock icon is shown. #### `dock.isVisible()` _macOS_ -Returns `Boolean` - Whether the dock icon is visible. +Returns `boolean` - Whether the dock icon is visible. #### `dock.setMenu(menu)` _macOS_ @@ -73,6 +76,6 @@ Returns `Menu | null` - The application's [dock menu][dock-menu]. #### `dock.setIcon(image)` _macOS_ -* `image` ([NativeImage](native-image.md) | String) +* `image` ([NativeImage](native-image.md) | string) Sets the `image` associated with this dock icon. diff --git a/docs/api/download-item.md b/docs/api/download-item.md index a52a9b978a631..0a362d522f91e 100644 --- a/docs/api/download-item.md +++ b/docs/api/download-item.md @@ -2,7 +2,8 @@ > Control file downloads from remote sources. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ `DownloadItem` is an [EventEmitter][event-emitter] that represents a download item in Electron. It is used in `will-download` event of `Session` class, and allows users to @@ -11,7 +12,7 @@ control the download item. ```javascript // In the main process. const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() win.webContents.session.on('will-download', (event, item, webContents) => { // Set the save path, making Electron not to prompt a save dialog. item.setSavePath('/tmp/save.pdf') @@ -44,7 +45,7 @@ win.webContents.session.on('will-download', (event, item, webContents) => { Returns: * `event` Event -* `state` String - Can be `progressing` or `interrupted`. +* `state` string - Can be `progressing` or `interrupted`. Emitted when the download has been updated and is not done. @@ -58,7 +59,7 @@ The `state` can be one of following: Returns: * `event` Event -* `state` String - Can be `completed`, `cancelled` or `interrupted`. +* `state` string - Can be `completed`, `cancelled` or `interrupted`. Emitted when the download is in a terminal state. This includes a completed download, a cancelled download (via `downloadItem.cancel()`), and interrupted @@ -76,22 +77,19 @@ The `downloadItem` object has the following methods: #### `downloadItem.setSavePath(path)` -* `path` String - Set the save file path of the download item. +* `path` string - Set the save file path of the download item. The API is only available in session's `will-download` callback function. +If `path` doesn't exist, Electron will try to make the directory recursively. If user doesn't set the save path via the API, Electron will use the original routine to determine the save path; this usually prompts a save dialog. -**[Deprecated](modernization/property-updates.md): use the `savePath` property instead.** - #### `downloadItem.getSavePath()` -Returns `String` - The save path of the download item. This will be either the path +Returns `string` - The save path of the download item. This will be either the path set via `downloadItem.setSavePath(path)` or the path selected from the shown save dialog. -**[Deprecated](modernization/property-updates.md): use the `savePath` property instead.** - #### `downloadItem.setSaveDialogOptions(options)` * `options` SaveDialogOptions - Set the save file dialog options. This object has the same @@ -111,7 +109,7 @@ Pauses the download. #### `downloadItem.isPaused()` -Returns `Boolean` - Whether the download is paused. +Returns `boolean` - Whether the download is paused. #### `downloadItem.resume()` @@ -121,7 +119,7 @@ Resumes the download that has been paused. #### `downloadItem.canResume()` -Returns `Boolean` - Whether the download can resume. +Returns `boolean` - Whether the download can resume. #### `downloadItem.cancel()` @@ -129,19 +127,19 @@ Cancels the download operation. #### `downloadItem.getURL()` -Returns `String` - The origin URL where the item is downloaded from. +Returns `string` - The origin URL where the item is downloaded from. #### `downloadItem.getMimeType()` -Returns `String` - The files mime type. +Returns `string` - The files mime type. #### `downloadItem.hasUserGesture()` -Returns `Boolean` - Whether the download has user gesture. +Returns `boolean` - Whether the download has user gesture. #### `downloadItem.getFilename()` -Returns `String` - The file name of the download item. +Returns `string` - The file name of the download item. **Note:** The file name is not always the same as the actual one saved in local disk. If user changes the file name in a prompted download saving dialog, the @@ -159,27 +157,27 @@ Returns `Integer` - The received bytes of the download item. #### `downloadItem.getContentDisposition()` -Returns `String` - The Content-Disposition field from the response +Returns `string` - The Content-Disposition field from the response header. #### `downloadItem.getState()` -Returns `String` - The current state. Can be `progressing`, `completed`, `cancelled` or `interrupted`. +Returns `string` - The current state. Can be `progressing`, `completed`, `cancelled` or `interrupted`. **Note:** The following methods are useful specifically to resume a `cancelled` item when session is restarted. #### `downloadItem.getURLChain()` -Returns `String[]` - The complete URL chain of the item including any redirects. +Returns `string[]` - The complete URL chain of the item including any redirects. #### `downloadItem.getLastModifiedTime()` -Returns `String` - Last-Modified header value. +Returns `string` - Last-Modified header value. #### `downloadItem.getETag()` -Returns `String` - ETag header value. +Returns `string` - ETag header value. #### `downloadItem.getStartTime()` @@ -190,7 +188,7 @@ started. #### `downloadItem.savePath` -A `String` property that determines the save file path of the download item. +A `string` property that determines the save file path of the download item. The property is only available in session's `will-download` callback function. If user doesn't set the save path via the property, Electron will use the original diff --git a/docs/api/environment-variables.md b/docs/api/environment-variables.md index d31d769614f94..09b12e3124fb6 100644 --- a/docs/api/environment-variables.md +++ b/docs/api/environment-variables.md @@ -44,20 +44,32 @@ Unsupported options are: --use-openssl-ca ``` -`NODE_OPTIONS` are explicitly disallowed in packaged apps. +`NODE_OPTIONS` are explicitly disallowed in packaged apps, except for the following: + +```sh +--max-http-header-size +--http-parser +``` ### `GOOGLE_API_KEY` -You can provide an API key for making requests to Google's geocoding webservice. To do this, place the following code in your main process -file, before opening any browser windows that will make geocoding requests: +Geolocation support in Electron requires the use of Google Cloud Platform's +geolocation webservice. To enable this feature, acquire a +[Google API key](https://developers.google.com/maps/documentation/geolocation/get-api-key) +and place the following code in your main process file, before opening any +browser windows that will make geolocation requests: ```javascript process.env.GOOGLE_API_KEY = 'YOUR_KEY_HERE' ``` -For instructions on how to acquire a Google API key, visit [this page](https://developers.google.com/maps/documentation/javascript/get-api-key). -By default, a newly generated Google API key may not be allowed to make -geocoding requests. To enable geocoding requests, visit [this page](https://developers.google.com/maps/documentation/geocoding/get-api-key). +By default, a newly generated Google API key may not be allowed to make geolocation requests. +To enable the geolocation webservice for your project, enable it through the +[API library](https://console.cloud.google.com/apis/library). + +N.B. You will need to add a +[Billing Account](https://cloud.google.com/billing/docs/how-to/payment-methods#add_a_payment_method) +to the project associated to the API key for the geolocation webservice to work. ### `ELECTRON_NO_ASAR` @@ -68,6 +80,18 @@ and spawned child processes that set `ELECTRON_RUN_AS_NODE`. Starts the process as a normal Node.js process. +In this mode, you will be able to pass [cli options](https://nodejs.org/api/cli.html) to Node.js as +you would when running the normal Node.js executable, with the exception of the following flags: + +* "--openssl-config" +* "--use-bundled-ca" +* "--use-openssl-ca", +* "--force-fips" +* "--enable-fips" + +These flags are disabled owing to the fact that Electron uses BoringSSL instead of OpenSSL when building Node.js' +`crypto` module, and so will not work as designed. + ### `ELECTRON_NO_ATTACH_CONSOLE` _Windows_ Don't attach to the current console session. @@ -81,6 +105,7 @@ Don't use the global menu bar on Linux. Set the trash implementation on Linux. Default is `gio`. Options: + * `gvfs-trash` * `trash-cli` * `kioclient5` @@ -91,10 +116,40 @@ Options: The following environment variables are intended primarily for development and debugging purposes. - ### `ELECTRON_ENABLE_LOGGING` -Prints Chrome's internal logging to the console. +Prints Chromium's internal logging to the console. + +Setting this variable is the same as passing `--enable-logging` +on the command line. For more info, see `--enable-logging` in [command-line +switches](./command-line-switches.md#--enable-loggingfile). + +### `ELECTRON_LOG_FILE` + +Sets the file destination for Chromium's internal logging. + +Setting this variable is the same as passing `--log-file` +on the command line. For more info, see `--log-file` in [command-line +switches](./command-line-switches.md#--log-filepath). + +### `ELECTRON_DEBUG_DRAG_REGIONS` + +Adds coloration to draggable regions on [`BrowserView`](./browser-view.md)s on macOS - draggable regions will be colored +green and non-draggable regions will be colored red to aid debugging. + +### `ELECTRON_DEBUG_NOTIFICATIONS` + +Adds extra logs to [`Notification`](./notification.md) lifecycles on macOS to aid in debugging. Extra logging will be displayed when new Notifications are created or activated. They will also be displayed when common a +tions are taken: a notification is shown, dismissed, its button is clicked, or it is replied to. + +Sample output: + +```sh +Notification created (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44) +Notification displayed (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44) +Notification activated (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44) +Notification replied to (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44) +``` ### `ELECTRON_LOG_ASAR_READS` @@ -121,5 +176,16 @@ the `electron` command to use the specified build of Electron instead of the one downloaded by `npm install`. Usage: ```sh -export ELECTRON_OVERRIDE_DIST_PATH=/Users/username/projects/electron/out/Debug +export ELECTRON_OVERRIDE_DIST_PATH=/Users/username/projects/electron/out/Testing ``` + +## Set By Electron + +Electron sets some variables in your environment at runtime. + +### `ORIGINAL_XDG_CURRENT_DESKTOP` + +This variable is set to the value of `XDG_CURRENT_DESKTOP` that your application +originally launched with. Electron sometimes modifies the value of `XDG_CURRENT_DESKTOP` +to affect other logic within Chromium so if you want access to the _original_ value +you should look up this environment variable instead. diff --git a/docs/api/extensions.md b/docs/api/extensions.md new file mode 100644 index 0000000000000..c8fbb015a1f74 --- /dev/null +++ b/docs/api/extensions.md @@ -0,0 +1,127 @@ +# Chrome Extension Support + +Electron supports a subset of the [Chrome Extensions +API][chrome-extensions-api-index], primarily to support DevTools extensions and +Chromium-internal extensions, but it also happens to support some other +extension capabilities. + +[chrome-extensions-api-index]: https://developer.chrome.com/extensions/api_index + +> **Note:** Electron does not support arbitrary Chrome extensions from the +> store, and it is a **non-goal** of the Electron project to be perfectly +> compatible with Chrome's implementation of Extensions. + +## Loading extensions + +Electron only supports loading unpacked extensions (i.e., `.crx` files do not +work). Extensions are installed per-`session`. To load an extension, call +[`ses.loadExtension`](session.md#sesloadextensionpath-options): + +```js +const { session } = require('electron') + +session.loadExtension('path/to/unpacked/extension').then(({ id }) => { + // ... +}) +``` + +Loaded extensions will not be automatically remembered across exits; if you do +not call `loadExtension` when the app runs, the extension will not be loaded. + +Note that loading extensions is only supported in persistent sessions. +Attempting to load an extension into an in-memory session will throw an error. + +See the [`session`](session.md) documentation for more information about +loading, unloading, and querying active extensions. + +## Supported Extensions APIs + +We support the following extensions APIs, with some caveats. Other APIs may +additionally be supported, but support for any APIs not listed here is +provisional and may be removed. + +### `chrome.devtools.inspectedWindow` + +All features of this API are supported. + +### `chrome.devtools.network` + +All features of this API are supported. + +### `chrome.devtools.panels` + +All features of this API are supported. + +### `chrome.extension` + +The following properties of `chrome.extension` are supported: + +- `chrome.extension.lastError` + +The following methods of `chrome.extension` are supported: + +- `chrome.extension.getURL` +- `chrome.extension.getBackgroundPage` + +### `chrome.runtime` + +The following properties of `chrome.runtime` are supported: + +- `chrome.runtime.lastError` +- `chrome.runtime.id` + +The following methods of `chrome.runtime` are supported: + +- `chrome.runtime.getBackgroundPage` +- `chrome.runtime.getManifest` +- `chrome.runtime.getPlatformInfo` +- `chrome.runtime.getURL` +- `chrome.runtime.connect` +- `chrome.runtime.sendMessage` +- `chrome.runtime.reload` + +The following events of `chrome.runtime` are supported: + +- `chrome.runtime.onStartup` +- `chrome.runtime.onInstalled` +- `chrome.runtime.onSuspend` +- `chrome.runtime.onSuspendCanceled` +- `chrome.runtime.onConnect` +- `chrome.runtime.onMessage` + +### `chrome.storage` + +Only `chrome.storage.local` is supported; `chrome.storage.sync` and +`chrome.storage.managed` are not. + +### `chrome.tabs` + +The following methods of `chrome.tabs` are supported: + +- `chrome.tabs.sendMessage` +- `chrome.tabs.reload` +- `chrome.tabs.executeScript` +- `chrome.tabs.update` (partial support) + - supported properties: `url`, `muted`. + +> **Note:** In Chrome, passing `-1` as a tab ID signifies the "currently active +> tab". Since Electron has no such concept, passing `-1` as a tab ID is not +> supported and will raise an error. + +### `chrome.management` + +The following methods of `chrome.management` are supported: + +- `chrome.management.getAll` +- `chrome.management.get` +- `chrome.management.getSelf` +- `chrome.management.getPermissionWarningsById` +- `chrome.management.getPermissionWarningsByManifest` +- `chrome.management.onEnabled` +- `chrome.management.onDisabled` + +### `chrome.webRequest` + +All features of this API are supported. + +> **NOTE:** Electron's [`webRequest`](web-request.md) module takes precedence over `chrome.webRequest` if there are conflicting handlers. diff --git a/docs/api/frameless-window.md b/docs/api/frameless-window.md deleted file mode 100644 index fd5ff554de04e..0000000000000 --- a/docs/api/frameless-window.md +++ /dev/null @@ -1,180 +0,0 @@ -# Frameless Window - -> Open a window without toolbars, borders, or other graphical "chrome". - -A frameless window is a window that has no -[chrome](https://developer.mozilla.org/en-US/docs/Glossary/Chrome), the parts of -the window, like toolbars, that are not a part of the web page. These are -options on the [`BrowserWindow`](browser-window.md) class. - -## Create a frameless window - -To create a frameless window, you need to set `frame` to `false` in -[BrowserWindow](browser-window.md)'s `options`: - - -```javascript -const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ width: 800, height: 600, frame: false }) -win.show() -``` - -### Alternatives on macOS - -There's an alternative way to specify a chromeless window. -Instead of setting `frame` to `false` which disables both the titlebar and window controls, -you may want to have the title bar hidden and your content extend to the full window size, -yet still preserve the window controls ("traffic lights") for standard window actions. -You can do so by specifying the `titleBarStyle` option: - -#### `hidden` - -Results in a hidden title bar and a full size content window, yet the title bar still has the standard window controls (“traffic lights”) in the top left. - -```javascript -const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ titleBarStyle: 'hidden' }) -win.show() -``` - -#### `hiddenInset` - -Results in a hidden title bar with an alternative look where the traffic light buttons are slightly more inset from the window edge. - -```javascript -const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ titleBarStyle: 'hiddenInset' }) -win.show() -``` - -#### `customButtonsOnHover` - -Uses custom drawn close, and miniaturize buttons that display -when hovering in the top left of the window. The fullscreen button -is not available due to restrictions of frameless windows as they -interface with Apple's MacOS window masks. These custom buttons prevent -issues with mouse events that occur with the standard window toolbar buttons. -This option is only applicable for frameless windows. - -```javascript -const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ titleBarStyle: 'customButtonsOnHover', frame: false }) -win.show() -``` - -## Transparent window - -By setting the `transparent` option to `true`, you can also make the frameless -window transparent: - -```javascript -const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ transparent: true, frame: false }) -win.show() -``` - -### Limitations - -* You can not click through the transparent area. We are going to introduce an - API to set window shape to solve this, see - [our issue](https://github.com/electron/electron/issues/1335) for details. -* Transparent windows are not resizable. Setting `resizable` to `true` may make - a transparent window stop working on some platforms. -* The `blur` filter only applies to the web page, so there is no way to apply - blur effect to the content below the window (i.e. other applications open on - the user's system). -* On Windows operating systems, transparent windows will not work when DWM is - disabled. -* On Linux, users have to put `--enable-transparent-visuals --disable-gpu` in - the command line to disable GPU and allow ARGB to make transparent window, - this is caused by an upstream bug that [alpha channel doesn't work on some - NVidia drivers](https://code.google.com/p/chromium/issues/detail?id=369209) on - Linux. -* On Mac, the native window shadow will not be shown on a transparent window. - -## Click-through window - -To create a click-through window, i.e. making the window ignore all mouse -events, you can call the [win.setIgnoreMouseEvents(ignore)][ignore-mouse-events] -API: - -```javascript -const { BrowserWindow } = require('electron') -let win = new BrowserWindow() -win.setIgnoreMouseEvents(true) -``` - -### Forwarding - -Ignoring mouse messages makes the web page oblivious to mouse movement, meaning -that mouse movement events will not be emitted. On Windows operating systems an -optional parameter can be used to forward mouse move messages to the web page, -allowing events such as `mouseleave` to be emitted: - -```javascript -let win = require('electron').remote.getCurrentWindow() -let el = document.getElementById('clickThroughElement') -el.addEventListener('mouseenter', () => { - win.setIgnoreMouseEvents(true, { forward: true }) -}) -el.addEventListener('mouseleave', () => { - win.setIgnoreMouseEvents(false) -}) -``` - -This makes the web page click-through when over `el`, and returns to normal -outside it. - -## Draggable region - -By default, the frameless window is non-draggable. Apps need to specify -`-webkit-app-region: drag` in CSS to tell Electron which regions are draggable -(like the OS's standard titlebar), and apps can also use -`-webkit-app-region: no-drag` to exclude the non-draggable area from the - draggable region. Note that only rectangular shapes are currently supported. - -Note: `-webkit-app-region: drag` is known to have problems while the developer tools are open. See this [GitHub issue](https://github.com/electron/electron/issues/3647) for more information including a workaround. - -To make the whole window draggable, you can add `-webkit-app-region: drag` as -`body`'s style: - -```html -<body style="-webkit-app-region: drag"> -</body> -``` - -And note that if you have made the whole window draggable, you must also mark -buttons as non-draggable, otherwise it would be impossible for users to click on -them: - -```css -button { - -webkit-app-region: no-drag; -} -``` - -If you're only setting a custom titlebar as draggable, you also need to make all -buttons in titlebar non-draggable. - -## Text selection - -In a frameless window the dragging behaviour may conflict with selecting text. -For example, when you drag the titlebar you may accidentally select the text on -the titlebar. To prevent this, you need to disable text selection within a -draggable area like this: - -```css -.titlebar { - -webkit-user-select: none; - -webkit-app-region: drag; -} -``` - -## Context menu - -On some platforms, the draggable area will be treated as a non-client frame, so -when you right click on it a system menu will pop up. To make the context menu -behave correctly on all platforms you should never use a custom context menu on -draggable areas. - -[ignore-mouse-events]: browser-window.md#winsetignoremouseeventsignore-options diff --git a/docs/api/global-shortcut.md b/docs/api/global-shortcut.md index 3d18a39f5cde1..52609c981dd58 100644 --- a/docs/api/global-shortcut.md +++ b/docs/api/global-shortcut.md @@ -9,13 +9,13 @@ with the operating system so that you can customize the operations for various shortcuts. **Note:** The shortcut is global; it will work even if the app does -not have the keyboard focus. You should not use this module until the `ready` +not have the keyboard focus. This module cannot be used before the `ready` event of the app module is emitted. ```javascript const { app, globalShortcut } = require('electron') -app.on('ready', () => { +app.whenReady().then(() => { // Register a 'CommandOrControl+X' shortcut listener. const ret = globalShortcut.register('CommandOrControl+X', () => { console.log('CommandOrControl+X is pressed') @@ -47,7 +47,7 @@ The `globalShortcut` module has the following methods: * `accelerator` [Accelerator](accelerator.md) * `callback` Function -Returns `Boolean` - Whether or not the shortcut was registered successfully. +Returns `boolean` - Whether or not the shortcut was registered successfully. Registers a global shortcut of `accelerator`. The `callback` is called when the registered shortcut is pressed by the user. @@ -66,7 +66,7 @@ the app has been authorized as a [trusted accessibility client](https://develope ### `globalShortcut.registerAll(accelerators, callback)` -* `accelerators` String[] - an array of [Accelerator](accelerator.md)s. +* `accelerators` string[] - an array of [Accelerator](accelerator.md)s. * `callback` Function Registers a global shortcut of all `accelerator` items in `accelerators`. The `callback` is called when any of the registered shortcuts are pressed by the user. @@ -87,7 +87,7 @@ the app has been authorized as a [trusted accessibility client](https://develope * `accelerator` [Accelerator](accelerator.md) -Returns `Boolean` - Whether this application has registered `accelerator`. +Returns `boolean` - Whether this application has registered `accelerator`. When the accelerator is already taken by other applications, this call will still return `false`. This behavior is intended by operating systems, since they diff --git a/docs/api/in-app-purchase.md b/docs/api/in-app-purchase.md index c30f3c2c9fae4..1520a0ce3a927 100644 --- a/docs/api/in-app-purchase.md +++ b/docs/api/in-app-purchase.md @@ -23,16 +23,16 @@ The `inAppPurchase` module has the following methods: ### `inAppPurchase.purchaseProduct(productID[, quantity])` -* `productID` String - The identifiers of the product to purchase. (The identifier of `com.example.app.product1` is `product1`). +* `productID` string - The identifiers of the product to purchase. (The identifier of `com.example.app.product1` is `product1`). * `quantity` Integer (optional) - The number of items the user wants to purchase. -Returns `Promise<Boolean>` - Returns `true` if the product is valid and added to the payment queue. +Returns `Promise<boolean>` - Returns `true` if the product is valid and added to the payment queue. You should listen for the `transactions-updated` event as soon as possible and certainly before you call `purchaseProduct`. ### `inAppPurchase.getProducts(productIDs)` -* `productIDs` String[] - The identifiers of the products to get. +* `productIDs` string[] - The identifiers of the products to get. Returns `Promise<Product[]>` - Resolves with an array of [`Product`](structures/product.md) objects. @@ -40,11 +40,17 @@ Retrieves the product descriptions. ### `inAppPurchase.canMakePayments()` -Returns `Boolean`, whether a user can make a payment. +Returns `boolean` - whether a user can make a payment. + +### `inAppPurchase.restoreCompletedTransactions()` + +Restores finished transactions. This method can be called either to install purchases on additional devices, or to restore purchases for an application that the user deleted and reinstalled. + +[The payment queue](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc) delivers a new transaction for each previously completed transaction that can be restored. Each transaction includes a copy of the original transaction. ### `inAppPurchase.getReceiptURL()` -Returns `String`, the path to the receipt. +Returns `string` - the path to the receipt. ### `inAppPurchase.finishAllTransactions()` @@ -52,6 +58,6 @@ Completes all pending transactions. ### `inAppPurchase.finishTransactionByDate(date)` -* `date` String - The ISO formatted date of the transaction to finish. +* `date` string - The ISO formatted date of the transaction to finish. Completes the pending transactions corresponding to the date. diff --git a/docs/api/incoming-message.md b/docs/api/incoming-message.md index cad08f9bc0b73..92b9dec58f169 100644 --- a/docs/api/incoming-message.md +++ b/docs/api/incoming-message.md @@ -2,7 +2,8 @@ > Handle responses to HTTP/HTTPS requests. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ `IncomingMessage` implements the [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams) interface and is therefore an [EventEmitter][event-emitter]. @@ -20,7 +21,7 @@ applicative code. #### Event: 'end' -Indicates that response body has ended. +Indicates that response body has ended. Must be placed before 'data' event. #### Event: 'aborted' @@ -47,20 +48,25 @@ An `Integer` indicating the HTTP response status code. #### `response.statusMessage` -A `String` representing the HTTP status message. +A `string` representing the HTTP status message. #### `response.headers` -An `Object` representing the response HTTP headers. The `headers` object is +A `Record<string, string | string[]>` representing the HTTP response headers. The `headers` object is formatted as follows: * All header names are lowercased. -* Each header name produces an array-valued property on the headers object. -* Each header value is pushed into the array associated with its header name. +* Duplicates of `age`, `authorization`, `content-length`, `content-type`, +`etag`, `expires`, `from`, `host`, `if-modified-since`, `if-unmodified-since`, +`last-modified`, `location`, `max-forwards`, `proxy-authorization`, `referer`, +`retry-after`, `server`, or `user-agent` are discarded. +* `set-cookie` is always an array. Duplicates are added to the array. +* For duplicate `cookie` headers, the values are joined together with '; '. +* For all other headers, the values are joined together with ', '. #### `response.httpVersion` -A `String` indicating the HTTP protocol version number. Typical values are '1.0' +A `string` indicating the HTTP protocol version number. Typical values are '1.0' or '1.1'. Additionally `httpVersionMajor` and `httpVersionMinor` are two Integer-valued readable properties that return respectively the HTTP major and minor version numbers. @@ -74,3 +80,25 @@ An `Integer` indicating the HTTP protocol major version number. An `Integer` indicating the HTTP protocol minor version number. [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter + +#### `response.rawHeaders` + +A `string[]` containing the raw HTTP response headers exactly as they were +received. The keys and values are in the same list. It is not a list of +tuples. So, the even-numbered offsets are key values, and the odd-numbered +offsets are the associated values. Header names are not lowercased, and +duplicates are not merged. + +```javascript +// Prints something like: +// +// [ 'user-agent', +// 'this is invalid because there can be only one', +// 'User-Agent', +// 'curl/7.22.0', +// 'Host', +// '127.0.0.1:8000', +// 'ACCEPT', +// '*/*' ] +console.log(request.rawHeaders) +``` diff --git a/docs/api/ipc-main.md b/docs/api/ipc-main.md index 8f4fdb12ca860..f55e848c16398 100644 --- a/docs/api/ipc-main.md +++ b/docs/api/ipc-main.md @@ -40,6 +40,8 @@ ipcMain.on('synchronous-message', (event, arg) => { ```javascript // In renderer process (web page). +// NB. Electron APIs are only accessible from preload, unless contextIsolation is disabled. +// See https://www.electronjs.org/docs/tutorial/process-model#preload-scripts for more details. const { ipcRenderer } = require('electron') console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong" @@ -55,7 +57,7 @@ The `ipcMain` module has the following method to listen for events: ### `ipcMain.on(channel, listener)` -* `channel` String +* `channel` string * `listener` Function * `event` IpcMainEvent * `...args` any[] @@ -65,7 +67,7 @@ Listens to `channel`, when a new message arrives `listener` would be called with ### `ipcMain.once(channel, listener)` -* `channel` String +* `channel` string * `listener` Function * `event` IpcMainEvent * `...args` any[] @@ -75,22 +77,23 @@ only the next time a message is sent to `channel`, after which it is removed. ### `ipcMain.removeListener(channel, listener)` -* `channel` String +* `channel` string * `listener` Function + * `...args` any[] Removes the specified `listener` from the listener array for the specified `channel`. ### `ipcMain.removeAllListeners([channel])` -* `channel` String (optional) +* `channel` string (optional) Removes listeners of the specified `channel`. ### `ipcMain.handle(channel, listener)` -* `channel` String -* `listener` Function<Promise> | Function<any> +* `channel` string +* `listener` Function<Promise\<void> | any> * `event` IpcMainInvokeEvent * `...args` any[] @@ -119,10 +122,15 @@ The `event` that is passed as the first argument to the handler is the same as that passed to a regular event listener. It includes information about which WebContents is the source of the invoke request. +Errors thrown through `handle` in the main process are not transparent as they +are serialized and only the `message` property from the original error is +provided to the renderer process. Please refer to +[#24427](https://github.com/electron/electron/issues/24427) for details. + ### `ipcMain.handleOnce(channel, listener)` -* `channel` String -* `listener` Function<Promise> | Function<any> +* `channel` string +* `listener` Function<Promise\<void> | any> * `event` IpcMainInvokeEvent * `...args` any[] @@ -131,7 +139,7 @@ Handles a single `invoke`able IPC message, then removes the listener. See ### `ipcMain.removeHandler(channel)` -* `channel` String +* `channel` string Removes any handler for `channel`, if present. diff --git a/docs/api/ipc-renderer.md b/docs/api/ipc-renderer.md index a6c99b20c35c7..383c7c0857339 100644 --- a/docs/api/ipc-renderer.md +++ b/docs/api/ipc-renderer.md @@ -17,7 +17,7 @@ The `ipcRenderer` module has the following method to listen for events and send ### `ipcRenderer.on(channel, listener)` -* `channel` String +* `channel` string * `listener` Function * `event` IpcRendererEvent * `...args` any[] @@ -27,7 +27,7 @@ Listens to `channel`, when a new message arrives `listener` would be called with ### `ipcRenderer.once(channel, listener)` -* `channel` String +* `channel` string * `listener` Function * `event` IpcRendererEvent * `...args` any[] @@ -37,7 +37,7 @@ only the next time a message is sent to `channel`, after which it is removed. ### `ipcRenderer.removeListener(channel, listener)` -* `channel` String +* `channel` string * `listener` Function * `...args` any[] @@ -46,37 +46,62 @@ Removes the specified `listener` from the listener array for the specified ### `ipcRenderer.removeAllListeners(channel)` -* `channel` String +* `channel` string Removes all listeners, or those of the specified `channel`. ### `ipcRenderer.send(channel, ...args)` -* `channel` String +* `channel` string * `...args` any[] -Send a message to the main process asynchronously via `channel`, you can also -send arbitrary arguments. Arguments will be serialized as JSON internally and -hence no functions or prototype chain will be included. +Send an asynchronous message to the main process via `channel`, along with +arguments. Arguments will be serialized with the [Structured Clone +Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be +included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will +throw an exception. + +> **NOTE:** Sending non-standard JavaScript types such as DOM objects or +> special Electron objects will throw an exception. +> +> Since the main process does not have support for DOM objects such as +> `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over +> Electron's IPC to the main process, as the main process would have no way to decode +> them. Attempting to send such objects over IPC will result in an error. The main process handles it by listening for `channel` with the [`ipcMain`](ipc-main.md) module. +If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer). + +If you want to receive a single response from the main process, like the result of a method call, consider using [`ipcRenderer.invoke`](#ipcrendererinvokechannel-args). + ### `ipcRenderer.invoke(channel, ...args)` -* `channel` String +* `channel` string * `...args` any[] Returns `Promise<any>` - Resolves with the response from the main process. -Send a message to the main process asynchronously via `channel` and expect an -asynchronous result. Arguments will be serialized as JSON internally and -hence no functions or prototype chain will be included. +Send a message to the main process via `channel` and expect a result +asynchronously. Arguments will be serialized with the [Structured Clone +Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be +included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will +throw an exception. + +> **NOTE:** Sending non-standard JavaScript types such as DOM objects or +> special Electron objects will throw an exception. +> +> Since the main process does not have support for DOM objects such as +> `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over +> Electron's IPC to the main process, as the main process would have no way to decode +> them. Attempting to send such objects over IPC will result in an error. The main process should listen for `channel` with [`ipcMain.handle()`](ipc-main.md#ipcmainhandlechannel-listener). For example: + ```javascript // Renderer process ipcRenderer.invoke('some-name', someArgument).then((result) => { @@ -90,34 +115,80 @@ ipcMain.handle('some-name', async (event, someArgument) => { }) ``` +If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer). + +If you do not need a response to the message, consider using [`ipcRenderer.send`](#ipcrenderersendchannel-args). + ### `ipcRenderer.sendSync(channel, ...args)` -* `channel` String +* `channel` string * `...args` any[] Returns `any` - The value sent back by the [`ipcMain`](ipc-main.md) handler. -Send a message to the main process synchronously via `channel`, you can also -send arbitrary arguments. Arguments will be serialized in JSON internally and -hence no functions or prototype chain will be included. +Send a message to the main process via `channel` and expect a result +synchronously. Arguments will be serialized with the [Structured Clone +Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be +included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will +throw an exception. + +> **NOTE:** Sending non-standard JavaScript types such as DOM objects or +> special Electron objects will throw an exception. +> +> Since the main process does not have support for DOM objects such as +> `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over +> Electron's IPC to the main process, as the main process would have no way to decode +> them. Attempting to send such objects over IPC will result in an error. The main process handles it by listening for `channel` with [`ipcMain`](ipc-main.md) module, and replies by setting `event.returnValue`. -**Note:** Sending a synchronous message will block the whole renderer process, -unless you know what you are doing you should never use it. +> :warning: **WARNING**: Sending a synchronous message will block the whole +> renderer process until the reply is received, so use this method only as a +> last resort. It's much better to use the asynchronous version, +> [`invoke()`](ipc-renderer.md#ipcrendererinvokechannel-args). + +### `ipcRenderer.postMessage(channel, message, [transfer])` + +* `channel` string +* `message` any +* `transfer` MessagePort[] (optional) + +Send a message to the main process, optionally transferring ownership of zero +or more [`MessagePort`][] objects. + +The transferred `MessagePort` objects will be available in the main process as +[`MessagePortMain`](message-port-main.md) objects by accessing the `ports` +property of the emitted event. + +For example: + +```js +// Renderer process +const { port1, port2 } = new MessageChannel() +ipcRenderer.postMessage('port', { message: 'hello' }, [port1]) + +// Main process +ipcMain.on('port', (e, msg) => { + const [port] = e.ports + // ... +}) +``` + +For more information on using `MessagePort` and `MessageChannel`, see the [MDN +documentation](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel). ### `ipcRenderer.sendTo(webContentsId, channel, ...args)` -* `webContentsId` Number -* `channel` String +* `webContentsId` number +* `channel` string * `...args` any[] Sends a message to a window with `webContentsId` via `channel`. ### `ipcRenderer.sendToHost(channel, ...args)` -* `channel` String +* `channel` string * `...args` any[] Like `ipcRenderer.send` but the event will be sent to the `<webview>` element in @@ -129,3 +200,6 @@ The documentation for the `event` object passed to the `callback` can be found in the [`ipc-renderer-event`](structures/ipc-renderer-event.md) structure docs. [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter +[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[`window.postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage +[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort diff --git a/docs/api/locales.md b/docs/api/locales.md deleted file mode 100644 index a45fdbcbe5774..0000000000000 --- a/docs/api/locales.md +++ /dev/null @@ -1,142 +0,0 @@ -# Locales - -> Locale values returned by `app.getLocale()`. - -Electron uses Chromium's `l10n_util` library to fetch the locale. Possible -values are listed below: - -| Language Code | Language Name | -|---------------|---------------| -| af | Afrikaans | -| am | Amharic | -| ar | Arabic | -| az | Azerbaijani | -| be | Belarusian | -| bg | Bulgarian | -| bh | Bihari | -| bn | Bengali | -| br | Breton | -| bs | Bosnian | -| ca | Catalan | -| co | Corsican | -| cs | Czech | -| cy | Welsh | -| da | Danish | -| de | German | -| de-AT | German (Austria) | -| de-CH | German (Switzerland) | -| de-DE | German (Germany) | -| el | Greek | -| en | English | -| en-AU | English (Australia) | -| en-CA | English (Canada) | -| en-GB | English (UK) | -| en-NZ | English (New Zealand) | -| en-US | English (US) | -| en-ZA | English (South Africa) | -| eo | Esperanto | -| es | Spanish | -| es-419 | Spanish (Latin America) | -| et | Estonian | -| eu | Basque | -| fa | Persian | -| fi | Finnish | -| fil | Filipino | -| fo | Faroese | -| fr | French | -| fr-CA | French (Canada) | -| fr-CH | French (Switzerland) | -| fr-FR | French (France) | -| fy | Frisian | -| ga | Irish | -| gd | Scots Gaelic | -| gl | Galician | -| gn | Guarani | -| gu | Gujarati | -| ha | Hausa | -| haw | Hawaiian | -| he | Hebrew | -| hi | Hindi | -| hr | Croatian | -| hu | Hungarian | -| hy | Armenian | -| ia | Interlingua | -| id | Indonesian | -| is | Icelandic | -| it | Italian | -| it-CH | Italian (Switzerland) | -| it-IT | Italian (Italy) | -| ja | Japanese | -| jw | Javanese | -| ka | Georgian | -| kk | Kazakh | -| km | Cambodian | -| kn | Kannada | -| ko | Korean | -| ku | Kurdish | -| ky | Kyrgyz | -| la | Latin | -| ln | Lingala | -| lo | Laothian | -| lt | Lithuanian | -| lv | Latvian | -| mk | Macedonian | -| ml | Malayalam | -| mn | Mongolian | -| mo | Moldavian | -| mr | Marathi | -| ms | Malay | -| mt | Maltese | -| nb | Norwegian (Bokmal) | -| ne | Nepali | -| nl | Dutch | -| nn | Norwegian (Nynorsk) | -| no | Norwegian | -| oc | Occitan | -| om | Oromo | -| or | Oriya | -| pa | Punjabi | -| pl | Polish | -| ps | Pashto | -| pt | Portuguese | -| pt-BR | Portuguese (Brazil) | -| pt-PT | Portuguese (Portugal) | -| qu | Quechua | -| rm | Romansh | -| ro | Romanian | -| ru | Russian | -| sd | Sindhi | -| sh | Serbo-Croatian | -| si | Sinhalese | -| sk | Slovak | -| sl | Slovenian | -| sn | Shona | -| so | Somali | -| sq | Albanian | -| sr | Serbian | -| st | Sesotho | -| su | Sundanese | -| sv | Swedish | -| sw | Swahili | -| ta | Tamil | -| te | Telugu | -| tg | Tajik | -| th | Thai | -| ti | Tigrinya | -| tk | Turkmen | -| to | Tonga | -| tr | Turkish | -| tt | Tatar | -| tw | Twi | -| ug | Uighur | -| uk | Ukrainian | -| ur | Urdu | -| uz | Uzbek | -| vi | Vietnamese | -| xh | Xhosa | -| yi | Yiddish | -| yo | Yoruba | -| zh | Chinese | -| zh-CN | Chinese (Simplified) | -| zh-TW | Chinese (Traditional) | -| zu | Zulu | diff --git a/docs/api/menu-item.md b/docs/api/menu-item.md index 1f3b7070d95a3..3d38a292b4c41 100644 --- a/docs/api/menu-item.md +++ b/docs/api/menu-item.md @@ -12,41 +12,42 @@ See [`Menu`](menu.md) for examples. * `click` Function (optional) - Will be called with `click(menuItem, browserWindow, event)` when the menu item is clicked. * `menuItem` MenuItem - * `browserWindow` [BrowserWindow](browser-window.md) + * `browserWindow` [BrowserWindow](browser-window.md) | undefined - This will not be defined if no window is open. * `event` [KeyboardEvent](structures/keyboard-event.md) - * `role` String (optional) - Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteandmatchstyle`, `delete`, `selectall`, `reload`, `forcereload`, `toggledevtools`, `resetzoom`, `zoomin`, `zoomout`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideothers`, `unhide`, `quit`, `startspeaking`, `stopspeaking`, `close`, `minimize`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu` or `windowMenu` - Define the action of the menu item, when specified the + * `role` string (optional) - Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteAndMatchStyle`, `delete`, `selectAll`, `reload`, `forceReload`, `toggleDevTools`, `resetZoom`, `zoomIn`, `zoomOut`, `toggleSpellChecker`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideOthers`, `unhide`, `quit`, 'showSubstitutions', 'toggleSmartQuotes', 'toggleSmartDashes', 'toggleTextReplacement', `startSpeaking`, `stopSpeaking`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu`, `shareMenu`, `recentDocuments`, `toggleTabBar`, `selectNextTab`, `selectPreviousTab`, `mergeAllWindows`, `clearRecentDocuments`, `moveTabToNewWindow` or `windowMenu` - Define the action of the menu item, when specified the `click` property will be ignored. See [roles](#roles). - * `type` String (optional) - Can be `normal`, `separator`, `submenu`, `checkbox` or + * `type` string (optional) - Can be `normal`, `separator`, `submenu`, `checkbox` or `radio`. - * `label` String (optional) - * `sublabel` String (optional) - * `toolTip` String (optional) _macOS_ - Hover text for this menu item. + * `label` string (optional) + * `sublabel` string (optional) + * `toolTip` string (optional) _macOS_ - Hover text for this menu item. * `accelerator` [Accelerator](accelerator.md) (optional) - * `icon` ([NativeImage](native-image.md) | String) (optional) - * `enabled` Boolean (optional) - If false, the menu item will be greyed out and + * `icon` ([NativeImage](native-image.md) | string) (optional) + * `enabled` boolean (optional) - If false, the menu item will be greyed out and unclickable. - * `acceleratorWorksWhenHidden` Boolean (optional) _macOS_ - default is `true`, and when `false` will prevent the accelerator from triggering the item if the item is not visible`. - * `visible` Boolean (optional) - If false, the menu item will be entirely hidden. - * `checked` Boolean (optional) - Should only be specified for `checkbox` or `radio` type + * `acceleratorWorksWhenHidden` boolean (optional) _macOS_ - default is `true`, and when `false` will prevent the accelerator from triggering the item if the item is not visible`. + * `visible` boolean (optional) - If false, the menu item will be entirely hidden. + * `checked` boolean (optional) - Should only be specified for `checkbox` or `radio` type menu items. - * `registerAccelerator` Boolean (optional) _Linux_ _Windows_ - If false, the accelerator won't be registered + * `registerAccelerator` boolean (optional) _Linux_ _Windows_ - If false, the accelerator won't be registered with the system, but it will still be displayed. Defaults to true. + * `sharingItem` SharingItem (optional) _macOS_ - The item to share when the `role` is `shareMenu`. * `submenu` (MenuItemConstructorOptions[] | [Menu](menu.md)) (optional) - Should be specified for `submenu` type menu items. If `submenu` is specified, the `type: 'submenu'` can be omitted. If the value is not a [`Menu`](menu.md) then it will be automatically converted to one using `Menu.buildFromTemplate`. - * `id` String (optional) - Unique within a single menu. If defined then it can be used + * `id` string (optional) - Unique within a single menu. If defined then it can be used as a reference to this item by the position attribute. - * `before` String[] (optional) - Inserts this item before the item with the specified label. If + * `before` string[] (optional) - Inserts this item before the item with the specified label. If the referenced item doesn't exist the item will be inserted at the end of the menu. Also implies that the menu item in question should be placed in the same “group” as the item. - * `after` String[] (optional) - Inserts this item after the item with the specified label. If the + * `after` string[] (optional) - Inserts this item after the item with the specified label. If the referenced item doesn't exist the item will be inserted at the end of the menu. - * `beforeGroupContaining` String[] (optional) - Provides a means for a single context menu to declare + * `beforeGroupContaining` string[] (optional) - Provides a means for a single context menu to declare the placement of their containing group before the containing group of the item with the specified label. - * `afterGroupContaining` String[] (optional) - Provides a means for a single context menu to declare + * `afterGroupContaining` string[] (optional) - Provides a means for a single context menu to declare the placement of their containing group after the containing group of the item with the specified label. @@ -69,6 +70,7 @@ a `type`. The `role` property can have following values: * `undo` +* `about` - Trigger a native about panel (custom message box on Window, which does not provide its own). * `redo` * `cut` * `copy` @@ -82,10 +84,11 @@ The `role` property can have following values: * `reload` - Reload the current window. * `forceReload` - Reload the current window ignoring the cache. * `toggleDevTools` - Toggle developer tools in the current window. -* `toggleFullScreen` - Toggle full screen mode on the current window. +* `togglefullscreen` - Toggle full screen mode on the current window. * `resetZoom` - Reset the focused page's zoom level to the original size. * `zoomIn` - Zoom in the focused page by 10%. * `zoomOut` - Zoom out the focused page by 10%. +* `toggleSpellChecker` - Enable/disable builtin spell checker. * `fileMenu` - Whole default "File" menu (Close / Quit) * `editMenu` - Whole default "Edit" menu (Undo, Copy, etc.). * `viewMenu` - Whole default "View" menu (Reload, Toggle Developer Tools, etc.) @@ -94,10 +97,13 @@ The `role` property can have following values: The following additional roles are available on _macOS_: * `appMenu` - Whole default "App" menu (About, Services, etc.) -* `about` - Map to the `orderFrontStandardAboutPanel` action. * `hide` - Map to the `hide` action. * `hideOthers` - Map to the `hideOtherApplications` action. * `unhide` - Map to the `unhideAllApplications` action. +* `showSubstitutions` - Map to the `orderFrontSubstitutionsPanel` action. +* `toggleSmartQuotes` - Map to the `toggleAutomaticQuoteSubstitution` action. +* `toggleSmartDashes` - Map to the `toggleAutomaticDashSubstitution` action. +* `toggleTextReplacement` - Map to the `toggleAutomaticTextReplacement` action. * `startSpeaking` - Map to the `startSpeaking` action. * `stopSpeaking` - Map to the `stopSpeaking` action. * `front` - Map to the `arrangeInFront` action. @@ -112,12 +118,13 @@ The following additional roles are available on _macOS_: * `services` - The submenu is a ["Services"](https://developer.apple.com/documentation/appkit/nsapplication/1428608-servicesmenu?language=objc) menu. This is only intended for use in the Application Menu and is *not* the same as the "Services" submenu used in context menus in macOS apps, which is not implemented in Electron. * `recentDocuments` - The submenu is an "Open Recent" menu. * `clearRecentDocuments` - Map to the `clearRecentDocuments` action. +* `shareMenu` - The submenu is [share menu][ShareMenu]. The `sharingItem` property must also be set to indicate the item to share. When specifying a `role` on macOS, `label` and `accelerator` are the only options that will affect the menu item. All other options will be ignored. Lowercase `role`, e.g. `toggledevtools`, is still supported. -**Nota Bene:** The `enabled` and `visibility` properties are not available for top-level menu items in the tray on MacOS. +**Note:** The `enabled` and `visibility` properties are not available for top-level menu items in the tray on macOS. ### Instance Properties @@ -125,18 +132,18 @@ The following properties are available on instances of `MenuItem`: #### `menuItem.id` -A `String` indicating the item's unique id, this property can be +A `string` indicating the item's unique id, this property can be dynamically changed. #### `menuItem.label` -A `String` indicating the item's visible label, this property can be -dynamically changed. +A `string` indicating the item's visible label. #### `menuItem.click` A `Function` that is fired when the MenuItem receives a click event. It can be called with `menuItem.click(event, focusedWindow, focusedWebContents)`. + * `event` [KeyboardEvent](structures/keyboard-event.md) * `focusedWindow` [BrowserWindow](browser-window.md) * `focusedWebContents` [WebContents](web-contents.md) @@ -148,42 +155,48 @@ item's submenu, if present. #### `menuItem.type` -A `String` indicating the type of the item. Can be `normal`, `separator`, `submenu`, `checkbox` or `radio`. +A `string` indicating the type of the item. Can be `normal`, `separator`, `submenu`, `checkbox` or `radio`. #### `menuItem.role` -A `String` (optional) indicating the item's role, if set. Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteandmatchstyle`, `delete`, `selectall`, `reload`, `forcereload`, `toggledevtools`, `resetzoom`, `zoomin`, `zoomout`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideothers`, `unhide`, `quit`, `startspeaking`, `stopspeaking`, `close`, `minimize`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu` or `windowMenu` +A `string` (optional) indicating the item's role, if set. Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteAndMatchStyle`, `delete`, `selectAll`, `reload`, `forceReload`, `toggleDevTools`, `resetZoom`, `zoomIn`, `zoomOut`, `toggleSpellChecker`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideOthers`, `unhide`, `quit`, `startSpeaking`, `stopSpeaking`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu`, `shareMenu`, `recentDocuments`, `toggleTabBar`, `selectNextTab`, `selectPreviousTab`, `mergeAllWindows`, `clearRecentDocuments`, `moveTabToNewWindow` or `windowMenu` #### `menuItem.accelerator` -A `Accelerator` (optional) indicating the item's accelerator, if set. +An `Accelerator` (optional) indicating the item's accelerator, if set. + +#### `menuItem.userAccelerator` _Readonly_ _macOS_ + +An `Accelerator | null` indicating the item's [user-assigned accelerator](https://developer.apple.com/documentation/appkit/nsmenuitem/1514850-userkeyequivalent?language=objc) for the menu item. + +**Note:** This property is only initialized after the `MenuItem` has been added to a `Menu`. Either via `Menu.buildFromTemplate` or via `Menu.append()/insert()`. Accessing before initialization will just return `null`. #### `menuItem.icon` -A `NativeImage | String` (optional) indicating the +A `NativeImage | string` (optional) indicating the item's icon, if set. #### `menuItem.sublabel` -A `String` indicating the item's sublabel, this property can be dynamically changed. +A `string` indicating the item's sublabel. #### `menuItem.toolTip` _macOS_ -A `String` indicating the item's hover text. +A `string` indicating the item's hover text. #### `menuItem.enabled` -A `Boolean` indicating whether the item is enabled, this property can be +A `boolean` indicating whether the item is enabled, this property can be dynamically changed. #### `menuItem.visible` -A `Boolean` indicating whether the item is visible, this property can be +A `boolean` indicating whether the item is visible, this property can be dynamically changed. #### `menuItem.checked` -A `Boolean` indicating whether the item is checked, this property can be +A `boolean` indicating whether the item is checked, this property can be dynamically changed. A `checkbox` menu item will toggle the `checked` property on and off when @@ -196,13 +209,23 @@ You can add a `click` function for additional behavior. #### `menuItem.registerAccelerator` -A `Boolean` indicating if the accelerator should be registered with the -system or just displayed, this property can be dynamically changed. +A `boolean` indicating if the accelerator should be registered with the +system or just displayed. + +This property can be dynamically changed. + +#### `menuItem.sharingItem` _macOS_ + +A `SharingItem` indicating the item to share when the `role` is `shareMenu`. + +This property can be dynamically changed. #### `menuItem.commandId` -A `Number` indicating an item's sequential unique id. +A `number` indicating an item's sequential unique id. #### `menuItem.menu` A `Menu` that the item is a part of. + +[ShareMenu]: https://developer.apple.com/design/human-interface-guidelines/macos/extensions/share-extensions/ diff --git a/docs/api/menu.md b/docs/api/menu.md index cd9e177ae08da..ec5d46100e991 100644 --- a/docs/api/menu.md +++ b/docs/api/menu.md @@ -1,3 +1,5 @@ +# Menu + ## Class: Menu > Create native application menus and context menus. @@ -22,8 +24,10 @@ Sets `menu` as the application menu on macOS. On Windows and Linux, the Also on Windows and Linux, you can use a `&` in the top-level item name to indicate which letter should get a generated accelerator. For example, using `&File` for the file menu would result in a generated `Alt-F` accelerator that -opens the associated menu. The indicated character in the button label gets an -underline. The `&` character is not displayed on the button label. +opens the associated menu. The indicated character in the button label then gets an +underline, and the `&` character is not displayed on the button label. + +In order to escape the `&` character in an item name, add a proceeding `&`. For example, `&&File` would result in `&File` displayed on the button label. Passing `null` will suppress the default menu. On Windows and Linux, this has the additional effect of removing the menu bar from the window. @@ -41,7 +45,7 @@ be dynamically modified. #### `Menu.sendActionToFirstResponder(action)` _macOS_ -* `action` String +* `action` string Sends the `action` to the first responder of application. This is used for emulating default macOS menu behaviors. Usually you would use the @@ -69,11 +73,11 @@ The `menu` object has the following instance methods: * `options` Object (optional) * `window` [BrowserWindow](browser-window.md) (optional) - Default is the focused window. - * `x` Number (optional) - Default is the current mouse cursor position. + * `x` number (optional) - Default is the current mouse cursor position. Must be declared if `y` is declared. - * `y` Number (optional) - Default is the current mouse cursor position. + * `y` number (optional) - Default is the current mouse cursor position. Must be declared if `x` is declared. - * `positioningItem` Number (optional) _macOS_ - The index of the menu item to + * `positioningItem` number (optional) _macOS_ - The index of the menu item to be positioned under the mouse cursor at the specified coordinates. Default is -1. * `callback` Function (optional) - Called when menu is closed. @@ -94,9 +98,9 @@ Appends the `menuItem` to the menu. #### `menu.getMenuItemById(id)` -* `id` String +* `id` string -Returns `MenuItem` the item with the specified `id` +Returns `MenuItem | null` the item with the specified `id` #### `menu.insert(pos, menuItem)` @@ -141,13 +145,7 @@ can have a submenu. ## Examples -The `Menu` class is only available in the main process, but you can also use it -in the render process via the [`remote`](remote.md) module. - -### Main process - -An example of creating the application menu in the main process with the -simple template API: +An example of creating the application menu with the simple template API: ```javascript const { app, Menu } = require('electron') @@ -164,7 +162,7 @@ const template = [ { role: 'services' }, { type: 'separator' }, { role: 'hide' }, - { role: 'hideothers' }, + { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } @@ -195,8 +193,8 @@ const template = [ { label: 'Speech', submenu: [ - { role: 'startspeaking' }, - { role: 'stopspeaking' } + { role: 'startSpeaking' }, + { role: 'stopSpeaking' } ] } ] : [ @@ -211,12 +209,12 @@ const template = [ label: 'View', submenu: [ { role: 'reload' }, - { role: 'forcereload' }, - { role: 'toggledevtools' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, { type: 'separator' }, - { role: 'resetzoom' }, - { role: 'zoomin' }, - { role: 'zoomout' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' } ] @@ -257,26 +255,36 @@ Menu.setApplicationMenu(menu) ### Render process -Below is an example of creating a menu dynamically in a web page -(render process) by using the [`remote`](remote.md) module, and showing it when -the user right clicks the page: - -```html -<!-- index.html --> -<script> -const { remote } = require('electron') -const { Menu, MenuItem } = remote +To create menus initiated by the renderer process, send the required +information to the main process using IPC and have the main process display the +menu on behalf of the renderer. -const menu = new Menu() -menu.append(new MenuItem({ label: 'MenuItem1', click() { console.log('item 1 clicked') } })) -menu.append(new MenuItem({ type: 'separator' })) -menu.append(new MenuItem({ label: 'MenuItem2', type: 'checkbox', checked: true })) +Below is an example of showing a menu when the user right clicks the page: +```js +// renderer window.addEventListener('contextmenu', (e) => { e.preventDefault() - menu.popup({ window: remote.getCurrentWindow() }) -}, false) -</script> + ipcRenderer.send('show-context-menu') +}) + +ipcRenderer.on('context-menu-command', (e, command) => { + // ... +}) + +// main +ipcMain.on('show-context-menu', (event) => { + const template = [ + { + label: 'Menu Item 1', + click: () => { event.sender.send('context-menu-command', 'menu-item-1') } + }, + { type: 'separator' }, + { label: 'Menu Item 2', type: 'checkbox', checked: true } + ] + const menu = Menu.buildFromTemplate(template) + menu.popup(BrowserWindow.fromWebContents(event.sender)) +}) ``` ## Notes on macOS Application Menu @@ -397,4 +405,4 @@ Menu: ``` [AboutInformationPropertyListFiles]: https://developer.apple.com/library/ios/documentation/general/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html -[setMenu]: https://github.com/electron/electron/blob/master/docs/api/browser-window.md#winsetmenumenu-linux-windows +[setMenu]: browser-window.md#winsetmenumenu-linux-windows diff --git a/docs/api/message-channel-main.md b/docs/api/message-channel-main.md new file mode 100644 index 0000000000000..670cda868f599 --- /dev/null +++ b/docs/api/message-channel-main.md @@ -0,0 +1,46 @@ +# MessageChannelMain + +`MessageChannelMain` is the main-process-side equivalent of the DOM +[`MessageChannel`][] object. Its singular function is to create a pair of +connected [`MessagePortMain`](message-port-main.md) objects. + +See the [Channel Messaging API][] documentation for more information on using +channel messaging. + +## Class: MessageChannelMain + +> Channel interface for channel messaging in the main process. + +Process: [Main](../glossary.md#main-process) + +Example: + +```js +// Main process +const { MessageChannelMain } = require('electron') +const { port1, port2 } = new MessageChannelMain() +w.webContents.postMessage('port', null, [port2]) +port1.postMessage({ some: 'message' }) + +// Renderer process +const { ipcRenderer } = require('electron') +ipcRenderer.on('port', (e) => { + // e.ports is a list of ports sent along with this message + e.ports[0].on('message', (messageEvent) => { + console.log(messageEvent.data) + }) +}) +``` + +### Instance Properties + +#### `channel.port1` + +A [`MessagePortMain`](message-port-main.md) property. + +#### `channel.port2` + +A [`MessagePortMain`](message-port-main.md) property. + +[`MessageChannel`]: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel +[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API diff --git a/docs/api/message-port-main.md b/docs/api/message-port-main.md new file mode 100644 index 0000000000000..5309843916653 --- /dev/null +++ b/docs/api/message-port-main.md @@ -0,0 +1,58 @@ +# MessagePortMain + +`MessagePortMain` is the main-process-side equivalent of the DOM +[`MessagePort`][] object. It behaves similarly to the DOM version, with the +exception that it uses the Node.js `EventEmitter` event system, instead of the +DOM `EventTarget` system. This means you should use `port.on('message', ...)` +to listen for events, instead of `port.onmessage = ...` or +`port.addEventListener('message', ...)` + +See the [Channel Messaging API][] documentation for more information on using +channel messaging. + +`MessagePortMain` is an [EventEmitter][event-emitter]. + +## Class: MessagePortMain + +> Port interface for channel messaging in the main process. + +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ + +### Instance Methods + +#### `port.postMessage(message, [transfer])` + +* `message` any +* `transfer` MessagePortMain[] (optional) + +Sends a message from the port, and optionally, transfers ownership of objects +to other browsing contexts. + +#### `port.start()` + +Starts the sending of messages queued on the port. Messages will be queued +until this method is called. + +#### `port.close()` + +Disconnects the port, so it is no longer active. + +### Instance Events + +#### Event: 'message' + +Returns: + +* `messageEvent` Object + * `data` any + * `ports` MessagePortMain[] + +Emitted when a MessagePortMain object receives a message. + +#### Event: 'close' + +Emitted when the remote end of a MessagePortMain object becomes disconnected. + +[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API diff --git a/docs/api/modernization/overview.md b/docs/api/modernization/overview.md deleted file mode 100644 index f0df4d9994768..0000000000000 --- a/docs/api/modernization/overview.md +++ /dev/null @@ -1,10 +0,0 @@ -## Modernization - -The Electron team is currently undergoing an initiative to modernize our API in a few concrete ways. These include: updating our modules to use idiomatic JS properties instead of separate `getPropertyX` and `setpropertyX`, converting callbacks to promises, and removing some other anti-patterns present in our APIs. The current status of the Promise intiative can be tracked in the [promisification](promisification.md) tracking file. - -As we work to perform these updates, we seek to create the least disruptive amount of change at any given time, so as many changes as possible will be introduced in a backward compatible manner and deprecated after enough time has passed to give users a chance to upgrade their API calls. - -This document and its child documents will be updated to reflect the latest status of our API changes. - -* [Promisification](promisification.md) -* [Property Updates](property-updates.md) diff --git a/docs/api/modernization/promisification.md b/docs/api/modernization/promisification.md deleted file mode 100644 index a56c73966117e..0000000000000 --- a/docs/api/modernization/promisification.md +++ /dev/null @@ -1,53 +0,0 @@ -## Promisification - -The Electron team is currently undergoing an initiative to convert callback-based functions in Electron to return Promises. During this transition period, both the callback and Promise-based versions of these functions will work correctly, and will both be documented. - -To enable deprecation warnings for these updated functions, use the `process.enablePromiseAPI` runtime flag. - -When a majority of affected functions are migrated, this flag will be enabled by default and all developers will be able to see these deprecation warnings. At that time, the callback-based versions will also be removed from documentation. This document will be continuously updated as more functions are converted. - -### Candidate Functions - -- [app.importCertificate(options, callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#importCertificate) -- [contents.print([options], [callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#print) - -### Converted Functions - -- [app.getFileIcon(path[, options], callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#getFileIcon) -- [contents.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#capturePage) -- [contents.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#executeJavaScript) -- [contents.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#printToPDF) -- [contents.savePage(fullPath, saveType, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#savePage) -- [contentTracing.getCategories(callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#getCategories) -- [contentTracing.startRecording(options, callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#startRecording) -- [contentTracing.stopRecording(resultFilePath, callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#stopRecording) -- [contentTracing.getTraceBufferUsage(callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#getTraceBufferUsage) -- [cookies.flushStore(callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#flushStore) -- [cookies.get(filter, callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#get) -- [cookies.remove(url, name, callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#remove) -- [cookies.set(details, callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#set) -- [debugger.sendCommand(method[, commandParams, callback])](https://github.com/electron/electron/blob/master/docs/api/debugger.md#sendCommand) -- [desktopCapturer.getSources(options, callback)](https://github.com/electron/electron/blob/master/docs/api/desktop-capturer.md#getSources) -- [dialog.showOpenDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showOpenDialog) -- [dialog.showSaveDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showSaveDialog) -- [inAppPurchase.purchaseProduct(productID, quantity, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#purchaseProduct) -- [inAppPurchase.getProducts(productIDs, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#getProducts) -- [dialog.showMessageBox([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showMessageBox) -- [dialog.showCertificateTrustDialog([browserWindow, ]options, callback)](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showCertificateTrustDialog) -- [netLog.stopLogging([callback])](https://github.com/electron/electron/blob/master/docs/api/net-log.md#stopLogging) -- [protocol.isProtocolHandled(scheme, callback)](https://github.com/electron/electron/blob/master/docs/api/protocol.md#isProtocolHandled) -- [ses.clearHostResolverCache([callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearHostResolverCache) -- [ses.clearStorageData([options, callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearStorageData) -- [ses.setProxy(config, callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#setProxy) -- [ses.resolveProxy(url, callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#resolveProxy) -- [ses.getCacheSize(callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#getCacheSize) -- [ses.clearAuthCache(options[, callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearAuthCache) -- [ses.clearCache(callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#clearCache) -- [ses.getBlobData(identifier, callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#getBlobData) -- [shell.openExternal(url[, options, callback])](https://github.com/electron/electron/blob/master/docs/api/shell.md#openExternal) -- [webFrame.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScript) -- [webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScriptInIsolatedWorld) -- [webviewTag.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#capturePage) -- [webviewTag.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#executeJavaScript) -- [webviewTag.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#printToPDF) -- [win.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/browser-window.md#capturePage) diff --git a/docs/api/modernization/property-updates.md b/docs/api/modernization/property-updates.md deleted file mode 100644 index 7df49c26a43f2..0000000000000 --- a/docs/api/modernization/property-updates.md +++ /dev/null @@ -1,53 +0,0 @@ -## Property Updates - -The Electron team is currently undergoing an initiative to convert separate getter and setter functions in Electron to bespoke properties with `get` and `set` functionality. During this transition period, both the new properties and old getters and setters of these functions will work correctly and be documented. - -## Candidates - -* `BrowserWindow` - * `fullscreen` - * `simpleFullscreen` - * `alwaysOnTop` - * `title` - * `documentEdited` - * `hasShadow` - * `menubarVisible` - * `visibleOnAllWorkspaces` -* `crashReporter` module - * `uploadToServer` -* `webFrame` modules - * `zoomFactor` - * `zoomLevel` - * `audioMuted` -* `<webview>` - * `zoomFactor` - * `zoomLevel` - * `audioMuted` - -## Converted Properties - -* `app` module - * `accessibilitySupport` - * `applicationMenu` - * `badgeCount` - * `name` -* `DownloadItem` class - * `savePath` -* `BrowserWindow` module - * `autohideMenuBar` - * `resizable` - * `maximizable` - * `minimizable` - * `fullscreenable` - * `movable` - * `closable` -* `NativeImage` - * `isMacTemplateImage` -* `SystemPreferences` module - * `appLevelAppearance` -* `webContents` module - * `audioMuted` - * `frameRate` - * `userAgent` - * `zoomFactor` - * `zoomLevel` diff --git a/docs/api/native-image.md b/docs/api/native-image.md index ef67bd6d73653..a4d969d094e27 100644 --- a/docs/api/native-image.md +++ b/docs/api/native-image.md @@ -8,17 +8,17 @@ In Electron, for the APIs that take images, you can pass either file paths or `NativeImage` instances. An empty image will be used when `null` is passed. For example, when creating a tray or setting a window's icon, you can pass an -image file path as a `String`: +image file path as a `string`: ```javascript const { BrowserWindow, Tray } = require('electron') const appIcon = new Tray('/Users/somebody/images/icon.png') -let win = new BrowserWindow({ icon: '/Users/somebody/images/window.png' }) +const win = new BrowserWindow({ icon: '/Users/somebody/images/window.png' }) console.log(appIcon, win) ``` -Or read the image from the clipboard which returns a `NativeImage`: +Or read the image from the clipboard, which returns a `NativeImage`: ```javascript const { clipboard, Tray } = require('electron') @@ -33,19 +33,19 @@ Currently `PNG` and `JPEG` image formats are supported. `PNG` is recommended because of its support for transparency and lossless compression. On Windows, you can also load `ICO` icons from file paths. For best visual -quality it is recommended to include at least the following sizes in the: +quality, it is recommended to include at least the following sizes in the: * Small icon - * 16x16 (100% DPI scale) - * 20x20 (125% DPI scale) - * 24x24 (150% DPI scale) - * 32x32 (200% DPI scale) + * 16x16 (100% DPI scale) + * 20x20 (125% DPI scale) + * 24x24 (150% DPI scale) + * 32x32 (200% DPI scale) * Large icon - * 32x32 (100% DPI scale) - * 40x40 (125% DPI scale) - * 48x48 (150% DPI scale) - * 64x64 (200% DPI scale) -* 256x256 + * 32x32 (100% DPI scale) + * 40x40 (125% DPI scale) + * 48x48 (150% DPI scale) + * 64x64 (200% DPI scale) + * 256x256 Check the *Size requirements* section in [this article][icons]. @@ -56,7 +56,7 @@ Check the *Size requirements* section in [this article][icons]. On platforms that have high-DPI support such as Apple Retina displays, you can append `@2x` after image's base filename to mark it as a high resolution image. -For example if `icon.png` is a normal image that has standard resolution, then +For example, if `icon.png` is a normal image that has standard resolution, then `icon@2x.png` will be treated as a high resolution image that has double DPI density. @@ -73,11 +73,11 @@ images/ ```javascript const { Tray } = require('electron') -let appIcon = new Tray('/Users/somebody/images/icon.png') +const appIcon = new Tray('/Users/somebody/images/icon.png') console.log(appIcon) ``` -Following suffixes for DPI are also supported: +The following suffixes for DPI are also supported: * `@1x` * `@1.25x` @@ -97,7 +97,7 @@ Template images consist of black and an alpha channel. Template images are not intended to be used as standalone images and are usually mixed with other content to create the desired final appearance. -The most common case is to use template images for a menu bar icon so it can +The most common case is to use template images for a menu bar icon, so it can adapt to both light and dark menu bars. **Note:** Template image is only supported on macOS. @@ -119,9 +119,16 @@ Returns `NativeImage` Creates an empty `NativeImage` instance. +### `nativeImage.createThumbnailFromPath(path, maxSize)` _macOS_ _Windows_ + +* `path` string - path to a file that we intend to construct a thumbnail out of. +* `maxSize` [Size](structures/size.md) - the maximum width and height (positive numbers) the thumbnail returned can be. The Windows implementation will ignore `maxSize.height` and scale the height according to `maxSize.width`. + +Returns `Promise<NativeImage>` - fulfilled with the file's thumbnail preview image, which is a [NativeImage](native-image.md). + ### `nativeImage.createFromPath(path)` -* `path` String +* `path` string Returns `NativeImage` @@ -132,7 +139,7 @@ a valid image. ```javascript const nativeImage = require('electron').nativeImage -let image = nativeImage.createFromPath('/Users/somebody/images/icon.png') +const image = nativeImage.createFromPath('/Users/somebody/images/icon.png') console.log(image) ``` @@ -163,7 +170,7 @@ Creates a new `NativeImage` instance from `buffer`. Tries to decode as PNG or JP ### `nativeImage.createFromDataURL(dataURL)` -* `dataURL` String +* `dataURL` string Returns `NativeImage` @@ -171,8 +178,8 @@ Creates a new `NativeImage` instance from `dataURL`. ### `nativeImage.createFromNamedImage(imageName[, hslShift])` _macOS_ -* `imageName` String -* `hslShift` Number[] (optional) +* `imageName` string +* `hslShift` number[] (optional) Returns `NativeImage` @@ -180,7 +187,8 @@ Creates a new `NativeImage` instance from the NSImage that maps to the given image name. See [`System Icons`](https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/system-icons/) for a list of possible values. -The `hslShift` is applied to the image with the following rules +The `hslShift` is applied to the image with the following rules: + * `hsl_shift[0]` (hue): The absolute hue value for the image - 0 and 1 map to 0 and 360 on the hue color wheel (red). * `hsl_shift[1]` (saturation): A saturation shift for the image, with the @@ -207,7 +215,8 @@ where `SYSTEM_IMAGE_NAME` should be replaced with any value from [this list](htt > Natively wrap images such as tray, dock, and application icons. -Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process) +Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ ### Instance Methods @@ -239,7 +248,7 @@ data. * `options` Object (optional) * `scaleFactor` Double (optional) - Defaults to 1.0. -Returns `String` - The data URL of the image. +Returns `string` - The data URL of the image. #### `image.getBitmap([options])` @@ -248,9 +257,9 @@ Returns `String` - The data URL of the image. Returns `Buffer` - A [Buffer][buffer] that contains the image's raw bitmap pixel data. -The difference between `getBitmap()` and `toBitmap()` is, `getBitmap()` does not +The difference between `getBitmap()` and `toBitmap()` is that `getBitmap()` does not copy the bitmap data, so you have to use the returned Buffer immediately in -current event loop tick, otherwise the data might be changed or destroyed. +current event loop tick; otherwise the data might be changed or destroyed. #### `image.getNativeHandle()` _macOS_ @@ -263,25 +272,25 @@ image instead of a copy, so you _must_ ensure that the associated #### `image.isEmpty()` -Returns `Boolean` - Whether the image is empty. +Returns `boolean` - Whether the image is empty. + +#### `image.getSize([scaleFactor])` -#### `image.getSize()` +* `scaleFactor` Double (optional) - Defaults to 1.0. -Returns [`Size`](structures/size.md) +Returns [`Size`](structures/size.md). + +If `scaleFactor` is passed, this will return the size corresponding to the image representation most closely matching the passed value. #### `image.setTemplateImage(option)` -* `option` Boolean +* `option` boolean Marks the image as a template image. -**[Deprecated](modernization/property-updates.md)** - #### `image.isTemplateImage()` -Returns `Boolean` - Whether the image is a template image. - -**[Deprecated](modernization/property-updates.md)** +Returns `boolean` - Whether the image is a template image. #### `image.crop(rect)` @@ -294,8 +303,8 @@ Returns `NativeImage` - The cropped image. * `options` Object * `width` Integer (optional) - Defaults to the image's width. * `height` Integer (optional) - Defaults to the image's height. - * `quality` String (optional) - The desired quality of the resize image. - Possible values are `good`, `better` or `best`. The default is `best`. + * `quality` string (optional) - The desired quality of the resize image. + Possible values are `good`, `better`, or `best`. The default is `best`. These values express a desired quality/speed tradeoff. They are translated into an algorithm-specific method that depends on the capabilities (CPU, GPU) of the underlying platform. It is possible for all three methods @@ -306,10 +315,18 @@ Returns `NativeImage` - The resized image. If only the `height` or the `width` are specified then the current aspect ratio will be preserved in the resized image. -#### `image.getAspectRatio()` +#### `image.getAspectRatio([scaleFactor])` + +* `scaleFactor` Double (optional) - Defaults to 1.0. Returns `Float` - The image's aspect ratio. +If `scaleFactor` is passed, this will return the aspect ratio corresponding to the image representation most closely matching the passed value. + +#### `image.getScaleFactors()` + +Returns `Float[]` - An array of all scale factors corresponding to representations for a given nativeImage. + #### `image.addRepresentation(options)` * `options` Object @@ -319,7 +336,7 @@ Returns `Float` - The image's aspect ratio. * `height` Integer (optional) - Defaults to 0. Required if a bitmap buffer is specified as `buffer`. * `buffer` Buffer (optional) - The buffer containing the raw image data. - * `dataURL` String (optional) - The data URL containing either a base 64 + * `dataURL` string (optional) - The data URL containing either a base 64 encoded PNG or JPEG image. Add an image representation for a specific scale factor. This can be used @@ -328,10 +345,10 @@ can be called on empty images. [buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer -## Properties +### Instance Properties -### `nativeImage.isMacTemplateImage` _macOS_ +#### `nativeImage.isMacTemplateImage` _macOS_ -A `Boolean` property that determines whether the image is considered a [template image](https://developer.apple.com/documentation/appkit/nsimage/1520017-template). +A `boolean` property that determines whether the image is considered a [template image](https://developer.apple.com/documentation/appkit/nsimage/1520017-template). Please note that this property only has an effect on macOS. diff --git a/docs/api/native-theme.md b/docs/api/native-theme.md new file mode 100644 index 0000000000000..3235eba9a78da --- /dev/null +++ b/docs/api/native-theme.md @@ -0,0 +1,74 @@ +# nativeTheme + +> Read and respond to changes in Chromium's native color theme. + +Process: [Main](../glossary.md#main-process) + +## Events + +The `nativeTheme` module emits the following events: + +### Event: 'updated' + +Emitted when something in the underlying NativeTheme has changed. This normally +means that either the value of `shouldUseDarkColors`, +`shouldUseHighContrastColors` or `shouldUseInvertedColorScheme` has changed. +You will have to check them to determine which one has changed. + +## Properties + +The `nativeTheme` module has the following properties: + +### `nativeTheme.shouldUseDarkColors` _Readonly_ + +A `boolean` for if the OS / Chromium currently has a dark mode enabled or is +being instructed to show a dark-style UI. If you want to modify this value you +should use `themeSource` below. + +### `nativeTheme.themeSource` + +A `string` property that can be `system`, `light` or `dark`. It is used to override and supersede +the value that Chromium has chosen to use internally. + +Setting this property to `system` will remove the override and +everything will be reset to the OS default. By default `themeSource` is `system`. + +Settings this property to `dark` will have the following effects: + +* `nativeTheme.shouldUseDarkColors` will be `true` when accessed +* Any UI Electron renders on Linux and Windows including context menus, devtools, etc. will use the dark UI. +* Any UI the OS renders on macOS including menus, window frames, etc. will use the dark UI. +* The [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) CSS query will match `dark` mode. +* The `updated` event will be emitted + +Settings this property to `light` will have the following effects: + +* `nativeTheme.shouldUseDarkColors` will be `false` when accessed +* Any UI Electron renders on Linux and Windows including context menus, devtools, etc. will use the light UI. +* Any UI the OS renders on macOS including menus, window frames, etc. will use the light UI. +* The [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) CSS query will match `light` mode. +* The `updated` event will be emitted + +The usage of this property should align with a classic "dark mode" state machine in your application +where the user has three options. + +* `Follow OS` --> `themeSource = 'system'` +* `Dark Mode` --> `themeSource = 'dark'` +* `Light Mode` --> `themeSource = 'light'` + +Your application should then always use `shouldUseDarkColors` to determine what CSS to apply. + +### `nativeTheme.shouldUseHighContrastColors` _macOS_ _Windows_ _Readonly_ + +A `boolean` for if the OS / Chromium currently has high-contrast mode enabled +or is being instructed to show a high-contrast UI. + +### `nativeTheme.shouldUseInvertedColorScheme` _macOS_ _Windows_ _Readonly_ + +A `boolean` for if the OS / Chromium currently has an inverted color scheme +or is being instructed to use an inverted color scheme. + +### `nativeTheme.inForcedColorsMode` _Windows_ _Readonly_ + +A `boolean` indicating whether Chromium is in forced colors mode, controlled by system accessibility settings. +Currently, Windows high contrast is the only system setting that triggers forced colors mode. diff --git a/docs/api/net-log.md b/docs/api/net-log.md index bdf1ade1586a2..fec5f3d14432a 100644 --- a/docs/api/net-log.md +++ b/docs/api/net-log.md @@ -7,7 +7,7 @@ Process: [Main](../glossary.md#main-process) ```javascript const { netLog } = require('electron') -app.on('ready', async () => { +app.whenReady().then(async () => { await netLog.startLogging('/path/to/net-log') // After some network events const path = await netLog.stopLogging() @@ -15,7 +15,7 @@ app.on('ready', async () => { }) ``` -See [`--log-net-log`](chrome-command-line-switches.md#--log-net-logpath) to log network events throughout the app's lifecycle. +See [`--log-net-log`](command-line-switches.md#--log-net-logpath) to log network events throughout the app's lifecycle. **Note:** All methods unless specified can only be used after the `ready` event of the `app` module gets emitted. @@ -24,14 +24,14 @@ of the `app` module gets emitted. ### `netLog.startLogging(path[, options])` -* `path` String - File path to record network logs. +* `path` string - File path to record network logs. * `options` Object (optional) - * `captureMode` String (optional) - What kinds of data should be captured. By + * `captureMode` string (optional) - What kinds of data should be captured. By default, only metadata about requests will be captured. Setting this to `includeSensitive` will include cookies and authentication data. Setting it to `everything` will include all bytes transferred on sockets. Can be `default`, `includeSensitive` or `everything`. - * `maxFileSize` Number (optional) - When the log grows beyond this size, + * `maxFileSize` number (optional) - When the log grows beyond this size, logging will automatically stop. Defaults to unlimited. Returns `Promise<void>` - resolves when the net log has begun recording. @@ -40,7 +40,7 @@ Starts recording network events to `path`. ### `netLog.stopLogging()` -Returns `Promise<String>` - resolves with a file path to which network logs were recorded. +Returns `Promise<void>` - resolves when the net log has been flushed to disk. Stops recording network events. If not called, net logging will automatically end when app quits. @@ -48,8 +48,4 @@ Stops recording network events. If not called, net logging will automatically en ### `netLog.currentlyLogging` _Readonly_ -A `Boolean` property that indicates whether network logs are recorded. - -### `netLog.currentlyLoggingPath` _Readonly_ _Deprecated_ - -A `String` property that returns the path to the current log file. +A `boolean` property that indicates whether network logs are currently being recorded. diff --git a/docs/api/net.md b/docs/api/net.md index d0e530940dee2..ffbff49d054b4 100644 --- a/docs/api/net.md +++ b/docs/api/net.md @@ -8,7 +8,7 @@ The `net` module is a client-side API for issuing HTTP(S) requests. It is similar to the [HTTP](https://nodejs.org/api/http.html) and [HTTPS](https://nodejs.org/api/https.html) modules of Node.js but uses Chromium's native networking library instead of the Node.js implementation, -offering better support for web proxies. +offering better support for web proxies. It also supports checking network status. The following is a non-exhaustive list of why you may consider using the `net` module instead of the native Node.js modules: @@ -28,7 +28,7 @@ Example usage: ```javascript const { app } = require('electron') -app.on('ready', () => { +app.whenReady().then(() => { const { net } = require('electron') const request = net.request('https://github.com') request.on('response', (response) => { @@ -54,7 +54,7 @@ The `net` module has the following methods: ### `net.request(options)` -* `options` (Object | String) - The `ClientRequest` constructor options. +* `options` (ClientRequestConstructorOptions | string) - The `ClientRequest` constructor options. Returns [`ClientRequest`](./client-request.md) @@ -62,3 +62,25 @@ Creates a [`ClientRequest`](./client-request.md) instance using the provided `options` which are directly forwarded to the `ClientRequest` constructor. The `net.request` method would be used to issue both secure and insecure HTTP requests according to the specified protocol scheme in the `options` object. + +### `net.isOnline()` + +Returns `boolean` - Whether there is currently internet connection. + +A return value of `false` is a pretty strong indicator that the user +won't be able to connect to remote sites. However, a return value of +`true` is inconclusive; even if some link is up, it is uncertain +whether a particular connection attempt to a particular remote site +will be successful. + +## Properties + +### `net.online` _Readonly_ + +A `boolean` property. Whether there is currently internet connection. + +A return value of `false` is a pretty strong indicator that the user +won't be able to connect to remote sites. However, a return value of +`true` is inconclusive; even if some link is up, it is uncertain +whether a particular connection attempt to a particular remote site +will be successful. diff --git a/docs/api/notification.md b/docs/api/notification.md index 61a0c3aabf23e..9731efca08813 100644 --- a/docs/api/notification.md +++ b/docs/api/notification.md @@ -24,21 +24,24 @@ The `Notification` class has the following static methods: #### `Notification.isSupported()` -Returns `Boolean` - Whether or not desktop notifications are supported on the current system +Returns `boolean` - Whether or not desktop notifications are supported on the current system -### `new Notification([options])` _Experimental_ +### `new Notification([options])` * `options` Object (optional) - * `title` String - A title for the notification, which will be shown at the top of the notification window when it is shown. - * `subtitle` String (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title. - * `body` String - The body text of the notification, which will be displayed below the title or subtitle. - * `silent` Boolean (optional) - Whether or not to emit an OS notification noise when showing the notification. - * `icon` (String | [NativeImage](native-image.md)) (optional) - An icon to use in the notification. - * `hasReply` Boolean (optional) _macOS_ - Whether or not to add an inline reply option to the notification. - * `replyPlaceholder` String (optional) _macOS_ - The placeholder to write in the inline reply input field. - * `sound` String (optional) _macOS_ - The name of the sound file to play when the notification is shown. + * `title` string (optional) - A title for the notification, which will be shown at the top of the notification window when it is shown. + * `subtitle` string (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title. + * `body` string (optional) - The body text of the notification, which will be displayed below the title or subtitle. + * `silent` boolean (optional) - Whether or not to emit an OS notification noise when showing the notification. + * `icon` (string | [NativeImage](native-image.md)) (optional) - An icon to use in the notification. + * `hasReply` boolean (optional) _macOS_ - Whether or not to add an inline reply option to the notification. + * `timeoutType` string (optional) _Linux_ _Windows_ - The timeout duration of the notification. Can be 'default' or 'never'. + * `replyPlaceholder` string (optional) _macOS_ - The placeholder to write in the inline reply input field. + * `sound` string (optional) _macOS_ - The name of the sound file to play when the notification is shown. + * `urgency` string (optional) _Linux_ - The urgency level of the notification. Can be 'normal', 'critical', or 'low'. * `actions` [NotificationAction[]](structures/notification-action.md) (optional) _macOS_ - Actions to add to the notification. Please read the available actions and limitations in the `NotificationAction` documentation. - * `closeButtonText` String (optional) _macOS_ - A custom title for the close button of an alert. An empty string will cause the default localized text to be used. + * `closeButtonText` string (optional) _macOS_ - A custom title for the close button of an alert. An empty string will cause the default localized text to be used. + * `toastXml` string (optional) _Windows_ - A custom description of the Notification on Windows superseding all properties above. Provides full customization of design and behavior of the notification. ### Instance Events @@ -81,7 +84,7 @@ is closed. Returns: * `event` Event -* `reply` String - The string the user entered into the inline reply field. +* `reply` string - The string the user entered into the inline reply field. Emitted when the user clicks the "Reply" button on a notification with `hasReply: true`. @@ -90,7 +93,16 @@ Emitted when the user clicks the "Reply" button on a notification with `hasReply Returns: * `event` Event -* `index` Number - The index of the action that was activated. +* `index` number - The index of the action that was activated. + +#### Event: 'failed' _Windows_ + +Returns: + +* `event` Event +* `error` string - The error encountered during execution of the `show()` method. + +Emitted when an error is encountered while creating and showing the native notification. ### Instance Methods @@ -114,40 +126,56 @@ Dismisses the notification. #### `notification.title` -A `String` property representing the title of the notification. +A `string` property representing the title of the notification. #### `notification.subtitle` -A `String` property representing the subtitle of the notification. +A `string` property representing the subtitle of the notification. #### `notification.body` -A `String` property representing the body of the notification. +A `string` property representing the body of the notification. #### `notification.replyPlaceholder` -A `String` property representing the reply placeholder of the notification. +A `string` property representing the reply placeholder of the notification. #### `notification.sound` -A `String` property representing the sound of the notification. +A `string` property representing the sound of the notification. #### `notification.closeButtonText` -A `String` property representing the close button text of the notification. +A `string` property representing the close button text of the notification. #### `notification.silent` -A `Boolean` property representing whether the notification is silent. +A `boolean` property representing whether the notification is silent. #### `notification.hasReply` -A `Boolean` property representing whether the notification has a reply action. +A `boolean` property representing whether the notification has a reply action. + +#### `notification.urgency` _Linux_ + +A `string` property representing the urgency level of the notification. Can be 'normal', 'critical', or 'low'. + +Default is 'low' - see [NotifyUrgency](https://developer.gnome.org/notification-spec/#urgency-levels) for more information. + +#### `notification.timeoutType` _Linux_ _Windows_ + +A `string` property representing the type of timeout duration for the notification. Can be 'default' or 'never'. + +If `timeoutType` is set to 'never', the notification never expires. It stays open until closed by the calling API or the user. #### `notification.actions` A [`NotificationAction[]`](structures/notification-action.md) property representing the actions of the notification. +#### `notification.toastXml` _Windows_ + +A `string` property representing the custom Toast XML of the notification. + ### Playing Sounds On macOS, you can specify the name of the sound you'd like to play when the diff --git a/docs/api/power-monitor.md b/docs/api/power-monitor.md index a2bf7eb3035d5..1292ea1f05fce 100644 --- a/docs/api/power-monitor.md +++ b/docs/api/power-monitor.md @@ -4,22 +4,6 @@ Process: [Main](../glossary.md#main-process) - -This module cannot be used until the `ready` event of the `app` -module is emitted. - -For example: - -```javascript -const { app, powerMonitor } = require('electron') - -app.on('ready', () => { - powerMonitor.on('suspend', () => { - console.log('The system is going to sleep') - }) -}) -``` - ## Events The `powerMonitor` module emits the following events: @@ -32,11 +16,11 @@ Emitted when the system is suspending. Emitted when system is resuming. -### Event: 'on-ac' _Windows_ +### Event: 'on-ac' _macOS_ _Windows_ Emitted when the system changes to AC power. -### Event: 'on-battery' _Windows_ +### Event: 'on-battery' _macOS_ _Windows_ Emitted when system changes to battery power. @@ -55,6 +39,14 @@ Emitted when the system is about to lock the screen. Emitted as soon as the systems screen is unlocked. +### Event: 'user-did-become-active' _macOS_ + +Emitted when a login session is activated. See [documentation](https://developer.apple.com/documentation/appkit/nsworkspacesessiondidbecomeactivenotification?language=objc) for more information. + +### Event: 'user-did-resign-active' _macOS_ + +Emitted when a login session is deactivated. See [documentation](https://developer.apple.com/documentation/appkit/nsworkspacesessiondidresignactivenotification?language=objc) for more information. + ## Methods The `powerMonitor` module has the following methods: @@ -63,7 +55,7 @@ The `powerMonitor` module has the following methods: * `idleThreshold` Integer -Returns `String` - The system's current state. Can be `active`, `idle`, `locked` or `unknown`. +Returns `string` - The system's current state. Can be `active`, `idle`, `locked` or `unknown`. Calculate the system idle state. `idleThreshold` is the amount of time (in seconds) before considered idle. `locked` is available on supported systems only. @@ -73,3 +65,18 @@ before considered idle. `locked` is available on supported systems only. Returns `Integer` - Idle time in seconds Calculate system idle time in seconds. + +### `powerMonitor.isOnBatteryPower()` + +Returns `boolean` - Whether the system is on battery power. + +To monitor for changes in this property, use the `on-battery` and `on-ac` +events. + +## Properties + +### `powerMonitor.onBatteryPower` + +A `boolean` property. True if the system is on battery power. + +See [`powerMonitor.isOnBatteryPower()`](#powermonitorisonbatterypower). diff --git a/docs/api/power-save-blocker.md b/docs/api/power-save-blocker.md index 548b02756c678..257fc76f3da54 100644 --- a/docs/api/power-save-blocker.md +++ b/docs/api/power-save-blocker.md @@ -21,7 +21,7 @@ The `powerSaveBlocker` module has the following methods: ### `powerSaveBlocker.start(type)` -* `type` String - Power save blocker type. +* `type` string - Power save blocker type. * `prevent-app-suspension` - Prevent the application from being suspended. Keeps system active but allows screen to be turned off. Example use cases: downloading a file or playing audio. @@ -53,4 +53,4 @@ Stops the specified power save blocker. * `id` Integer - The power save blocker id returned by `powerSaveBlocker.start`. -Returns `Boolean` - Whether the corresponding `powerSaveBlocker` has started. +Returns `boolean` - Whether the corresponding `powerSaveBlocker` has started. diff --git a/docs/api/process.md b/docs/api/process.md index 92418975462cf..4573899912997 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -11,28 +11,32 @@ It adds the following events, properties, and methods: ## Sandbox In sandboxed renderers the `process` object contains only a subset of the APIs: -- `crash()` -- `hang()` -- `getCreationTime()` -- `getHeapStatistics()` -- `getBlinkMemoryInfo()` -- `getProcessMemoryInfo()` -- `getSystemMemoryInfo()` -- `getSystemVersion()` -- `getCPUUsage()` -- `getIOCounters()` -- `argv` -- `execPath` -- `env` -- `pid` -- `arch` -- `platform` -- `sandboxed` -- `type` -- `version` -- `versions` -- `mas` -- `windowsStore` + +* `crash()` +* `hang()` +* `getCreationTime()` +* `getHeapStatistics()` +* `getBlinkMemoryInfo()` +* `getProcessMemoryInfo()` +* `getSystemMemoryInfo()` +* `getSystemVersion()` +* `getCPUUsage()` +* `getIOCounters()` +* `uptime()` +* `argv` +* `execPath` +* `env` +* `pid` +* `arch` +* `platform` +* `sandboxed` +* `contextIsolated` +* `type` +* `version` +* `versions` +* `mas` +* `windowsStore` +* `contextId` ## Events @@ -41,97 +45,95 @@ In sandboxed renderers the `process` object contains only a subset of the APIs: Emitted when Electron has loaded its internal initialization script and is beginning to load the web page or the main script. -It can be used by the preload script to add removed Node global symbols back to -the global scope when node integration is turned off: - -```javascript -// preload.js -const _setImmediate = setImmediate -const _clearImmediate = clearImmediate -process.once('loaded', () => { - global.setImmediate = _setImmediate - global.clearImmediate = _clearImmediate -}) -``` - ## Properties ### `process.defaultApp` _Readonly_ -A `Boolean`. When app is started by being passed as parameter to the default app, this +A `boolean`. When app is started by being passed as parameter to the default app, this property is `true` in the main process, otherwise it is `undefined`. ### `process.isMainFrame` _Readonly_ -A `Boolean`, `true` when the current renderer context is the "main" renderer +A `boolean`, `true` when the current renderer context is the "main" renderer frame. If you want the ID of the current frame you should use `webFrame.routingId`. ### `process.mas` _Readonly_ -A `Boolean`. For Mac App Store build, this property is `true`, for other builds it is +A `boolean`. For Mac App Store build, this property is `true`, for other builds it is `undefined`. ### `process.noAsar` -A `Boolean` that controls ASAR support inside your application. Setting this to `true` +A `boolean` that controls ASAR support inside your application. Setting this to `true` will disable the support for `asar` archives in Node's built-in modules. ### `process.noDeprecation` -A `Boolean` that controls whether or not deprecation warnings are printed to `stderr`. +A `boolean` that controls whether or not deprecation warnings are printed to `stderr`. Setting this to `true` will silence deprecation warnings. This property is used instead of the `--no-deprecation` command line flag. -### `process.enablePromiseAPIs` - -A `Boolean` that controls whether or not deprecation warnings are printed to `stderr` when -formerly callback-based APIs converted to Promises are invoked using callbacks. Setting this to `true` -will enable deprecation warnings. - ### `process.resourcesPath` _Readonly_ -A `String` representing the path to the resources directory. +A `string` representing the path to the resources directory. ### `process.sandboxed` _Readonly_ -A `Boolean`. When the renderer process is sandboxed, this property is `true`, +A `boolean`. When the renderer process is sandboxed, this property is `true`, otherwise it is `undefined`. +### `process.contextIsolated` _Readonly_ + +A `boolean` that indicates whether the current renderer context has `contextIsolation` enabled. +It is `undefined` in the main process. + ### `process.throwDeprecation` -A `Boolean` that controls whether or not deprecation warnings will be thrown as +A `boolean` that controls whether or not deprecation warnings will be thrown as exceptions. Setting this to `true` will throw errors for deprecations. This property is used instead of the `--throw-deprecation` command line flag. ### `process.traceDeprecation` -A `Boolean` that controls whether or not deprecations printed to `stderr` include +A `boolean` that controls whether or not deprecations printed to `stderr` include their stack trace. Setting this to `true` will print stack traces for deprecations. This property is instead of the `--trace-deprecation` command line flag. ### `process.traceProcessWarnings` -A `Boolean` that controls whether or not process warnings printed to `stderr` include + +A `boolean` that controls whether or not process warnings printed to `stderr` include their stack trace. Setting this to `true` will print stack traces for process warnings (including deprecations). This property is instead of the `--trace-warnings` command line flag. ### `process.type` _Readonly_ -A `String` representing the current process's type, can be `"browser"` (i.e. main process), `"renderer"`, or `"worker"` (i.e. web worker). +A `string` representing the current process's type, can be: + +* `browser` - The main process +* `renderer` - A renderer process +* `worker` - In a web worker ### `process.versions.chrome` _Readonly_ -A `String` representing Chrome's version string. +A `string` representing Chrome's version string. ### `process.versions.electron` _Readonly_ -A `String` representing Electron's version string. +A `string` representing Electron's version string. ### `process.windowsStore` _Readonly_ -A `Boolean`. If the app is running as a Windows Store app (appx), this property is `true`, +A `boolean`. If the app is running as a Windows Store app (appx), this property is `true`, for otherwise it is `undefined`. +### `process.contextId` _Readonly_ + +A `string` (optional) representing a globally unique ID of the current JavaScript context. +Each frame has its own JavaScript context. When contextIsolation is enabled, the isolated +world also has a separate JavaScript context. +This property is only available in the renderer process. + ## Methods The `process` object has the following methods: @@ -142,7 +144,7 @@ Causes the main thread of the current process crash. ### `process.getCreationTime()` -Returns `Number | null` - The number of milliseconds since epoch, or `null` if the information is unavailable +Returns `number | null` - The number of milliseconds since epoch, or `null` if the information is unavailable Indicates the creation time of the application. The time is represented as number of milliseconds since epoch. It returns null if it is unable to get the process creation time. @@ -167,7 +169,7 @@ Returns `Object`: * `heapSizeLimit` Integer * `mallocedMemory` Integer * `peakMallocedMemory` Integer -* `doesZapGarbage` Boolean +* `doesZapGarbage` boolean Returns an object with V8 heap statistics. Note that all statistics are reported in Kilobytes. @@ -176,7 +178,6 @@ Returns an object with V8 heap statistics. Note that all statistics are reported Returns `Object`: * `allocated` Integer - Size of all allocated objects in Kilobytes. -* `marked` Integer - Size of all marked objects in Kilobytes. * `total` Integer - Total allocated space in Kilobytes. Returns an object with Blink memory information. @@ -215,21 +216,25 @@ that all statistics are reported in Kilobytes. ### `process.getSystemVersion()` -Returns `String` - The version of the host operating system. +Returns `string` - The version of the host operating system. -Examples: +Example: -* `macOS` -> `10.13.6` -* `Windows` -> `10.0.17763` -* `Linux` -> `4.15.0-45-generic` +```js +const version = process.getSystemVersion() +console.log(version) +// On macOS -> '10.13.6' +// On Windows -> '10.0.17763' +// On Linux -> '4.15.0-45-generic' +``` **Note:** It returns the actual operating system version instead of kernel version on macOS unlike `os.release()`. ### `process.takeHeapSnapshot(filePath)` -* `filePath` String - Path to the output file. +* `filePath` string - Path to the output file. -Returns `Boolean` - Indicates whether the snapshot has been created successfully. +Returns `boolean` - Indicates whether the snapshot has been created successfully. Takes a V8 heap snapshot and saves it to `filePath`. diff --git a/docs/api/protocol-ns.md b/docs/api/protocol-ns.md deleted file mode 100644 index 45a96f4511dac..0000000000000 --- a/docs/api/protocol-ns.md +++ /dev/null @@ -1,309 +0,0 @@ -# protocol (NetworkService) (Draft) - -This document describes the new protocol APIs based on the [NetworkService](https://www.chromium.org/servicification). - -We don't currently have an estimate of when we will enable the `NetworkService` by -default in Electron, but as Chromium is already removing non-`NetworkService` -code, we will probably switch before Electron 10. - -The content of this document should be moved to `protocol.md` after we have -enabled the `NetworkService` by default in Electron. - -> Register a custom protocol and intercept existing protocol requests. - -Process: [Main](../glossary.md#main-process) - -An example of implementing a protocol that has the same effect as the -`file://` protocol: - -```javascript -const { app, protocol } = require('electron') -const path = require('path') - -app.on('ready', () => { - protocol.registerFileProtocol('atom', (request, callback) => { - const url = request.url.substr(7) - callback({ path: path.normalize(`${__dirname}/${url}`) }) - }) -}) -``` - -**Note:** All methods unless specified can only be used after the `ready` event -of the `app` module gets emitted. - -## Using `protocol` with a custom `partition` or `session` - -A protocol is registered to a specific Electron [`session`](./session.md) -object. If you don't specify a session, then your `protocol` will be applied to -the default session that Electron uses. However, if you define a `partition` or -`session` on your `browserWindow`'s `webPreferences`, then that window will use -a different session and your custom protocol will not work if you just use -`electron.protocol.XXX`. - -To have your custom protocol work in combination with a custom session, you need -to register it to that session explicitly. - -```javascript -const { session, app, protocol } = require('electron') -const path = require('path') - -app.on('ready', () => { - const partition = 'persist:example' - const ses = session.fromPartition(partition) - - ses.protocol.registerFileProtocol('atom', (request, callback) => { - const url = request.url.substr(7) - callback({ path: path.normalize(`${__dirname}/${url}`) }) - }) - - mainWindow = new BrowserWindow({ webPreferences: { partition } }) -}) -``` - -## Methods - -The `protocol` module has the following methods: - -### `protocol.registerSchemesAsPrivileged(customSchemes)` - -* `customSchemes` [CustomScheme[]](structures/custom-scheme.md) - -**Note:** This method can only be used before the `ready` event of the `app` -module gets emitted and can be called only once. - -Registers the `scheme` as standard, secure, bypasses content security policy for -resources, allows registering ServiceWorker and supports fetch API. Specify a -privilege with the value of `true` to enable the capability. - -An example of registering a privileged scheme, that bypasses Content Security -Policy: - -```javascript -const { protocol } = require('electron') -protocol.registerSchemesAsPrivileged([ - { scheme: 'foo', privileges: { bypassCSP: true } } -]) -``` - -A standard scheme adheres to what RFC 3986 calls [generic URI -syntax](https://tools.ietf.org/html/rfc3986#section-3). For example `http` and -`https` are standard schemes, while `file` is not. - -Registering a scheme as standard allows relative and absolute resources to -be resolved correctly when served. Otherwise the scheme will behave like the -`file` protocol, but without the ability to resolve relative URLs. - -For example when you load following page with custom protocol without -registering it as standard scheme, the image will not be loaded because -non-standard schemes can not recognize relative URLs: - -```html -<body> - <img src='test.png'> -</body> -``` - -Registering a scheme as standard will allow access to files through the -[FileSystem API][file-system-api]. Otherwise the renderer will throw a security -error for the scheme. - -By default web storage apis (localStorage, sessionStorage, webSQL, indexedDB, -cookies) are disabled for non standard schemes. So in general if you want to -register a custom protocol to replace the `http` protocol, you have to register -it as a standard scheme. - -### `protocol.registerFileProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (String | [ProtocolResponse](structures/protocol-response.md)) - -Registers a protocol of `scheme` that will send a file as the response. The -`handler` will be called with `request` and `callback` where `request` is -an incoming request for the `scheme`. - -To handle the `request`, the `callback` should be called with either the file's -path or an object that has a `path` property, e.g. `callback(filePath)` or -`callback({ path: filePath })`. The `filePath` must be an absolute path. - -By default the `scheme` is treated like `http:`, which is parsed differently -from protocols that follow the "generic URI syntax" like `file:`. - -### `protocol.registerBufferProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (Buffer | [ProtocolResponse](structures/protocol-response.md)) - -Registers a protocol of `scheme` that will send a `Buffer` as a response. - -The usage is the same with `registerFileProtocol`, except that the `callback` -should be called with either a `Buffer` object or an object that has the `data` -property. - -Example: - -```javascript -protocol.registerBufferProtocol('atom', (request, callback) => { - callback({ mimeType: 'text/html', data: Buffer.from('<h5>Response</h5>') }) -}) -``` - -### `protocol.registerStringProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (String | [ProtocolResponse](structures/protocol-response.md)) - -Registers a protocol of `scheme` that will send a `String` as a response. - -The usage is the same with `registerFileProtocol`, except that the `callback` -should be called with either a `String` or an object that has the `data` -property. - -### `protocol.registerHttpProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` ProtocolResponse - -Registers a protocol of `scheme` that will send an HTTP request as a response. - -The usage is the same with `registerFileProtocol`, except that the `callback` -should be called with an object that has the `url` property. - -### `protocol.registerStreamProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (ReadableStream | [ProtocolResponse](structures/protocol-response.md)) - -Registers a protocol of `scheme` that will send a stream as a response. - -The usage is the same with `registerFileProtocol`, except that the -`callback` should be called with either a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) object or an object that -has the `data` property. - -Example: - -```javascript -const { protocol } = require('electron') -const { PassThrough } = require('stream') - -function createStream (text) { - const rv = new PassThrough() // PassThrough is also a Readable stream - rv.push(text) - rv.push(null) - return rv -} - -protocol.registerStreamProtocol('atom', (request, callback) => { - callback({ - statusCode: 200, - headers: { - 'content-type': 'text/html' - }, - data: createStream('<h5>Response</h5>') - }) -}) -``` - -It is possible to pass any object that implements the readable stream API (emits -`data`/`end`/`error` events). For example, here's how a file could be returned: - -```javascript -protocol.registerStreamProtocol('atom', (request, callback) => { - callback(fs.createReadStream('index.html')) -}) -``` - -### `protocol.unregisterProtocol(scheme)` - -* `scheme` String - -Unregisters the custom protocol of `scheme`. - -### `protocol.isProtocolRegistered(scheme)` - -* `scheme` String - -Returns `Boolean` - Whether `scheme` is already registered. - -### `protocol.interceptFileProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (String | [ProtocolResponse](structures/protocol-response.md)) - -Intercepts `scheme` protocol and uses `handler` as the protocol's new handler -which sends a file as a response. - -### `protocol.interceptStringProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (String | [ProtocolResponse](structures/protocol-response.md)) - -Intercepts `scheme` protocol and uses `handler` as the protocol's new handler -which sends a `String` as a response. - -### `protocol.interceptBufferProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (Buffer | [ProtocolResponse](structures/protocol-response.md)) - -Intercepts `scheme` protocol and uses `handler` as the protocol's new handler -which sends a `Buffer` as a response. - -### `protocol.interceptHttpProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` ProtocolResponse - -Intercepts `scheme` protocol and uses `handler` as the protocol's new handler -which sends a new HTTP request as a response. - -### `protocol.interceptStreamProtocol(scheme, handler)` - -* `scheme` String -* `handler` Function - * `request` ProtocolRequest - * `callback` Function - * `response` (ReadableStream | [ProtocolResponse](structures/protocol-response.md)) - -Same as `protocol.registerStreamProtocol`, except that it replaces an existing -protocol handler. - -### `protocol.uninterceptProtocol(scheme)` - -* `scheme` String - -Remove the interceptor installed for `scheme` and restore its original handler. - -### `protocol.isProtocolIntercepted(scheme)` - -* `scheme` String - -Returns `Boolean` - Whether `scheme` is already intercepted. - -[file-system-api]: https://developer.mozilla.org/en-US/docs/Web/API/LocalFileSystem diff --git a/docs/api/protocol.md b/docs/api/protocol.md index 215fbe6673dee..7a2b334016191 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -11,12 +11,10 @@ An example of implementing a protocol that has the same effect as the const { app, protocol } = require('electron') const path = require('path') -app.on('ready', () => { +app.whenReady().then(() => { protocol.registerFileProtocol('atom', (request, callback) => { const url = request.url.substr(7) callback({ path: path.normalize(`${__dirname}/${url}`) }) - }, (error) => { - if (error) console.error('Failed to register protocol') }) }) ``` @@ -26,32 +24,30 @@ of the `app` module gets emitted. ## Using `protocol` with a custom `partition` or `session` -A protocol is registered to a specific Electron [`session`](./session.md) object. If you don't specify a session, then your `protocol` will be applied to the default session that Electron uses. However, if you define a `partition` or `session` on your `browserWindow`'s `webPreferences`, then that window will use a different session and your custom protocol will not work if you just use `electron.protocol.XXX`. +A protocol is registered to a specific Electron [`session`](./session.md) +object. If you don't specify a session, then your `protocol` will be applied to +the default session that Electron uses. However, if you define a `partition` or +`session` on your `browserWindow`'s `webPreferences`, then that window will use +a different session and your custom protocol will not work if you just use +`electron.protocol.XXX`. -To have your custom protocol work in combination with a custom session, you need to register it to that session explicitly. +To have your custom protocol work in combination with a custom session, you need +to register it to that session explicitly. ```javascript const { session, app, protocol } = require('electron') const path = require('path') -app.on('ready', () => { +app.whenReady().then(() => { const partition = 'persist:example' const ses = session.fromPartition(partition) ses.protocol.registerFileProtocol('atom', (request, callback) => { const url = request.url.substr(7) callback({ path: path.normalize(`${__dirname}/${url}`) }) - }, (error) => { - if (error) console.error('Failed to register protocol') }) - mainWindow = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - partition: partition - } - }) + mainWindow = new BrowserWindow({ webPreferences: { partition } }) }) ``` @@ -63,15 +59,15 @@ The `protocol` module has the following methods: * `customSchemes` [CustomScheme[]](structures/custom-scheme.md) - **Note:** This method can only be used before the `ready` event of the `app` module gets emitted and can be called only once. -Registers the `scheme` as standard, secure, bypasses content security policy for resources, -allows registering ServiceWorker and supports fetch API. +Registers the `scheme` as standard, secure, bypasses content security policy for +resources, allows registering ServiceWorker, supports fetch API, and streaming +video/audio. Specify a privilege with the value of `true` to enable the capability. -Specify a privilege with the value of `true` to enable the capability. -An example of registering a privileged scheme, with bypassing Content Security Policy: +An example of registering a privileged scheme, that bypasses Content Security +Policy: ```javascript const { protocol } = require('electron') @@ -84,7 +80,7 @@ A standard scheme adheres to what RFC 3986 calls [generic URI syntax](https://tools.ietf.org/html/rfc3986#section-3). For example `http` and `https` are standard schemes, while `file` is not. -Registering a scheme as standard, will allow relative and absolute resources to +Registering a scheme as standard allows relative and absolute resources to be resolved correctly when served. Otherwise the scheme will behave like the `file` protocol, but without the ability to resolve relative URLs. @@ -102,168 +98,107 @@ Registering a scheme as standard will allow access to files through the [FileSystem API][file-system-api]. Otherwise the renderer will throw a security error for the scheme. -By default web storage apis (localStorage, sessionStorage, webSQL, indexedDB, cookies) -are disabled for non standard schemes. So in general if you want to register a -custom protocol to replace the `http` protocol, you have to register it as a standard scheme. - -`protocol.registerSchemesAsPrivileged` can be used to replicate the functionality of the previous `protocol.registerStandardSchemes`, `webFrame.registerURLSchemeAs*` and `protocol.registerServiceWorkerSchemes` functions that existed prior to Electron 5.0.0, for example: - -**before (<= v4.x)** -```javascript -// Main -protocol.registerStandardSchemes(['scheme1', 'scheme2'], { secure: true }) -// Renderer -webFrame.registerURLSchemeAsPrivileged('scheme1', { secure: true }) -webFrame.registerURLSchemeAsPrivileged('scheme2', { secure: true }) -``` +By default web storage apis (localStorage, sessionStorage, webSQL, indexedDB, +cookies) are disabled for non standard schemes. So in general if you want to +register a custom protocol to replace the `http` protocol, you have to register +it as a standard scheme. -**after (>= v5.x)** -```javascript -protocol.registerSchemesAsPrivileged([ - { scheme: 'scheme1', privileges: { standard: true, secure: true } }, - { scheme: 'scheme2', privileges: { standard: true, secure: true } } -]) -``` +Protocols that use streams (http and stream protocols) should set `stream: true`. +The `<video>` and `<audio>` HTML elements expect protocols to buffer their +responses by default. The `stream` flag configures those elements to correctly +expect streaming responses. -### `protocol.registerFileProtocol(scheme, handler[, completion])` +### `protocol.registerFileProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `filePath` String | [FilePathWithHeaders](structures/file-path-with-headers.md) (optional) -* `completion` Function (optional) - * `error` Error + * `response` (string | [ProtocolResponse](structures/protocol-response.md)) -Registers a protocol of `scheme` that will send the file as a response. The -`handler` will be called with `handler(request, callback)` when a `request` is -going to be created with `scheme`. `completion` will be called with -`completion(null)` when `scheme` is successfully registered or -`completion(error)` when failed. +Returns `boolean` - Whether the protocol was successfully registered + +Registers a protocol of `scheme` that will send a file as the response. The +`handler` will be called with `request` and `callback` where `request` is +an incoming request for the `scheme`. To handle the `request`, the `callback` should be called with either the file's path or an object that has a `path` property, e.g. `callback(filePath)` or -`callback({ path: filePath })`. The object may also have a `headers` property -which gives a map of headers to values for the response headers, e.g. -`callback({ path: filePath, headers: {"Content-Security-Policy": "default-src 'none'"]})`. - -When `callback` is called with nothing, a number, or an object that has an -`error` property, the `request` will fail with the `error` number you -specified. For the available error numbers you can use, please see the -[net error list][net-error]. +`callback({ path: filePath })`. The `filePath` must be an absolute path. By default the `scheme` is treated like `http:`, which is parsed differently -than protocols that follow the "generic URI syntax" like `file:`. +from protocols that follow the "generic URI syntax" like `file:`. -### `protocol.registerBufferProtocol(scheme, handler[, completion])` +### `protocol.registerBufferProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `buffer` (Buffer | [MimeTypedBuffer](structures/mime-typed-buffer.md)) (optional) -* `completion` Function (optional) - * `error` Error + * `response` (Buffer | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully registered Registers a protocol of `scheme` that will send a `Buffer` as a response. The usage is the same with `registerFileProtocol`, except that the `callback` -should be called with either a `Buffer` object or an object that has the `data`, -`mimeType`, and `charset` properties. +should be called with either a `Buffer` object or an object that has the `data` +property. Example: ```javascript -const { protocol } = require('electron') - protocol.registerBufferProtocol('atom', (request, callback) => { callback({ mimeType: 'text/html', data: Buffer.from('<h5>Response</h5>') }) -}, (error) => { - if (error) console.error('Failed to register protocol') }) ``` -### `protocol.registerStringProtocol(scheme, handler[, completion])` +### `protocol.registerStringProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `data` (String | [StringProtocolResponse](structures/string-protocol-response.md)) (optional) -* `completion` Function (optional) - * `error` Error + * `response` (string | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully registered -Registers a protocol of `scheme` that will send a `String` as a response. +Registers a protocol of `scheme` that will send a `string` as a response. The usage is the same with `registerFileProtocol`, except that the `callback` -should be called with either a `String` or an object that has the `data`, -`mimeType`, and `charset` properties. +should be called with either a `string` or an object that has the `data` +property. -### `protocol.registerHttpProtocol(scheme, handler[, completion])` +### `protocol.registerHttpProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `redirectRequest` Object - * `url` String - * `method` String (optional) - * `session` Object (optional) - * `uploadData` [ProtocolResponseUploadData](structures/protocol-response-upload-data.md) (optional) -* `completion` Function (optional) - * `error` Error + * `response` ProtocolResponse + +Returns `boolean` - Whether the protocol was successfully registered Registers a protocol of `scheme` that will send an HTTP request as a response. The usage is the same with `registerFileProtocol`, except that the `callback` -should be called with a `redirectRequest` object that has the `url`, `method`, -`referrer`, `uploadData` and `session` properties. - -By default the HTTP request will reuse the current session. If you want the -request to have a different session you should set `session` to `null`. +should be called with an object that has the `url` property. -For POST requests the `uploadData` object must be provided. +### `protocol.registerStreamProtocol(scheme, handler)` -### `protocol.registerStreamProtocol(scheme, handler[, completion])` - -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `stream` (ReadableStream | [StreamProtocolResponse](structures/stream-protocol-response.md)) (optional) -* `completion` Function (optional) - * `error` Error + * `response` (ReadableStream | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully registered -Registers a protocol of `scheme` that will send a `Readable` as a response. +Registers a protocol of `scheme` that will send a stream as a response. -The usage is similar to the other `register{Any}Protocol`, except that the -`callback` should be called with either a `Readable` object or an object that -has the `data`, `statusCode`, and `headers` properties. +The usage is the same with `registerFileProtocol`, except that the +`callback` should be called with either a [`ReadableStream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) object or an object that +has the `data` property. Example: @@ -286,8 +221,6 @@ protocol.registerStreamProtocol('atom', (request, callback) => { }, data: createStream('<h5>Response</h5>') }) -}, (error) => { - if (error) console.error('Failed to register protocol') }) ``` @@ -295,134 +228,102 @@ It is possible to pass any object that implements the readable stream API (emits `data`/`end`/`error` events). For example, here's how a file could be returned: ```javascript -const { protocol } = require('electron') -const fs = require('fs') - protocol.registerStreamProtocol('atom', (request, callback) => { callback(fs.createReadStream('index.html')) -}, (error) => { - if (error) console.error('Failed to register protocol') }) ``` -### `protocol.unregisterProtocol(scheme[, completion])` +### `protocol.unregisterProtocol(scheme)` -* `scheme` String -* `completion` Function (optional) - * `error` Error +* `scheme` string + +Returns `boolean` - Whether the protocol was successfully unregistered Unregisters the custom protocol of `scheme`. -### `protocol.isProtocolHandled(scheme)` +### `protocol.isProtocolRegistered(scheme)` -* `scheme` String +* `scheme` string -Returns `Promise<Boolean>` - fulfilled with a boolean that indicates whether there is -already a handler for `scheme`. +Returns `boolean` - Whether `scheme` is already registered. -### `protocol.interceptFileProtocol(scheme, handler[, completion])` +### `protocol.interceptFileProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `filePath` String -* `completion` Function (optional) - * `error` Error + * `response` (string | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully intercepted Intercepts `scheme` protocol and uses `handler` as the protocol's new handler which sends a file as a response. -### `protocol.interceptStringProtocol(scheme, handler[, completion])` +### `protocol.interceptStringProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `data` (String | [StringProtocolResponse](structures/string-protocol-response.md)) (optional) -* `completion` Function (optional) - * `error` Error + * `response` (string | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully intercepted Intercepts `scheme` protocol and uses `handler` as the protocol's new handler -which sends a `String` as a response. +which sends a `string` as a response. -### `protocol.interceptBufferProtocol(scheme, handler[, completion])` +### `protocol.interceptBufferProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `buffer` Buffer (optional) -* `completion` Function (optional) - * `error` Error + * `response` (Buffer | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully intercepted Intercepts `scheme` protocol and uses `handler` as the protocol's new handler which sends a `Buffer` as a response. -### `protocol.interceptHttpProtocol(scheme, handler[, completion])` +### `protocol.interceptHttpProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `redirectRequest` Object - * `url` String - * `method` String (optional) - * `session` Object | null (optional) - * `uploadData` Object (optional) - * `contentType` String - MIME type of the content. - * `data` String - Content to be sent. -* `completion` Function (optional) - * `error` Error + * `response` [ProtocolResponse](structures/protocol-response.md) + +Returns `boolean` - Whether the protocol was successfully intercepted Intercepts `scheme` protocol and uses `handler` as the protocol's new handler which sends a new HTTP request as a response. -### `protocol.interceptStreamProtocol(scheme, handler[, completion])` +### `protocol.interceptStreamProtocol(scheme, handler)` -* `scheme` String +* `scheme` string * `handler` Function - * `request` Object - * `url` String - * `headers` Record<String, String> - * `referrer` String - * `method` String - * `uploadData` [UploadData[]](structures/upload-data.md) + * `request` [ProtocolRequest](structures/protocol-request.md) * `callback` Function - * `stream` (ReadableStream | [StreamProtocolResponse](structures/stream-protocol-response.md)) (optional) -* `completion` Function (optional) - * `error` Error + * `response` (ReadableStream | [ProtocolResponse](structures/protocol-response.md)) + +Returns `boolean` - Whether the protocol was successfully intercepted Same as `protocol.registerStreamProtocol`, except that it replaces an existing protocol handler. -### `protocol.uninterceptProtocol(scheme[, completion])` +### `protocol.uninterceptProtocol(scheme)` -* `scheme` String -* `completion` Function (optional) - * `error` Error +* `scheme` string + +Returns `boolean` - Whether the protocol was successfully unintercepted Remove the interceptor installed for `scheme` and restore its original handler. -[net-error]: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h +### `protocol.isProtocolIntercepted(scheme)` + +* `scheme` string + +Returns `boolean` - Whether `scheme` is already intercepted. + [file-system-api]: https://developer.mozilla.org/en-US/docs/Web/API/LocalFileSystem diff --git a/docs/api/remote.md b/docs/api/remote.md deleted file mode 100644 index e49e738ffb176..0000000000000 --- a/docs/api/remote.md +++ /dev/null @@ -1,208 +0,0 @@ -# remote - -> Use main process modules from the renderer process. - -Process: [Renderer](../glossary.md#renderer-process) - -The `remote` module provides a simple way to do inter-process communication -(IPC) between the renderer process (web page) and the main process. - -In Electron, GUI-related modules (such as `dialog`, `menu` etc.) are only -available in the main process, not in the renderer process. In order to use them -from the renderer process, the `ipc` module is necessary to send inter-process -messages to the main process. With the `remote` module, you can invoke methods -of the main process object without explicitly sending inter-process messages, -similar to Java's [RMI][rmi]. An example of creating a browser window from a -renderer process: - -```javascript -const { BrowserWindow } = require('electron').remote -let win = new BrowserWindow({ width: 800, height: 600 }) -win.loadURL('https://github.com') -``` - -**Note:** For the reverse (access the renderer process from the main process), -you can use [webContents.executeJavaScript](web-contents.md#contentsexecutejavascriptcode-usergesture). - -**Note:** The remote module can be disabled for security reasons in the following contexts: -- [`BrowserWindow`](browser-window.md) - by setting the `enableRemoteModule` option to `false`. -- [`<webview>`](webview-tag.md) - by setting the `enableremotemodule` attribute to `false`. - -## Remote Objects - -Each object (including functions) returned by the `remote` module represents an -object in the main process (we call it a remote object or remote function). -When you invoke methods of a remote object, call a remote function, or create -a new object with the remote constructor (function), you are actually sending -synchronous inter-process messages. - -In the example above, both [`BrowserWindow`](browser-window.md) and `win` were remote objects and -`new BrowserWindow` didn't create a `BrowserWindow` object in the renderer -process. Instead, it created a `BrowserWindow` object in the main process and -returned the corresponding remote object in the renderer process, namely the -`win` object. - -**Note:** Only [enumerable properties][enumerable-properties] which are present -when the remote object is first referenced are accessible via remote. - -**Note:** Arrays and Buffers are copied over IPC when accessed via the `remote` -module. Modifying them in the renderer process does not modify them in the main -process and vice versa. - -## Lifetime of Remote Objects - -Electron makes sure that as long as the remote object in the renderer process -lives (in other words, has not been garbage collected), the corresponding object -in the main process will not be released. When the remote object has been -garbage collected, the corresponding object in the main process will be -dereferenced. - -If the remote object is leaked in the renderer process (e.g. stored in a map but -never freed), the corresponding object in the main process will also be leaked, -so you should be very careful not to leak remote objects. - -Primary value types like strings and numbers, however, are sent by copy. - -## Passing callbacks to the main process - -Code in the main process can accept callbacks from the renderer - for instance -the `remote` module - but you should be extremely careful when using this -feature. - -First, in order to avoid deadlocks, the callbacks passed to the main process -are called asynchronously. You should not expect the main process to -get the return value of the passed callbacks. - -For instance you can't use a function from the renderer process in an -`Array.map` called in the main process: - -```javascript -// main process mapNumbers.js -exports.withRendererCallback = (mapper) => { - return [1, 2, 3].map(mapper) -} - -exports.withLocalCallback = () => { - return [1, 2, 3].map(x => x + 1) -} -``` - -```javascript -// renderer process -const mapNumbers = require('electron').remote.require('./mapNumbers') -const withRendererCb = mapNumbers.withRendererCallback(x => x + 1) -const withLocalCb = mapNumbers.withLocalCallback() - -console.log(withRendererCb, withLocalCb) -// [undefined, undefined, undefined], [2, 3, 4] -``` - -As you can see, the renderer callback's synchronous return value was not as -expected, and didn't match the return value of an identical callback that lives -in the main process. - -Second, the callbacks passed to the main process will persist until the -main process garbage-collects them. - -For example, the following code seems innocent at first glance. It installs a -callback for the `close` event on a remote object: - -```javascript -require('electron').remote.getCurrentWindow().on('close', () => { - // window was closed... -}) -``` - -But remember the callback is referenced by the main process until you -explicitly uninstall it. If you do not, each time you reload your window the -callback will be installed again, leaking one callback for each restart. - -To make things worse, since the context of previously installed callbacks has -been released, exceptions will be raised in the main process when the `close` -event is emitted. - -To avoid this problem, ensure you clean up any references to renderer callbacks -passed to the main process. This involves cleaning up event handlers, or -ensuring the main process is explicitly told to dereference callbacks that came -from a renderer process that is exiting. - -## Accessing built-in modules in the main process - -The built-in modules in the main process are added as getters in the `remote` -module, so you can use them directly like the `electron` module. - -```javascript -const app = require('electron').remote.app -console.log(app) -``` - -## Methods - -The `remote` module has the following methods: - -### `remote.require(module)` - -* `module` String - -Returns `any` - The object returned by `require(module)` in the main process. -Modules specified by their relative path will resolve relative to the entrypoint -of the main process. - -e.g. - -```sh -project/ -├── main -│   ├── foo.js -│   └── index.js -├── package.json -└── renderer - └── index.js -``` - -```js -// main process: main/index.js -const { app } = require('electron') -app.on('ready', () => { /* ... */ }) -``` - -```js -// some relative module: main/foo.js -module.exports = 'bar' -``` - -```js -// renderer process: renderer/index.js -const foo = require('electron').remote.require('./foo') // bar -``` - -### `remote.getCurrentWindow()` - -Returns [`BrowserWindow`](browser-window.md) - The window to which this web page -belongs. - -**Note:** Do not use `removeAllListeners` on [`BrowserWindow`](browser-window.md). -Use of this can remove all [`blur`](https://developer.mozilla.org/en-US/docs/Web/Events/blur) -listeners, disable click events on touch bar buttons, and other unintended -consequences. - -### `remote.getCurrentWebContents()` - -Returns [`WebContents`](web-contents.md) - The web contents of this web page. - -### `remote.getGlobal(name)` - -* `name` String - -Returns `any` - The global variable of `name` (e.g. `global[name]`) in the main -process. - -## Properties - -### `remote.process` _Readonly_ - -A `NodeJS.Process` object. The `process` object in the main process. This is the same as -`remote.getGlobal('process')` but is cached. - -[rmi]: https://en.wikipedia.org/wiki/Java_remote_method_invocation -[enumerable-properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties diff --git a/docs/api/safe-storage.md b/docs/api/safe-storage.md new file mode 100644 index 0000000000000..471351c9c1cd3 --- /dev/null +++ b/docs/api/safe-storage.md @@ -0,0 +1,40 @@ +# safeStorage + +> Allows access to simple encryption and decryption of strings for storage on the local machine. + +Process: [Main](../glossary.md#main-process) + +This module protects data stored on disk from being accessed by other applications or users with full disk access. + +Note that on Mac, access to the system Keychain is required and +these calls can block the current thread to collect user input. +The same is true for Linux, if a password management tool is available. + +## Methods + +The `safeStorage` module has the following methods: + +### `safeStorage.isEncryptionAvailable()` + +Returns `boolean` - Whether encryption is available. + +On Linux, returns true if the app has emitted the `ready` event and the secret key is available. +On MacOS, returns true if Keychain is available. +On Windows, returns true once the app has emitted the `ready` event. + +### `safeStorage.encryptString(plainText)` + +* `plainText` string + +Returns `Buffer` - An array of bytes representing the encrypted string. + +This function will throw an error if encryption fails. + +### `safeStorage.decryptString(encrypted)` + +* `encrypted` Buffer + +Returns `string` - the decrypted string. Decrypts the encrypted buffer +obtained with `safeStorage.encryptString` back into a string. + +This function will throw an error if decryption fails. diff --git a/docs/api/sandbox-option.md b/docs/api/sandbox-option.md deleted file mode 100644 index 6fb46d31ffa36..0000000000000 --- a/docs/api/sandbox-option.md +++ /dev/null @@ -1,177 +0,0 @@ -# `sandbox` Option - -> Create a browser window with a sandboxed renderer. With this -option enabled, the renderer must communicate via IPC to the main process in order to access node APIs. - -One of the key security features of Chromium is that all blink rendering/JavaScript -code is executed within a sandbox. This sandbox uses OS-specific features to ensure -that exploits in the renderer process cannot harm the system. - -In other words, when the sandbox is enabled, the renderers can only make changes -to the system by delegating tasks to the main process via IPC. -[Here's](https://www.chromium.org/developers/design-documents/sandbox) more -information about the sandbox. - -Since a major feature in Electron is the ability to run Node.js in the -renderer process (making it easier to develop desktop applications using web -technologies), the sandbox is disabled by electron. This is because -most Node.js APIs require system access. `require()` for example, is not -possible without file system permissions, which are not available in a sandboxed -environment. - -Usually this is not a problem for desktop applications since the code is always -trusted, but it makes Electron less secure than Chromium for displaying -untrusted web content. For applications that require more security, the -`sandbox` flag will force Electron to spawn a classic Chromium renderer that is -compatible with the sandbox. - -A sandboxed renderer doesn't have a Node.js environment running and doesn't -expose Node.js JavaScript APIs to client code. The only exception is the preload script, -which has access to a subset of the Electron renderer API. - -Another difference is that sandboxed renderers don't modify any of the default -JavaScript APIs. Consequently, some APIs such as `window.open` will work as they -do in Chromium (i.e. they do not return a [`BrowserWindowProxy`](browser-window-proxy.md)). - -## Example - -To create a sandboxed window, pass `sandbox: true` to `webPreferences`: - -```js -let win -app.on('ready', () => { - win = new BrowserWindow({ - webPreferences: { - sandbox: true - } - }) - win.loadURL('http://google.com') -}) -``` - -In the above code the [`BrowserWindow`](browser-window.md) that was created has Node.js disabled and can communicate -only via IPC. The use of this option stops Electron from creating a Node.js runtime in the renderer. Also, -within this new window `window.open` follows the native behaviour (by default Electron creates a [`BrowserWindow`](browser-window.md) -and returns a proxy to this via `window.open`). - -[`app.enableSandbox`](app.md#appenablesandbox-experimental) can be used to force `sandbox: true` for all `BrowserWindow` instances. - -```js -let win -app.enableSandbox() -app.on('ready', () => { - // no need to pass `sandbox: true` since `app.enableSandbox()` was called. - win = new BrowserWindow() - win.loadURL('http://google.com') -}) -``` - -## Preload - -An app can make customizations to sandboxed renderers using a preload script. -Here's an example: - -```js -let win -app.on('ready', () => { - win = new BrowserWindow({ - webPreferences: { - sandbox: true, - preload: path.join(app.getAppPath(), 'preload.js') - } - }) - win.loadURL('http://google.com') -}) -``` - -and preload.js: - -```js -// This file is loaded whenever a javascript context is created. It runs in a -// private scope that can access a subset of Electron renderer APIs. We must be -// careful to not leak any objects into the global scope! -const { ipcRenderer, remote } = require('electron') -const fs = remote.require('fs') - -// read a configuration file using the `fs` module -const buf = fs.readFileSync('allowed-popup-urls.json') -const allowedUrls = JSON.parse(buf.toString('utf8')) - -const defaultWindowOpen = window.open - -function customWindowOpen (url, ...args) { - if (allowedUrls.indexOf(url) === -1) { - ipcRenderer.sendSync('blocked-popup-notification', location.origin, url) - return null - } - return defaultWindowOpen(url, ...args) -} - -window.open = customWindowOpen -``` - -Important things to notice in the preload script: - -- Even though the sandboxed renderer doesn't have Node.js running, it still has - access to a limited node-like environment: `Buffer`, `process`, `setImmediate`, - `clearImmediate` and `require` are available. -- The preload script can indirectly access all APIs from the main process through the - `remote` and `ipcRenderer` modules. -- The preload script must be contained in a single script, but it is possible to have - complex preload code composed with multiple modules by using a tool like - webpack or browserify. An example of using browserify is below. - -To create a browserify bundle and use it as a preload script, something like -the following should be used: - -```sh - browserify preload/index.js \ - -x electron \ - --insert-global-vars=__filename,__dirname -o preload.js -``` - -The `-x` flag should be used with any required module that is already exposed in -the preload scope, and tells browserify to use the enclosing `require` function -for it. `--insert-global-vars` will ensure that `process`, `Buffer` and -`setImmediate` are also taken from the enclosing scope(normally browserify -injects code for those). - -Currently the `require` function provided in the preload scope exposes the -following modules: - -- `electron` - - `crashReporter` - - `desktopCapturer` - - `ipcRenderer` - - `nativeImage` - - `remote` - - `webFrame` -- `events` -- `timers` -- `url` - -More may be added as needed to expose more Electron APIs in the sandbox, but any -module in the main process can already be used through -`electron.remote.require`. - -## Status - -Please use the `sandbox` option with care, as it is still an experimental -feature. We are still not aware of the security implications of exposing some -Electron renderer APIs to the preload script, but here are some things to -consider before rendering untrusted content: - -- A preload script can accidentally leak privileged APIs to untrusted code, - unless [`contextIsolation`](../tutorial/security.md#3-enable-context-isolation-for-remote-content) - is also enabled. -- Some bug in V8 engine may allow malicious code to access the renderer preload - APIs, effectively granting full access to the system through the `remote` - module. Therefore, it is highly recommended to - [disable the `remote` module](../tutorial/security.md#15-disable-the-remote-module). - If disabling is not feasible, you should selectively - [filter the `remote` module](../tutorial/security.md#16-filter-the-remote-module). - -Since rendering untrusted content in Electron is still uncharted territory, -the APIs exposed to the sandbox preload script should be considered more -unstable than the rest of Electron APIs, and may have breaking changes to fix -security issues. diff --git a/docs/api/screen.md b/docs/api/screen.md index 7d56da13b0ee4..a576a5902c523 100644 --- a/docs/api/screen.md +++ b/docs/api/screen.md @@ -14,11 +14,11 @@ property, so writing `let { screen } = require('electron')` will not work. An example of creating a window that fills the whole screen: -```javascript +```javascript fiddle='docs/fiddles/screen/fit-screen' const { app, BrowserWindow, screen } = require('electron') let win -app.on('ready', () => { +app.whenReady().then(() => { const { width, height } = screen.getPrimaryDisplay().workAreaSize win = new BrowserWindow({ width, height }) win.loadURL('https://github.com') @@ -32,9 +32,9 @@ const { app, BrowserWindow, screen } = require('electron') let win -app.on('ready', () => { - let displays = screen.getAllDisplays() - let externalDisplay = displays.find((display) => { +app.whenReady().then(() => { + const displays = screen.getAllDisplays() + const externalDisplay = displays.find((display) => { return display.bounds.x !== 0 || display.bounds.y !== 0 }) @@ -76,7 +76,7 @@ Returns: * `event` Event * `display` [Display](structures/display.md) -* `changedMetrics` String[] +* `changedMetrics` string[] Emitted when one or more metrics change in a `display`. The `changedMetrics` is an array of strings that describe the changes. Possible changes are `bounds`, @@ -92,6 +92,8 @@ Returns [`Point`](structures/point.md) The current absolute position of the mouse pointer. +**Note:** The return value is a DIP point, not a screen physical point. + ### `screen.getPrimaryDisplay()` Returns [`Display`](structures/display.md) - The primary display. diff --git a/docs/api/service-workers.md b/docs/api/service-workers.md new file mode 100644 index 0000000000000..ec9f33d3e18ac --- /dev/null +++ b/docs/api/service-workers.md @@ -0,0 +1,73 @@ +## Class: ServiceWorkers + +> Query and receive events from a sessions active service workers. + +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ + +Instances of the `ServiceWorkers` class are accessed by using `serviceWorkers` property of +a `Session`. + +For example: + +```javascript +const { session } = require('electron') + +// Get all service workers. +console.log(session.defaultSession.serviceWorkers.getAllRunning()) + +// Handle logs and get service worker info +session.defaultSession.serviceWorkers.on('console-message', (event, messageDetails) => { + console.log( + 'Got service worker message', + messageDetails, + 'from', + session.defaultSession.serviceWorkers.getFromVersionID(messageDetails.versionId) + ) +}) +``` + +### Instance Events + +The following events are available on instances of `ServiceWorkers`: + +#### Event: 'console-message' + +Returns: + +* `event` Event +* `messageDetails` Object - Information about the console message + * `message` string - The actual console message + * `versionId` number - The version ID of the service worker that sent the log message + * `source` string - The type of source for this message. Can be `javascript`, `xml`, `network`, `console-api`, `storage`, `rendering`, `security`, `deprecation`, `worker`, `violation`, `intervention`, `recommendation` or `other`. + * `level` number - The log level, from 0 to 3. In order it matches `verbose`, `info`, `warning` and `error`. + * `sourceUrl` string - The URL the message came from + * `lineNumber` number - The line number of the source that triggered this console message + +Emitted when a service worker logs something to the console. + +#### Event: 'registration-completed' + +Returns: + +* `event` Event +* `details` Object - Information about the registered service worker + * `scope` string - The base URL that a service worker is registered for + +Emitted when a service worker has been registered. Can occur after a call to [`navigator.serviceWorker.register('/sw.js')`](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) successfully resolves or when a Chrome extension is loaded. + +### Instance Methods + +The following methods are available on instances of `ServiceWorkers`: + +#### `serviceWorkers.getAllRunning()` + +Returns `Record<number, ServiceWorkerInfo>` - A [ServiceWorkerInfo](structures/service-worker-info.md) object where the keys are the service worker version ID and the values are the information about that service worker. + +#### `serviceWorkers.getFromVersionID(versionId)` + +* `versionId` number + +Returns [`ServiceWorkerInfo`](structures/service-worker-info.md) - Information about this service worker + +If the service worker does not exist or is not running this method will throw an exception. diff --git a/docs/api/session.md b/docs/api/session.md index 82da931e92814..8bb1ad5ab3c5c 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -12,7 +12,7 @@ property of [`WebContents`](web-contents.md), or from the `session` module. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ width: 800, height: 600 }) +const win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL('http://github.com') const ses = win.webContents.session @@ -25,9 +25,9 @@ The `session` module has the following methods: ### `session.fromPartition(partition[, options])` -* `partition` String +* `partition` string * `options` Object (optional) - * `cache` Boolean - Whether to enable cache. + * `cache` boolean - Whether to enable cache. Returns `Session` - A session instance from `partition` string. When there is an existing `Session` with the same `partition`, it will be returned; otherwise a new @@ -54,7 +54,8 @@ A `Session` object, the default session object of the app. > Get and set properties of a session. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ You can create a `Session` object in the `session` module: @@ -85,12 +86,280 @@ available from next tick of the process. const { session } = require('electron') session.defaultSession.on('will-download', (event, item, webContents) => { event.preventDefault() - require('request')(item.getURL(), (data) => { - require('fs').writeFileSync('/somewhere', data) + require('got')(item.getURL()).then((response) => { + require('fs').writeFileSync('/somewhere', response.body) + }) +}) +``` + +#### Event: 'extension-loaded' + +Returns: + +* `event` Event +* `extension` [Extension](structures/extension.md) + +Emitted after an extension is loaded. This occurs whenever an extension is +added to the "enabled" set of extensions. This includes: + +* Extensions being loaded from `Session.loadExtension`. +* Extensions being reloaded: + * from a crash. + * if the extension requested it ([`chrome.runtime.reload()`](https://developer.chrome.com/extensions/runtime#method-reload)). + +#### Event: 'extension-unloaded' + +Returns: + +* `event` Event +* `extension` [Extension](structures/extension.md) + +Emitted after an extension is unloaded. This occurs when +`Session.removeExtension` is called. + +#### Event: 'extension-ready' + +Returns: + +* `event` Event +* `extension` [Extension](structures/extension.md) + +Emitted after an extension is loaded and all necessary browser state is +initialized to support the start of the extension's background page. + +#### Event: 'preconnect' + +Returns: + +* `event` Event +* `preconnectUrl` string - The URL being requested for preconnection by the + renderer. +* `allowCredentials` boolean - True if the renderer is requesting that the + connection include credentials (see the + [spec](https://w3c.github.io/resource-hints/#preconnect) for more details.) + +Emitted when a render process requests preconnection to a URL, generally due to +a [resource hint](https://w3c.github.io/resource-hints/). + +#### Event: 'spellcheck-dictionary-initialized' + +Returns: + +* `event` Event +* `languageCode` string - The language code of the dictionary file + +Emitted when a hunspell dictionary file has been successfully initialized. This +occurs after the file has been downloaded. + +#### Event: 'spellcheck-dictionary-download-begin' + +Returns: + +* `event` Event +* `languageCode` string - The language code of the dictionary file + +Emitted when a hunspell dictionary file starts downloading + +#### Event: 'spellcheck-dictionary-download-success' + +Returns: + +* `event` Event +* `languageCode` string - The language code of the dictionary file + +Emitted when a hunspell dictionary file has been successfully downloaded + +#### Event: 'spellcheck-dictionary-download-failure' + +Returns: + +* `event` Event +* `languageCode` string - The language code of the dictionary file + +Emitted when a hunspell dictionary file download fails. For details +on the failure you should collect a netlog and inspect the download +request. + +#### Event: 'select-hid-device' + +Returns: + +* `event` Event +* `details` Object + * `deviceList` [HIDDevice[]](structures/hid-device.md) + * `frame` [WebFrameMain](web-frame-main.md) +* `callback` Function + * `deviceId` string | null (optional) + +Emitted when a HID device needs to be selected when a call to +`navigator.hid.requestDevice` is made. `callback` should be called with +`deviceId` to be selected; passing no arguments to `callback` will +cancel the request. Additionally, permissioning on `navigator.hid` can +be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler) +and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler). + +```javascript +const { app, BrowserWindow } = require('electron') + +let win = null + +app.whenReady().then(() => { + win = new BrowserWindow() + + win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'hid') { + // Add logic here to determine if permission should be given to allow HID selection + return true + } + return false + }) + + // Optionally, retrieve previously persisted devices from a persistent store + const grantedDevices = fetchGrantedDevices() + + win.webContents.session.setDevicePermissionHandler((details) => { + if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') { + if (details.device.vendorId === 123 && details.device.productId === 345) { + // Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first) + return true + } + + // Search through the list of devices that have previously been granted permission + return grantedDevices.some((grantedDevice) => { + return grantedDevice.vendorId === details.device.vendorId && + grantedDevice.productId === details.device.productId && + grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber + }) + } + return false + }) + + win.webContents.session.on('select-hid-device', (event, details, callback) => { + event.preventDefault() + const selectedDevice = details.deviceList.find((device) => { + return device.vendorId === '9025' && device.productId === '67' + }) + callback(selectedPort?.deviceId) + }) +}) +``` + +#### Event: 'hid-device-added' + +Returns: + +* `event` Event +* `details` Object + * `device` [HIDDevice[]](structures/hid-device.md) + * `frame` [WebFrameMain](web-frame-main.md) + +Emitted when a new HID device becomes available. For example, when a new USB device is plugged in. + +This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired. + +#### Event: 'hid-device-removed' + +Returns: + +* `event` Event +* `details` Object + * `device` [HIDDevice[]](structures/hid-device.md) + * `frame` [WebFrameMain](web-frame-main.md) + +Emitted when a HID device has been removed. For example, this event will fire when a USB device is unplugged. + +This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired. + +#### Event: 'select-serial-port' + +Returns: + +* `event` Event +* `portList` [SerialPort[]](structures/serial-port.md) +* `webContents` [WebContents](web-contents.md) +* `callback` Function + * `portId` string + +Emitted when a serial port needs to be selected when a call to +`navigator.serial.requestPort` is made. `callback` should be called with +`portId` to be selected, passing an empty string to `callback` will +cancel the request. Additionally, permissioning on `navigator.serial` can +be managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler) +with the `serial` permission. + +```javascript +const { app, BrowserWindow } = require('electron') + +let win = null + +app.whenReady().then(() => { + win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'serial') { + // Add logic here to determine if permission should be given to allow serial selection + return true + } + return false + }) + + // Optionally, retrieve previously persisted devices from a persistent store + const grantedDevices = fetchGrantedDevices() + + win.webContents.session.setDevicePermissionHandler((details) => { + if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'serial') { + if (details.device.vendorId === 123 && details.device.productId === 345) { + // Always allow this type of device (this allows skipping the call to `navigator.serial.requestPort` first) + return true + } + + // Search through the list of devices that have previously been granted permission + return grantedDevices.some((grantedDevice) => { + return grantedDevice.vendorId === details.device.vendorId && + grantedDevice.productId === details.device.productId && + grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber + }) + } + return false + }) + + win.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + event.preventDefault() + const selectedPort = portList.find((device) => { + return device.vendorId === '9025' && device.productId === '67' + }) + if (!selectedPort) { + callback('') + } else { + callback(selectedPort.portId) + } }) }) ``` +#### Event: 'serial-port-added' + +Returns: + +* `event` Event +* `port` [SerialPort](structures/serial-port.md) +* `webContents` [WebContents](web-contents.md) + +Emitted after `navigator.serial.requestPort` has been called and `select-serial-port` has fired if a new serial port becomes available. For example, this event will fire when a new USB device is plugged in. + +#### Event: 'serial-port-removed' + +Returns: + +* `event` Event +* `port` [SerialPort](structures/serial-port.md) +* `webContents` [WebContents](web-contents.md) + +Emitted after `navigator.serial.requestPort` has been called and `select-serial-port` has fired if a serial port has been removed. For example, this event will fire when a USB device is unplugged. + ### Instance Methods The following methods are available on instances of `Session`: @@ -108,13 +377,14 @@ Clears the session’s HTTP cache. #### `ses.clearStorageData([options])` * `options` Object (optional) - * `origin` String (optional) - Should follow `window.location.origin`’s representation + * `origin` string (optional) - Should follow `window.location.origin`’s representation `scheme://host:port`. - * `storages` String[] (optional) - The types of storages to clear, can contain: + * `storages` string[] (optional) - The types of storages to clear, can contain: `appcache`, `cookies`, `filesystem`, `indexdb`, `localstorage`, - `shadercache`, `websql`, `serviceworkers`, `cachestorage`. - * `quotas` String[] (optional) - The types of quotas to clear, can contain: - `temporary`, `persistent`, `syncable`. + `shadercache`, `websql`, `serviceworkers`, `cachestorage`. If not + specified, clear all storage types. + * `quotas` string[] (optional) - The types of quotas to clear, can contain: + `temporary`, `persistent`, `syncable`. If not specified, clear all quotas. Returns `Promise<void>` - resolves when the storage data has been cleared. @@ -125,18 +395,42 @@ Writes any unwritten DOMStorage data to disk. #### `ses.setProxy(config)` * `config` Object - * `pacScript` String - The URL associated with the PAC file. - * `proxyRules` String - Rules indicating which proxies to use. - * `proxyBypassRules` String - Rules indicating which URLs should + * `mode` string (optional) - The proxy mode. Should be one of `direct`, + `auto_detect`, `pac_script`, `fixed_servers` or `system`. If it's + unspecified, it will be automatically determined based on other specified + options. + * `direct` + In direct mode all connections are created directly, without any proxy involved. + * `auto_detect` + In auto_detect mode the proxy configuration is determined by a PAC script that can + be downloaded at http://wpad/wpad.dat. + * `pac_script` + In pac_script mode the proxy configuration is determined by a PAC script that is + retrieved from the URL specified in the `pacScript`. This is the default mode + if `pacScript` is specified. + * `fixed_servers` + In fixed_servers mode the proxy configuration is specified in `proxyRules`. + This is the default mode if `proxyRules` is specified. + * `system` + In system mode the proxy configuration is taken from the operating system. + Note that the system mode is different from setting no proxy configuration. + In the latter case, Electron falls back to the system settings + only if no command-line options influence the proxy configuration. + * `pacScript` string (optional) - The URL associated with the PAC file. + * `proxyRules` string (optional) - Rules indicating which proxies to use. + * `proxyBypassRules` string (optional) - Rules indicating which URLs should bypass the proxy settings. Returns `Promise<void>` - Resolves when the proxy setting process is complete. Sets the proxy settings. -When `pacScript` and `proxyRules` are provided together, the `proxyRules` +When `mode` is unspecified, `pacScript` and `proxyRules` are provided together, the `proxyRules` option is ignored and `pacScript` configuration is applied. +You may need `ses.closeAllConnections` to close currently in flight connections to prevent +pooled sockets using previous proxy from being reused by future requests. + The `proxyRules` has to follow the rules below: ```sh @@ -172,7 +466,7 @@ The `proxyBypassRules` is a comma separated list of rules described below: "foobar.com", "*foobar.com", "*.foobar.com", "*foobar.com:99", "https://x.*.y.com:99" - * `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]` +* `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]` Match a particular domain suffix. @@ -203,11 +497,15 @@ The `proxyBypassRules` is a comma separated list of rules described below: * `url` URL -Returns `Promise<String>` - Resolves with the proxy information for `url`. +Returns `Promise<string>` - Resolves with the proxy information for `url`. + +#### `ses.forceReloadProxyConfig()` + +Returns `Promise<void>` - Resolves when the all internal states of proxy service is reset and the latest proxy configuration is reapplied if it's already available. The pac script will be fetched from `pacScript` again if the proxy mode is `pac_script`. #### `ses.setDownloadPath(path)` -* `path` String - The download location. +* `path` string - The download location. Sets download saving directory. By default, the download directory will be the `Downloads` under the respective app folder. @@ -215,7 +513,7 @@ Sets download saving directory. By default, the download directory will be the #### `ses.enableNetworkEmulation(options)` * `options` Object - * `offline` Boolean (optional) - Whether to emulate network outage. Defaults + * `offline` boolean (optional) - Whether to emulate network outage. Defaults to false. * `latency` Double (optional) - RTT in ms. Defaults to 0 which will disable latency throttling. @@ -238,6 +536,20 @@ window.webContents.session.enableNetworkEmulation({ window.webContents.session.enableNetworkEmulation({ offline: true }) ``` +#### `ses.preconnect(options)` + +* `options` Object + * `url` string - URL for preconnect. Only the origin is relevant for opening the socket. + * `numSockets` number (optional) - number of sockets to preconnect. Must be between 1 and 6. Defaults to 1. + +Preconnects the given number of sockets to an origin. + +#### `ses.closeAllConnections()` + +Returns `Promise<void>` - Resolves when all connections are closed. + +**Note:** It will terminate / fail all requests currently in flight. + #### `ses.disableNetworkEmulation()` Disables any network emulation already active for the `session`. Resets to @@ -245,15 +557,17 @@ the original network configuration. #### `ses.setCertificateVerifyProc(proc)` -* `proc` Function +* `proc` Function | null * `request` Object - * `hostname` String + * `hostname` string * `certificate` [Certificate](structures/certificate.md) - * `verificationResult` String - Verification result from chromium. + * `validatedCertificate` [Certificate](structures/certificate.md) + * `isIssuedByKnownRoot` boolean - `true` if Chromium recognises the root CA as a standard root. If it isn't then it's probably the case that this certificate was generated by a MITM proxy whose root has been installed locally (for example, by a corporate proxy). This should not be trusted if the `verificationResult` is not `OK`. + * `verificationResult` string - `OK` if the certificate is trusted, otherwise an error like `CERT_REVOKED`. * `errorCode` Integer - Error code. * `callback` Function * `verificationResult` Integer - Value can be one of certificate error codes - from [here](https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h). + from [here](https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h). Apart from the certificate error codes, the following special codes can be used. * `0` - Indicates success and disables Certificate Transparency verification. * `-2` - Indicates failure. @@ -269,7 +583,7 @@ verify proc. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() win.webContents.session.setCertificateVerifyProc((request, callback) => { const { hostname } = request @@ -281,24 +595,40 @@ win.webContents.session.setCertificateVerifyProc((request, callback) => { }) ``` +> **NOTE:** The result of this procedure is cached by the network service. + #### `ses.setPermissionRequestHandler(handler)` * `handler` Function | null * `webContents` [WebContents](web-contents.md) - WebContents requesting the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. - * `permission` String - Enum of 'media', 'geolocation', 'notifications', 'midiSysex', - 'pointerLock', 'fullscreen', 'openExternal'. + * `permission` string - The type of requested permission. + * `clipboard-read` - Request access to read from the clipboard. + * `media` - Request access to media devices such as camera, microphone and speakers. + * `display-capture` - Request access to capture the screen. + * `mediaKeySystem` - Request access to DRM protected content. + * `geolocation` - Request access to user's current location. + * `notifications` - Request notification creation and the ability to display them in the user's system tray. + * `midi` - Request MIDI access in the `webmidi` API. + * `midiSysex` - Request the use of system exclusive messages in the `webmidi` API. + * `pointerLock` - Request to directly interpret mouse movements as an input method. Click [here](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API) to know more. + * `fullscreen` - Request for the app to enter fullscreen mode. + * `openExternal` - Request to open links in external applications. + * `unknown` - An unrecognized permission request * `callback` Function - * `permissionGranted` Boolean - Allow or deny the permission. + * `permissionGranted` boolean - Allow or deny the permission. * `details` Object - Some properties are only available on certain permission types. - * `externalURL` String (optional) - The url of the `openExternal` request. - * `mediaTypes` String[] (optional) - The types of media access being requested, elements can be `video` + * `externalURL` string (optional) - The url of the `openExternal` request. + * `securityOrigin` string (optional) - The security origin of the `media` request. + * `mediaTypes` string[] (optional) - The types of media access being requested, elements can be `video` or `audio` - * `requestingUrl` String - The last URL the requesting frame loaded - * `isMainFrame` Boolean - Whether the frame making the request is the main frame + * `requestingUrl` string - The last URL the requesting frame loaded + * `isMainFrame` boolean - Whether the frame making the request is the main frame Sets the handler which can be used to respond to permission requests for the `session`. Calling `callback(true)` will allow the permission and `callback(false)` will reject it. -To clear the handler, call `setPermissionRequestHandler(null)`. +To clear the handler, call `setPermissionRequestHandler(null)`. Please note that +you must also implement `setPermissionCheckHandler` to get complete permission handling. +Most web APIs do a permission check and then make a permission request if the check is denied. ```javascript const { session } = require('electron') @@ -313,29 +643,105 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents #### `ses.setPermissionCheckHandler(handler)` -* `handler` Function<Boolean> | null - * `webContents` [WebContents](web-contents.md) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. - * `permission` String - Enum of 'media'. - * `requestingOrigin` String - The origin URL of the permission check +* `handler` Function\<boolean> | null + * `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively. + * `permission` string - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`. + * `requestingOrigin` string - The origin URL of the permission check * `details` Object - Some properties are only available on certain permission types. - * `securityOrigin` String - The security orign of the `media` check. - * `mediaType` String - The type of media access being requested, can be `video`, + * `embeddingOrigin` string (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks. + * `securityOrigin` string (optional) - The security origin of the `media` check. + * `mediaType` string (optional) - The type of media access being requested, can be `video`, `audio` or `unknown` - * `requestingUrl` String - The last URL the requesting frame loaded - * `isMainFrame` Boolean - Whether the frame making the request is the main frame + * `requestingUrl` string (optional) - The last URL the requesting frame loaded. This is not provided for cross-origin sub frames making permission checks. + * `isMainFrame` boolean - Whether the frame making the request is the main frame Sets the handler which can be used to respond to permission checks for the `session`. -Returning `true` will allow the permission and `false` will reject it. +Returning `true` will allow the permission and `false` will reject it. Please note that +you must also implement `setPermissionRequestHandler` to get complete permission handling. +Most web APIs do a permission check and then make a permission request if the check is denied. To clear the handler, call `setPermissionCheckHandler(null)`. ```javascript const { session } = require('electron') -session.fromPartition('some-partition').setPermissionCheckHandler((webContents, permission) => { - if (webContents.getURL() === 'some-host' && permission === 'notifications') { - return false // denied +const url = require('url') +session.fromPartition('some-partition').setPermissionCheckHandler((webContents, permission, requestingOrigin) => { + if (new URL(requestingOrigin).hostname === 'some-host' && permission === 'notifications') { + return true // granted } - return true + return false // denied +}) +``` + +#### `ses.setDevicePermissionHandler(handler)` + +* `handler` Function\<boolean> | null + * `details` Object + * `deviceType` string - The type of device that permission is being requested on, can be `hid` or `serial`. + * `origin` string - The origin URL of the device permission check. + * `device` [HIDDevice](structures/hid-device.md) | [SerialPort](structures/serial-port.md)- the device that permission is being requested for. + * `frame` [WebFrameMain](web-frame-main.md) - WebFrameMain checking the device permission. + +Sets the handler which can be used to respond to device permission checks for the `session`. +Returning `true` will allow the device to be permitted and `false` will reject it. +To clear the handler, call `setDevicePermissionHandler(null)`. +This handler can be used to provide default permissioning to devices without first calling for permission +to devices (eg via `navigator.hid.requestDevice`). If this handler is not defined, the default device +permissions as granted through device selection (eg via `navigator.hid.requestDevice`) will be used. +Additionally, the default behavior of Electron is to store granted device permision through the lifetime +of the corresponding WebContents. If longer term storage is needed, a developer can store granted device +permissions (eg when handling the `select-hid-device` event) and then read from that storage with `setDevicePermissionHandler`. + +```javascript +const { app, BrowserWindow } = require('electron') + +let win = null + +app.whenReady().then(() => { + win = new BrowserWindow() + + win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'hid') { + // Add logic here to determine if permission should be given to allow HID selection + return true + } else if (permission === 'serial') { + // Add logic here to determine if permission should be given to allow serial port selection + } + return false + }) + + // Optionally, retrieve previously persisted devices from a persistent store + const grantedDevices = fetchGrantedDevices() + + win.webContents.session.setDevicePermissionHandler((details) => { + if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') { + if (details.device.vendorId === 123 && details.device.productId === 345) { + // Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first) + return true + } + + // Search through the list of devices that have previously been granted permission + return grantedDevices.some((grantedDevice) => { + return grantedDevice.vendorId === details.device.vendorId && + grantedDevice.productId === details.device.productId && + grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber + }) + } else if (details.deviceType === 'serial') { + if (details.device.vendorId === 123 && details.device.productId === 345) { + // Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first) + return true + } + } + return false + }) + + win.webContents.session.on('select-hid-device', (event, details, callback) => { + event.preventDefault() + const selectedDevice = details.deviceList.find((device) => { + return device.vendorId === '9025' && device.productId === '67' + }) + callback(selectedPort?.deviceId) + }) }) ``` @@ -347,7 +753,7 @@ Clears the host resolver cache. #### `ses.allowNTLMCredentialsForDomains(domains)` -* `domains` String - A comma-separated list of servers for which +* `domains` string - A comma-separated list of servers for which integrated authentication is enabled. Dynamically sets whether to always send credentials for HTTP NTLM or Negotiate @@ -365,8 +771,8 @@ session.defaultSession.allowNTLMCredentialsForDomains('*') #### `ses.setUserAgent(userAgent[, acceptLanguages])` -* `userAgent` String -* `acceptLanguages` String (optional) +* `userAgent` string +* `acceptLanguages` string (optional) Overrides the `userAgent` and `acceptLanguages` for this session. @@ -376,26 +782,67 @@ example `"en-US,fr,de,ko,zh-CN,ja"`. This doesn't affect existing `WebContents`, and each `WebContents` can use `webContents.setUserAgent` to override the session-wide user agent. +#### `ses.isPersistent()` + +Returns `boolean` - Whether or not this session is a persistent one. The default +`webContents` session of a `BrowserWindow` is persistent. When creating a session +from a partition, session prefixed with `persist:` will be persistent, while others +will be temporary. + #### `ses.getUserAgent()` -Returns `String` - The user agent for this session. +Returns `string` - The user agent for this session. + +#### `ses.setSSLConfig(config)` + +* `config` Object + * `minVersion` string (optional) - Can be `tls1`, `tls1.1`, `tls1.2` or `tls1.3`. The + minimum SSL version to allow when connecting to remote servers. Defaults to + `tls1`. + * `maxVersion` string (optional) - Can be `tls1.2` or `tls1.3`. The maximum SSL version + to allow when connecting to remote servers. Defaults to `tls1.3`. + * `disabledCipherSuites` Integer[] (optional) - List of cipher suites which + should be explicitly prevented from being used in addition to those + disabled by the net built-in policy. + Supported literal forms: 0xAABB, where AA is `cipher_suite[0]` and BB is + `cipher_suite[1]`, as defined in RFC 2246, Section 7.4.1.2. Unrecognized but + parsable cipher suites in this form will not return an error. + Ex: To disable TLS_RSA_WITH_RC4_128_MD5, specify 0x0004, while to + disable TLS_ECDH_ECDSA_WITH_RC4_128_SHA, specify 0xC002. + Note that TLSv1.3 ciphers cannot be disabled using this mechanism. + +Sets the SSL configuration for the session. All subsequent network requests +will use the new configuration. Existing network connections (such as WebSocket +connections) will not be terminated, but old sockets in the pool will not be +reused for new connections. #### `ses.getBlobData(identifier)` -* `identifier` String - Valid UUID. +* `identifier` string - Valid UUID. Returns `Promise<Buffer>` - resolves with blob data. +#### `ses.downloadURL(url)` + +* `url` string + +Initiates a download of the resource at `url`. +The API will generate a [DownloadItem](download-item.md) that can be accessed +with the [will-download](#event-will-download) event. + +**Note:** This does not perform any security checks that relate to a page's origin, +unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl). + #### `ses.createInterruptedDownload(options)` * `options` Object - * `path` String - Absolute path of the download. - * `urlChain` String[] - Complete URL chain for the download. - * `mimeType` String (optional) + * `path` string - Absolute path of the download. + * `urlChain` string[] - Complete URL chain for the download. + * `mimeType` string (optional) * `offset` Integer - Start range for the download. * `length` Integer - Total length of the download. - * `lastModified` String - Last-Modified header value. - * `eTag` String - ETag header value. + * `lastModified` string (optional) - Last-Modified header value. + * `eTag` string (optional) - ETag header value. * `startTime` Double (optional) - Time when download was started in number of seconds since UNIX epoch. @@ -405,32 +852,208 @@ event. The [DownloadItem](download-item.md) will not have any `WebContents` asso the initial state will be `interrupted`. The download will start only when the `resume` API is called on the [DownloadItem](download-item.md). -#### `ses.clearAuthCache(options)` - -* `options` ([RemovePassword](structures/remove-password.md) | [RemoveClientCertificate](structures/remove-client-certificate.md)) +#### `ses.clearAuthCache()` Returns `Promise<void>` - resolves when the session’s HTTP authentication cache has been cleared. #### `ses.setPreloads(preloads)` -* `preloads` String[] - An array of absolute path to preload scripts +* `preloads` string[] - An array of absolute path to preload scripts Adds scripts that will be executed on ALL web contents that are associated with this session just before normal `preload` scripts run. #### `ses.getPreloads()` -Returns `String[]` an array of paths to preload scripts that have been +Returns `string[]` an array of paths to preload scripts that have been registered. +#### `ses.setCodeCachePath(path)` + +* `path` String - Absolute path to store the v8 generated JS code cache from the renderer. + +Sets the directory to store the generated JS [code cache](https://v8.dev/blog/code-caching-for-devs) for this session. The directory is not required to be created by the user before this call, the runtime will create if it does not exist otherwise will use the existing directory. If directory cannot be created, then code cache will not be used and all operations related to code cache will fail silently inside the runtime. By default, the directory will be `Code Cache` under the +respective user data folder. + +#### `ses.clearCodeCaches(options)` + +* `options` Object + * `urls` String[] (optional) - An array of url corresponding to the resource whose generated code cache needs to be removed. If the list is empty then all entries in the cache directory will be removed. + +Returns `Promise<void>` - resolves when the code cache clear operation is complete. + +#### `ses.setSpellCheckerEnabled(enable)` + +* `enable` boolean + +Sets whether to enable the builtin spell checker. + +#### `ses.isSpellCheckerEnabled()` + +Returns `boolean` - Whether the builtin spell checker is enabled. + +#### `ses.setSpellCheckerLanguages(languages)` + +* `languages` string[] - An array of language codes to enable the spellchecker for. + +The built in spellchecker does not automatically detect what language a user is typing in. In order for the +spell checker to correctly check their words you must call this API with an array of language codes. You can +get the list of supported language codes with the `ses.availableSpellCheckerLanguages` property. + +**Note:** On macOS the OS spellchecker is used and will detect your language automatically. This API is a no-op on macOS. + +#### `ses.getSpellCheckerLanguages()` + +Returns `string[]` - An array of language codes the spellchecker is enabled for. If this list is empty the spellchecker +will fallback to using `en-US`. By default on launch if this setting is an empty list Electron will try to populate this +setting with the current OS locale. This setting is persisted across restarts. + +**Note:** On macOS the OS spellchecker is used and has its own list of languages. This API is a no-op on macOS. + +#### `ses.setSpellCheckerDictionaryDownloadURL(url)` + +* `url` string - A base URL for Electron to download hunspell dictionaries from. + +By default Electron will download hunspell dictionaries from the Chromium CDN. If you want to override this +behavior you can use this API to point the dictionary downloader at your own hosted version of the hunspell +dictionaries. We publish a `hunspell_dictionaries.zip` file with each release which contains the files you need +to host here. + +The file server must be **case insensitive**. If you cannot do this, you must upload each file twice: once with +the case it has in the ZIP file and once with the filename as all lowercase. + +If the files present in `hunspell_dictionaries.zip` are available at `https://example.com/dictionaries/language-code.bdic` +then you should call this api with `ses.setSpellCheckerDictionaryDownloadURL('https://example.com/dictionaries/')`. Please +note the trailing slash. The URL to the dictionaries is formed as `${url}${filename}`. + +**Note:** On macOS the OS spellchecker is used and therefore we do not download any dictionary files. This API is a no-op on macOS. + +#### `ses.listWordsInSpellCheckerDictionary()` + +Returns `Promise<string[]>` - An array of all words in app's custom dictionary. +Resolves when the full dictionary is loaded from disk. + +#### `ses.addWordToSpellCheckerDictionary(word)` + +* `word` string - The word you want to add to the dictionary + +Returns `boolean` - Whether the word was successfully written to the custom dictionary. This API +will not work on non-persistent (in-memory) sessions. + +**Note:** On macOS and Windows 10 this word will be written to the OS custom dictionary as well + +#### `ses.removeWordFromSpellCheckerDictionary(word)` + +* `word` string - The word you want to remove from the dictionary + +Returns `boolean` - Whether the word was successfully removed from the custom dictionary. This API +will not work on non-persistent (in-memory) sessions. + +**Note:** On macOS and Windows 10 this word will be removed from the OS custom dictionary as well + +#### `ses.loadExtension(path[, options])` + +* `path` string - Path to a directory containing an unpacked Chrome extension +* `options` Object (optional) + * `allowFileAccess` boolean - Whether to allow the extension to read local files over `file://` + protocol and inject content scripts into `file://` pages. This is required e.g. for loading + devtools extensions on `file://` URLs. Defaults to false. + +Returns `Promise<Extension>` - resolves when the extension is loaded. + +This method will raise an exception if the extension could not be loaded. If +there are warnings when installing the extension (e.g. if the extension +requests an API that Electron does not support) then they will be logged to the +console. + +Note that Electron does not support the full range of Chrome extensions APIs. +See [Supported Extensions APIs](extensions.md#supported-extensions-apis) for +more details on what is supported. + +Note that in previous versions of Electron, extensions that were loaded would +be remembered for future runs of the application. This is no longer the case: +`loadExtension` must be called on every boot of your app if you want the +extension to be loaded. + +```js +const { app, session } = require('electron') +const path = require('path') + +app.on('ready', async () => { + await session.defaultSession.loadExtension( + path.join(__dirname, 'react-devtools'), + // allowFileAccess is required to load the devtools extension on file:// URLs. + { allowFileAccess: true } + ) + // Note that in order to use the React DevTools extension, you'll need to + // download and unzip a copy of the extension. +}) +``` + +This API does not support loading packed (.crx) extensions. + +**Note:** This API cannot be called before the `ready` event of the `app` module +is emitted. + +**Note:** Loading extensions into in-memory (non-persistent) sessions is not +supported and will throw an error. + +#### `ses.removeExtension(extensionId)` + +* `extensionId` string - ID of extension to remove + +Unloads an extension. + +**Note:** This API cannot be called before the `ready` event of the `app` module +is emitted. + +#### `ses.getExtension(extensionId)` + +* `extensionId` string - ID of extension to query + +Returns `Extension` | `null` - The loaded extension with the given ID. + +**Note:** This API cannot be called before the `ready` event of the `app` module +is emitted. + +#### `ses.getAllExtensions()` + +Returns `Extension[]` - A list of all loaded extensions. + +**Note:** This API cannot be called before the `ready` event of the `app` module +is emitted. + +#### `ses.getStoragePath()` + +A `string | null` indicating the absolute file system path where data for this +session is persisted on disk. For in memory sessions this returns `null`. + ### Instance Properties The following properties are available on instances of `Session`: +#### `ses.availableSpellCheckerLanguages` _Readonly_ + +A `string[]` array which consists of all the known available spell checker languages. Providing a language +code to the `setSpellCheckerLanguages` API that isn't in this array will result in an error. + +#### `ses.spellCheckerEnabled` + +A `boolean` indicating whether builtin spell checker is enabled. + +#### `ses.storagePath` _Readonly_ + +A `string | null` indicating the absolute file system path where data for this +session is persisted on disk. For in memory sessions this returns `null`. + #### `ses.cookies` _Readonly_ A [`Cookies`](cookies.md) object for this session. +#### `ses.serviceWorkers` _Readonly_ + +A [`ServiceWorkers`](service-workers.md) object for this session. + #### `ses.webRequest` _Readonly_ A [`WebRequest`](web-request.md) object for this session. @@ -443,14 +1066,14 @@ A [`Protocol`](protocol.md) object for this session. const { app, session } = require('electron') const path = require('path') -app.on('ready', function () { +app.whenReady().then(() => { const protocol = session.fromPartition('some-partition').protocol - protocol.registerFileProtocol('atom', function (request, callback) { - var url = request.url.substr(7) + if (!protocol.registerFileProtocol('atom', (request, callback) => { + const url = request.url.substr(7) callback({ path: path.normalize(`${__dirname}/${url}`) }) - }, function (error) { - if (error) console.error('Failed to register protocol') - }) + })) { + console.error('Failed to register protocol') + } }) ``` @@ -461,7 +1084,7 @@ A [`NetLog`](net-log.md) object for this session. ```javascript const { app, session } = require('electron') -app.on('ready', async function () { +app.whenReady().then(async () => { const netLog = session.fromPartition('some-partition').netLog netLog.startLogging('/path/to/net-log') // After some network events diff --git a/docs/api/share-menu.md b/docs/api/share-menu.md new file mode 100644 index 0000000000000..a886ea52682af --- /dev/null +++ b/docs/api/share-menu.md @@ -0,0 +1,47 @@ +# ShareMenu + +The `ShareMenu` class creates [Share Menu][share-menu] on macOS, which can be +used to share information from the current context to apps, social media +accounts, and other services. + +For including the share menu as a submenu of other menus, please use the +`shareMenu` role of [`MenuItem`](menu-item.md). + +## Class: ShareMenu + +> Create share menu on macOS. + +Process: [Main](../glossary.md#main-process) + +### `new ShareMenu(sharingItem)` + +* `sharingItem` SharingItem - The item to share. + +Creates a new share menu. + +### Instance Methods + +The `shareMenu` object has the following instance methods: + +#### `shareMenu.popup([options])` + +* `options` PopupOptions (optional) + * `browserWindow` [BrowserWindow](browser-window.md) (optional) - Default is the focused window. + * `x` number (optional) - Default is the current mouse cursor position. + Must be declared if `y` is declared. + * `y` number (optional) - Default is the current mouse cursor position. + Must be declared if `x` is declared. + * `positioningItem` number (optional) _macOS_ - The index of the menu item to + be positioned under the mouse cursor at the specified coordinates. Default + is -1. + * `callback` Function (optional) - Called when menu is closed. + +Pops up this menu as a context menu in the [`BrowserWindow`](browser-window.md). + +#### `shareMenu.closePopup([browserWindow])` + +* `browserWindow` [BrowserWindow](browser-window.md) (optional) - Default is the focused window. + +Closes the context menu in the `browserWindow`. + +[share-menu]: https://developer.apple.com/design/human-interface-guidelines/macos/extensions/share-extensions/ diff --git a/docs/api/shell.md b/docs/api/shell.md index 6f6b9306fe28f..a0e34c0dbe61d 100644 --- a/docs/api/shell.md +++ b/docs/api/shell.md @@ -2,7 +2,7 @@ > Manage files and URLs using their default applications. -Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process) +Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process) (non-sandboxed only) The `shell` module provides functions related to desktop integration. @@ -14,42 +14,46 @@ const { shell } = require('electron') shell.openExternal('https://github.com') ``` +**Note:** While the `shell` module can be used in the renderer process, it will not function in a sandboxed renderer. + ## Methods The `shell` module has the following methods: ### `shell.showItemInFolder(fullPath)` -* `fullPath` String +* `fullPath` string Show the given file in a file manager. If possible, select the file. -### `shell.openItem(fullPath)` +### `shell.openPath(path)` -* `fullPath` String +* `path` string -Returns `Boolean` - Whether the item was successfully opened. +Returns `Promise<string>` - Resolves with a string containing the error message corresponding to the failure if a failure occurred, otherwise "". Open the given file in the desktop's default manner. ### `shell.openExternal(url[, options])` -* `url` String - Max 2081 characters on windows. +* `url` string - Max 2081 characters on windows. * `options` Object (optional) - * `activate` Boolean (optional) _macOS_ - `true` to bring the opened application to the foreground. The default is `true`. - * `workingDirectory` String (optional) _Windows_ - The working directory. + * `activate` boolean (optional) _macOS_ - `true` to bring the opened application to the foreground. The default is `true`. + * `workingDirectory` string (optional) _Windows_ - The working directory. Returns `Promise<void>` Open the given external protocol URL in the desktop's default manner. (For example, mailto: URLs in the user's default mail agent). -### `shell.moveItemToTrash(fullPath)` +### `shell.trashItem(path)` -* `fullPath` String +* `path` string - path to the item to be moved to the trash. -Returns `Boolean` - Whether the item was successfully moved to the trash. +Returns `Promise<void>` - Resolves when the operation has been completed. +Rejects if there was an error while deleting the requested item. -Move the given file to trash and returns a boolean status for the operation. +This moves a path to the OS-specific trash location (Trash on macOS, Recycle +Bin on Windows, and a desktop-environment-specific location on Linux). ### `shell.beep()` @@ -57,21 +61,21 @@ Play the beep sound. ### `shell.writeShortcutLink(shortcutPath[, operation], options)` _Windows_ -* `shortcutPath` String -* `operation` String (optional) - Default is `create`, can be one of following: +* `shortcutPath` string +* `operation` string (optional) - Default is `create`, can be one of following: * `create` - Creates a new shortcut, overwriting if necessary. * `update` - Updates specified properties only on an existing shortcut. * `replace` - Overwrites an existing shortcut, fails if the shortcut doesn't exist. * `options` [ShortcutDetails](structures/shortcut-details.md) -Returns `Boolean` - Whether the shortcut was created successfully. +Returns `boolean` - Whether the shortcut was created successfully. Creates or updates a shortcut link at `shortcutPath`. ### `shell.readShortcutLink(shortcutPath)` _Windows_ -* `shortcutPath` String +* `shortcutPath` string Returns [`ShortcutDetails`](structures/shortcut-details.md) diff --git a/docs/api/structures/bluetooth-device.md b/docs/api/structures/bluetooth-device.md index 33d3bb51f94da..b3b408a2739b1 100644 --- a/docs/api/structures/bluetooth-device.md +++ b/docs/api/structures/bluetooth-device.md @@ -1,4 +1,4 @@ # BluetoothDevice Object -* `deviceName` String -* `deviceId` String +* `deviceName` string +* `deviceId` string diff --git a/docs/api/structures/certificate-principal.md b/docs/api/structures/certificate-principal.md index 2eb5574bffe05..e070dfd137e1b 100644 --- a/docs/api/structures/certificate-principal.md +++ b/docs/api/structures/certificate-principal.md @@ -1,8 +1,8 @@ # CertificatePrincipal Object -* `commonName` String - Common Name. -* `organizations` String[] - Organization names. -* `organizationUnits` String[] - Organization Unit names. -* `locality` String - Locality. -* `state` String - State or province. -* `country` String - Country or region. +* `commonName` string - Common Name. +* `organizations` string[] - Organization names. +* `organizationUnits` string[] - Organization Unit names. +* `locality` string - Locality. +* `state` string - State or province. +* `country` string - Country or region. diff --git a/docs/api/structures/certificate.md b/docs/api/structures/certificate.md index 3c521b2d360e5..7fc240bf3b49b 100644 --- a/docs/api/structures/certificate.md +++ b/docs/api/structures/certificate.md @@ -1,12 +1,12 @@ # Certificate Object -* `data` String - PEM encoded data +* `data` string - PEM encoded data * `issuer` [CertificatePrincipal](certificate-principal.md) - Issuer principal -* `issuerName` String - Issuer's Common Name +* `issuerName` string - Issuer's Common Name * `issuerCert` Certificate - Issuer certificate (if not self-signed) * `subject` [CertificatePrincipal](certificate-principal.md) - Subject principal -* `subjectName` String - Subject's Common Name -* `serialNumber` String - Hex value represented string -* `validStart` Number - Start date of the certificate being valid in seconds -* `validExpiry` Number - End date of the certificate being valid in seconds -* `fingerprint` String - Fingerprint of the certificate +* `subjectName` string - Subject's Common Name +* `serialNumber` string - Hex value represented string +* `validStart` number - Start date of the certificate being valid in seconds +* `validExpiry` number - End date of the certificate being valid in seconds +* `fingerprint` string - Fingerprint of the certificate diff --git a/docs/api/structures/cookie.md b/docs/api/structures/cookie.md index def78f539ea30..a314cf84f5d7d 100644 --- a/docs/api/structures/cookie.md +++ b/docs/api/structures/cookie.md @@ -1,14 +1,15 @@ # Cookie Object -* `name` String - The name of the cookie. -* `value` String - The value of the cookie. -* `domain` String (optional) - The domain of the cookie; this will be normalized with a preceding dot so that it's also valid for subdomains. -* `hostOnly` Boolean (optional) - Whether the cookie is a host-only cookie; this will only be `true` if no domain was passed. -* `path` String (optional) - The path of the cookie. -* `secure` Boolean (optional) - Whether the cookie is marked as secure. -* `httpOnly` Boolean (optional) - Whether the cookie is marked as HTTP only. -* `session` Boolean (optional) - Whether the cookie is a session cookie or a persistent +* `name` string - The name of the cookie. +* `value` string - The value of the cookie. +* `domain` string (optional) - The domain of the cookie; this will be normalized with a preceding dot so that it's also valid for subdomains. +* `hostOnly` boolean (optional) - Whether the cookie is a host-only cookie; this will only be `true` if no domain was passed. +* `path` string (optional) - The path of the cookie. +* `secure` boolean (optional) - Whether the cookie is marked as secure. +* `httpOnly` boolean (optional) - Whether the cookie is marked as HTTP only. +* `session` boolean (optional) - Whether the cookie is a session cookie or a persistent cookie with an expiration date. * `expirationDate` Double (optional) - The expiration date of the cookie as the number of seconds since the UNIX epoch. Not provided for session cookies. +* `sameSite` string - The [Same Site](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_cookies) policy applied to this cookie. Can be `unspecified`, `no_restriction`, `lax` or `strict`. diff --git a/docs/api/structures/cpu-usage.md b/docs/api/structures/cpu-usage.md index 4d896ee2dbe0c..00c267533a1ee 100644 --- a/docs/api/structures/cpu-usage.md +++ b/docs/api/structures/cpu-usage.md @@ -1,7 +1,7 @@ # CPUUsage Object -* `percentCPUUsage` Number - Percentage of CPU used since the last call to getCPUUsage. +* `percentCPUUsage` number - Percentage of CPU used since the last call to getCPUUsage. First call returns 0. -* `idleWakeupsPerSecond` Number - The number of average idle CPU wakeups per second +* `idleWakeupsPerSecond` number - The number of average idle CPU wakeups per second since the last call to getCPUUsage. First call returns 0. Will always return 0 on Windows. diff --git a/docs/api/structures/crash-report.md b/docs/api/structures/crash-report.md index 5248cba8043f2..c5de28a1a6f1f 100644 --- a/docs/api/structures/crash-report.md +++ b/docs/api/structures/crash-report.md @@ -1,4 +1,4 @@ # CrashReport Object * `date` Date -* `id` String +* `id` string diff --git a/docs/api/structures/custom-scheme.md b/docs/api/structures/custom-scheme.md index 1a720a76686e6..402a17506a1bd 100644 --- a/docs/api/structures/custom-scheme.md +++ b/docs/api/structures/custom-scheme.md @@ -1,10 +1,11 @@ # CustomScheme Object -* `scheme` String - Custom schemes to be registered with options. +* `scheme` string - Custom schemes to be registered with options. * `privileges` Object (optional) - * `standard` Boolean (optional) - Default false. - * `secure` Boolean (optional) - Default false. - * `bypassCSP` Boolean (optional) - Default false. - * `allowServiceWorkers` Boolean (optional) - Default false. - * `supportFetchAPI` Boolean (optional) - Default false. - * `corsEnabled` Boolean (optional) - Default false. + * `standard` boolean (optional) - Default false. + * `secure` boolean (optional) - Default false. + * `bypassCSP` boolean (optional) - Default false. + * `allowServiceWorkers` boolean (optional) - Default false. + * `supportFetchAPI` boolean (optional) - Default false. + * `corsEnabled` boolean (optional) - Default false. + * `stream` boolean (optional) - Default false. diff --git a/docs/api/structures/desktop-capturer-source.md b/docs/api/structures/desktop-capturer-source.md index ffb34b79b7dda..1740ee7924f52 100644 --- a/docs/api/structures/desktop-capturer-source.md +++ b/docs/api/structures/desktop-capturer-source.md @@ -1,10 +1,13 @@ # DesktopCapturerSource Object -* `id` String - The identifier of a window or screen that can be used as a +* `id` string - The identifier of a window or screen that can be used as a `chromeMediaSourceId` constraint when calling [`navigator.webkitGetUserMedia`]. The format of the identifier will be - `window:XX` or `screen:XX`, where `XX` is a random generated number. -* `name` String - A screen source will be named either `Entire Screen` or + `window:XX:YY` or `screen:ZZ:0`. XX is the windowID/handle. YY is 1 for + the current process, and 0 for all others. ZZ is a sequential number + that represents the screen, and it does not equal to the index in the + source's name. +* `name` string - A screen source will be named either `Entire Screen` or `Screen <index>`, while the name of a window source will match the window title. * `thumbnail` [NativeImage](../native-image.md) - A thumbnail image. **Note:** @@ -12,12 +15,12 @@ `thumbnailSize` specified in the `options` passed to `desktopCapturer.getSources`. The actual size depends on the scale of the screen or window. -* `display_id` String - A unique identifier that will correspond to the `id` of +* `display_id` string - A unique identifier that will correspond to the `id` of the matching [Display](display.md) returned by the [Screen API](../screen.md). On some platforms, this is equivalent to the `XX` portion of the `id` field above and on others it will differ. It will be an empty string if not available. * `appIcon` [NativeImage](../native-image.md) - An icon image of the application that owns the window or null if the source has a type screen. - The size of the icon is not known in advance and depends on what the + The size of the icon is not known in advance and depends on what the application provides. diff --git a/docs/api/structures/display.md b/docs/api/structures/display.md index b7cf205cec9ed..8782d34d1b46b 100644 --- a/docs/api/structures/display.md +++ b/docs/api/structures/display.md @@ -1,20 +1,21 @@ # Display Object -* `id` Number - Unique identifier associated with the display. -* `rotation` Number - Can be 0, 90, 180, 270, represents screen rotation in +* `id` number - Unique identifier associated with the display. +* `rotation` number - Can be 0, 90, 180, 270, represents screen rotation in clock-wise degrees. -* `scaleFactor` Number - Output device's pixel scale factor. -* `touchSupport` String - Can be `available`, `unavailable`, `unknown`. -* `monochrome` Boolean - Whether or not the display is a monochrome display. -* `accelerometerSupport` String - Can be `available`, `unavailable`, `unknown`. -* `colorSpace` String - represent a color space (three-dimensional object which contains all realizable color combinations) for the purpose of color conversions -* `colorDepth` Number - The number of bits per pixel. -* `depthPerComponent` Number - The number of bits per color component. -* `bounds` [Rectangle](rectangle.md) +* `scaleFactor` number - Output device's pixel scale factor. +* `touchSupport` string - Can be `available`, `unavailable`, `unknown`. +* `monochrome` boolean - Whether or not the display is a monochrome display. +* `accelerometerSupport` string - Can be `available`, `unavailable`, `unknown`. +* `colorSpace` string - represent a color space (three-dimensional object which contains all realizable color combinations) for the purpose of color conversions +* `colorDepth` number - The number of bits per pixel. +* `depthPerComponent` number - The number of bits per color component. +* `displayFrequency` number - The display refresh rate. +* `bounds` [Rectangle](rectangle.md) - the bounds of the display in DIP points. * `size` [Size](size.md) -* `workArea` [Rectangle](rectangle.md) +* `workArea` [Rectangle](rectangle.md) - the work area of the display in DIP points. * `workAreaSize` [Size](size.md) -* `internal` Boolean - `true` for an internal display and `false` for an external display +* `internal` boolean - `true` for an internal display and `false` for an external display The `Display` object represents a physical display connected to the system. A fake `Display` may exist on a headless system, or a `Display` may correspond to diff --git a/docs/api/structures/extension-info.md b/docs/api/structures/extension-info.md new file mode 100644 index 0000000000000..1e8adec301c73 --- /dev/null +++ b/docs/api/structures/extension-info.md @@ -0,0 +1,4 @@ +# ExtensionInfo Object + +* `name` string +* `version` string diff --git a/docs/api/structures/extension.md b/docs/api/structures/extension.md new file mode 100644 index 0000000000000..c679398778a38 --- /dev/null +++ b/docs/api/structures/extension.md @@ -0,0 +1,8 @@ +# Extension Object + +* `id` string +* `manifest` any - Copy of the [extension's manifest data](https://developer.chrome.com/extensions/manifest). +* `name` string +* `path` string - The extension's file path. +* `version` string +* `url` string - The extension's `chrome-extension://` URL. diff --git a/docs/api/structures/file-filter.md b/docs/api/structures/file-filter.md index 014350a60f861..1da4feafd5be4 100644 --- a/docs/api/structures/file-filter.md +++ b/docs/api/structures/file-filter.md @@ -1,4 +1,4 @@ # FileFilter Object -* `name` String -* `extensions` String[] +* `name` string +* `extensions` string[] diff --git a/docs/api/structures/file-path-with-headers.md b/docs/api/structures/file-path-with-headers.md index 9bb1526edcd10..e07688eae1f38 100644 --- a/docs/api/structures/file-path-with-headers.md +++ b/docs/api/structures/file-path-with-headers.md @@ -1,4 +1,4 @@ # FilePathWithHeaders Object -* `path` String - The path to the file to send. +* `path` string - The path to the file to send. * `headers` Record<string, string> (optional) - Additional headers to be sent. diff --git a/docs/api/structures/gpu-feature-status.md b/docs/api/structures/gpu-feature-status.md index b0e5b8d15165b..eb5ee96926a54 100644 --- a/docs/api/structures/gpu-feature-status.md +++ b/docs/api/structures/gpu-feature-status.md @@ -1,18 +1,18 @@ # GPUFeatureStatus Object -* `2d_canvas` String - Canvas. -* `flash_3d` String - Flash. -* `flash_stage3d` String - Flash Stage3D. -* `flash_stage3d_baseline` String - Flash Stage3D Baseline profile. -* `gpu_compositing` String - Compositing. -* `multiple_raster_threads` String - Multiple Raster Threads. -* `native_gpu_memory_buffers` String - Native GpuMemoryBuffers. -* `rasterization` String - Rasterization. -* `video_decode` String - Video Decode. -* `video_encode` String - Video Encode. -* `vpx_decode` String - VPx Video Decode. -* `webgl` String - WebGL. -* `webgl2` String - WebGL2. +* `2d_canvas` string - Canvas. +* `flash_3d` string - Flash. +* `flash_stage3d` string - Flash Stage3D. +* `flash_stage3d_baseline` string - Flash Stage3D Baseline profile. +* `gpu_compositing` string - Compositing. +* `multiple_raster_threads` string - Multiple Raster Threads. +* `native_gpu_memory_buffers` string - Native GpuMemoryBuffers. +* `rasterization` string - Rasterization. +* `video_decode` string - Video Decode. +* `video_encode` string - Video Encode. +* `vpx_decode` string - VPx Video Decode. +* `webgl` string - WebGL. +* `webgl2` string - WebGL2. Possible values: diff --git a/docs/api/structures/hid-device.md b/docs/api/structures/hid-device.md new file mode 100644 index 0000000000000..a6a097061dc22 --- /dev/null +++ b/docs/api/structures/hid-device.md @@ -0,0 +1,8 @@ +# HIDDevice Object + +* `deviceId` string - Unique identifier for the device. +* `name` string - Name of the device. +* `vendorId` Integer - The USB vendor ID. +* `productId` Integer - The USB product ID. +* `serialNumber` string (optional) - The USB device serial number. +* `guid` string (optional) - Unique identifier for the HID interface. A device may have multiple HID interfaces. diff --git a/docs/api/structures/input-event.md b/docs/api/structures/input-event.md index c24cb528a79dd..21efec36d3e41 100644 --- a/docs/api/structures/input-event.md +++ b/docs/api/structures/input-event.md @@ -1,6 +1,6 @@ # InputEvent Object -* `modifiers` String[] - An array of modifiers of the event, can - be `shift`, `control`, `alt`, `meta`, `isKeypad`, `isAutoRepeat`, - `leftButtonDown`, `middleButtonDown`, `rightButtonDown`, `capsLock`, - `numLock`, `left`, `right`. +* `modifiers` string[] (optional) - An array of modifiers of the event, can + be `shift`, `control`, `ctrl`, `alt`, `meta`, `command`, `cmd`, `isKeypad`, + `isAutoRepeat`, `leftButtonDown`, `middleButtonDown`, `rightButtonDown`, + `capsLock`, `numLock`, `left`, `right`. diff --git a/docs/api/structures/io-counters.md b/docs/api/structures/io-counters.md index 62ad39a90b098..23a6883b07f52 100644 --- a/docs/api/structures/io-counters.md +++ b/docs/api/structures/io-counters.md @@ -1,8 +1,8 @@ # IOCounters Object -* `readOperationCount` Number - The number of I/O read operations. -* `writeOperationCount` Number - The number of I/O write operations. -* `otherOperationCount` Number - Then number of I/O other operations. -* `readTransferCount` Number - The number of I/O read transfers. -* `writeTransferCount` Number - The number of I/O write transfers. -* `otherTransferCount` Number - Then number of I/O other transfers. +* `readOperationCount` number - The number of I/O read operations. +* `writeOperationCount` number - The number of I/O write operations. +* `otherOperationCount` number - Then number of I/O other operations. +* `readTransferCount` number - The number of I/O read transfers. +* `writeTransferCount` number - The number of I/O write transfers. +* `otherTransferCount` number - Then number of I/O other transfers. diff --git a/docs/api/structures/ipc-main-event.md b/docs/api/structures/ipc-main-event.md index 711a2d7b33873..868fe4d0fb54e 100644 --- a/docs/api/structures/ipc-main-event.md +++ b/docs/api/structures/ipc-main-event.md @@ -1,7 +1,11 @@ # IpcMainEvent Object extends `Event` +* `processId` Integer - The internal ID of the renderer process that sent this message * `frameId` Integer - The ID of the renderer frame that sent this message * `returnValue` any - Set this to the value to be returned in a synchronous message * `sender` WebContents - Returns the `webContents` that sent the message +* `senderFrame` WebFrameMain _Readonly_ - The frame that sent this message +* `ports` MessagePortMain[] - A list of MessagePorts that were transferred with this message * `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guarantee the reply will go to the correct process and frame. + * `channel` string * `...args` any[] diff --git a/docs/api/structures/ipc-main-invoke-event.md b/docs/api/structures/ipc-main-invoke-event.md index 235b219c2d6ea..d15f44df40685 100644 --- a/docs/api/structures/ipc-main-invoke-event.md +++ b/docs/api/structures/ipc-main-invoke-event.md @@ -1,4 +1,6 @@ # IpcMainInvokeEvent Object extends `Event` +* `processId` Integer - The internal ID of the renderer process that sent this message * `frameId` Integer - The ID of the renderer frame that sent this message * `sender` WebContents - Returns the `webContents` that sent the message +* `senderFrame` WebFrameMain _Readonly_ - The frame that sent this message diff --git a/docs/api/structures/ipc-renderer-event.md b/docs/api/structures/ipc-renderer-event.md index 6f56d8cf3f677..1ac2def46d4aa 100644 --- a/docs/api/structures/ipc-renderer-event.md +++ b/docs/api/structures/ipc-renderer-event.md @@ -2,5 +2,6 @@ * `sender` IpcRenderer - The `IpcRenderer` instance that emitted the event originally * `senderId` Integer - The `webContents.id` that sent the message, you can call `event.sender.sendTo(event.senderId, ...)` to reply to the message, see [ipcRenderer.sendTo][ipc-renderer-sendto] for more information. This only applies to messages sent from a different renderer. Messages sent directly from the main process set `event.senderId` to `0`. +* `ports` MessagePort[] - A list of MessagePorts that were transferred with this message -[ipc-renderer-sendto]: #ipcrenderersendtowindowid-channel--arg1-arg2- +[ipc-renderer-sendto]: ../ipc-renderer.md#ipcrenderersendtowebcontentsid-channel-args diff --git a/docs/api/structures/jump-list-category.md b/docs/api/structures/jump-list-category.md index 07627e78c98e7..117483f1a8e5e 100644 --- a/docs/api/structures/jump-list-category.md +++ b/docs/api/structures/jump-list-category.md @@ -1,6 +1,6 @@ # JumpListCategory Object -* `type` String (optional) - One of the following: +* `type` string (optional) - One of the following: * `tasks` - Items in this category will be placed into the standard `Tasks` category. There can be only one such category, and it will always be displayed at the bottom of the Jump List. @@ -10,7 +10,7 @@ of the category and its items are set by Windows. Items may be added to this category indirectly using `app.addRecentDocument(path)`. * `custom` - Displays tasks or file links, `name` must be set by the app. -* `name` String (optional) - Must be set if `type` is `custom`, otherwise it should be +* `name` string (optional) - Must be set if `type` is `custom`, otherwise it should be omitted. * `items` JumpListItem[] (optional) - Array of [`JumpListItem`](jump-list-item.md) objects if `type` is `tasks` or `custom`, otherwise it should be omitted. @@ -19,3 +19,7 @@ property set then its `type` is assumed to be `tasks`. If the `name` property is set but the `type` property is omitted then the `type` is assumed to be `custom`. + +**Note:** The maximum length of a Jump List item's `description` property is +260 characters. Beyond this limit, the item will not be added to the Jump +List, nor will it be displayed. diff --git a/docs/api/structures/jump-list-item.md b/docs/api/structures/jump-list-item.md index d75637cb472dc..1731602215bb3 100644 --- a/docs/api/structures/jump-list-item.md +++ b/docs/api/structures/jump-list-item.md @@ -1,29 +1,29 @@ # JumpListItem Object -* `type` String (optional) - One of the following: +* `type` string (optional) - One of the following: * `task` - A task will launch an app with specific arguments. * `separator` - Can be used to separate items in the standard `Tasks` category. * `file` - A file link will open a file using the app that created the Jump List, for this to work the app must be registered as a handler for the file type (though it doesn't have to be the default handler). -* `path` String (optional) - Path of the file to open, should only be set if `type` is +* `path` string (optional) - Path of the file to open, should only be set if `type` is `file`. -* `program` String (optional) - Path of the program to execute, usually you should +* `program` string (optional) - Path of the program to execute, usually you should specify `process.execPath` which opens the current program. Should only be set if `type` is `task`. -* `args` String (optional) - The command line arguments when `program` is executed. Should +* `args` string (optional) - The command line arguments when `program` is executed. Should only be set if `type` is `task`. -* `title` String (optional) - The text to be displayed for the item in the Jump List. +* `title` string (optional) - The text to be displayed for the item in the Jump List. Should only be set if `type` is `task`. -* `description` String (optional) - Description of the task (displayed in a tooltip). - Should only be set if `type` is `task`. -* `iconPath` String (optional) - The absolute path to an icon to be displayed in a +* `description` string (optional) - Description of the task (displayed in a tooltip). + Should only be set if `type` is `task`. Maximum length 260 characters. +* `iconPath` string (optional) - The absolute path to an icon to be displayed in a Jump List, which can be an arbitrary resource file that contains an icon (e.g. `.ico`, `.exe`, `.dll`). You can usually specify `process.execPath` to show the program icon. -* `iconIndex` Number (optional) - The index of the icon in the resource file. If a +* `iconIndex` number (optional) - The index of the icon in the resource file. If a resource file contains multiple icons this value can be used to specify the zero-based index of the icon that should be displayed for this task. If a resource file contains only one icon, this property should be set to zero. -* `workingDirectory` String (optional) - The working directory. Default is empty. +* `workingDirectory` string (optional) - The working directory. Default is empty. diff --git a/docs/api/structures/keyboard-event.md b/docs/api/structures/keyboard-event.md index 95442ee0e5744..b68c4dad136e0 100644 --- a/docs/api/structures/keyboard-event.md +++ b/docs/api/structures/keyboard-event.md @@ -1,7 +1,7 @@ -# KeyboardEvent Object extends `Event` +# KeyboardEvent Object -* `ctrlKey` Boolean (optional) - whether the Control key was used in an accelerator to trigger the Event -* `metaKey` Boolean (optional) - whether a meta key was used in an accelerator to trigger the Event -* `shiftKey` Boolean (optional) - whether a Shift key was used in an accelerator to trigger the Event -* `altKey` Boolean (optional) - whether an Alt key was used in an accelerator to trigger the Event -* `triggeredByAccelerator` Boolean (optional) - whether an accelerator was used to trigger the event as opposed to another user gesture like mouse click +* `ctrlKey` boolean (optional) - whether the Control key was used in an accelerator to trigger the Event +* `metaKey` boolean (optional) - whether a meta key was used in an accelerator to trigger the Event +* `shiftKey` boolean (optional) - whether a Shift key was used in an accelerator to trigger the Event +* `altKey` boolean (optional) - whether an Alt key was used in an accelerator to trigger the Event +* `triggeredByAccelerator` boolean (optional) - whether an accelerator was used to trigger the event as opposed to another user gesture like mouse click diff --git a/docs/api/structures/keyboard-input-event.md b/docs/api/structures/keyboard-input-event.md index 96ce3bf77f458..de7bef2d770f6 100644 --- a/docs/api/structures/keyboard-input-event.md +++ b/docs/api/structures/keyboard-input-event.md @@ -1,6 +1,6 @@ # KeyboardInputEvent Object extends `InputEvent` -* `type` String - The type of the event, can be `keyDown`, `keyUp` or `char`. -* `keyCode` String - The character that will be sent +* `type` string - The type of the event, can be `keyDown`, `keyUp` or `char`. +* `keyCode` string - The character that will be sent as the keyboard event. Should only use the valid key codes in [Accelerator](../accelerator.md). diff --git a/docs/api/structures/memory-usage-details.md b/docs/api/structures/memory-usage-details.md index d77e07dedfc26..2b55f98b6ee62 100644 --- a/docs/api/structures/memory-usage-details.md +++ b/docs/api/structures/memory-usage-details.md @@ -1,5 +1,5 @@ # MemoryUsageDetails Object -* `count` Number -* `size` Number -* `liveSize` Number +* `count` number +* `size` number +* `liveSize` number diff --git a/docs/api/structures/mime-typed-buffer.md b/docs/api/structures/mime-typed-buffer.md index 470dd3a3f7d70..0a8459335c7bb 100644 --- a/docs/api/structures/mime-typed-buffer.md +++ b/docs/api/structures/mime-typed-buffer.md @@ -1,4 +1,5 @@ # MimeTypedBuffer Object -* `mimeType` String - The mimeType of the Buffer that you are sending. +* `mimeType` string (optional) - MIME type of the buffer. +* `charset` string (optional) - Charset of the buffer. * `data` Buffer - The actual Buffer content. diff --git a/docs/api/structures/mouse-input-event.md b/docs/api/structures/mouse-input-event.md index 879f669cf2ca1..8bc04ac48fe36 100644 --- a/docs/api/structures/mouse-input-event.md +++ b/docs/api/structures/mouse-input-event.md @@ -1,10 +1,10 @@ # MouseInputEvent Object extends `InputEvent` -* `type` String - The type of the event, can be `mouseDown`, +* `type` string - The type of the event, can be `mouseDown`, `mouseUp`, `mouseEnter`, `mouseLeave`, `contextMenu`, `mouseWheel` or `mouseMove`. * `x` Integer * `y` Integer -* `button` String (optional) - The button pressed, can be `left`, `middle`, `right`. +* `button` string (optional) - The button pressed, can be `left`, `middle`, `right`. * `globalX` Integer (optional) * `globalY` Integer (optional) * `movementX` Integer (optional) diff --git a/docs/api/structures/mouse-wheel-input-event.md b/docs/api/structures/mouse-wheel-input-event.md index 50540e7cd8786..6a1c71ba1f75a 100644 --- a/docs/api/structures/mouse-wheel-input-event.md +++ b/docs/api/structures/mouse-wheel-input-event.md @@ -1,11 +1,11 @@ # MouseWheelInputEvent Object extends `MouseInputEvent` -* `type` String - The type of the event, can be `mouseWheel`. +* `type` string - The type of the event, can be `mouseWheel`. * `deltaX` Integer (optional) * `deltaY` Integer (optional) * `wheelTicksX` Integer (optional) * `wheelTicksY` Integer (optional) * `accelerationRatioX` Integer (optional) * `accelerationRatioY` Integer (optional) -* `hasPreciseScrollingDeltas` Boolean (optional) -* `canScroll` Boolean (optional) +* `hasPreciseScrollingDeltas` boolean (optional) +* `canScroll` boolean (optional) diff --git a/docs/api/structures/new-window-web-contents-event.md b/docs/api/structures/new-window-web-contents-event.md new file mode 100644 index 0000000000000..b56da02603b89 --- /dev/null +++ b/docs/api/structures/new-window-web-contents-event.md @@ -0,0 +1,3 @@ +# NewWindowWebContentsEvent Object extends `Event` + +* `newGuest` BrowserWindow (optional) diff --git a/docs/api/structures/notification-action.md b/docs/api/structures/notification-action.md index e5e14f87f2290..db313aa945248 100644 --- a/docs/api/structures/notification-action.md +++ b/docs/api/structures/notification-action.md @@ -1,7 +1,7 @@ # NotificationAction Object -* `type` String - The type of action, can be `button`. -* `text` String (optional) - The label for the given action. +* `type` string - The type of action, can be `button`. +* `text` string (optional) - The label for the given action. ## Platform / Action Support diff --git a/docs/api/structures/notification-response.md b/docs/api/structures/notification-response.md new file mode 100644 index 0000000000000..32fff8625e1c5 --- /dev/null +++ b/docs/api/structures/notification-response.md @@ -0,0 +1,7 @@ +# NotificationResponse Object + +* `actionIdentifier` string - The identifier string of the action that the user selected. +* `date` number - The delivery date of the notification. +* `identifier` string - The unique identifier for this notification request. +* `userInfo` Record<string, any> - A dictionary of custom information associated with the notification. +* `userText` string (optional) - The text entered or chosen by the user. diff --git a/docs/api/structures/payment-discount.md b/docs/api/structures/payment-discount.md new file mode 100644 index 0000000000000..95e095397808b --- /dev/null +++ b/docs/api/structures/payment-discount.md @@ -0,0 +1,7 @@ +# PaymentDiscount Object + +* `identifier` string - A string used to uniquely identify a discount offer for a product. +* `keyIdentifier` string - A string that identifies the key used to generate the signature. +* `nonce` string - A universally unique ID (UUID) value that you define. +* `signature` string - A UTF-8 string representing the properties of a specific discount offer, cryptographically signed. +* `timestamp` number - The date and time of the signature's creation in milliseconds, formatted in Unix epoch time. diff --git a/docs/api/structures/point.md b/docs/api/structures/point.md index 34e85ef6e55e4..5b792cea0f9c7 100644 --- a/docs/api/structures/point.md +++ b/docs/api/structures/point.md @@ -1,7 +1,7 @@ # Point Object -* `x` Number -* `y` Number +* `x` number +* `y` number **Note:** Both `x` and `y` must be whole integers, when providing a point object as input to an Electron API we will automatically round your `x` and `y` values diff --git a/docs/api/structures/post-body.md b/docs/api/structures/post-body.md new file mode 100644 index 0000000000000..0488988ce456e --- /dev/null +++ b/docs/api/structures/post-body.md @@ -0,0 +1,9 @@ +# PostBody Object + +* `data` ([UploadRawData](upload-raw-data.md) | [UploadFile](upload-file.md))[] - The post data to be sent to the + new window. +* `contentType` string - The `content-type` header used for the data. One of + `application/x-www-form-urlencoded` or `multipart/form-data`. Corresponds to + the `enctype` attribute of the submitted HTML form. +* `boundary` string (optional) - The boundary used to separate multiple parts of + the message. Only valid when `contentType` is `multipart/form-data`. diff --git a/docs/api/structures/printer-info.md b/docs/api/structures/printer-info.md index ed8ed6b7118fd..716759a6dddd7 100644 --- a/docs/api/structures/printer-info.md +++ b/docs/api/structures/printer-info.md @@ -1,9 +1,13 @@ # PrinterInfo Object -* `name` String -* `description` String -* `status` Number -* `isDefault` Boolean +* `name` string - the name of the printer as understood by the OS. +* `displayName` string - the name of the printer as shown in Print Preview. +* `description` string - a longer description of the printer's type. +* `status` number - the current status of the printer. +* `isDefault` boolean - whether or not a given printer is set as the default printer on the OS. +* `options` Object - an object containing a variable number of platform-specific printer information. + +The number represented by `status` means different things on different platforms: on Windows its potential values can be found [here](https://docs.microsoft.com/en-us/windows/win32/printdocs/printer-info-2), and on Linux and macOS they can be found [here](https://www.cups.org/doc/cupspm.html). ## Example @@ -12,13 +16,14 @@ may be different on each platform. ```javascript { - name: 'Zebra_LP2844', - description: 'Zebra LP2844', + name: 'Austin_4th_Floor_Printer___C02XK13BJHD4', + displayName: 'Austin 4th Floor Printer @ C02XK13BJHD4', + description: 'TOSHIBA ColorMFP', status: 3, isDefault: false, options: { copies: '1', - 'device-uri': 'usb://Zebra/LP2844?location=14200000', + 'device-uri': 'dnssd://Austin%204th%20Floor%20Printer%20%40%20C02XK13BJHD4._ipps._tcp.local./?uuid=71687f1e-1147-3274-6674-22de61b110bd', finishings: '3', 'job-cancel-after': '10800', 'job-hold-until': 'no-hold', @@ -26,18 +31,19 @@ may be different on each platform. 'job-sheets': 'none,none', 'marker-change-time': '0', 'number-up': '1', - 'printer-commands': 'none', - 'printer-info': 'Zebra LP2844', + 'printer-commands': 'ReportLevels,PrintSelfTestPage,com.toshiba.ColourProfiles.update,com.toshiba.EFiling.update,com.toshiba.EFiling.checkPassword', + 'printer-info': 'Austin 4th Floor Printer @ C02XK13BJHD4', 'printer-is-accepting-jobs': 'true', - 'printer-is-shared': 'true', + 'printer-is-shared': 'false', + 'printer-is-temporary': 'false', 'printer-location': '', - 'printer-make-and-model': 'Zebra EPL2 Label Printer', + 'printer-make-and-model': 'TOSHIBA ColorMFP', 'printer-state': '3', - 'printer-state-change-time': '1484872644', - 'printer-state-reasons': 'offline-report', - 'printer-type': '36932', - 'printer-uri-supported': 'ipp://localhost/printers/Zebra_LP2844', - system_driverinfo: 'Z' + 'printer-state-change-time': '1573472937', + 'printer-state-reasons': 'offline-report,com.toshiba.snmp.failed', + 'printer-type': '10531038', + 'printer-uri-supported': 'ipp://localhost/printers/Austin_4th_Floor_Printer___C02XK13BJHD4', + system_driverinfo: 'T' } } ``` diff --git a/docs/api/structures/process-metric.md b/docs/api/structures/process-metric.md index bd5a349def623..ca5ef674d3037 100644 --- a/docs/api/structures/process-metric.md +++ b/docs/api/structures/process-metric.md @@ -1,7 +1,7 @@ # ProcessMetric Object * `pid` Integer - Process id of the process. -* `type` String - Process type. One of the following values: +* `type` string - Process type. One of the following values: * `Browser` * `Tab` * `Utility` @@ -11,14 +11,17 @@ * `Pepper Plugin` * `Pepper Plugin Broker` * `Unknown` +* `serviceName` string (optional) - The non-localized name of the process. +* `name` string (optional) - The name of the process. + Examples for utility: `Audio Service`, `Content Decryption Module Service`, `Network Service`, `Video Capture`, etc. * `cpu` [CPUUsage](cpu-usage.md) - CPU usage of the process. -* `creationTime` Number - Creation time for this process. +* `creationTime` number - Creation time for this process. The time is represented as number of milliseconds since epoch. Since the `pid` can be reused after a process dies, it is useful to use both the `pid` and the `creationTime` to uniquely identify a process. * `memory` [MemoryInfo](memory-info.md) - Memory information for the process. -* `sandboxed` Boolean (optional) _macOS_ _Windows_ - Whether the process is sandboxed on OS level. -* `integrityLevel` String (optional) _Windows_ - One of the following values: +* `sandboxed` boolean (optional) _macOS_ _Windows_ - Whether the process is sandboxed on OS level. +* `integrityLevel` string (optional) _Windows_ - One of the following values: * `untrusted` * `low` * `medium` diff --git a/docs/api/structures/product-discount.md b/docs/api/structures/product-discount.md new file mode 100644 index 0000000000000..d0debc5f872c2 --- /dev/null +++ b/docs/api/structures/product-discount.md @@ -0,0 +1,9 @@ +# ProductDiscount Object + +* `identifier` string - A string used to uniquely identify a discount offer for a product. +* `type` number - The type of discount offer. +* `price` number - The discount price of the product in the local currency. +* `priceLocale` string - The locale used to format the discount price of the product. +* `paymentMode` string - The payment mode for this product discount. Can be `payAsYouGo`, `payUpFront`, or `freeTrial`. +* `numberOfPeriods` number - An integer that indicates the number of periods the product discount is available. +* `subscriptionPeriod` [ProductSubscriptionPeriod](product-subscription-period.md) (optional) - An object that defines the period for the product discount. diff --git a/docs/api/structures/product-subscription-period.md b/docs/api/structures/product-subscription-period.md new file mode 100644 index 0000000000000..3551f80d054e3 --- /dev/null +++ b/docs/api/structures/product-subscription-period.md @@ -0,0 +1,4 @@ +# ProductSubscriptionPeriod Object + +* `numberOfUnits` number - The number of units per subscription period. +* `unit` string - The increment of time that a subscription period is specified in. Can be `day`, `week`, `month`, `year`. diff --git a/docs/api/structures/product.md b/docs/api/structures/product.md index 09edff1859f66..d37a57771fd56 100644 --- a/docs/api/structures/product.md +++ b/docs/api/structures/product.md @@ -1,10 +1,18 @@ # Product Object -* `productIdentifier` String - The string that identifies the product to the Apple App Store. -* `localizedDescription` String - A description of the product. -* `localizedTitle` String - The name of the product. -* `contentVersion` String - A string that identifies the version of the content. -* `contentLengths` Number[] - The total size of the content, in bytes. -* `price` Number - The cost of the product in the local currency. -* `formattedPrice` String - The locale formatted price of the product. -* `isDownloadable` Boolean - A Boolean value that indicates whether the App Store has downloadable content for this product. `true` if at least one file has been associated with the product. +* `productIdentifier` string - The string that identifies the product to the Apple App Store. +* `localizedDescription` string - A description of the product. +* `localizedTitle` string - The name of the product. +* `contentVersion` string - A string that identifies the version of the content. +* `contentLengths` number[] - The total size of the content, in bytes. +* `price` number - The cost of the product in the local currency. +* `formattedPrice` string - The locale formatted price of the product. +* `currencyCode` string - 3 character code presenting a product's currency based on the ISO 4217 standard. +* `introductoryPrice` [ProductDiscount](product-discount.md) (optional) - The object containing introductory price information for the product. +available for the product. +* `discounts` [ProductDiscount](product-discount.md)[] - An array of discount offers +* `subscriptionGroupIdentifier` string - The identifier of the subscription group to which the subscription belongs. +* `subscriptionPeriod` [ProductSubscriptionPeriod](product-subscription-period.md) (optional) - The period details for products that are subscriptions. +* `isDownloadable` boolean - A boolean value that indicates whether the App Store has downloadable content for this product. `true` if at least one file has been associated with the product. +* `downloadContentVersion` string - A string that identifies the version of the content. +* `downloadContentLengths` number[] - The total size of the content, in bytes. diff --git a/docs/api/structures/protocol-request.md b/docs/api/structures/protocol-request.md index 4251c93e25c92..aacc062f9996f 100644 --- a/docs/api/structures/protocol-request.md +++ b/docs/api/structures/protocol-request.md @@ -1,6 +1,7 @@ # ProtocolRequest Object -* `url` String -* `referrer` String -* `method` String +* `url` string +* `referrer` string +* `method` string * `uploadData` [UploadData[]](upload-data.md) (optional) +* `headers` Record<string, string> diff --git a/docs/api/structures/protocol-response-upload-data.md b/docs/api/structures/protocol-response-upload-data.md index bcb70da071ed4..f26dade70d7cd 100644 --- a/docs/api/structures/protocol-response-upload-data.md +++ b/docs/api/structures/protocol-response-upload-data.md @@ -1,4 +1,4 @@ # ProtocolResponseUploadData Object -* `contentType` String - MIME type of the content. -* `data` String - Content to be sent. +* `contentType` string - MIME type of the content. +* `data` string | Buffer - Content to be sent. diff --git a/docs/api/structures/protocol-response.md b/docs/api/structures/protocol-response.md index 0d139ed4f85aa..a20873ebb3b46 100644 --- a/docs/api/structures/protocol-response.md +++ b/docs/api/structures/protocol-response.md @@ -3,32 +3,32 @@ * `error` Integer (optional) - When assigned, the `request` will fail with the `error` number . For the available error numbers you can use, please see the [net error list][net-error]. -* `statusCode` Number (optional) - The HTTP response code, default is 200. -* `charset` String (optional) - The charset of response body, default is +* `statusCode` number (optional) - The HTTP response code, default is 200. +* `charset` string (optional) - The charset of response body, default is `"utf-8"`. -* `mimeType` String (optional) - The MIME type of response body, default is +* `mimeType` string (optional) - The MIME type of response body, default is `"text/html"`. Setting `mimeType` would implicitly set the `content-type` header in response, but if `content-type` is already set in `headers`, the `mimeType` would be ignored. * `headers` Record<string, string | string[]> (optional) - An object containing the response headers. The - keys must be String, and values must be either String or Array of String. -* `data` (Buffer | String | ReadableStream) (optional) - The response body. When + keys must be string, and values must be either string or Array of string. +* `data` (Buffer | string | ReadableStream) (optional) - The response body. When returning stream as response, this is a Node.js readable stream representing the response body. When returning `Buffer` as response, this is a `Buffer`. - When returning `String` as response, this is a `String`. This is ignored for + When returning `string` as response, this is a `string`. This is ignored for other types of responses. -* `path` String (optional) - Path to the file which would be sent as response +* `path` string (optional) - Path to the file which would be sent as response body. This is only used for file responses. -* `url` String (optional) - Download the `url` and pipe the result as response +* `url` string (optional) - Download the `url` and pipe the result as response body. This is only used for URL responses. -* `referrer` String (optional) - The `referrer` URL. This is only used for file +* `referrer` string (optional) - The `referrer` URL. This is only used for file and URL responses. -* `method` String (optional) - The HTTP `method`. This is only used for file +* `method` string (optional) - The HTTP `method`. This is only used for file and URL responses. * `session` Session (optional) - The session used for requesting URL, by default the HTTP request will reuse the current session. Setting `session` to `null` would use a random independent session. This is only used for URL responses. -* `uploadData` ProtocolResponseUploadData (optional) - The data used as upload data. This is only +* `uploadData` [ProtocolResponseUploadData](protocol-response-upload-data.md) (optional) - The data used as upload data. This is only used for URL responses when `method` is `"POST"`. -[net-error]: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h +[net-error]: https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h diff --git a/docs/api/structures/rectangle.md b/docs/api/structures/rectangle.md index 9f7a000967d09..58ea74c0e28c0 100644 --- a/docs/api/structures/rectangle.md +++ b/docs/api/structures/rectangle.md @@ -1,6 +1,6 @@ # Rectangle Object -* `x` Number - The x coordinate of the origin of the rectangle (must be an integer). -* `y` Number - The y coordinate of the origin of the rectangle (must be an integer). -* `width` Number - The width of the rectangle (must be an integer). -* `height` Number - The height of the rectangle (must be an integer). +* `x` number - The x coordinate of the origin of the rectangle (must be an integer). +* `y` number - The y coordinate of the origin of the rectangle (must be an integer). +* `width` number - The width of the rectangle (must be an integer). +* `height` number - The height of the rectangle (must be an integer). diff --git a/docs/api/structures/referrer.md b/docs/api/structures/referrer.md index 54741d5797c88..96fdad11edea3 100644 --- a/docs/api/structures/referrer.md +++ b/docs/api/structures/referrer.md @@ -1,7 +1,7 @@ # Referrer Object -* `url` String - HTTP Referrer URL. -* `policy` String - Can be `default`, `unsafe-url`, +* `url` string - HTTP Referrer URL. +* `policy` string - Can be `default`, `unsafe-url`, `no-referrer-when-downgrade`, `no-referrer`, `origin`, `strict-origin-when-cross-origin`, `same-origin` or `strict-origin`. See the [Referrer-Policy spec][1] for more details on the diff --git a/docs/api/structures/remove-client-certificate.md b/docs/api/structures/remove-client-certificate.md deleted file mode 100644 index 7ec853f1633b6..0000000000000 --- a/docs/api/structures/remove-client-certificate.md +++ /dev/null @@ -1,5 +0,0 @@ -# RemoveClientCertificate Object - -* `type` String - `clientCertificate`. -* `origin` String - Origin of the server whose associated client certificate - must be removed from the cache. diff --git a/docs/api/structures/remove-password.md b/docs/api/structures/remove-password.md deleted file mode 100644 index 28a9ed8ae104d..0000000000000 --- a/docs/api/structures/remove-password.md +++ /dev/null @@ -1,15 +0,0 @@ -# RemovePassword Object - -* `type` String - `password`. -* `origin` String (optional) - When provided, the authentication info - related to the origin will only be removed otherwise the entire cache - will be cleared. -* `scheme` String (optional) - Scheme of the authentication. - Can be `basic`, `digest`, `ntlm`, `negotiate`. Must be provided if - removing by `origin`. -* `realm` String (optional) - Realm of the authentication. Must be provided if - removing by `origin`. -* `username` String (optional) - Credentials of the authentication. Must be - provided if removing by `origin`. -* `password` String (optional) - Credentials of the authentication. Must be - provided if removing by `origin`. diff --git a/docs/api/structures/scrubber-item.md b/docs/api/structures/scrubber-item.md index 538579684242b..ce631ea9f9bc3 100644 --- a/docs/api/structures/scrubber-item.md +++ b/docs/api/structures/scrubber-item.md @@ -1,4 +1,4 @@ # ScrubberItem Object -* `label` String (optional) - The text to appear in this item. +* `label` string (optional) - The text to appear in this item. * `icon` NativeImage (optional) - The image to appear in this item. diff --git a/docs/api/structures/segmented-control-segment.md b/docs/api/structures/segmented-control-segment.md index 47dfaa2402755..3b2f468a19d64 100644 --- a/docs/api/structures/segmented-control-segment.md +++ b/docs/api/structures/segmented-control-segment.md @@ -1,5 +1,5 @@ # SegmentedControlSegment Object -* `label` String (optional) - The text to appear in this segment. +* `label` string (optional) - The text to appear in this segment. * `icon` NativeImage (optional) - The image to appear in this segment. -* `enabled` Boolean (optional) - Whether this segment is selectable. Default: true. +* `enabled` boolean (optional) - Whether this segment is selectable. Default: true. diff --git a/docs/api/structures/serial-port.md b/docs/api/structures/serial-port.md new file mode 100644 index 0000000000000..052b961141c29 --- /dev/null +++ b/docs/api/structures/serial-port.md @@ -0,0 +1,10 @@ +# SerialPort Object + +* `portId` string - Unique identifier for the port. +* `portName` string - Name of the port. +* `displayName` string - A string suitable for display to the user for describing this device. +* `vendorId` string - Optional USB vendor ID. +* `productId` string - Optional USB product ID. +* `serialNumber` string - The USB device serial number. +* `usbDriverName` string (optional) - Represents a single serial port on macOS can be enumerated by multiple drivers. +* `deviceInstanceId` string (optional) - A stable identifier on Windows that can be used for device permissions. diff --git a/docs/api/structures/service-worker-info.md b/docs/api/structures/service-worker-info.md new file mode 100644 index 0000000000000..37dd691d96243 --- /dev/null +++ b/docs/api/structures/service-worker-info.md @@ -0,0 +1,5 @@ +# ServiceWorkerInfo Object + +* `scriptUrl` string - The full URL to the script that this service worker runs +* `scope` string - The base URL that this service worker is active for. +* `renderProcessId` number - The virtual ID of the process that this service worker is running in. This is not an OS level PID. This aligns with the ID set used for `webContents.getProcessId()`. diff --git a/docs/api/structures/shared-worker-info.md b/docs/api/structures/shared-worker-info.md new file mode 100644 index 0000000000000..dac1b52bfd5c3 --- /dev/null +++ b/docs/api/structures/shared-worker-info.md @@ -0,0 +1,4 @@ +# SharedWorkerInfo Object + +* `id` string - The unique id of the shared worker. +* `url` string - The url of the shared worker. diff --git a/docs/api/structures/sharing-item.md b/docs/api/structures/sharing-item.md new file mode 100644 index 0000000000000..6e9f451b32b01 --- /dev/null +++ b/docs/api/structures/sharing-item.md @@ -0,0 +1,5 @@ +# SharingItem Object + +* `texts` string[] (optional) - An array of text to share. +* `filePaths` string[] (optional) - An array of files to share. +* `urls` string[] (optional) - An array of URLs to share. diff --git a/docs/api/structures/shortcut-details.md b/docs/api/structures/shortcut-details.md index e7b272d09994f..27d9c4e88bd2e 100644 --- a/docs/api/structures/shortcut-details.md +++ b/docs/api/structures/shortcut-details.md @@ -1,15 +1,17 @@ # ShortcutDetails Object -* `target` String - The target to launch from this shortcut. -* `cwd` String (optional) - The working directory. Default is empty. -* `args` String (optional) - The arguments to be applied to `target` when +* `target` string - The target to launch from this shortcut. +* `cwd` string (optional) - The working directory. Default is empty. +* `args` string (optional) - The arguments to be applied to `target` when launching from this shortcut. Default is empty. -* `description` String (optional) - The description of the shortcut. Default +* `description` string (optional) - The description of the shortcut. Default is empty. -* `icon` String (optional) - The path to the icon, can be a DLL or EXE. `icon` +* `icon` string (optional) - The path to the icon, can be a DLL or EXE. `icon` and `iconIndex` have to be set together. Default is empty, which uses the target's icon. -* `iconIndex` Number (optional) - The resource ID of icon when `icon` is a +* `iconIndex` number (optional) - The resource ID of icon when `icon` is a DLL or EXE. Default is 0. -* `appUserModelId` String (optional) - The Application User Model ID. Default +* `appUserModelId` string (optional) - The Application User Model ID. Default is empty. +* `toastActivatorClsid` string (optional) - The Application Toast Activator CLSID. Needed +for participating in Action Center. diff --git a/docs/api/structures/size.md b/docs/api/structures/size.md index 1d9c8b1f5a123..417c57761b606 100644 --- a/docs/api/structures/size.md +++ b/docs/api/structures/size.md @@ -1,4 +1,4 @@ # Size Object -* `width` Number -* `height` Number +* `width` number +* `height` number diff --git a/docs/api/structures/stream-protocol-response.md b/docs/api/structures/stream-protocol-response.md deleted file mode 100644 index ac5718d07fdf4..0000000000000 --- a/docs/api/structures/stream-protocol-response.md +++ /dev/null @@ -1,5 +0,0 @@ -# StreamProtocolResponse Object - -* `statusCode` Number (optional) - The HTTP response code. -* `headers` Record<String, String | String[]> (optional) - An object containing the response headers. -* `data` ReadableStream | null - A Node.js readable stream representing the response body. diff --git a/docs/api/structures/string-protocol-response.md b/docs/api/structures/string-protocol-response.md deleted file mode 100644 index 19414e3f2aa7c..0000000000000 --- a/docs/api/structures/string-protocol-response.md +++ /dev/null @@ -1,5 +0,0 @@ -# StringProtocolResponse Object - -* `mimeType` String (optional) - MIME type of the response. -* `charset` String (optional) - Charset of the response. -* `data` String | null - A string representing the response body. diff --git a/docs/api/structures/task.md b/docs/api/structures/task.md index 161a9afecc610..b8ea501086c8a 100644 --- a/docs/api/structures/task.md +++ b/docs/api/structures/task.md @@ -1,15 +1,15 @@ # Task Object -* `program` String - Path of the program to execute, usually you should +* `program` string - Path of the program to execute, usually you should specify `process.execPath` which opens the current program. -* `arguments` String - The command line arguments when `program` is +* `arguments` string - The command line arguments when `program` is executed. -* `title` String - The string to be displayed in a JumpList. -* `description` String - Description of this task. -* `iconPath` String - The absolute path to an icon to be displayed in a +* `title` string - The string to be displayed in a JumpList. +* `description` string - Description of this task. +* `iconPath` string - The absolute path to an icon to be displayed in a JumpList, which can be an arbitrary resource file that contains an icon. You can usually specify `process.execPath` to show the icon of the program. -* `iconIndex` Number - The icon index in the icon file. If an icon file +* `iconIndex` number - The icon index in the icon file. If an icon file consists of two or more icons, set this value to identify the icon. If an icon file consists of one icon, this value is 0. -* `workingDirectory` String (optional) - The working directory. Default is empty. +* `workingDirectory` string (optional) - The working directory. Default is empty. diff --git a/docs/api/structures/thumbar-button.md b/docs/api/structures/thumbar-button.md index 259195852a4f2..a5e5815147251 100644 --- a/docs/api/structures/thumbar-button.md +++ b/docs/api/structures/thumbar-button.md @@ -3,11 +3,11 @@ * `icon` [NativeImage](../native-image.md) - The icon showing in thumbnail toolbar. * `click` Function -* `tooltip` String (optional) - The text of the button's tooltip. -* `flags` String[] (optional) - Control specific states and behaviors of the +* `tooltip` string (optional) - The text of the button's tooltip. +* `flags` string[] (optional) - Control specific states and behaviors of the button. By default, it is `['enabled']`. -The `flags` is an array that can include following `String`s: +The `flags` is an array that can include following `string`s: * `enabled` - The button is active and available to the user. * `disabled` - The button is disabled. It is present, but has a visual state diff --git a/docs/api/structures/trace-categories-and-options.md b/docs/api/structures/trace-categories-and-options.md index 8db0638c9b112..5965ec093bd1f 100644 --- a/docs/api/structures/trace-categories-and-options.md +++ b/docs/api/structures/trace-categories-and-options.md @@ -1,11 +1,11 @@ # TraceCategoriesAndOptions Object -* `categoryFilter` String - A filter to control what category groups +* `categoryFilter` string - A filter to control what category groups should be traced. A filter can have an optional '-' prefix to exclude category groups that contain a matching category. Having both included and excluded category patterns in the same list is not supported. Examples: `test_MyTest*`, `test_MyTest*,test_OtherStuff`, `-excluded_category1,-excluded_category2`. -* `traceOptions` String - Controls what kind of tracing is enabled, +* `traceOptions` string - Controls what kind of tracing is enabled, it is a comma-delimited sequence of the following strings: `record-until-full`, `record-continuously`, `trace-to-console`, `enable-sampling`, `enable-systrace`, e.g. `'record-until-full,enable-sampling'`. diff --git a/docs/api/structures/trace-config.md b/docs/api/structures/trace-config.md index 5ce4d7d742bcf..d5b3795f228fd 100644 --- a/docs/api/structures/trace-config.md +++ b/docs/api/structures/trace-config.md @@ -1,25 +1,25 @@ # TraceConfig Object -* `recording_mode` String (optional) - Can be `record-until-full`, `record-continuously`, `record-as-much-as-possible` or `trace-to-console`. Defaults to `record-until-full`. +* `recording_mode` string (optional) - Can be `record-until-full`, `record-continuously`, `record-as-much-as-possible` or `trace-to-console`. Defaults to `record-until-full`. * `trace_buffer_size_in_kb` number (optional) - maximum size of the trace recording buffer in kilobytes. Defaults to 100MB. * `trace_buffer_size_in_events` number (optional) - maximum size of the trace recording buffer in events. * `enable_argument_filter` boolean (optional) - if true, filter event data - according to a whitelist of events that have been manually vetted to not + according to a specific list of events that have been manually vetted to not include any PII. See [the implementation in Chromium][trace_event_args_whitelist.cc] for specifics. -* `included_categories` String[] (optional) - a list of tracing categories to +* `included_categories` string[] (optional) - a list of tracing categories to include. Can include glob-like patterns using `*` at the end of the category name. See [tracing categories][] for the list of categories. -* `excluded_categories` String[] (optional) - a list of tracing categories to +* `excluded_categories` string[] (optional) - a list of tracing categories to exclude. Can include glob-like patterns using `*` at the end of the category name. See [tracing categories][] for the list of categories. * `included_process_ids` number[] (optional) - a list of process IDs to include in the trace. If not specified, trace all processes. -* `histogram_names` String[] (optional) - a list of [histogram][] names to report +* `histogram_names` string[] (optional) - a list of [histogram][] names to report with the trace. -* `memory_dump_config` Object (optional) - if the +* `memory_dump_config` Record<string, any> (optional) - if the `disabled-by-default-memory-infra` category is enabled, this contains optional additional configuration for data collection. See the [Chromium memory-infra docs][memory-infra docs] for more information. @@ -41,7 +41,7 @@ An example TraceConfig that roughly matches what Chrome DevTools records: 'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler.hires' ], - excluded_categories: [ '*' ] + excluded_categories: ['*'] } ``` diff --git a/docs/api/structures/transaction.md b/docs/api/structures/transaction.md index 7349c0996f8bf..0c12ef9961958 100644 --- a/docs/api/structures/transaction.md +++ b/docs/api/structures/transaction.md @@ -1,11 +1,13 @@ # Transaction Object -* `transactionIdentifier` String - A string that uniquely identifies a successful payment transaction. -* `transactionDate` String - The date the transaction was added to the App Store’s payment queue. -* `originalTransactionIdentifier` String - The identifier of the restored transaction by the App Store. -* `transactionState` String - The transaction state, can be `purchasing`, `purchased`, `failed`, `restored` or `deferred`. +* `transactionIdentifier` string - A string that uniquely identifies a successful payment transaction. +* `transactionDate` string - The date the transaction was added to the App Store’s payment queue. +* `originalTransactionIdentifier` string - The identifier of the restored transaction by the App Store. +* `transactionState` string - The transaction state, can be `purchasing`, `purchased`, `failed`, `restored` or `deferred`. * `errorCode` Integer - The error code if an error occurred while processing the transaction. -* `errorMessage` String - The error message if an error occurred while processing the transaction. +* `errorMessage` string - The error message if an error occurred while processing the transaction. * `payment` Object - * `productIdentifier` String - The identifier of the purchased product. + * `productIdentifier` string - The identifier of the purchased product. * `quantity` Integer - The quantity purchased. + * `applicationUsername` string - An opaque identifier for the user’s account on your system. + * `paymentDiscount` [PaymentDiscount](payment-discount.md) (optional) - The details of the discount offer to apply to the payment. diff --git a/docs/api/structures/upload-blob.md b/docs/api/structures/upload-blob.md deleted file mode 100644 index be93cacb495e4..0000000000000 --- a/docs/api/structures/upload-blob.md +++ /dev/null @@ -1,4 +0,0 @@ -# UploadBlob Object - -* `type` String - `blob`. -* `blobUUID` String - UUID of blob data to upload. diff --git a/docs/api/structures/upload-data.md b/docs/api/structures/upload-data.md index bcbed755b2b9b..7aa4261e5ec80 100644 --- a/docs/api/structures/upload-data.md +++ b/docs/api/structures/upload-data.md @@ -1,6 +1,6 @@ # UploadData Object * `bytes` Buffer - Content being sent. -* `file` String (optional) - Path of file being uploaded. -* `blobUUID` String (optional) - UUID of blob data. Use [ses.getBlobData](../session.md#sesgetblobdataidentifier) method +* `file` string (optional) - Path of file being uploaded. +* `blobUUID` string (optional) - UUID of blob data. Use [ses.getBlobData](../session.md#sesgetblobdataidentifier) method to retrieve the data. diff --git a/docs/api/structures/upload-file.md b/docs/api/structures/upload-file.md index ae231bdaf895f..8361cd3e91dc4 100644 --- a/docs/api/structures/upload-file.md +++ b/docs/api/structures/upload-file.md @@ -1,7 +1,7 @@ # UploadFile Object -* `type` String - `file`. -* `filePath` String - Path of file to be uploaded. +* `type` 'file' - `file`. +* `filePath` string - Path of file to be uploaded. * `offset` Integer - Defaults to `0`. * `length` Integer - Number of bytes to read from `offset`. Defaults to `0`. diff --git a/docs/api/structures/upload-raw-data.md b/docs/api/structures/upload-raw-data.md index 4fe162311fa1f..e80eaa9075833 100644 --- a/docs/api/structures/upload-raw-data.md +++ b/docs/api/structures/upload-raw-data.md @@ -1,4 +1,4 @@ # UploadRawData Object -* `type` String - `rawData`. +* `type` 'rawData' - `rawData`. * `bytes` Buffer - Data to be uploaded. diff --git a/docs/api/structures/user-default-types.md b/docs/api/structures/user-default-types.md new file mode 100644 index 0000000000000..cdca5cdc7257e --- /dev/null +++ b/docs/api/structures/user-default-types.md @@ -0,0 +1,12 @@ +# UserDefaultTypes Object + +* `string` string +* `boolean` boolean +* `integer` number +* `float` number +* `double` number +* `url` string +* `array` Array\<unknown> +* `dictionary` Record\<string, unknown> + +This type is a helper alias, no object will never exist of this type. diff --git a/docs/api/structures/web-request-filter.md b/docs/api/structures/web-request-filter.md new file mode 100644 index 0000000000000..ae54cea7bfc08 --- /dev/null +++ b/docs/api/structures/web-request-filter.md @@ -0,0 +1,3 @@ +# WebRequestFilter Object + +* `urls` string[] - Array of URL patterns that will be used to filter out the requests that do not match the URL patterns. diff --git a/docs/api/structures/web-source.md b/docs/api/structures/web-source.md index 74c34f372d31e..0a1de87bb1721 100644 --- a/docs/api/structures/web-source.md +++ b/docs/api/structures/web-source.md @@ -1,5 +1,4 @@ # WebSource Object -* `code` String -* `url` String (optional) -* `startLine` Integer (optional) - Default is 1. +* `code` string +* `url` string (optional) diff --git a/docs/api/synopsis.md b/docs/api/synopsis.md index 8b3ea56094754..667c1dcd35a4d 100644 --- a/docs/api/synopsis.md +++ b/docs/api/synopsis.md @@ -9,11 +9,11 @@ the [native modules](../tutorial/using-native-node-modules.md)). Electron also provides some extra built-in modules for developing native desktop applications. Some modules are only available in the main process, some are only available in the renderer process (web page), and some can be used in -both processes. +either process type. The basic rule is: if a module is [GUI][gui] or low-level system related, then it should be only available in the main process. You need to be familiar with -the concept of [main process vs. renderer process](../tutorial/application-architecture.md#main-and-renderer-processes) +the concept of main process vs. renderer process scripts to be able to use those modules. The main process script is like a normal Node.js script: @@ -22,29 +22,27 @@ The main process script is like a normal Node.js script: const { app, BrowserWindow } = require('electron') let win = null -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL('https://github.com') }) ``` The renderer process is no different than a normal web page, except for the -extra ability to use node modules: +extra ability to use node modules if `nodeIntegration` is enabled: ```html <!DOCTYPE html> <html> <body> <script> - const { app } = require('electron').remote - console.log(app.getVersion()) + const fs = require('fs') + console.log(fs.readFileSync(__filename, 'utf8')) </script> </body> </html> ``` -To run your app, read [Run your app](../tutorial/first-app.md#running-your-app). - ## Destructuring assignment As of 0.37, you can use @@ -56,7 +54,7 @@ const { app, BrowserWindow } = require('electron') let win -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow() win.loadURL('https://github.com') }) @@ -71,7 +69,7 @@ const { app, BrowserWindow } = electron let win -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow() win.loadURL('https://github.com') }) @@ -85,7 +83,7 @@ const app = electron.app const BrowserWindow = electron.BrowserWindow let win -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow() win.loadURL('https://github.com') }) diff --git a/docs/api/system-preferences.md b/docs/api/system-preferences.md index 672709817a3cc..69124edda9f87 100644 --- a/docs/api/system-preferences.md +++ b/docs/api/system-preferences.md @@ -18,7 +18,7 @@ The `systemPreferences` object emits the following events: Returns: * `event` Event -* `newColor` String - The new RGBA color the user assigned to be their system +* `newColor` string - The new RGBA color the user assigned to be their system accent color. ### Event: 'color-changed' _Windows_ @@ -27,66 +27,70 @@ Returns: * `event` Event -### Event: 'inverted-color-scheme-changed' _Windows_ +### Event: 'inverted-color-scheme-changed' _Windows_ _Deprecated_ Returns: * `event` Event -* `invertedColorScheme` Boolean - `true` if an inverted color scheme (a high contrast color scheme with light text and dark backgrounds) is being used, `false` otherwise. +* `invertedColorScheme` boolean - `true` if an inverted color scheme (a high contrast color scheme with light text and dark backgrounds) is being used, `false` otherwise. -### Event: 'high-contrast-color-scheme-changed' _Windows_ +**Deprecated:** Should use the new [`updated`](native-theme.md#event-updated) event on the `nativeTheme` module. + +### Event: 'high-contrast-color-scheme-changed' _Windows_ _Deprecated_ Returns: * `event` Event -* `highContrastColorScheme` Boolean - `true` if a high contrast theme is being used, `false` otherwise. +* `highContrastColorScheme` boolean - `true` if a high contrast theme is being used, `false` otherwise. + +**Deprecated:** Should use the new [`updated`](native-theme.md#event-updated) event on the `nativeTheme` module. ## Methods -### `systemPreferences.isDarkMode()` _macOS_ _Windows_ +### `systemPreferences.isDarkMode()` _macOS_ _Windows_ _Deprecated_ -Returns `Boolean` - Whether the system is in Dark Mode. +Returns `boolean` - Whether the system is in Dark Mode. -**Note:** On macOS 10.15 Catalina in order for this API to return the correct value when in the "automatic" dark mode setting you must either have `NSRequiresAquaSystemAppearance=false` in your `Info.plist` or be on Electron `>=7.0.0`. See the [dark mode guide](../tutorial/mojave-dark-mode-guide.md) for more information. +**Deprecated:** Should use the new [`nativeTheme.shouldUseDarkColors`](native-theme.md#nativethemeshouldusedarkcolors-readonly) API. ### `systemPreferences.isSwipeTrackingFromScrollEventsEnabled()` _macOS_ -Returns `Boolean` - Whether the Swipe between pages setting is on. +Returns `boolean` - Whether the Swipe between pages setting is on. ### `systemPreferences.postNotification(event, userInfo[, deliverImmediately])` _macOS_ -* `event` String -* `userInfo` Object -* `deliverImmediately` Boolean (optional) - `true` to post notifications immediately even when the subscribing app is inactive. +* `event` string +* `userInfo` Record<string, any> +* `deliverImmediately` boolean (optional) - `true` to post notifications immediately even when the subscribing app is inactive. Posts `event` as native notifications of macOS. The `userInfo` is an Object that contains the user information dictionary sent along with the notification. ### `systemPreferences.postLocalNotification(event, userInfo)` _macOS_ -* `event` String -* `userInfo` Object +* `event` string +* `userInfo` Record<string, any> Posts `event` as native notifications of macOS. The `userInfo` is an Object that contains the user information dictionary sent along with the notification. ### `systemPreferences.postWorkspaceNotification(event, userInfo)` _macOS_ -* `event` String -* `userInfo` Object +* `event` string +* `userInfo` Record<string, any> Posts `event` as native notifications of macOS. The `userInfo` is an Object that contains the user information dictionary sent along with the notification. ### `systemPreferences.subscribeNotification(event, callback)` _macOS_ -* `event` String +* `event` string | null * `callback` Function - * `event` String - * `userInfo` Object - * `object` String + * `event` string + * `userInfo` Record<string, unknown> + * `object` string -Returns `Number` - The ID of this subscription +Returns `number` - The ID of this subscription Subscribes to native notifications of macOS, `callback` will be called with `callback(event, userInfo)` when the corresponding `event` happens. The @@ -105,30 +109,38 @@ example values of `event` are: * `AppleColorPreferencesChangedNotification` * `AppleShowScrollBarsSettingChanged` +If `event` is null, the `NSDistributedNotificationCenter` doesn’t use it as criteria for delivery to the observer. See [docs](https://developer.apple.com/documentation/foundation/nsnotificationcenter/1411723-addobserverforname?language=objc) for more information. + ### `systemPreferences.subscribeLocalNotification(event, callback)` _macOS_ -* `event` String +* `event` string | null * `callback` Function - * `event` String - * `userInfo` Object - * `object` String + * `event` string + * `userInfo` Record<string, unknown> + * `object` string -Returns `Number` - The ID of this subscription +Returns `number` - The ID of this subscription Same as `subscribeNotification`, but uses `NSNotificationCenter` for local defaults. This is necessary for events such as `NSUserDefaultsDidChangeNotification`. +If `event` is null, the `NSNotificationCenter` doesn’t use it as criteria for delivery to the observer. See [docs](https://developer.apple.com/documentation/foundation/nsnotificationcenter/1411723-addobserverforname?language=objc) for more information. + ### `systemPreferences.subscribeWorkspaceNotification(event, callback)` _macOS_ -* `event` String +* `event` string | null * `callback` Function - * `event` String - * `userInfo` Object - * `object` String + * `event` string + * `userInfo` Record<string, unknown> + * `object` string + +Returns `number` - The ID of this subscription Same as `subscribeNotification`, but uses `NSWorkspace.sharedWorkspace.notificationCenter`. This is necessary for events such as `NSWorkspaceDidActivateApplicationNotification`. +If `event` is null, the `NSWorkspaceNotificationCenter` doesn’t use it as criteria for delivery to the observer. See [docs](https://developer.apple.com/documentation/foundation/nsnotificationcenter/1411723-addobserverforname?language=objc) for more information. + ### `systemPreferences.unsubscribeNotification(id)` _macOS_ * `id` Integer @@ -149,17 +161,17 @@ Same as `unsubscribeNotification`, but removes the subscriber from `NSWorkspace. ### `systemPreferences.registerDefaults(defaults)` _macOS_ -* `defaults` Object - a dictionary of (`key: value`) user defaults +* `defaults` Record<string, string | boolean | number> - a dictionary of (`key: value`) user defaults Add the specified defaults to your application's `NSUserDefaults`. -### `systemPreferences.getUserDefault(key, type)` _macOS_ +### `systemPreferences.getUserDefault<Type extends keyof UserDefaultTypes>(key, type)` _macOS_ -* `key` String -* `type` String - Can be `string`, `boolean`, `integer`, `float`, `double`, +* `key` string +* `type` Type - Can be `string`, `boolean`, `integer`, `float`, `double`, `url`, `array` or `dictionary`. -Returns `any` - The value of `key` in `NSUserDefaults`. +Returns [`UserDefaultTypes[Type]`](structures/user-default-types.md) - The value of `key` in `NSUserDefaults`. Some popular `key` and `type`s are: @@ -173,9 +185,9 @@ Some popular `key` and `type`s are: ### `systemPreferences.setUserDefault(key, type, value)` _macOS_ -* `key` String -* `type` String - See [`getUserDefault`](#systempreferencesgetuserdefaultkey-type-macos). -* `value` String +* `key` string +* `type` string - Can be `string`, `boolean`, `integer`, `float`, `double`, `url`, `array` or `dictionary`. +* `value` string Set the value of `key` in `NSUserDefaults`. @@ -188,14 +200,14 @@ Some popular `key` and `type`s are: ### `systemPreferences.removeUserDefault(key)` _macOS_ -* `key` String +* `key` string Removes the `key` in `NSUserDefaults`. This can be used to restore the default or global value of a `key` previously set with `setUserDefault`. ### `systemPreferences.isAeroGlassEnabled()` _Windows_ -Returns `Boolean` - `true` if [DWM composition][dwm-composition] (Aero Glass) is +Returns `boolean` - `true` if [DWM composition][dwm-composition] (Aero Glass) is enabled, and `false` otherwise. An example of using it to determine if you should create a transparent window or @@ -203,7 +215,7 @@ not (transparent windows won't work correctly when DWM composition is disabled): ```javascript const { BrowserWindow, systemPreferences } = require('electron') -let browserOptions = { width: 1000, height: 800 } +const browserOptions = { width: 1000, height: 800 } // Make the window transparent only if the platform supports it. if (process.platform !== 'win32' || systemPreferences.isAeroGlassEnabled()) { @@ -212,7 +224,7 @@ if (process.platform !== 'win32' || systemPreferences.isAeroGlassEnabled()) { } // Create the window. -let win = new BrowserWindow(browserOptions) +const win = new BrowserWindow(browserOptions) // Navigate. if (browserOptions.transparent) { @@ -227,7 +239,7 @@ if (browserOptions.transparent) { ### `systemPreferences.getAccentColor()` _Windows_ _macOS_ -Returns `String` - The users current system wide accent color preference in RGBA +Returns `string` - The users current system wide accent color preference in RGBA hexadecimal form. ```js @@ -242,7 +254,7 @@ This API is only available on macOS 10.14 Mojave or newer. ### `systemPreferences.getColor(color)` _Windows_ _macOS_ -* `color` String - One of the following values: +* `color` string - One of the following values: * On **Windows**: * `3d-dark-shadow` - Dark shadow for three-dimensional display elements. * `3d-face` - Face color for three-dimensional display elements and for dialog @@ -285,7 +297,7 @@ This API is only available on macOS 10.14 Mojave or newer. * `window-frame` - Window frame. * `window-text` - Text in windows. * On **macOS** - * `alternate-selected-control-text` - The text on a selected surface in a list or table. + * `alternate-selected-control-text` - The text on a selected surface in a list or table. _deprecated_ * `control-background` - The background of a large interface element, such as a browser or table. * `control` - The surface of a control. * `control-text` -The text of a control that isn’t disabled. @@ -304,7 +316,7 @@ This API is only available on macOS 10.14 Mojave or newer. * `selected-content-background` - The background for selected content in a key window or view. * `selected-control` - The surface of a selected control. * `selected-control-text` - The text of a selected control. - * `selected-menu-item` - The text of a selected menu. + * `selected-menu-item-text` - The text of a selected menu. * `selected-text-background` - The background of selected text. * `selected-text` - Selected text. * `separator` - A separator between different sections of content. @@ -319,15 +331,17 @@ This API is only available on macOS 10.14 Mojave or newer. * `window-background` - The background of a window. * `window-frame-text` - The text in the window's titlebar area. -Returns `String` - The system color setting in RGB hexadecimal form (`#ABCDEF`). -See the [Windows docs][windows-colors] and the [MacOS docs][macos-colors] for more details. +Returns `string` - The system color setting in RGB hexadecimal form (`#ABCDEF`). +See the [Windows docs][windows-colors] and the [macOS docs][macos-colors] for more details. + +The following colors are only available on macOS 10.14: `find-highlight`, `selected-content-background`, `separator`, `unemphasized-selected-content-background`, `unemphasized-selected-text-background`, and `unemphasized-selected-text`. [windows-colors]:https://msdn.microsoft.com/en-us/library/windows/desktop/ms724371(v=vs.85).aspx [macos-colors]:https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/color#dynamic-system-colors ### `systemPreferences.getSystemColor(color)` _macOS_ -* `color` String - One of the following values: +* `color` string - One of the following values: * `blue` * `brown` * `gray` @@ -338,63 +352,53 @@ See the [Windows docs][windows-colors] and the [MacOS docs][macos-colors] for mo * `red` * `yellow` -Returns `String` - The standard system color formatted as `#RRGGBBAA`. +Returns `string` - The standard system color formatted as `#RRGGBBAA`. Returns one of several standard system colors that automatically adapt to vibrancy and changes in accessibility settings like 'Increase contrast' and 'Reduce transparency'. See [Apple Documentation](https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/color#system-colors) for more details. -### `systemPreferences.isInvertedColorScheme()` _Windows_ +### `systemPreferences.isInvertedColorScheme()` _Windows_ _Deprecated_ + +Returns `boolean` - `true` if an inverted color scheme (a high contrast color scheme with light text and dark backgrounds) is active, `false` otherwise. + +**Deprecated:** Should use the new [`nativeTheme.shouldUseInvertedColorScheme`](native-theme.md#nativethemeshoulduseinvertedcolorscheme-macos-windows-readonly) API. -Returns `Boolean` - `true` if an inverted color scheme (a high contrast color scheme with light text and dark backgrounds) is active, `false` otherwise. +### `systemPreferences.isHighContrastColorScheme()` _macOS_ _Windows_ _Deprecated_ -### `systemPreferences.isHighContrastColorScheme()` _macOS_ _Windows_ +Returns `boolean` - `true` if a high contrast theme is active, `false` otherwise. -Returns `Boolean` - `true` if a high contrast theme is active, `false` otherwise. +**Deprecated:** Should use the new [`nativeTheme.shouldUseHighContrastColors`](native-theme.md#nativethemeshouldusehighcontrastcolors-macos-windows-readonly) API. ### `systemPreferences.getEffectiveAppearance()` _macOS_ -Returns `String` - Can be `dark`, `light` or `unknown`. +Returns `string` - Can be `dark`, `light` or `unknown`. Gets the macOS appearance setting that is currently applied to your application, maps to [NSApplication.effectiveAppearance](https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance?language=objc) -Please note that until Electron is built targeting the 10.14 SDK, your application's -`effectiveAppearance` will default to 'light' and won't inherit the OS preference. In -the interim in order for your application to inherit the OS preference you must set the -`NSRequiresAquaSystemAppearance` key in your apps `Info.plist` to `false`. If you are -using `electron-packager` or `electron-forge` just set the `enableDarwinDarkMode` -packager option to `true`. See the [Electron Packager API](https://github.com/electron/electron-packager/blob/master/docs/api.md#darwindarkmodesupport) -for more details. +### `systemPreferences.getAppLevelAppearance()` _macOS_ _Deprecated_ -### `systemPreferences.getAppLevelAppearance()` _macOS_ - -Returns `String` | `null` - Can be `dark`, `light` or `unknown`. +Returns `string` | `null` - Can be `dark`, `light` or `unknown`. Gets the macOS appearance setting that you have declared you want for your application, maps to [NSApplication.appearance](https://developer.apple.com/documentation/appkit/nsapplication/2967170-appearance?language=objc). You can use the `setAppLevelAppearance` API to set this value. -**[Deprecated](modernization/property-updates.md)** - -### `systemPreferences.setAppLevelAppearance(appearance)` _macOS_ +### `systemPreferences.setAppLevelAppearance(appearance)` _macOS_ _Deprecated_ -* `appearance` String | null - Can be `dark` or `light` +* `appearance` string | null - Can be `dark` or `light` Sets the appearance setting for your application, this should override the system default and override the value of `getEffectiveAppearance`. -**[Deprecated](modernization/property-updates.md)** - ### `systemPreferences.canPromptTouchID()` _macOS_ -Returns `Boolean` - whether or not this device has the ability to use Touch ID. +Returns `boolean` - whether or not this device has the ability to use Touch ID. **NOTE:** This API will return `false` on macOS systems older than Sierra 10.12.2. -**[Deprecated](modernization/property-updates.md)** - ### `systemPreferences.promptTouchID(reason)` _macOS_ -* `reason` String - The reason you are asking for Touch ID authentication +* `reason` string - The reason you are asking for Touch ID authentication Returns `Promise<void>` - resolves if the user has successfully authenticated with Touch ID. @@ -408,31 +412,36 @@ systemPreferences.promptTouchID('To get consent for a Security-Gated Thing').the }) ``` -This API itself will not protect your user data; rather, it is a mechanism to allow you to do so. Native apps will need to set [Access Control Constants](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags?language=objc) like [`kSecAccessControlUserPresence`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontroluserpresence?language=objc) on the their keychain entry so that reading it would auto-prompt for Touch ID biometric consent. This could be done with [`node-keytar`](https://github.com/atom/node-keytar), such that one would store an encryption key with `node-keytar` and only fetch it if `promptTouchID()` resolves. +This API itself will not protect your user data; rather, it is a mechanism to allow you to do so. Native apps will need to set [Access Control Constants](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags?language=objc) like [`kSecAccessControlUserPresence`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontroluserpresence?language=objc) on their keychain entry so that reading it would auto-prompt for Touch ID biometric consent. This could be done with [`node-keytar`](https://github.com/atom/node-keytar), such that one would store an encryption key with `node-keytar` and only fetch it if `promptTouchID()` resolves. **NOTE:** This API will return a rejected Promise on macOS systems older than Sierra 10.12.2. ### `systemPreferences.isTrustedAccessibilityClient(prompt)` _macOS_ -* `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted. +* `prompt` boolean - whether or not the user will be informed via prompt if the current process is untrusted. + +Returns `boolean` - `true` if the current process is a trusted accessibility client and `false` if it is not. -Returns `Boolean` - `true` if the current process is a trusted accessibility client and `false` if it is not. +### `systemPreferences.getMediaAccessStatus(mediaType)` _Windows_ _macOS_ -### `systemPreferences.getMediaAccessStatus(mediaType)` _macOS_ +* `mediaType` string - Can be `microphone`, `camera` or `screen`. -* `mediaType` String - `microphone` or `camera`. +Returns `string` - Can be `not-determined`, `granted`, `denied`, `restricted` or `unknown`. -Returns `String` - Can be `not-determined`, `granted`, `denied`, `restricted` or `unknown`. +This user consent was not required on macOS 10.13 High Sierra or lower so this method will always return `granted`. +macOS 10.14 Mojave or higher requires consent for `microphone` and `camera` access. +macOS 10.15 Catalina or higher requires consent for `screen` access. -This user consent was not required until macOS 10.14 Mojave, so this method will always return `granted` if your system is running 10.13 High Sierra or lower. +Windows 10 has a global setting controlling `microphone` and `camera` access for all win32 applications. +It will always return `granted` for `screen` and for all media types on older versions of Windows. ### `systemPreferences.askForMediaAccess(mediaType)` _macOS_ -* `mediaType` String - the type of media being requested; can be `microphone`, `camera`. +* `mediaType` string - the type of media being requested; can be `microphone`, `camera`. -Returns `Promise<Boolean>` - A promise that resolves with `true` if consent was granted and `false` if it was denied. If an invalid `mediaType` is passed, the promise will be rejected. If an access request was denied and later is changed through the System Preferences pane, a restart of the app will be required for the new permissions to take effect. If access has already been requested and denied, it _must_ be changed through the preference pane; an alert will not pop up and the promise will resolve with the existing access status. +Returns `Promise<boolean>` - A promise that resolves with `true` if consent was granted and `false` if it was denied. If an invalid `mediaType` is passed, the promise will be rejected. If an access request was denied and later is changed through the System Preferences pane, a restart of the app will be required for the new permissions to take effect. If access has already been requested and denied, it _must_ be changed through the preference pane; an alert will not pop up and the promise will resolve with the existing access status. -**Important:** In order to properly leverage this API, you [must set](https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_macos?language=objc) the `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` strings in your app's `Info.plist` file. The values for these keys will be used to populate the permission dialogs so that the user will be properly informed as to the purpose of the permission request. See [Electron Application Distribution](https://electronjs.org/docs/tutorial/application-distribution#macos) for more information about how to set these in the context of Electron. +**Important:** In order to properly leverage this API, you [must set](https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_macos?language=objc) the `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` strings in your app's `Info.plist` file. The values for these keys will be used to populate the permission dialogs so that the user will be properly informed as to the purpose of the permission request. See [Electron Application Distribution](../tutorial/application-distribution.md#macos) for more information about how to set these in the context of Electron. This user consent was not required until macOS 10.14 Mojave, so this method will always return `true` if your system is running 10.13 High Sierra or lower. @@ -440,9 +449,9 @@ This user consent was not required until macOS 10.14 Mojave, so this method will Returns `Object`: -* `shouldRenderRichAnimation` Boolean - Returns true if rich animations should be rendered. Looks at session type (e.g. remote desktop) and accessibility settings to give guidance for heavy animations. -* `scrollAnimationsEnabledBySystem` Boolean - Determines on a per-platform basis whether scroll animations (e.g. produced by home/end key) should be enabled. -* `prefersReducedMotion` Boolean - Determines whether the user desires reduced motion based on platform APIs. +* `shouldRenderRichAnimation` boolean - Returns true if rich animations should be rendered. Looks at session type (e.g. remote desktop) and accessibility settings to give guidance for heavy animations. +* `scrollAnimationsEnabledBySystem` boolean - Determines on a per-platform basis whether scroll animations (e.g. produced by home/end key) should be enabled. +* `prefersReducedMotion` boolean - Determines whether the user desires reduced motion based on platform APIs. Returns an object with system animation settings. @@ -450,10 +459,17 @@ Returns an object with system animation settings. ### `systemPreferences.appLevelAppearance` _macOS_ -A `String` property that determines the macOS appearance setting for +A `string` property that can be `dark`, `light` or `unknown`. It determines the macOS appearance setting for your application. This maps to values in: [NSApplication.appearance](https://developer.apple.com/documentation/appkit/nsapplication/2967170-appearance?language=objc). Setting this will override the system default as well as the value of `getEffectiveAppearance`. Possible values that can be set are `dark` and `light`, and possible return values are `dark`, `light`, and `unknown`. This property is only available on macOS 10.14 Mojave or newer. + +### `systemPreferences.effectiveAppearance` _macOS_ _Readonly_ + +A `string` property that can be `dark`, `light` or `unknown`. + +Returns the macOS appearance setting that is currently applied to your application, +maps to [NSApplication.effectiveAppearance](https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance?language=objc) diff --git a/docs/api/touch-bar-button.md b/docs/api/touch-bar-button.md index aa9e177ecd3ce..4d9caaf4822f0 100644 --- a/docs/api/touch-bar-button.md +++ b/docs/api/touch-bar-button.md @@ -2,33 +2,50 @@ > Create a button in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarButton(options)` _Experimental_ +### `new TouchBarButton(options)` * `options` Object - * `label` String (optional) - Button text. - * `backgroundColor` String (optional) - Button background color in hex format, + * `label` string (optional) - Button text. + * `accessibilityLabel` string (optional) - A short description of the button for use by screenreaders like VoiceOver. + * `backgroundColor` string (optional) - Button background color in hex format, i.e `#ABCDEF`. - * `icon` [NativeImage](native-image.md) | String (optional) - Button icon. - * `iconPosition` String (optional) - Can be `left`, `right` or `overlay`. + * `icon` [NativeImage](native-image.md) | string (optional) - Button icon. + * `iconPosition` string (optional) - Can be `left`, `right` or `overlay`. Defaults to `overlay`. * `click` Function (optional) - Function to call when the button is clicked. + * `enabled` boolean (optional) - Whether the button is in an enabled state. Default is `true`. + +When defining `accessibilityLabel`, ensure you have considered macOS [best practices](https://developer.apple.com/documentation/appkit/nsaccessibilitybutton/1524910-accessibilitylabel?language=objc). ### Instance Properties The following properties are available on instances of `TouchBarButton`: +#### `touchBarButton.accessibilityLabel` + +A `string` representing the description of the button to be read by a screen reader. Will only be read by screen readers if no label is set. + #### `touchBarButton.label` -A `String` representing the button's current text. Changing this value immediately updates the button +A `string` representing the button's current text. Changing this value immediately updates the button in the touch bar. #### `touchBarButton.backgroundColor` -A `String` hex code representing the button's current background color. Changing this value immediately updates +A `string` hex code representing the button's current background color. Changing this value immediately updates the button in the touch bar. #### `touchBarButton.icon` A `NativeImage` representing the button's current icon. Changing this value immediately updates the button in the touch bar. + +#### `touchBarButton.iconPosition` + +A `string` - Can be `left`, `right` or `overlay`. Defaults to `overlay`. + +#### `touchBarButton.enabled` + +A `boolean` representing whether the button is in an enabled state. diff --git a/docs/api/touch-bar-color-picker.md b/docs/api/touch-bar-color-picker.md index b8dfcb7b5a37e..a80954adbb6a6 100644 --- a/docs/api/touch-bar-color-picker.md +++ b/docs/api/touch-bar-color-picker.md @@ -2,17 +2,18 @@ > Create a color picker in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarColorPicker(options)` _Experimental_ +### `new TouchBarColorPicker(options)` * `options` Object - * `availableColors` String[] (optional) - Array of hex color strings to + * `availableColors` string[] (optional) - Array of hex color strings to appear as possible colors to select. - * `selectedColor` String (optional) - The selected hex color in the picker, + * `selectedColor` string (optional) - The selected hex color in the picker, i.e `#ABCDEF`. * `change` Function (optional) - Function to call when a color is selected. - * `color` String - The color that the user selected from the picker. + * `color` string - The color that the user selected from the picker. ### Instance Properties @@ -20,10 +21,10 @@ The following properties are available on instances of `TouchBarColorPicker`: #### `touchBarColorPicker.availableColors` -A `String[]` array representing the color picker's available colors to select. Changing this value immediately +A `string[]` array representing the color picker's available colors to select. Changing this value immediately updates the color picker in the touch bar. #### `touchBarColorPicker.selectedColor` -A `String` hex code representing the color picker's currently selected color. Changing this value immediately +A `string` hex code representing the color picker's currently selected color. Changing this value immediately updates the color picker in the touch bar. diff --git a/docs/api/touch-bar-group.md b/docs/api/touch-bar-group.md index a4731e3da12a8..a04b2279d0b5d 100644 --- a/docs/api/touch-bar-group.md +++ b/docs/api/touch-bar-group.md @@ -2,9 +2,10 @@ > Create a group in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarGroup(options)` _Experimental_ +### `new TouchBarGroup(options)` * `options` Object * `items` [TouchBar](touch-bar.md) - Items to display as a group. diff --git a/docs/api/touch-bar-label.md b/docs/api/touch-bar-label.md index 6e66f8b45bdbe..c774fb804690b 100644 --- a/docs/api/touch-bar-label.md +++ b/docs/api/touch-bar-label.md @@ -2,13 +2,17 @@ > Create a label in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarLabel(options)` _Experimental_ +### `new TouchBarLabel(options)` * `options` Object - * `label` String (optional) - Text to display. - * `textColor` String (optional) - Hex color of text, i.e `#ABCDEF`. + * `label` string (optional) - Text to display. + * `accessibilityLabel` string (optional) - A short description of the button for use by screenreaders like VoiceOver. + * `textColor` string (optional) - Hex color of text, i.e `#ABCDEF`. + +When defining `accessibilityLabel`, ensure you have considered macOS [best practices](https://developer.apple.com/documentation/appkit/nsaccessibilitybutton/1524910-accessibilitylabel?language=objc). ### Instance Properties @@ -16,10 +20,14 @@ The following properties are available on instances of `TouchBarLabel`: #### `touchBarLabel.label` -A `String` representing the label's current text. Changing this value immediately updates the label in +A `string` representing the label's current text. Changing this value immediately updates the label in the touch bar. +#### `touchBarLabel.accessibilityLabel` + +A `string` representing the description of the label to be read by a screen reader. + #### `touchBarLabel.textColor` -A `String` hex code representing the label's current text color. Changing this value immediately updates the +A `string` hex code representing the label's current text color. Changing this value immediately updates the label in the touch bar. diff --git a/docs/api/touch-bar-other-items-proxy.md b/docs/api/touch-bar-other-items-proxy.md new file mode 100644 index 0000000000000..a00cd4b0b1588 --- /dev/null +++ b/docs/api/touch-bar-other-items-proxy.md @@ -0,0 +1,13 @@ +## Class: TouchBarOtherItemsProxy + +> Instantiates a special "other items proxy", which nests TouchBar elements inherited +> from Chromium at the space indicated by the proxy. By default, this proxy is added +> to each TouchBar at the end of the input. For more information, see the AppKit docs on +> [NSTouchBarItemIdentifierOtherItemsProxy](https://developer.apple.com/documentation/appkit/nstouchbaritemidentifierotheritemsproxy) +> +> Note: Only one instance of this class can be added per TouchBar. + +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ + +### `new TouchBarOtherItemsProxy()` diff --git a/docs/api/touch-bar-popover.md b/docs/api/touch-bar-popover.md index 329ead36e5d1f..2fae3b1af3f6e 100644 --- a/docs/api/touch-bar-popover.md +++ b/docs/api/touch-bar-popover.md @@ -2,15 +2,16 @@ > Create a popover in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarPopover(options)` _Experimental_ +### `new TouchBarPopover(options)` * `options` Object - * `label` String (optional) - Popover button text. + * `label` string (optional) - Popover button text. * `icon` [NativeImage](native-image.md) (optional) - Popover button icon. - * `items` [TouchBar](touch-bar.md) (optional) - Items to display in the popover. - * `showCloseButton` Boolean (optional) - `true` to display a close button + * `items` [TouchBar](touch-bar.md) - Items to display in the popover. + * `showCloseButton` boolean (optional) - `true` to display a close button on the left of the popover, `false` to not show it. Default is `true`. ### Instance Properties @@ -19,7 +20,7 @@ The following properties are available on instances of `TouchBarPopover`: #### `touchBarPopover.label` -A `String` representing the popover's current button text. Changing this value immediately updates the +A `string` representing the popover's current button text. Changing this value immediately updates the popover in the touch bar. #### `touchBarPopover.icon` diff --git a/docs/api/touch-bar-scrubber.md b/docs/api/touch-bar-scrubber.md index 428119749512f..59aa373f15041 100644 --- a/docs/api/touch-bar-scrubber.md +++ b/docs/api/touch-bar-scrubber.md @@ -2,9 +2,10 @@ > Create a scrubber (a scrollable selector) -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarScrubber(options)` _Experimental_ +### `new TouchBarScrubber(options)` * `options` Object * `items` [ScrubberItem[]](structures/scrubber-item.md) - An array of items to place in this scrubber. @@ -12,11 +13,11 @@ Process: [Main](../tutorial/application-architecture.md#main-and-renderer-proces * `selectedIndex` Integer - The index of the item the user selected. * `highlight` Function (optional) - Called when the user taps any item. * `highlightedIndex` Integer - The index of the item the user touched. - * `selectedStyle` String (optional) - Selected item style. Defaults to `null`. - * `overlayStyle` String (optional) - Selected overlay item style. Defaults to `null`. - * `showArrowButtons` Boolean (optional) - Defaults to `false`. - * `mode` String (optional) - Defaults to `free`. - * `continuous` Boolean (optional) - Defaults to `true`. + * `selectedStyle` string (optional) - Selected item style. Can be `background`, `outline` or `none`. Defaults to `none`. + * `overlayStyle` string (optional) - Selected overlay item style. Can be `background`, `outline` or `none`. Defaults to `none`. + * `showArrowButtons` boolean (optional) - Whether to show arrow buttons. Defaults to `false` and is only shown if `items` is non-empty. + * `mode` string (optional) - Can be `fixed` or `free`. The default is `free`. + * `continuous` boolean (optional) - Defaults to `true`. ### Instance Properties @@ -29,31 +30,31 @@ updates the control in the touch bar. Updating deep properties inside this array #### `touchBarScrubber.selectedStyle` -A `String` representing the style that selected items in the scrubber should have. Updating this value immediately +A `string` representing the style that selected items in the scrubber should have. Updating this value immediately updates the control in the touch bar. Possible values: * `background` - Maps to `[NSScrubberSelectionStyle roundedBackgroundStyle]`. * `outline` - Maps to `[NSScrubberSelectionStyle outlineOverlayStyle]`. -* `null` - Actually null, not a string, removes all styles. +* `none` - Removes all styles. #### `touchBarScrubber.overlayStyle` -A `String` representing the style that selected items in the scrubber should have. This style is overlayed on top +A `string` representing the style that selected items in the scrubber should have. This style is overlayed on top of the scrubber item instead of being placed behind it. Updating this value immediately updates the control in the touch bar. Possible values: * `background` - Maps to `[NSScrubberSelectionStyle roundedBackgroundStyle]`. * `outline` - Maps to `[NSScrubberSelectionStyle outlineOverlayStyle]`. -* `null` - Actually null, not a string, removes all styles. +* `none` - Removes all styles. #### `touchBarScrubber.showArrowButtons` -A `Boolean` representing whether to show the left / right selection arrows in this scrubber. Updating this value +A `boolean` representing whether to show the left / right selection arrows in this scrubber. Updating this value immediately updates the control in the touch bar. #### `touchBarScrubber.mode` -A `String` representing the mode of this scrubber. Updating this value immediately +A `string` representing the mode of this scrubber. Updating this value immediately updates the control in the touch bar. Possible values: * `fixed` - Maps to `NSScrubberModeFixed`. @@ -61,5 +62,5 @@ updates the control in the touch bar. Possible values: #### `touchBarScrubber.continuous` -A `Boolean` representing whether this scrubber is continuous or not. Updating this value immediately +A `boolean` representing whether this scrubber is continuous or not. Updating this value immediately updates the control in the touch bar. diff --git a/docs/api/touch-bar-segmented-control.md b/docs/api/touch-bar-segmented-control.md index f258afd59cc1b..53d60622480d3 100644 --- a/docs/api/touch-bar-segmented-control.md +++ b/docs/api/touch-bar-segmented-control.md @@ -2,34 +2,35 @@ > Create a segmented control (a button group) where one button has a selected state -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarSegmentedControl(options)` _Experimental_ +### `new TouchBarSegmentedControl(options)` * `options` Object - * `segmentStyle` String (optional) - Style of the segments: + * `segmentStyle` string (optional) - Style of the segments: * `automatic` - Default. The appearance of the segmented control is automatically determined based on the type of window in which the control - is displayed and the position within the window. - * `rounded` - The control is displayed using the rounded style. + is displayed and the position within the window. Maps to `NSSegmentStyleAutomatic`. + * `rounded` - The control is displayed using the rounded style. Maps to `NSSegmentStyleRounded`. * `textured-rounded` - The control is displayed using the textured rounded - style. - * `round-rect` - The control is displayed using the round rect style. + style. Maps to `NSSegmentStyleTexturedRounded`. + * `round-rect` - The control is displayed using the round rect style. Maps to `NSSegmentStyleRoundRect`. * `textured-square` - The control is displayed using the textured square - style. - * `capsule` - The control is displayed using the capsule style. - * `small-square` - The control is displayed using the small square style. + style. Maps to `NSSegmentStyleTexturedSquare`. + * `capsule` - The control is displayed using the capsule style. Maps to `NSSegmentStyleCapsule`. + * `small-square` - The control is displayed using the small square style. Maps to `NSSegmentStyleSmallSquare`. * `separated` - The segments in the control are displayed very close to each - other but not touching. - * `mode` String (optional) - The selection mode of the control: - * `single` - Default. One item selected at a time, selecting one deselects the previously selected item. - * `multiple` - Multiple items can be selected at a time. - * `buttons` - Make the segments act as buttons, each segment can be pressed and released but never marked as active. + other but not touching. Maps to `NSSegmentStyleSeparated`. + * `mode` string (optional) - The selection mode of the control: + * `single` - Default. One item selected at a time, selecting one deselects the previously selected item. Maps to `NSSegmentSwitchTrackingSelectOne`. + * `multiple` - Multiple items can be selected at a time. Maps to `NSSegmentSwitchTrackingSelectAny`. + * `buttons` - Make the segments act as buttons, each segment can be pressed and released but never marked as active. Maps to `NSSegmentSwitchTrackingMomentary`. * `segments` [SegmentedControlSegment[]](structures/segmented-control-segment.md) - An array of segments to place in this control. - * `selectedIndex` Integer (optional) - The index of the currently selected segment, will update automatically with user interaction. When the mode is multiple it will be the last selected item. + * `selectedIndex` Integer (optional) - The index of the currently selected segment, will update automatically with user interaction. When the mode is `multiple` it will be the last selected item. * `change` Function (optional) - Called when the user selects a new segment. * `selectedIndex` Integer - The index of the segment the user selected. - * `isSelected` Boolean - Whether as a result of user selection the segment is selected or not. + * `isSelected` boolean - Whether as a result of user selection the segment is selected or not. ### Instance Properties @@ -37,7 +38,7 @@ The following properties are available on instances of `TouchBarSegmentedControl #### `touchBarSegmentedControl.segmentStyle` -A `String` representing the controls current segment style. Updating this value immediately updates the control +A `string` representing the controls current segment style. Updating this value immediately updates the control in the touch bar. #### `touchBarSegmentedControl.segments` @@ -49,3 +50,7 @@ updates the control in the touch bar. Updating deep properties inside this array An `Integer` representing the currently selected segment. Changing this value immediately updates the control in the touch bar. User interaction with the touch bar will update this value automatically. + +#### `touchBarSegmentedControl.mode` + +A `string` representing the current selection mode of the control. Can be `single`, `multiple` or `buttons`. diff --git a/docs/api/touch-bar-slider.md b/docs/api/touch-bar-slider.md index 3f6c98fcd1814..16036ffcf5b56 100644 --- a/docs/api/touch-bar-slider.md +++ b/docs/api/touch-bar-slider.md @@ -2,17 +2,18 @@ > Create a slider in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarSlider(options)` _Experimental_ +### `new TouchBarSlider(options)` * `options` Object - * `label` String (optional) - Label text. + * `label` string (optional) - Label text. * `value` Integer (optional) - Selected value. * `minValue` Integer (optional) - Minimum value. * `maxValue` Integer (optional) - Maximum value. * `change` Function (optional) - Function to call when the slider is changed. - * `newValue` Number - The value that the user selected on the Slider. + * `newValue` number - The value that the user selected on the Slider. ### Instance Properties @@ -20,20 +21,20 @@ The following properties are available on instances of `TouchBarSlider`: #### `touchBarSlider.label` -A `String` representing the slider's current text. Changing this value immediately updates the slider +A `string` representing the slider's current text. Changing this value immediately updates the slider in the touch bar. #### `touchBarSlider.value` -A `Number` representing the slider's current value. Changing this value immediately updates the slider +A `number` representing the slider's current value. Changing this value immediately updates the slider in the touch bar. #### `touchBarSlider.minValue` -A `Number` representing the slider's current minimum value. Changing this value immediately updates the +A `number` representing the slider's current minimum value. Changing this value immediately updates the slider in the touch bar. #### `touchBarSlider.maxValue` -A `Number` representing the slider's current maximum value. Changing this value immediately updates the +A `number` representing the slider's current maximum value. Changing this value immediately updates the slider in the touch bar. diff --git a/docs/api/touch-bar-spacer.md b/docs/api/touch-bar-spacer.md index b79d4f5449c30..79f4a7bd5ecb4 100644 --- a/docs/api/touch-bar-spacer.md +++ b/docs/api/touch-bar-spacer.md @@ -2,12 +2,21 @@ > Create a spacer between two items in the touch bar for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ -### `new TouchBarSpacer(options)` _Experimental_ +### `new TouchBarSpacer(options)` * `options` Object - * `size` String (optional) - Size of spacer, possible values are: - * `small` - Small space between items. - * `large` - Large space between items. - * `flexible` - Take up all available space. + * `size` string (optional) - Size of spacer, possible values are: + * `small` - Small space between items. Maps to `NSTouchBarItemIdentifierFixedSpaceSmall`. This is the default. + * `large` - Large space between items. Maps to `NSTouchBarItemIdentifierFixedSpaceLarge`. + * `flexible` - Take up all available space. Maps to `NSTouchBarItemIdentifierFlexibleSpace`. + +### Instance Properties + +The following properties are available on instances of `TouchBarSpacer`: + +#### `touchBarSpacer.size` + +A `string` representing the size of the spacer. Can be `small`, `large` or `flexible`. diff --git a/docs/api/touch-bar.md b/docs/api/touch-bar.md index 28194d67c02b2..10bf400ab47b0 100644 --- a/docs/api/touch-bar.md +++ b/docs/api/touch-bar.md @@ -1,10 +1,12 @@ +# TouchBar + ## Class: TouchBar > Create TouchBar layouts for native macOS applications -Process: [Main](../tutorial/application-architecture.md#main-and-renderer-processes) +Process: [Main](../glossary.md#main-process) -### `new TouchBar(options)` _Experimental_ +### `new TouchBar(options)` * `options` Object * `items` ([TouchBarButton](touch-bar-button.md) | [TouchBarColorPicker](touch-bar-color-picker.md) | [TouchBarGroup](touch-bar-group.md) | [TouchBarLabel](touch-bar-label.md) | [TouchBarPopover](touch-bar-popover.md) | [TouchBarScrubber](touch-bar-scrubber.md) | [TouchBarSegmentedControl](touch-bar-segmented-control.md) | [TouchBarSlider](touch-bar-slider.md) | [TouchBarSpacer](touch-bar-spacer.md))[] (optional) @@ -58,6 +60,10 @@ A [`typeof TouchBarSlider`](./touch-bar-slider.md) reference to the `TouchBarSli A [`typeof TouchBarSpacer`](./touch-bar-spacer.md) reference to the `TouchBarSpacer` class. +#### `TouchBarOtherItemsProxy` + +A [`typeof TouchBarOtherItemsProxy`](./touch-bar-other-items-proxy.md) reference to the `TouchBarOtherItemsProxy` class. + ### Instance Properties The following properties are available on instances of `TouchBar`: @@ -166,7 +172,7 @@ const touchBar = new TouchBar({ let window -app.once('ready', () => { +app.whenReady().then(() => { window = new BrowserWindow({ frame: false, titleBarStyle: 'hiddenInset', diff --git a/docs/api/tray.md b/docs/api/tray.md index c299f086d96ee..d8dac322f769d 100644 --- a/docs/api/tray.md +++ b/docs/api/tray.md @@ -1,3 +1,5 @@ +# Tray + ## Class: Tray > Add icons and context menus to the system's notification area. @@ -10,7 +12,7 @@ Process: [Main](../glossary.md#main-process) const { app, Menu, Tray } = require('electron') let tray = null -app.on('ready', () => { +app.whenReady().then(() => { tray = new Tray('/path/to/my/icon') const contextMenu = Menu.buildFromTemplate([ { label: 'Item1', type: 'radio' }, @@ -38,7 +40,7 @@ __Platform limitations:__ const { app, Menu, Tray } = require('electron') let appIcon = null -app.on('ready', () => { +app.whenReady().then(() => { appIcon = new Tray('/path/to/my/icon') const contextMenu = Menu.buildFromTemplate([ { label: 'Item1', type: 'radio' }, @@ -52,15 +54,16 @@ app.on('ready', () => { appIcon.setContextMenu(contextMenu) }) ``` + * On Windows it is recommended to use `ICO` icons to get best visual effects. If you want to keep exact same behaviors on all platforms, you should not rely on the `click` event and always attach a context menu to the tray icon. +### `new Tray(image, [guid])` -### `new Tray(image)` - -* `image` ([NativeImage](native-image.md) | String) +* `image` ([NativeImage](native-image.md) | string) +* `guid` string (optional) _Windows_ - Assigns a GUID to the tray icon. If the executable is signed and the signature contains an organization in the subject line then the GUID is permanently associated with that signature. OS level settings like the position of the tray icon in the system tray will persist even if the path to the executable changes. If the executable is not code-signed then the GUID is permanently associated with the path to the executable. Changing the path to the executable will break the creation of the tray icon and a new GUID must be used. However, it is highly recommended to use the GUID parameter only in conjunction with code-signed executable. If an App defines multiple tray icons then each icon must use a separate GUID. Creates a new tray icon associated with the `image`. @@ -118,7 +121,7 @@ Emitted when any dragged items are dropped on the tray icon. Returns: * `event` Event -* `files` String[] - The paths of the dropped files. +* `files` string[] - The paths of the dropped files. Emitted when dragged files are dropped in the tray icon. @@ -127,7 +130,7 @@ Emitted when dragged files are dropped in the tray icon. Returns: * `event` Event -* `text` String - the dropped text string. +* `text` string - the dropped text string. Emitted when dragged text is dropped in the tray icon. @@ -143,6 +146,26 @@ Emitted when a drag operation exits the tray icon. Emitted when a drag operation ends on the tray or ends at another location. +#### Event: 'mouse-up' _macOS_ + +Returns: + +* `event` [KeyboardEvent](structures/keyboard-event.md) +* `position` [Point](structures/point.md) - The position of the event. + +Emitted when the mouse is released from clicking the tray icon. + +Note: This will not be emitted if you have set a context menu for your Tray using `tray.setContextMenu`, as a result of macOS-level constraints. + +#### Event: 'mouse-down' _macOS_ + +Returns: + +* `event` [KeyboardEvent](structures/keyboard-event.md) +* `position` [Point](structures/point.md) - The position of the event. + +Emitted when the mouse clicks the tray icon. + #### Event: 'mouse-enter' _macOS_ Returns: @@ -180,35 +203,37 @@ Destroys the tray icon immediately. #### `tray.setImage(image)` -* `image` ([NativeImage](native-image.md) | String) +* `image` ([NativeImage](native-image.md) | string) Sets the `image` associated with this tray icon. #### `tray.setPressedImage(image)` _macOS_ -* `image` ([NativeImage](native-image.md) | String) +* `image` ([NativeImage](native-image.md) | string) Sets the `image` associated with this tray icon when pressed on macOS. #### `tray.setToolTip(toolTip)` -* `toolTip` String +* `toolTip` string Sets the hover text for this tray icon. -#### `tray.setTitle(title)` _macOS_ +#### `tray.setTitle(title[, options])` _macOS_ -* `title` String +* `title` string +* `options` Object (optional) + * `fontType` string (optional) - The font family variant to display, can be `monospaced` or `monospacedDigit`. `monospaced` is available in macOS 10.15+ and `monospacedDigit` is available in macOS 10.11+. When left blank, the title uses the default system font. Sets the title displayed next to the tray icon in the status bar (Support ANSI colors). #### `tray.getTitle()` _macOS_ -Returns `String` - the title displayed next to the tray icon in the status bar +Returns `string` - the title displayed next to the tray icon in the status bar #### `tray.setIgnoreDoubleClickEvents(ignore)` _macOS_ -* `ignore` Boolean +* `ignore` boolean Sets the option to ignore double click events. Ignoring these events allows you to detect every individual click of the tray icon. @@ -217,17 +242,36 @@ This value is set to false by default. #### `tray.getIgnoreDoubleClickEvents()` _macOS_ -Returns `Boolean` - Whether double click events will be ignored. +Returns `boolean` - Whether double click events will be ignored. #### `tray.displayBalloon(options)` _Windows_ * `options` Object - * `icon` ([NativeImage](native-image.md) | String) (optional) - - * `title` String - * `content` String + * `icon` ([NativeImage](native-image.md) | string) (optional) - Icon to use when `iconType` is `custom`. + * `iconType` string (optional) - Can be `none`, `info`, `warning`, `error` or `custom`. Default is `custom`. + * `title` string + * `content` string + * `largeIcon` boolean (optional) - The large version of the icon should be used. Default is `true`. Maps to [`NIIF_LARGE_ICON`][NIIF_LARGE_ICON]. + * `noSound` boolean (optional) - Do not play the associated sound. Default is `false`. Maps to [`NIIF_NOSOUND`][NIIF_NOSOUND]. + * `respectQuietTime` boolean (optional) - Do not display the balloon notification if the current user is in "quiet time". Default is `false`. Maps to [`NIIF_RESPECT_QUIET_TIME`][NIIF_RESPECT_QUIET_TIME]. Displays a tray balloon. +[NIIF_NOSOUND]: https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa#niif_nosound-0x00000010 +[NIIF_LARGE_ICON]: https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa#niif_large_icon-0x00000020 +[NIIF_RESPECT_QUIET_TIME]: https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa#niif_respect_quiet_time-0x00000080 + +#### `tray.removeBalloon()` _Windows_ + +Removes a tray balloon. + +#### `tray.focus()` _Windows_ + +Returns focus to the taskbar notification area. +Notification area icons should use this message when they have completed their UI operation. +For example, if the icon displays a shortcut menu, but the user presses ESC to cancel it, +use `tray.focus()` to return focus to the notification area. + #### `tray.popUpContextMenu([menu, position])` _macOS_ _Windows_ * `menu` Menu (optional) @@ -238,6 +282,10 @@ be shown instead of the tray icon's context menu. The `position` is only available on Windows, and it is (0, 0) by default. +#### `tray.closeContextMenu()` _macOS_ _Windows_ + +Closes an open context menu, as set by `tray.setContextMenu()`. + #### `tray.setContextMenu(menu)` * `menu` Menu | null @@ -252,6 +300,6 @@ The `bounds` of this tray icon as `Object`. #### `tray.isDestroyed()` -Returns `Boolean` - Whether the tray icon is destroyed. +Returns `boolean` - Whether the tray icon is destroyed. [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 13117882b1ded..d4a1233c5976a 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -12,10 +12,10 @@ the [`BrowserWindow`](browser-window.md) object. An example of accessing the ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ width: 800, height: 1500 }) +const win = new BrowserWindow({ width: 800, height: 1500 }) win.loadURL('http://github.com') -let contents = win.webContents +const contents = win.webContents console.log(contents) ``` @@ -42,13 +42,35 @@ returns `null`. * `id` Integer -Returns `WebContents` - A WebContents instance with the given ID. +Returns `WebContents` | undefined - A WebContents instance with the given ID, or +`undefined` if there is no WebContents associated with the given ID. + +### `webContents.fromDevToolsTargetId(targetId)` + +* `targetId` string - The Chrome DevTools Protocol [TargetID](https://chromedevtools.github.io/devtools-protocol/tot/Target/#type-TargetID) associated with the WebContents instance. + +Returns `WebContents` | undefined - A WebContents instance with the given TargetID, or +`undefined` if there is no WebContents associated with the given TargetID. + +When communicating with the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/), +it can be useful to lookup a WebContents instance based on its assigned TargetID. + +```js +async function lookupTargetId (browserWindow) { + const wc = browserWindow.webContents + await wc.debugger.attach('1.3') + const { targetInfo } = await wc.debugger.sendCommand('Target.getTargetInfo') + const { targetId } = targetInfo + const targetWebContents = await webContents.fromDevToolsTargetId(targetId) +} +``` ## Class: WebContents > Render and control the contents of a BrowserWindow instance. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ ### Instance Events @@ -63,14 +85,14 @@ Returns: * `event` Event * `errorCode` Integer -* `errorDescription` String -* `validatedURL` String -* `isMainFrame` Boolean +* `errorDescription` string +* `validatedURL` string +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer This event is like `did-finish-load` but emitted when the load failed. -The full list of error codes and their meaning is available [here](https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h). +The full list of error codes and their meaning is available [here](https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h). #### Event: 'did-fail-provisional-load' @@ -78,9 +100,9 @@ Returns: * `event` Event * `errorCode` Integer -* `errorDescription` String -* `validatedURL` String -* `isMainFrame` Boolean +* `errorDescription` string +* `validatedURL` string +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer @@ -92,7 +114,7 @@ This event is like `did-fail-load` but emitted when the load was cancelled Returns: * `event` Event -* `isMainFrame` Boolean +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer @@ -112,15 +134,15 @@ Returns: * `event` Event -Emitted when the document in the given frame is loaded. +Emitted when the document in the top-level frame is loaded. #### Event: 'page-title-updated' Returns: * `event` Event -* `title` String -* `explicitSet` Boolean +* `title` string +* `explicitSet` boolean Fired when page title is set during navigation. `explicitSet` is false when title is synthesized from file url. @@ -130,26 +152,33 @@ title is synthesized from file url. Returns: * `event` Event -* `favicons` String[] - Array of URLs. +* `favicons` string[] - Array of URLs. Emitted when page receives favicon urls. -#### Event: 'new-window' +#### Event: 'new-window' _Deprecated_ Returns: -* `event` Event -* `url` String -* `frameName` String -* `disposition` String - Can be `default`, `foreground-tab`, `background-tab`, +* `event` NewWindowWebContentsEvent +* `url` string +* `frameName` string +* `disposition` string - Can be `default`, `foreground-tab`, `background-tab`, `new-window`, `save-to-disk` and `other`. -* `options` Object - The options which will be used for creating the new +* `options` BrowserWindowConstructorOptions - The options which will be used for creating the new [`BrowserWindow`](browser-window.md). -* `additionalFeatures` String[] - The non-standard features (features not handled - by Chromium or Electron) given to `window.open()`. +* `additionalFeatures` string[] - The non-standard features (features not handled + by Chromium or Electron) given to `window.open()`. Deprecated, and will now + always be the empty array `[]`. * `referrer` [Referrer](structures/referrer.md) - The referrer that will be passed to the new window. May or may not result in the `Referer` header being sent, depending on the referrer policy. +* `postBody` [PostBody](structures/post-body.md) (optional) - The post data that + will be sent to the new window, along with the appropriate headers that will + be set. If no post data is to be sent, the value will be `null`. Only defined + when the window is being created by a form that set `target=_blank`. + +Deprecated in favor of [`webContents.setWindowOpenHandler`](web-contents.md#contentssetwindowopenhandlerhandler). Emitted when the page requests to open a new window for a `url`. It could be requested by `window.open` or an external link like `<a target='_blank'>`. @@ -162,7 +191,7 @@ new [`BrowserWindow`](browser-window.md). If you call `event.preventDefault()` a instance, failing to do so may result in unexpected behavior. For example: ```javascript -myBrowserWindow.webContents.on('new-window', (event, url, frameName, disposition, options) => { +myBrowserWindow.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures, referrer, postBody) => { event.preventDefault() const win = new BrowserWindow({ webContents: options.webContents, // use existing webContents if provided @@ -170,18 +199,59 @@ myBrowserWindow.webContents.on('new-window', (event, url, frameName, disposition }) win.once('ready-to-show', () => win.show()) if (!options.webContents) { - win.loadURL(url) // existing webContents will be navigated automatically + const loadOptions = { + httpReferrer: referrer + } + if (postBody != null) { + const { data, contentType, boundary } = postBody + loadOptions.postData = postBody.data + loadOptions.extraHeaders = `content-type: ${contentType}; boundary=${boundary}` + } + + win.loadURL(url, loadOptions) // existing webContents will be navigated automatically } event.newGuest = win }) ``` +#### Event: 'did-create-window' + +Returns: + +* `window` BrowserWindow +* `details` Object + * `url` string - URL for the created window. + * `frameName` string - Name given to the created window in the + `window.open()` call. + * `options` BrowserWindowConstructorOptions - The options used to create the + BrowserWindow. They are merged in increasing precedence: parsed options + from the `features` string from `window.open()`, security-related + webPreferences inherited from the parent, and options given by + [`webContents.setWindowOpenHandler`](web-contents.md#contentssetwindowopenhandlerhandler). + Unrecognized options are not filtered out. + * `referrer` [Referrer](structures/referrer.md) - The referrer that will be + passed to the new window. May or may not result in the `Referer` header + being sent, depending on the referrer policy. + * `postBody` [PostBody](structures/post-body.md) (optional) - The post data + that will be sent to the new window, along with the appropriate headers + that will be set. If no post data is to be sent, the value will be `null`. + Only defined when the window is being created by a form that set + `target=_blank`. + * `disposition` string - Can be `default`, `foreground-tab`, + `background-tab`, `new-window`, `save-to-disk` and `other`. + +Emitted _after_ successful creation of a window via `window.open` in the renderer. +Not emitted if the creation of the window is canceled from +[`webContents.setWindowOpenHandler`](web-contents.md#contentssetwindowopenhandlerhandler). + +See [`window.open()`](window-open.md) for more details and how to use this in conjunction with `webContents.setWindowOpenHandler`. + #### Event: 'will-navigate' Returns: * `event` Event -* `url` String +* `url` string Emitted when a user or the page wants to start navigation. It can happen when the `window.location` object is changed or a user clicks a link in the page. @@ -200,13 +270,13 @@ Calling `event.preventDefault()` will prevent the navigation. Returns: * `event` Event -* `url` String -* `isInPlace` Boolean -* `isMainFrame` Boolean +* `url` string +* `isInPlace` boolean +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer -Emitted when any frame (including main) starts navigating. `isInplace` will be +Emitted when any frame (including main) starts navigating. `isInPlace` will be `true` for in-page navigations. #### Event: 'will-redirect' @@ -214,13 +284,13 @@ Emitted when any frame (including main) starts navigating. `isInplace` will be Returns: * `event` Event -* `url` String -* `isInPlace` Boolean -* `isMainFrame` Boolean +* `url` string +* `isInPlace` boolean +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer -Emitted as a server side redirect occurs during navigation. For example a 302 +Emitted when a server side redirect occurs during navigation. For example a 302 redirect. This event will be emitted after `did-start-navigation` and always before the @@ -234,16 +304,16 @@ redirect). Returns: * `event` Event -* `url` String -* `isInPlace` Boolean -* `isMainFrame` Boolean +* `url` string +* `isInPlace` boolean +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer Emitted after a server side redirect occurs during navigation. For example a 302 redirect. -This event can not be prevented, if you want to prevent redirects you should +This event cannot be prevented, if you want to prevent redirects you should checkout out the `will-redirect` event above. #### Event: 'did-navigate' @@ -251,9 +321,9 @@ checkout out the `will-redirect` event above. Returns: * `event` Event -* `url` String +* `url` string * `httpResponseCode` Integer - -1 for non HTTP navigations -* `httpStatusText` String - empty for non HTTP navigations +* `httpStatusText` string - empty for non HTTP navigations Emitted when a main frame navigation is done. @@ -266,10 +336,10 @@ this purpose. Returns: * `event` Event -* `url` String +* `url` string * `httpResponseCode` Integer - -1 for non HTTP navigations -* `httpStatusText` String - empty for non HTTP navigations, -* `isMainFrame` Boolean +* `httpStatusText` string - empty for non HTTP navigations, +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer @@ -284,8 +354,8 @@ this purpose. Returns: * `event` Event -* `url` String -* `isMainFrame` Boolean +* `url` string +* `isMainFrame` boolean * `frameProcessId` Integer * `frameRoutingId` Integer @@ -310,7 +380,7 @@ and allow the page to be unloaded. const { BrowserWindow, dialog } = require('electron') const win = new BrowserWindow({ width: 800, height: 600 }) win.webContents.on('will-prevent-unload', (event) => { - const choice = dialog.showMessageBox(win, { + const choice = dialog.showMessageBoxSync(win, { type: 'question', buttons: ['Leave', 'Stay'], title: 'Do you want to leave this site?', @@ -325,15 +395,43 @@ win.webContents.on('will-prevent-unload', (event) => { }) ``` -#### Event: 'crashed' +**Note:** This will be emitted for `BrowserViews` but will _not_ be respected - this is because we have chosen not to tie the `BrowserView` lifecycle to its owning BrowserWindow should one exist per the [specification](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event). + +#### Event: 'crashed' _Deprecated_ Returns: * `event` Event -* `killed` Boolean +* `killed` boolean Emitted when the renderer process crashes or is killed. +**Deprecated:** This event is superceded by the `render-process-gone` event +which contains more information about why the render process disappeared. It +isn't always because it crashed. The `killed` boolean can be replaced by +checking `reason === 'killed'` when you switch to that event. + +#### Event: 'render-process-gone' + +Returns: + +* `event` Event +* `details` Object + * `reason` string - The reason the render process is gone. Possible values: + * `clean-exit` - Process exited with an exit code of zero + * `abnormal-exit` - Process exited with a non-zero exit code + * `killed` - Process was sent a SIGTERM or otherwise killed externally + * `crashed` - Process crashed + * `oom` - Process ran out of memory + * `launch-failed` - Process never successfully launched + * `integrity-failure` - Windows code integrity checks failed + * `exitCode` Integer - The exit code of the process, unless `reason` is + `launch-failed`, in which case `exitCode` will be a platform-specific + launch failure error code. + +Emitted when the renderer process unexpectedly disappears. This is normally +because it was crashed or killed. + #### Event: 'unresponsive' Emitted when the web page becomes unresponsive. @@ -347,8 +445,8 @@ Emitted when the unresponsive web page becomes responsive again. Returns: * `event` Event -* `name` String -* `version` String +* `name` string +* `version` string Emitted when a plugin process has crashed. @@ -362,26 +460,29 @@ Returns: * `event` Event * `input` Object - Input properties. - * `type` String - Either `keyUp` or `keyDown`. - * `key` String - Equivalent to [KeyboardEvent.key][keyboardevent]. - * `code` String - Equivalent to [KeyboardEvent.code][keyboardevent]. - * `isAutoRepeat` Boolean - Equivalent to [KeyboardEvent.repeat][keyboardevent]. - * `shift` Boolean - Equivalent to [KeyboardEvent.shiftKey][keyboardevent]. - * `control` Boolean - Equivalent to [KeyboardEvent.controlKey][keyboardevent]. - * `alt` Boolean - Equivalent to [KeyboardEvent.altKey][keyboardevent]. - * `meta` Boolean - Equivalent to [KeyboardEvent.metaKey][keyboardevent]. + * `type` string - Either `keyUp` or `keyDown`. + * `key` string - Equivalent to [KeyboardEvent.key][keyboardevent]. + * `code` string - Equivalent to [KeyboardEvent.code][keyboardevent]. + * `isAutoRepeat` boolean - Equivalent to [KeyboardEvent.repeat][keyboardevent]. + * `isComposing` boolean - Equivalent to [KeyboardEvent.isComposing][keyboardevent]. + * `shift` boolean - Equivalent to [KeyboardEvent.shiftKey][keyboardevent]. + * `control` boolean - Equivalent to [KeyboardEvent.controlKey][keyboardevent]. + * `alt` boolean - Equivalent to [KeyboardEvent.altKey][keyboardevent]. + * `meta` boolean - Equivalent to [KeyboardEvent.metaKey][keyboardevent]. + * `location` number - Equivalent to [KeyboardEvent.location][keyboardevent]. + * `modifiers` string[] - See [InputEvent.modifiers](structures/input-event.md). Emitted before dispatching the `keydown` and `keyup` events in the page. Calling `event.preventDefault` will prevent the page `keydown`/`keyup` events and the menu shortcuts. To only prevent the menu shortcuts, use -[`setIgnoreMenuShortcuts`](#contentssetignoremenushortcutsignore-experimental): +[`setIgnoreMenuShortcuts`](#contentssetignoremenushortcutsignore): ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ width: 800, height: 600 }) +const win = new BrowserWindow({ width: 800, height: 600 }) win.webContents.on('before-input-event', (event, input) => { // For example, only enable application menu keyboard shortcuts when @@ -401,11 +502,29 @@ Emitted when the window leaves a full-screen state triggered by HTML API. #### Event: 'zoom-changed' Returns: + * `event` Event -* `zoomDirection` String - Can be `in` or `out`. +* `zoomDirection` string - Can be `in` or `out`. Emitted when the user is requesting to change the zoom level using the mouse wheel. +#### Event: 'blur' + +Emitted when the `WebContents` loses focus. + +#### Event: 'focus' + +Emitted when the `WebContents` gains focus. + +Note that on macOS, having focus means the `WebContents` is the first responder +of window, so switching focus between windows would not trigger the `focus` and +`blur` events of `WebContents`, as the first responder of each window is not +changed. + +The `focus` and `blur` events of `WebContents` should only be used to detect +focus change between different `WebContents` and `BrowserView` in the same +window. + #### Event: 'devtools-opened' Emitted when DevTools is opened. @@ -423,11 +542,12 @@ Emitted when DevTools is focused / opened. Returns: * `event` Event -* `url` String -* `error` String - The error code. +* `url` string +* `error` string - The error code. * `certificate` [Certificate](structures/certificate.md) * `callback` Function - * `isTrusted` Boolean - Indicates whether the certificate can be considered trusted. + * `isTrusted` boolean - Indicates whether the certificate can be considered trusted. +* `isMainFrame` boolean Emitted when failed to verify the `certificate` for `url`. @@ -454,19 +574,17 @@ The usage is the same with [the `select-client-certificate` event of Returns: * `event` Event -* `request` Object - * `method` String +* `authenticationResponseDetails` Object * `url` URL - * `referrer` URL * `authInfo` Object - * `isProxy` Boolean - * `scheme` String - * `host` String + * `isProxy` boolean + * `scheme` string + * `host` string * `port` Integer - * `realm` String + * `realm` string * `callback` Function - * `username` String - * `password` String + * `username` string (optional) + * `password` string (optional) Emitted when `webContents` wants to do basic auth. @@ -481,8 +599,8 @@ Returns: * `requestId` Integer * `activeMatchOrdinal` Integer - Position of the active match. * `matches` Integer - Number of Matches. - * `selectionArea` Object - Coordinates of first match region. - * `finalUpdate` Boolean + * `selectionArea` Rectangle - Coordinates of first match region. + * `finalUpdate` boolean Emitted when a result is available for [`webContents.findInPage`] request. @@ -500,7 +618,7 @@ Emitted when media is paused or done playing. Returns: * `event` Event -* `color` (String | null) - Theme color is in format of '#rrggbb'. It is `null` when no theme color is set. +* `color` (string | null) - Theme color is in format of '#rrggbb'. It is `null` when no theme color is set. Emitted when a page's theme color changes. This is usually due to encountering a meta tag: @@ -514,7 +632,7 @@ a meta tag: Returns: * `event` Event -* `url` String +* `url` string Emitted when mouse moves over a link or the keyboard moves the focus to a link. @@ -523,7 +641,7 @@ Emitted when mouse moves over a link or the keyboard moves the focus to a link. Returns: * `event` Event -* `type` String +* `type` string * `image` [NativeImage](native-image.md) (optional) * `scale` Float (optional) - scaling factor for the custom cursor. * `size` [Size](structures/size.md) (optional) - the size of the `image`. @@ -550,54 +668,72 @@ Returns: * `params` Object * `x` Integer - x coordinate. * `y` Integer - y coordinate. - * `linkURL` String - URL of the link that encloses the node the context menu + * `frame` WebFrameMain - Frame from which the context menu was invoked. + * `linkURL` string - URL of the link that encloses the node the context menu was invoked on. - * `linkText` String - Text associated with the link. May be an empty + * `linkText` string - Text associated with the link. May be an empty string if the contents of the link are an image. - * `pageURL` String - URL of the top level page that the context menu was + * `pageURL` string - URL of the top level page that the context menu was invoked on. - * `frameURL` String - URL of the subframe that the context menu was invoked + * `frameURL` string - URL of the subframe that the context menu was invoked on. - * `srcURL` String - Source URL for the element that the context menu + * `srcURL` string - Source URL for the element that the context menu was invoked on. Elements with source URLs are images, audio and video. - * `mediaType` String - Type of the node the context menu was invoked on. Can + * `mediaType` string - Type of the node the context menu was invoked on. Can be `none`, `image`, `audio`, `video`, `canvas`, `file` or `plugin`. - * `hasImageContents` Boolean - Whether the context menu was invoked on an image + * `hasImageContents` boolean - Whether the context menu was invoked on an image which has non-empty contents. - * `isEditable` Boolean - Whether the context is editable. - * `selectionText` String - Text of the selection that the context menu was + * `isEditable` boolean - Whether the context is editable. + * `selectionText` string - Text of the selection that the context menu was invoked on. - * `titleText` String - Title or alt text of the selection that the context - was invoked on. - * `misspelledWord` String - The misspelled word under the cursor, if any. - * `frameCharset` String - The character encoding of the frame on which the + * `titleText` string - Title text of the selection that the context menu was + invoked on. + * `altText` string - Alt text of the selection that the context menu was + invoked on. + * `suggestedFilename` string - Suggested filename to be used when saving file through 'Save + Link As' option of context menu. + * `selectionRect` [Rectangle](structures/rectangle.md) - Rect representing the coordinates in the document space of the selection. + * `selectionStartOffset` number - Start position of the selection text. + * `referrerPolicy` [Referrer](structures/referrer.md) - The referrer policy of the frame on which the menu is invoked. + * `misspelledWord` string - The misspelled word under the cursor, if any. + * `dictionarySuggestions` string[] - An array of suggested words to show the + user to replace the `misspelledWord`. Only available if there is a misspelled + word and spellchecker is enabled. + * `frameCharset` string - The character encoding of the frame on which the menu was invoked. - * `inputFieldType` String - If the context menu was invoked on an input + * `inputFieldType` string - If the context menu was invoked on an input field, the type of that field. Possible values are `none`, `plainText`, `password`, `other`. - * `menuSourceType` String - Input source that invoked the context menu. - Can be `none`, `mouse`, `keyboard`, `touch` or `touchMenu`. + * `spellcheckEnabled` boolean - If the context is editable, whether or not spellchecking is enabled. + * `menuSourceType` string - Input source that invoked the context menu. + Can be `none`, `mouse`, `keyboard`, `touch`, `touchMenu`, `longPress`, `longTap`, `touchHandle`, `stylus`, `adjustSelection`, or `adjustSelectionReset`. * `mediaFlags` Object - The flags for the media element the context menu was invoked on. - * `inError` Boolean - Whether the media element has crashed. - * `isPaused` Boolean - Whether the media element is paused. - * `isMuted` Boolean - Whether the media element is muted. - * `hasAudio` Boolean - Whether the media element has audio. - * `isLooping` Boolean - Whether the media element is looping. - * `isControlsVisible` Boolean - Whether the media element's controls are + * `inError` boolean - Whether the media element has crashed. + * `isPaused` boolean - Whether the media element is paused. + * `isMuted` boolean - Whether the media element is muted. + * `hasAudio` boolean - Whether the media element has audio. + * `isLooping` boolean - Whether the media element is looping. + * `isControlsVisible` boolean - Whether the media element's controls are visible. - * `canToggleControls` Boolean - Whether the media element's controls are + * `canToggleControls` boolean - Whether the media element's controls are toggleable. - * `canRotate` Boolean - Whether the media element can be rotated. + * `canPrint` boolean - Whether the media element can be printed. + * `canSave` boolean - Whether or not the media element can be downloaded. + * `canShowPictureInPicture` boolean - Whether the media element can show picture-in-picture. + * `isShowingPictureInPicture` boolean - Whether the media element is currently showing picture-in-picture. + * `canRotate` boolean - Whether the media element can be rotated. + * `canLoop` boolean - Whether the media element can be looped. * `editFlags` Object - These flags indicate whether the renderer believes it is able to perform the corresponding action. - * `canUndo` Boolean - Whether the renderer believes it can undo. - * `canRedo` Boolean - Whether the renderer believes it can redo. - * `canCut` Boolean - Whether the renderer believes it can cut. - * `canCopy` Boolean - Whether the renderer believes it can copy - * `canPaste` Boolean - Whether the renderer believes it can paste. - * `canDelete` Boolean - Whether the renderer believes it can delete. - * `canSelectAll` Boolean - Whether the renderer believes it can select all. + * `canUndo` boolean - Whether the renderer believes it can undo. + * `canRedo` boolean - Whether the renderer believes it can redo. + * `canCut` boolean - Whether the renderer believes it can cut. + * `canCopy` boolean - Whether the renderer believes it can copy. + * `canPaste` boolean - Whether the renderer believes it can paste. + * `canDelete` boolean - Whether the renderer believes it can delete. + * `canSelectAll` boolean - Whether the renderer believes it can select all. + * `canEditRichly` boolean - Whether the renderer believes it can edit text richly. Emitted when there is a new context menu that needs to be handled. @@ -608,7 +744,7 @@ Returns: * `event` Event * `devices` [BluetoothDevice[]](structures/bluetooth-device.md) * `callback` Function - * `deviceId` String + * `deviceId` string Emitted when bluetooth device needs to be selected on call to `navigator.bluetooth.requestDevice`. To use `navigator.bluetooth` api @@ -617,17 +753,19 @@ first available device will be selected. `callback` should be called with `deviceId` to be selected, passing empty string to `callback` will cancel the request. +If no event listener is added for this event, all bluetooth requests will be cancelled. + ```javascript const { app, BrowserWindow } = require('electron') let win = null app.commandLine.appendSwitch('enable-experimental-web-platform-features') -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow({ width: 800, height: 600 }) win.webContents.on('select-bluetooth-device', (event, deviceList, callback) => { event.preventDefault() - let result = deviceList.find((device) => { + const result = deviceList.find((device) => { return device.deviceName === 'test' }) if (!result) { @@ -653,7 +791,7 @@ buffer. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ webPreferences: { offscreen: true } }) +const win = new BrowserWindow({ webPreferences: { offscreen: true } }) win.webContents.on('paint', (event, dirty, image) => { // updateBitmap(dirty, image.getBitmap()) }) @@ -669,10 +807,10 @@ Emitted when the devtools window instructs the webContents to reload Returns: * `event` Event -* `webPreferences` Object - The web preferences that will be used by the guest +* `webPreferences` WebPreferences - The web preferences that will be used by the guest page. This object can be modified to adjust the preferences for the guest page. -* `params` Object - The other `<webview>` parameters such as the `src` URL. +* `params` Record<string, string> - The other `<webview>` parameters such as the `src` URL. This object can be modified to adjust the parameters of the guest page. Emitted when a `<webview>`'s web contents is being attached to this web @@ -682,7 +820,7 @@ This event can be used to configure `webPreferences` for the `webContents` of a `<webview>` before it's loaded, and provides the ability to set settings that can't be set via `<webview>` attributes. -**Note:** The specified `preload` script option will be appear as `preloadURL` +**Note:** The specified `preload` script option will appear as `preloadURL` (not `preload`) in the `webPreferences` object emitted with this event. #### Event: 'did-attach-webview' @@ -700,20 +838,19 @@ Emitted when a `<webview>` has been attached to this web contents. Returns: * `event` Event -* `level` Integer -* `message` String -* `line` Integer -* `sourceId` String +* `level` Integer - The log level, from 0 to 3. In order it matches `verbose`, `info`, `warning` and `error`. +* `message` string - The actual console message +* `line` Integer - The line number of the source that triggered this console message +* `sourceId` string -Emitted when the associated window logs a console message. Will not be emitted -for windows with *offscreen rendering* enabled. +Emitted when the associated window logs a console message. #### Event: 'preload-error' Returns: * `event` Event -* `preloadPath` String +* `preloadPath` string * `error` Error Emitted when the preload script `preloadPath` throws an unhandled exception `error`. @@ -723,7 +860,7 @@ Emitted when the preload script `preloadPath` throws an unhandled exception `err Returns: * `event` Event -* `channel` String +* `channel` string * `...args` any[] Emitted when the renderer process sends an asynchronous message via `ipcRenderer.send()`. @@ -733,95 +870,45 @@ Emitted when the renderer process sends an asynchronous message via `ipcRenderer Returns: * `event` Event -* `channel` String +* `channel` string * `...args` any[] Emitted when the renderer process sends a synchronous message via `ipcRenderer.sendSync()`. -#### Event: 'desktop-capturer-get-sources' +#### Event: 'preferred-size-changed' Returns: * `event` Event +* `preferredSize` [Size](structures/size.md) - The minimum size needed to + contain the layout of the document—without requiring scrolling. -Emitted when `desktopCapturer.getSources()` is called in the renderer process. -Calling `event.preventDefault()` will make it return empty sources. +Emitted when the `WebContents` preferred size has changed. -#### Event: 'remote-require' - -Returns: +This event will only be emitted when `enablePreferredSizeMode` is set to `true` +in `webPreferences`. -* `event` IpcMainEvent -* `moduleName` String - -Emitted when `remote.require()` is called in the renderer process. -Calling `event.preventDefault()` will prevent the module from being returned. -Custom value can be returned by setting `event.returnValue`. - -#### Event: 'remote-get-global' - -Returns: - -* `event` IpcMainEvent -* `globalName` String - -Emitted when `remote.getGlobal()` is called in the renderer process. -Calling `event.preventDefault()` will prevent the global from being returned. -Custom value can be returned by setting `event.returnValue`. - -#### Event: 'remote-get-builtin' - -Returns: - -* `event` IpcMainEvent -* `moduleName` String - -Emitted when `remote.getBuiltin()` is called in the renderer process. -Calling `event.preventDefault()` will prevent the module from being returned. -Custom value can be returned by setting `event.returnValue`. - -#### Event: 'remote-get-current-window' - -Returns: - -* `event` IpcMainEvent - -Emitted when `remote.getCurrentWindow()` is called in the renderer process. -Calling `event.preventDefault()` will prevent the object from being returned. -Custom value can be returned by setting `event.returnValue`. - -#### Event: 'remote-get-current-web-contents' - -Returns: - -* `event` IpcMainEvent - -Emitted when `remote.getCurrentWebContents()` is called in the renderer process. -Calling `event.preventDefault()` will prevent the object from being returned. -Custom value can be returned by setting `event.returnValue`. - -#### Event: 'remote-get-guest-web-contents' +#### Event: 'frame-created' Returns: -* `event` IpcMainEvent -* `guestWebContents` [WebContents](web-contents.md) +* `event` Event +* `details` Object + * `frame` WebFrameMain -Emitted when `<webview>.getWebContents()` is called in the renderer process. -Calling `event.preventDefault()` will prevent the object from being returned. -Custom value can be returned by setting `event.returnValue`. +Emitted when the [mainFrame](web-contents.md#contentsmainframe-readonly), an `<iframe>`, or a nested `<iframe>` is loaded within the page. ### Instance Methods #### `contents.loadURL(url[, options])` -* `url` String +* `url` string * `options` Object (optional) - * `httpReferrer` (String | [Referrer](structures/referrer.md)) (optional) - An HTTP Referrer url. - * `userAgent` String (optional) - A user agent originating the request. - * `extraHeaders` String (optional) - Extra headers separated by "\n". - * `postData` ([UploadRawData[]](structures/upload-raw-data.md) | [UploadFile[]](structures/upload-file.md) | [UploadBlob[]](structures/upload-blob.md)) (optional) - * `baseURLForDataURL` String (optional) - Base url (with trailing path separator) for files to be loaded by the data url. This is needed only if the specified `url` is a data url and needs to load other files. + * `httpReferrer` (string | [Referrer](structures/referrer.md)) (optional) - An HTTP Referrer url. + * `userAgent` string (optional) - A user agent originating the request. + * `extraHeaders` string (optional) - Extra headers separated by "\n". + * `postData` ([UploadRawData](structures/upload-raw-data.md) | [UploadFile](structures/upload-file.md))[] (optional) + * `baseURLForDataURL` string (optional) - Base url (with trailing path separator) for files to be loaded by the data url. This is needed only if the specified `url` is a data url and needs to load other files. Returns `Promise<void>` - the promise will resolve when the page has finished loading (see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects @@ -840,11 +927,11 @@ webContents.loadURL('https://github.com', options) #### `contents.loadFile(filePath[, options])` -* `filePath` String +* `filePath` string * `options` Object (optional) - * `query` Object (optional) - Passed to `url.format()`. - * `search` String (optional) - Passed to `url.format()`. - * `hash` String (optional) - Passed to `url.format()`. + * `query` Record<string, string> (optional) - Passed to `url.format()`. + * `search` string (optional) - Passed to `url.format()`. + * `hash` string (optional) - Passed to `url.format()`. Returns `Promise<void>` - the promise will resolve when the page has finished loading (see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects @@ -870,31 +957,31 @@ win.loadFile('src/index.html') #### `contents.downloadURL(url)` -* `url` String +* `url` string Initiates a download of the resource at `url` without navigating. The `will-download` event of `session` will be triggered. #### `contents.getURL()` -Returns `String` - The URL of the current web page. +Returns `string` - The URL of the current web page. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ width: 800, height: 600 }) -win.loadURL('http://github.com') - -let currentURL = win.webContents.getURL() -console.log(currentURL) +const win = new BrowserWindow({ width: 800, height: 600 }) +win.loadURL('http://github.com').then(() => { + const currentURL = win.webContents.getURL() + console.log(currentURL) +}) ``` #### `contents.getTitle()` -Returns `String` - The title of the current web page. +Returns `string` - The title of the current web page. #### `contents.isDestroyed()` -Returns `Boolean` - Whether the web page is destroyed. +Returns `boolean` - Whether the web page is destroyed. #### `contents.focus()` @@ -902,20 +989,20 @@ Focuses the web page. #### `contents.isFocused()` -Returns `Boolean` - Whether the web page is focused. +Returns `boolean` - Whether the web page is focused. #### `contents.isLoading()` -Returns `Boolean` - Whether web page is still loading resources. +Returns `boolean` - Whether web page is still loading resources. #### `contents.isLoadingMainFrame()` -Returns `Boolean` - Whether the main frame (and not just iframes or frames within it) is +Returns `boolean` - Whether the main frame (and not just iframes or frames within it) is still loading. #### `contents.isWaitingForResponse()` -Returns `Boolean` - Whether the web page is waiting for a first-response from the main +Returns `boolean` - Whether the web page is waiting for a first-response from the main resource of the page. #### `contents.stop()` @@ -932,17 +1019,17 @@ Reloads current page and ignores cache. #### `contents.canGoBack()` -Returns `Boolean` - Whether the browser can go back to previous web page. +Returns `boolean` - Whether the browser can go back to previous web page. #### `contents.canGoForward()` -Returns `Boolean` - Whether the browser can go forward to next web page. +Returns `boolean` - Whether the browser can go forward to next web page. #### `contents.canGoToOffset(offset)` * `offset` Integer -Returns `Boolean` - Whether the web page can go to `offset`. +Returns `boolean` - Whether the web page can go to `offset`. #### `contents.clearHistory()` @@ -970,42 +1057,66 @@ Navigates to the specified offset from the "current entry". #### `contents.isCrashed()` -Returns `Boolean` - Whether the renderer process has crashed. +Returns `boolean` - Whether the renderer process has crashed. + +#### `contents.forcefullyCrashRenderer()` + +Forcefully terminates the renderer process that is currently hosting this +`webContents`. This will cause the `render-process-gone` event to be emitted +with the `reason=killed || reason=crashed`. Please note that some webContents share renderer +processes and therefore calling this method may also crash the host process +for other webContents as well. + +Calling `reload()` immediately after calling this +method will force the reload to occur in a new process. This should be used +when this process is unstable or unusable, for instance in order to recover +from the `unresponsive` event. + +```js +contents.on('unresponsive', async () => { + const { response } = await dialog.showMessageBox({ + message: 'App X has become unresponsive', + title: 'Do you want to try forcefully reloading the app?', + buttons: ['OK', 'Cancel'], + cancelId: 1 + }) + if (response === 0) { + contents.forcefullyCrashRenderer() + contents.reload() + } +}) +``` #### `contents.setUserAgent(userAgent)` -* `userAgent` String +* `userAgent` string Overrides the user agent for this web page. -**[Deprecated](modernization/property-updates.md)** - #### `contents.getUserAgent()` -Returns `String` - The user agent for this web page. - -**[Deprecated](modernization/property-updates.md)** +Returns `string` - The user agent for this web page. #### `contents.insertCSS(css[, options])` -* `css` String +* `css` string * `options` Object (optional) - * `cssOrigin` String (optional) - Can be either 'user' or 'author'; Specifying 'user' enables you to prevent websites from overriding the CSS you insert. Default is 'author'. + * `cssOrigin` string (optional) - Can be either 'user' or 'author'. Sets the [cascade origin](https://www.w3.org/TR/css3-cascade/#cascade-origin) of the inserted stylesheet. Default is 'author'. -Returns `Promise<String>` - A promise that resolves with a key for the inserted CSS that can later be used to remove the CSS via `contents.removeInsertedCSS(key)`. +Returns `Promise<string>` - A promise that resolves with a key for the inserted CSS that can later be used to remove the CSS via `contents.removeInsertedCSS(key)`. Injects CSS into the current web page and returns a unique key for the inserted stylesheet. ```js -contents.on('did-finish-load', function () { +contents.on('did-finish-load', () => { contents.insertCSS('html, body { background-color: #f00; }') }) ``` #### `contents.removeInsertedCSS(key)` -* `key` String +* `key` string Returns `Promise<void>` - Resolves if the removal was successful. @@ -1013,7 +1124,7 @@ Removes the inserted CSS from the current web page. The stylesheet is identified by its key, which is returned from `contents.insertCSS(css)`. ```js -contents.on('did-finish-load', async function () { +contents.on('did-finish-load', async () => { const key = await contents.insertCSS('html, body { background-color: #f00; }') contents.removeInsertedCSS(key) }) @@ -1021,8 +1132,8 @@ contents.on('did-finish-load', async function () { #### `contents.executeJavaScript(code[, userGesture])` -* `code` String -* `userGesture` Boolean (optional) - Default is `false`. +* `code` string +* `userGesture` boolean (optional) - Default is `false`. Returns `Promise<any>` - A promise that resolves with the result of the executed code or is rejected if the result of the code is a rejected promise. @@ -1042,66 +1153,100 @@ contents.executeJavaScript('fetch("https://jsonplaceholder.typicode.com/users/1" }) ``` -#### `contents.setIgnoreMenuShortcuts(ignore)` _Experimental_ +#### `contents.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture])` + +* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electron's `contextIsolation` feature. You can provide any integer here. +* `scripts` [WebSource[]](structures/web-source.md) +* `userGesture` boolean (optional) - Default is `false`. + +Returns `Promise<any>` - A promise that resolves with the result of the executed code +or is rejected if the result of the code is a rejected promise. + +Works like `executeJavaScript` but evaluates `scripts` in an isolated context. + +#### `contents.setIgnoreMenuShortcuts(ignore)` -* `ignore` Boolean +* `ignore` boolean Ignore application menu shortcuts while this web contents is focused. +#### `contents.setWindowOpenHandler(handler)` + +* `handler` Function<{action: 'deny'} | {action: 'allow', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions}> + * `details` Object + * `url` string - The _resolved_ version of the URL passed to `window.open()`. e.g. opening a window with `window.open('foo')` will yield something like `https://the-origin/the/current/path/foo`. + * `frameName` string - Name of the window provided in `window.open()` + * `features` string - Comma separated list of window features provided to `window.open()`. + * `disposition` string - Can be `default`, `foreground-tab`, `background-tab`, + `new-window`, `save-to-disk` or `other`. + * `referrer` [Referrer](structures/referrer.md) - The referrer that will be + passed to the new window. May or may not result in the `Referer` header being + sent, depending on the referrer policy. + * `postBody` [PostBody](structures/post-body.md) (optional) - The post data that + will be sent to the new window, along with the appropriate headers that will + be set. If no post data is to be sent, the value will be `null`. Only defined + when the window is being created by a form that set `target=_blank`. + + Returns `{action: 'deny'} | {action: 'allow', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions}` - `deny` cancels the creation of the new + window. `allow` will allow the new window to be created. Specifying `overrideBrowserWindowOptions` allows customization of the created window. + Returning an unrecognized value such as a null, undefined, or an object + without a recognized 'action' value will result in a console error and have + the same effect as returning `{action: 'deny'}`. + +Called before creating a window a new window is requested by the renderer, e.g. +by `window.open()`, a link with `target="_blank"`, shift+clicking on a link, or +submitting a form with `<form target="_blank">`. See +[`window.open()`](window-open.md) for more details and how to use this in +conjunction with `did-create-window`. + #### `contents.setAudioMuted(muted)` -* `muted` Boolean +* `muted` boolean Mute the audio on the current web page. -**[Deprecated](modernization/property-updates.md)** - #### `contents.isAudioMuted()` -Returns `Boolean` - Whether this page has been muted. - -**[Deprecated](modernization/property-updates.md)** +Returns `boolean` - Whether this page has been muted. #### `contents.isCurrentlyAudible()` -Returns `Boolean` - Whether audio is currently playing. +Returns `boolean` - Whether audio is currently playing. #### `contents.setZoomFactor(factor)` -* `factor` Number - Zoom factor. +* `factor` Double - Zoom factor; default is 1.0. Changes the zoom factor to the specified factor. Zoom factor is zoom percent divided by 100, so 300% = 3.0. -**[Deprecated](modernization/property-updates.md)** +The factor must be greater than 0.0. #### `contents.getZoomFactor()` -Returns `Number` - the current zoom factor. - -**[Deprecated](modernization/property-updates.md)** +Returns `number` - the current zoom factor. #### `contents.setZoomLevel(level)` -* `level` Number - Zoom level. +* `level` number - Zoom level. Changes the zoom level to the specified level. The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively. The formula for this is `scale := 1.2 ^ level`. -**[Deprecated](modernization/property-updates.md)** +> **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that the +> zoom level for a specific domain propagates across all instances of windows with +> the same domain. Differentiating the window URLs will make zoom work per-window. #### `contents.getZoomLevel()` -Returns `Number` - the current zoom level. - -**[Deprecated](modernization/property-updates.md)** +Returns `number` - the current zoom level. #### `contents.setVisualZoomLevelLimits(minimumLevel, maximumLevel)` -* `minimumLevel` Number -* `maximumLevel` Number +* `minimumLevel` number +* `maximumLevel` number Returns `Promise<void>` @@ -1113,15 +1258,6 @@ Sets the maximum and minimum pinch-to-zoom level. > contents.setVisualZoomLevelLimits(1, 3) > ``` -#### `contents.setLayoutZoomLevelLimits(minimumLevel, maximumLevel)` - -* `minimumLevel` Number -* `maximumLevel` Number - -Returns `Promise<void>` - -Sets the maximum and minimum layout-based (i.e. non-visual) zoom level. - #### `contents.undo()` Executes the editing command `undo` in web page. @@ -1167,19 +1303,19 @@ Executes the editing command `unselect` in web page. #### `contents.replace(text)` -* `text` String +* `text` string Executes the editing command `replace` in web page. #### `contents.replaceMisspelling(text)` -* `text` String +* `text` string Executes the editing command `replaceMisspelling` in web page. #### `contents.insertText(text)` -* `text` String +* `text` string Returns `Promise<void>` @@ -1187,19 +1323,12 @@ Inserts `text` to the focused element. #### `contents.findInPage(text[, options])` -* `text` String - Content to be searched, must not be empty. +* `text` string - Content to be searched, must not be empty. * `options` Object (optional) - * `forward` Boolean (optional) - Whether to search forward or backward, defaults to `true`. - * `findNext` Boolean (optional) - Whether the operation is first request or a follow up, - defaults to `false`. - * `matchCase` Boolean (optional) - Whether search should be case-sensitive, + * `forward` boolean (optional) - Whether to search forward or backward, defaults to `true`. + * `findNext` boolean (optional) - Whether to begin a new text finding session with this request. Should be `true` for initial requests, and `false` for follow-up requests. Defaults to `false`. + * `matchCase` boolean (optional) - Whether search should be case-sensitive, defaults to `false`. - * `wordStart` Boolean (optional) - Whether to look only at the start of words. - defaults to `false`. - * `medialCapitalAsWordStart` Boolean (optional) - When combined with `wordStart`, - accepts a match in the middle of a word if the match begins with an - uppercase letter followed by a lowercase or non-letter. - Accepts several other intra-word matches, defaults to `false`. Returns `Integer` - The request id used for the request. @@ -1208,7 +1337,7 @@ can be obtained by subscribing to [`found-in-page`](web-contents.md#event-found- #### `contents.stopFindInPage(action)` -* `action` String - Specifies the action to take place when ending +* `action` string - Specifies the action to take place when ending [`webContents.findInPage`] request. * `clearSelection` - Clear the selection. * `keepSelection` - Translate the selection into a normal selection. @@ -1234,53 +1363,97 @@ Returns `Promise<NativeImage>` - Resolves with a [NativeImage](native-image.md) Captures a snapshot of the page within `rect`. Omitting `rect` will capture the whole visible page. -#### `contents.getPrinters()` +#### `contents.isBeingCaptured()` + +Returns `boolean` - Whether this page is being captured. It returns true when the capturer count +is large then 0. + +#### `contents.incrementCapturerCount([size, stayHidden, stayAwake])` + +* `size` [Size](structures/size.md) (optional) - The preferred size for the capturer. +* `stayHidden` boolean (optional) - Keep the page hidden instead of visible. +* `stayAwake` boolean (optional) - Keep the system awake instead of allowing it to sleep. + +Increase the capturer count by one. The page is considered visible when its browser window is +hidden and the capturer count is non-zero. If you would like the page to stay hidden, you should ensure that `stayHidden` is set to true. + +This also affects the Page Visibility API. + +#### `contents.decrementCapturerCount([stayHidden, stayAwake])` + +* `stayHidden` boolean (optional) - Keep the page in hidden state instead of visible. +* `stayAwake` boolean (optional) - Keep the system awake instead of allowing it to sleep. + +Decrease the capturer count by one. The page will be set to hidden or occluded state when its +browser window is hidden or occluded and the capturer count reaches zero. If you want to +decrease the hidden capturer count instead you should set `stayHidden` to true. + +#### `contents.getPrinters()` _Deprecated_ Get the system printer list. -Returns [`PrinterInfo[]`](structures/printer-info.md). +Returns [`PrinterInfo[]`](structures/printer-info.md) + +**Deprecated:** Should use the new [`contents.getPrintersAsync`](web-contents.md#contentsgetprintersasync) API. + +#### `contents.getPrintersAsync()` + +Get the system printer list. + +Returns `Promise<PrinterInfo[]>` - Resolves with a [`PrinterInfo[]`](structures/printer-info.md) #### `contents.print([options], [callback])` * `options` Object (optional) - * `silent` Boolean (optional) - Don't ask user for print settings. Default is `false`. - * `printBackground` Boolean (optional) - Prints the background color and image of + * `silent` boolean (optional) - Don't ask user for print settings. Default is `false`. + * `printBackground` boolean (optional) - Prints the background color and image of the web page. Default is `false`. - * `deviceName` String (optional) - Set the printer device name to use. Default is `''`. - * `color` Boolean (optional) - Set whether the printed web page will be in color or grayscale. Default is `true`. + * `deviceName` string (optional) - Set the printer device name to use. Must be the system-defined name and not the 'friendly' name, e.g 'Brother_QL_820NWB' and not 'Brother QL-820NWB'. + * `color` boolean (optional) - Set whether the printed web page will be in color or grayscale. Default is `true`. * `margins` Object (optional) - * `marginType` String (optional) - Can be `default`, `none`, `printableArea`, or `custom`. If `custom` is chosen, you will also need to specify `top`, `bottom`, `left`, and `right`. - * `top` Number (optional) - The top margin of the printed web page, in pixels. - * `bottom` Number (optional) - The bottom margin of the printed web page, in pixels. - * `left` Number (optional) - The left margin of the printed web page, in pixels. - * `right` Number (optional) - The right margin of the printed web page, in pixels. - * `landscape` Boolean (optional) - Whether the web page should be printed in landscape mode. Default is `false`. - * `scaleFactor` Number (optional) - The scale factor of the web page. - * `pagesPerSheet` Number (optional) - The number of pages to print per page sheet. - * `collate` Boolean (optional) - Whether the web page should be collated. - * `copies` Number (optional) - The number of copies of the web page to print. - * `pageRanges` Record<string, number> (optional) - The page range to print. Should have two keys: `from` and `to`. - * `duplexMode` String (optional) - Set the duplex mode of the printed web page. Can be `simplex`, `shortEdge`, or `longEdge`. - * `dpi` Object (optional) - * `horizontal` Number (optional) - The horizontal dpi. - * `vertical` Number (optional) - The vertical dpi. + * `marginType` string (optional) - Can be `default`, `none`, `printableArea`, or `custom`. If `custom` is chosen, you will also need to specify `top`, `bottom`, `left`, and `right`. + * `top` number (optional) - The top margin of the printed web page, in pixels. + * `bottom` number (optional) - The bottom margin of the printed web page, in pixels. + * `left` number (optional) - The left margin of the printed web page, in pixels. + * `right` number (optional) - The right margin of the printed web page, in pixels. + * `landscape` boolean (optional) - Whether the web page should be printed in landscape mode. Default is `false`. + * `scaleFactor` number (optional) - The scale factor of the web page. + * `pagesPerSheet` number (optional) - The number of pages to print per page sheet. + * `collate` boolean (optional) - Whether the web page should be collated. + * `copies` number (optional) - The number of copies of the web page to print. + * `pageRanges` Object[] (optional) - The page range to print. On macOS, only one range is honored. + * `from` number - Index of the first page to print (0-based). + * `to` number - Index of the last page to print (inclusive) (0-based). + * `duplexMode` string (optional) - Set the duplex mode of the printed web page. Can be `simplex`, `shortEdge`, or `longEdge`. + * `dpi` Record<string, number> (optional) + * `horizontal` number (optional) - The horizontal dpi. + * `vertical` number (optional) - The vertical dpi. + * `header` string (optional) - string to be printed as page header. + * `footer` string (optional) - string to be printed as page footer. + * `pageSize` string | Size (optional) - Specify page size of the printed document. Can be `A3`, + `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height`. * `callback` Function (optional) - * `success` Boolean - Indicates success of the print call. - * `failureReason` String - Called back if the print fails; can be `cancelled` or `failed`. + * `success` boolean - Indicates success of the print call. + * `failureReason` string - Error description called back if the print fails. -Prints window's web page. When `silent` is set to `true`, Electron will pick -the system's default printer if `deviceName` is empty and the default settings -for printing. +When a custom `pageSize` is passed, Chromium attempts to validate platform specific minimum values for `width_microns` and `height_microns`. Width and height must both be minimum 353 microns but may be higher on some operating systems. -Calling `window.print()` in web page is equivalent to calling -`webContents.print({ silent: false, printBackground: false, deviceName: '' })`. +Prints window's web page. When `silent` is set to `true`, Electron will pick +the system's default printer if `deviceName` is empty and the default settings for printing. Use `page-break-before: always;` CSS style to force to print to a new page. Example usage: ```js -const options = { silent: true, deviceName: 'My-Printer' } +const options = { + silent: true, + deviceName: 'My-Printer', + pageRanges: [{ + from: 0, + to: 1 + }] +} win.webContents.print(options, (success, errorType) => { if (!success) console.log(errorType) }) @@ -1289,14 +1462,20 @@ win.webContents.print(options, (success, errorType) => { #### `contents.printToPDF(options)` * `options` Object + * `headerFooter` Record<string, string> (optional) - the header and footer for the PDF. + * `title` string - The title for the PDF header. + * `url` string - the url for the PDF footer. + * `landscape` boolean (optional) - `true` for landscape, `false` for portrait. * `marginsType` Integer (optional) - Specifies the type of margins to use. Uses 0 for default margin, 1 for no margin, and 2 for minimum margin. - * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`, - `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height` - and `width` in microns. - * `printBackground` Boolean (optional) - Whether to print CSS backgrounds. - * `printSelectionOnly` Boolean (optional) - Whether to print selection only. - * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait. + * `scaleFactor` number (optional) - The scale factor of the web page. Can range from 0 to 100. + * `pageRanges` Record<string, number> (optional) - The page range to print. + * `from` number - Index of the first page to print (0-based). + * `to` number - Index of the last page to print (inclusive) (0-based). + * `pageSize` string | Size (optional) - Specify page size of the generated PDF. Can be `A3`, + `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height` and `width` in microns. + * `printBackground` boolean (optional) - Whether to print CSS backgrounds. + * `printSelectionOnly` boolean (optional) - Whether to print selection only. Returns `Promise<Buffer>` - Resolves with the generated PDF data. @@ -1312,43 +1491,49 @@ By default, an empty `options` will be regarded as: marginsType: 0, printBackground: false, printSelectionOnly: false, - landscape: false + landscape: false, + pageSize: 'A4', + scaleFactor: 100 } ``` -Use `page-break-before: always; ` CSS style to force to print to a new page. +Use `page-break-before: always;` CSS style to force to print to a new page. An example of `webContents.printToPDF`: ```javascript const { BrowserWindow } = require('electron') const fs = require('fs') +const path = require('path') +const os = require('os') -let win = new BrowserWindow({ width: 800, height: 600 }) +const win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL('http://github.com') win.webContents.on('did-finish-load', () => { // Use default printing options - win.webContents.printToPDF({}, (error, data) => { - if (error) throw error - fs.writeFile('/tmp/print.pdf', data, (error) => { + const pdfPath = path.join(os.homedir(), 'Desktop', 'temp.pdf') + win.webContents.printToPDF({}).then(data => { + fs.writeFile(pdfPath, data, (error) => { if (error) throw error - console.log('Write PDF successfully.') + console.log(`Wrote PDF successfully to ${pdfPath}`) }) + }).catch(error => { + console.log(`Failed to write PDF to ${pdfPath}: `, error) }) }) ``` #### `contents.addWorkSpace(path)` -* `path` String +* `path` string Adds the specified path to DevTools workspace. Must be used after DevTools creation: ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() win.webContents.on('devtools-opened', () => { win.webContents.addWorkSpace(__dirname) }) @@ -1356,7 +1541,7 @@ win.webContents.on('devtools-opened', () => { #### `contents.removeWorkSpace(path)` -* `path` String +* `path` string Removes the specified path from DevTools workspace. @@ -1391,20 +1576,37 @@ An example of showing devtools in a `<webview>` tag: </head> <body> <webview id="browser" src="https://github.com"></webview> - <webview id="devtools"></webview> + <webview id="devtools" src="about:blank"></webview> <script> + const { ipcRenderer } = require('electron') + const emittedOnce = (element, eventName) => new Promise(resolve => { + element.addEventListener(eventName, event => resolve(event), { once: true }) + }) const browserView = document.getElementById('browser') const devtoolsView = document.getElementById('devtools') - browserView.addEventListener('dom-ready', () => { - const browser = browserView.getWebContents() - browser.setDevToolsWebContents(devtoolsView.getWebContents()) - browser.openDevTools() + const browserReady = emittedOnce(browserView, 'dom-ready') + const devtoolsReady = emittedOnce(devtoolsView, 'dom-ready') + Promise.all([browserReady, devtoolsReady]).then(() => { + const targetId = browserView.getWebContentsId() + const devtoolsId = devtoolsView.getWebContentsId() + ipcRenderer.send('open-devtools', targetId, devtoolsId) }) </script> </body> </html> ``` +```js +// Main process +const { ipcMain, webContents } = require('electron') +ipcMain.on('open-devtools', (event, targetContentsId, devtoolsContentsId) => { + const target = webContents.fromId(targetContentsId) + const devtools = webContents.fromId(devtoolsContentsId) + target.setDevToolsWebContents(devtools) + target.openDevTools() +}) +``` + An example of showing devtools in a `BrowserWindow`: ```js @@ -1413,7 +1615,7 @@ const { app, BrowserWindow } = require('electron') let win = null let devtools = null -app.once('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow() devtools = new BrowserWindow() win.loadURL('https://github.com') @@ -1425,10 +1627,10 @@ app.once('ready', () => { #### `contents.openDevTools([options])` * `options` Object (optional) - * `mode` String - Opens the devtools with specified dock state, can be - `right`, `bottom`, `undocked`, `detach`. Defaults to last used dock state. + * `mode` string - Opens the devtools with specified dock state, can be + `left`, `right`, `bottom`, `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's possible to dock back. In `detach` mode it's not. - * `activate` Boolean (optional) - Whether to bring the opened devtools window + * `activate` boolean (optional) - Whether to bring the opened devtools window to the foreground. The default is `true`. Opens the devtools. @@ -1442,11 +1644,11 @@ Closes the devtools. #### `contents.isDevToolsOpened()` -Returns `Boolean` - Whether the devtools is opened. +Returns `boolean` - Whether the devtools is opened. #### `contents.isDevToolsFocused()` -Returns `Boolean` - Whether the devtools view is focused . +Returns `boolean` - Whether the devtools view is focused . #### `contents.toggleDevTools()` @@ -1463,18 +1665,33 @@ Starts inspecting element at position (`x`, `y`). Opens the developer tools for the shared worker context. +#### `contents.inspectSharedWorkerById(workerId)` + +* `workerId` string + +Inspects the shared worker based on its ID. + +#### `contents.getAllSharedWorkers()` + +Returns [`SharedWorkerInfo[]`](structures/shared-worker-info.md) - Information about all Shared Workers. + #### `contents.inspectServiceWorker()` Opens the developer tools for the service worker context. #### `contents.send(channel, ...args)` -* `channel` String +* `channel` string * `...args` any[] -Send an asynchronous message to renderer process via `channel`, you can also -send arbitrary arguments. Arguments will be serialized in JSON internally and -hence no functions or prototype chain will be included. +Send an asynchronous message to the renderer process via `channel`, along with +arguments. Arguments will be serialized with the [Structured Clone +Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be +included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will +throw an exception. + +> **NOTE**: Sending non-standard JavaScript types such as DOM objects or +> special Electron objects will throw an exception. The renderer process can handle the message by listening to `channel` with the [`ipcRenderer`](ipc-renderer.md) module. @@ -1486,7 +1703,7 @@ An example of sending messages from the main process to the renderer process: const { app, BrowserWindow } = require('electron') let win = null -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL(`file://${__dirname}/index.html`) win.webContents.on('did-finish-load', () => { @@ -1510,13 +1727,20 @@ app.on('ready', () => { #### `contents.sendToFrame(frameId, channel, ...args)` -* `frameId` Integer -* `channel` String +* `frameId` Integer | [number, number] - the ID of the frame to send to, or a + pair of `[processId, frameId]` if the frame is in a different process to the + main frame. +* `channel` string * `...args` any[] Send an asynchronous message to a specific frame in a renderer process via -`channel`. Arguments will be serialized -as JSON internally and as such no functions or prototype chains will be included. +`channel`, along with arguments. Arguments will be serialized with the +[Structured Clone Algorithm][SCA], just like [`postMessage`][], so prototype +chains will not be included. Sending Functions, Promises, Symbols, WeakMaps, or +WeakSets will throw an exception. + +> **NOTE:** Sending non-standard JavaScript types such as DOM objects or +> special Electron objects will throw an exception. The renderer process can handle the message by listening to `channel` with the [`ipcRenderer`](ipc-renderer.md) module. @@ -1538,10 +1762,37 @@ ipcMain.on('ping', (event) => { }) ``` +#### `contents.postMessage(channel, message, [transfer])` + +* `channel` string +* `message` any +* `transfer` MessagePortMain[] (optional) + +Send a message to the renderer process, optionally transferring ownership of +zero or more [`MessagePortMain`][] objects. + +The transferred `MessagePortMain` objects will be available in the renderer +process by accessing the `ports` property of the emitted event. When they +arrive in the renderer, they will be native DOM `MessagePort` objects. + +For example: + +```js +// Main process +const { port1, port2 } = new MessageChannelMain() +webContents.postMessage('port', { message: 'hello' }, [port1]) + +// Renderer process +ipcRenderer.on('port', (e, msg) => { + const [port] = e.ports + // ... +}) +``` + #### `contents.enableDeviceEmulation(parameters)` * `parameters` Object - * `screenPosition` String - Specify the screen type to emulate + * `screenPosition` string - Specify the screen type to emulate (default: `desktop`): * `desktop` - Desktop screen type. * `mobile` - Mobile screen type. @@ -1563,13 +1814,6 @@ Disable device emulation enabled by `webContents.enableDeviceEmulation`. #### `contents.sendInputEvent(inputEvent)` * `inputEvent` [MouseInputEvent](structures/mouse-input-event.md) | [MouseWheelInputEvent](structures/mouse-wheel-input-event.md) | [KeyboardInputEvent](structures/keyboard-input-event.md) - * `type` String (**required**) - The type of the event, can be `mouseDown`, - `mouseUp`, `mouseEnter`, `mouseLeave`, `contextMenu`, `mouseWheel`, - `mouseMove`, `keyDown`, `keyUp` or `char`. - * `modifiers` String[] - An array of modifiers of the event, can - include `shift`, `control`, `alt`, `meta`, `isKeypad`, `isAutoRepeat`, - `leftButtonDown`, `middleButtonDown`, `rightButtonDown`, `capsLock`, - `numLock`, `left`, `right`. Sends an input `event` to the page. **Note:** The [`BrowserWindow`](browser-window.md) containing the contents needs to be focused for @@ -1577,7 +1821,7 @@ Sends an input `event` to the page. #### `contents.beginFrameSubscription([onlyDirty ,]callback)` -* `onlyDirty` Boolean (optional) - Defaults to `false`. +* `onlyDirty` boolean (optional) - Defaults to `false`. * `callback` Function * `image` [NativeImage](native-image.md) * `dirtyRect` [Rectangle](structures/rectangle.md) @@ -1601,9 +1845,10 @@ End subscribing for frame presentation events. #### `contents.startDrag(item)` * `item` Object - * `file` String[] | String - The path(s) to the file(s) being dragged. - * `icon` [NativeImage](native-image.md) - The image must be non-empty on - macOS. + * `file` string - The path to the file being dragged. + * `files` string[] (optional) - The paths to the files being dragged. (`files` will override `file` field) + * `icon` [NativeImage](native-image.md) | string - The image must be + non-empty on macOS. Sets the `item` as dragging item for current drag-drop operation, `file` is the absolute path of the file to be dragged, and `icon` is the image showing under @@ -1611,8 +1856,8 @@ the cursor when dragging. #### `contents.savePage(fullPath, saveType)` -* `fullPath` String - The full file path. -* `saveType` String - Specify the save type. +* `fullPath` string - The absolute file path. +* `saveType` string - Specify the save type. * `HTMLOnly` - Save only the HTML of the page. * `HTMLComplete` - Save complete-html page. * `MHTML` - Save complete-html page as MHTML. @@ -1621,7 +1866,7 @@ Returns `Promise<void>` - resolves if the page is saved. ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() win.loadURL('https://github.com') @@ -1640,7 +1885,7 @@ Shows pop-up dictionary that searches the selected word on the page. #### `contents.isOffscreen()` -Returns `Boolean` - Indicates whether *offscreen rendering* is enabled. +Returns `boolean` - Indicates whether *offscreen rendering* is enabled. #### `contents.startPainting()` @@ -1652,23 +1897,19 @@ If *offscreen rendering* is enabled and painting, stop painting. #### `contents.isPainting()` -Returns `Boolean` - If *offscreen rendering* is enabled returns whether it is currently painting. +Returns `boolean` - If *offscreen rendering* is enabled returns whether it is currently painting. #### `contents.setFrameRate(fps)` * `fps` Integer If *offscreen rendering* is enabled sets the frame rate to the specified number. -Only values between 1 and 60 are accepted. - -**[Deprecated](modernization/property-updates.md)** +Only values between 1 and 240 are accepted. #### `contents.getFrameRate()` Returns `Integer` - If *offscreen rendering* is enabled returns the current frame rate. -**[Deprecated](modernization/property-updates.md)** - #### `contents.invalidate()` Schedules a full repaint of the window this web contents is in. @@ -1678,11 +1919,11 @@ one through the `'paint'` event. #### `contents.getWebRTCIPHandlingPolicy()` -Returns `String` - Returns the WebRTC IP Handling Policy. +Returns `string` - Returns the WebRTC IP Handling Policy. #### `contents.setWebRTCIPHandlingPolicy(policy)` -* `policy` String - Specify the WebRTC IP Handling Policy. +* `policy` string - Specify the WebRTC IP Handling Policy. * `default` - Exposes user's public and local IPs. This is the default behavior. When this policy is used, WebRTC has the right to enumerate all interfaces and bind them to discover public interfaces. @@ -1701,6 +1942,14 @@ Setting the WebRTC IP handling policy allows you to control which IPs are exposed via WebRTC. See [BrowserLeaks](https://browserleaks.com/webrtc) for more details. +#### `contents.getMediaSourceId(requestWebContents)` + +* `requestWebContents` WebContents - Web contents that the id will be registered to. + +Returns `string` - The identifier of a WebContents stream. This identifier can be used +with `navigator.mediaDevices.getUserMedia` using a `chromeMediaSource` of `tab`. +The identifier is restricted to the web contents that it is registered to and is only valid for 10 seconds. + #### `contents.getOSProcessId()` Returns `Integer` - The operating system `pid` of the associated renderer @@ -1714,55 +1963,74 @@ be compared to the `frameProcessId` passed by frame specific navigation events #### `contents.takeHeapSnapshot(filePath)` -* `filePath` String - Path to the output file. +* `filePath` string - Path to the output file. Returns `Promise<void>` - Indicates whether the snapshot has been created successfully. Takes a V8 heap snapshot and saves it to `filePath`. +#### `contents.getBackgroundThrottling()` + +Returns `boolean` - whether or not this WebContents will throttle animations and timers +when the page becomes backgrounded. This also affects the Page Visibility API. + #### `contents.setBackgroundThrottling(allowed)` -* `allowed` Boolean +* `allowed` boolean Controls whether or not this WebContents will throttle animations and timers when the page becomes backgrounded. This also affects the Page Visibility API. #### `contents.getType()` -Returns `String` - the type of the webContent. Can be `backgroundPage`, `window`, `browserView`, `remote`, `webview` or `offscreen`. +Returns `string` - the type of the webContent. Can be `backgroundPage`, `window`, `browserView`, `remote`, `webview` or `offscreen`. + +#### `contents.setImageAnimationPolicy(policy)` + +* `policy` string - Can be `animate`, `animateOnce` or `noAnimation`. + +Sets the image animation policy for this webContents. The policy only affects +_new_ images, existing images that are currently being animated are unaffected. +This is a known limitation in Chromium, you can force image animation to be +recalculated with `img.src = img.src` which will result in no network traffic +but will update the animation policy. + +This corresponds to the [animationPolicy][] accessibility feature in Chromium. + +[animationPolicy]: https://developer.chrome.com/docs/extensions/reference/accessibilityFeatures/#property-animationPolicy ### Instance Properties #### `contents.audioMuted` -A `Boolean` property that determines whether this page is muted. +A `boolean` property that determines whether this page is muted. #### `contents.userAgent` -A `String` property that determines the user agent for this web page. +A `string` property that determines the user agent for this web page. #### `contents.zoomLevel` -A `Number` property that determines the zoom level for this web contents. +A `number` property that determines the zoom level for this web contents. The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively. The formula for this is `scale := 1.2 ^ level`. #### `contents.zoomFactor` -A `Number` property that determines the zoom factor for this web contents. +A `number` property that determines the zoom factor for this web contents. The zoom factor is the zoom percent divided by 100, so 300% = 3.0. #### `contents.frameRate` An `Integer` property that sets the frame rate of the web contents to the specified number. -Only values between 1 and 60 are accepted. +Only values between 1 and 240 are accepted. Only applicable if *offscreen rendering* is enabled. #### `contents.id` _Readonly_ -A `Integer` representing the unique ID of this WebContents. +A `Integer` representing the unique ID of this WebContents. Each ID is unique among all `WebContents` instances of the entire Electron application. #### `contents.session` _Readonly_ @@ -1774,7 +2042,7 @@ A [`WebContents`](web-contents.md) instance that might own this `WebContents`. #### `contents.devToolsWebContents` _Readonly_ -A `WebContents` of DevTools for this `WebContents`. +A `WebContents | null` property that represents the of DevTools `WebContents` associated with a given `WebContents`. **Note:** Users should never store this object because it may become `null` when the DevTools has been closed. @@ -1783,5 +2051,16 @@ when the DevTools has been closed. A [`Debugger`](debugger.md) instance for this webContents. +#### `contents.backgroundThrottling` + +A `boolean` property that determines whether or not this WebContents will throttle animations and timers +when the page becomes backgrounded. This also affects the Page Visibility API. + +#### `contents.mainFrame` _Readonly_ + +A [`WebFrameMain`](web-frame-main.md) property that represents the top frame of the page's frame hierarchy. + [keyboardevent]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter +[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage diff --git a/docs/api/web-frame-main.md b/docs/api/web-frame-main.md new file mode 100644 index 0000000000000..8ae0f525248e4 --- /dev/null +++ b/docs/api/web-frame-main.md @@ -0,0 +1,197 @@ +# webFrameMain + +> Control web pages and iframes. + +Process: [Main](../glossary.md#main-process) + +The `webFrameMain` module can be used to lookup frames across existing +[`WebContents`](web-contents.md) instances. Navigation events are the common +use case. + +```javascript +const { BrowserWindow, webFrameMain } = require('electron') + +const win = new BrowserWindow({ width: 800, height: 1500 }) +win.loadURL('https://twitter.com') + +win.webContents.on( + 'did-frame-navigate', + (event, url, httpResponseCode, httpStatusText, isMainFrame, frameProcessId, frameRoutingId) => { + const frame = webFrameMain.fromId(frameProcessId, frameRoutingId) + if (frame) { + const code = 'document.body.innerHTML = document.body.innerHTML.replaceAll("heck", "h*ck")' + frame.executeJavaScript(code) + } + } +) +``` + +You can also access frames of existing pages by using the `mainFrame` property +of [`WebContents`](web-contents.md). + +```javascript +const { BrowserWindow } = require('electron') + +async function main () { + const win = new BrowserWindow({ width: 800, height: 600 }) + await win.loadURL('https://reddit.com') + + const youtubeEmbeds = win.webContents.mainFrame.frames.filter((frame) => { + try { + const url = new URL(frame.url) + return url.host === 'www.youtube.com' + } catch { + return false + } + }) + + console.log(youtubeEmbeds) +} + +main() +``` + +## Methods + +These methods can be accessed from the `webFrameMain` module: + +### `webFrameMain.fromId(processId, routingId)` + +* `processId` Integer - An `Integer` representing the internal ID of the process which owns the frame. +* `routingId` Integer - An `Integer` representing the unique frame ID in the + current renderer process. Routing IDs can be retrieved from `WebFrameMain` + instances (`frame.routingId`) and are also passed by frame + specific `WebContents` navigation events (e.g. `did-frame-navigate`). + +Returns `WebFrameMain | undefined` - A frame with the given process and routing IDs, +or `undefined` if there is no WebFrameMain associated with the given IDs. + +## Class: WebFrameMain + +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ + +### Instance Events + +#### Event: 'dom-ready' + +Emitted when the document is loaded. + +### Instance Methods + +#### `frame.executeJavaScript(code[, userGesture])` + +* `code` string +* `userGesture` boolean (optional) - Default is `false`. + +Returns `Promise<unknown>` - A promise that resolves with the result of the executed +code or is rejected if execution throws or results in a rejected promise. + +Evaluates `code` in page. + +In the browser window some HTML APIs like `requestFullScreen` can only be +invoked by a gesture from the user. Setting `userGesture` to `true` will remove +this limitation. + +#### `frame.reload()` + +Returns `boolean` - Whether the reload was initiated successfully. Only results in `false` when the frame has no history. + +#### `frame.send(channel, ...args)` + +* `channel` string +* `...args` any[] + +Send an asynchronous message to the renderer process via `channel`, along with +arguments. Arguments will be serialized with the [Structured Clone +Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be +included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will +throw an exception. + +The renderer process can handle the message by listening to `channel` with the +[`ipcRenderer`](ipc-renderer.md) module. + +#### `frame.postMessage(channel, message, [transfer])` + +* `channel` string +* `message` any +* `transfer` MessagePortMain[] (optional) + +Send a message to the renderer process, optionally transferring ownership of +zero or more [`MessagePortMain`][] objects. + +The transferred `MessagePortMain` objects will be available in the renderer +process by accessing the `ports` property of the emitted event. When they +arrive in the renderer, they will be native DOM `MessagePort` objects. + +For example: + +```js +// Main process +const { port1, port2 } = new MessageChannelMain() +webContents.mainFrame.postMessage('port', { message: 'hello' }, [port1]) + +// Renderer process +ipcRenderer.on('port', (e, msg) => { + const [port] = e.ports + // ... +}) +``` + +### Instance Properties + +#### `frame.url` _Readonly_ + +A `string` representing the current URL of the frame. + +#### `frame.top` _Readonly_ + +A `WebFrameMain | null` representing top frame in the frame hierarchy to which `frame` +belongs. + +#### `frame.parent` _Readonly_ + +A `WebFrameMain | null` representing parent frame of `frame`, the property would be +`null` if `frame` is the top frame in the frame hierarchy. + +#### `frame.frames` _Readonly_ + +A `WebFrameMain[]` collection containing the direct descendents of `frame`. + +#### `frame.framesInSubtree` _Readonly_ + +A `WebFrameMain[]` collection containing every frame in the subtree of `frame`, +including itself. This can be useful when traversing through all frames. + +#### `frame.frameTreeNodeId` _Readonly_ + +An `Integer` representing the id of the frame's internal FrameTreeNode +instance. This id is browser-global and uniquely identifies a frame that hosts +content. The identifier is fixed at the creation of the frame and stays +constant for the lifetime of the frame. When the frame is removed, the id is +not used again. + +#### `frame.name` _Readonly_ + +A `string` representing the frame name. + +#### `frame.osProcessId` _Readonly_ + +An `Integer` representing the operating system `pid` of the process which owns this frame. + +#### `frame.processId` _Readonly_ + +An `Integer` representing the Chromium internal `pid` of the process which owns this frame. +This is not the same as the OS process ID; to read that use `frame.osProcessId`. + +#### `frame.routingId` _Readonly_ + +An `Integer` representing the unique frame id in the current renderer process. +Distinct `WebFrameMain` instances that refer to the same underlying frame will +have the same `routingId`. + +#### `frame.visibilityState` _Readonly_ + +A `string` representing the [visibility state](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState) of the frame. + +See also how the [Page Visibility API](browser-window.md#page-visibility) is affected by other Electron APIs. diff --git a/docs/api/web-frame.md b/docs/api/web-frame.md index 712aca43965f5..b59d98c54cb60 100644 --- a/docs/api/web-frame.md +++ b/docs/api/web-frame.md @@ -5,8 +5,8 @@ Process: [Renderer](../glossary.md#renderer-process) `webFrame` export of the Electron module is an instance of the `WebFrame` -class representing the top frame of the current `BrowserWindow`. Sub-frames can -be retrieved by certain properties and methods (e.g. `webFrame.firstChild`). +class representing the current frame. Sub-frames can be retrieved by +certain properties and methods (e.g. `webFrame.firstChild`). An example of zooming current page to 200%. @@ -22,31 +22,37 @@ The `WebFrame` class has the following instance methods: ### `webFrame.setZoomFactor(factor)` -* `factor` Number - Zoom factor. +* `factor` Double - Zoom factor; default is 1.0. Changes the zoom factor to the specified factor. Zoom factor is zoom percent divided by 100, so 300% = 3.0. +The factor must be greater than 0.0. + ### `webFrame.getZoomFactor()` -Returns `Number` - The current zoom factor. +Returns `number` - The current zoom factor. ### `webFrame.setZoomLevel(level)` -* `level` Number - Zoom level. +* `level` number - Zoom level. Changes the zoom level to the specified level. The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively. +> **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that the +> zoom level for a specific domain propagates across all instances of windows with +> the same domain. Differentiating the window URLs will make zoom work per-window. + ### `webFrame.getZoomLevel()` -Returns `Number` - The current zoom level. +Returns `number` - The current zoom level. ### `webFrame.setVisualZoomLevelLimits(minimumLevel, maximumLevel)` -* `minimumLevel` Number -* `maximumLevel` Number +* `minimumLevel` number +* `maximumLevel` number Sets the maximum and minimum pinch-to-zoom level. @@ -56,24 +62,33 @@ Sets the maximum and minimum pinch-to-zoom level. > webFrame.setVisualZoomLevelLimits(1, 3) > ``` -### `webFrame.setLayoutZoomLevelLimits(minimumLevel, maximumLevel)` - -* `minimumLevel` Number -* `maximumLevel` Number - -Sets the maximum and minimum layout-based (i.e. non-visual) zoom level. +> **NOTE**: Visual zoom only applies to pinch-to-zoom behavior. Cmd+/-/0 zoom shortcuts are +> controlled by the 'zoomIn', 'zoomOut', and 'resetZoom' MenuItem roles in the application +> Menu. To disable shortcuts, manually [define the Menu](./menu.md#examples) and omit zoom roles +> from the definition. ### `webFrame.setSpellCheckProvider(language, provider)` -* `language` String +* `language` string * `provider` Object * `spellCheck` Function - * `words` String[] + * `words` string[] * `callback` Function - * `misspeltWords` String[] + * `misspeltWords` string[] Sets a provider for spell checking in input fields and text areas. +If you want to use this method you must disable the builtin spellchecker when you +construct the window. + +```js +const mainWindow = new BrowserWindow({ + webPreferences: { + spellcheck: false + } +}) +``` + The `provider` must be an object that has a `spellCheck` method that accepts an array of individual words for spellchecking. The `spellCheck` function runs asynchronously and calls the `callback` function @@ -95,11 +110,13 @@ webFrame.setSpellCheckProvider('en-US', { }) ``` -### `webFrame.insertCSS(css)` +#### `webFrame.insertCSS(css[, options])` -* `css` String - CSS source code. +* `css` string +* `options` Object (optional) + * `cssOrigin` string (optional) - Can be either 'user' or 'author'. Sets the [cascade origin](https://www.w3.org/TR/css3-cascade/#cascade-origin) of the inserted stylesheet. Default is 'author'. -Returns `String` - A key for the inserted CSS that can later be used to remove +Returns `string` - A key for the inserted CSS that can later be used to remove the CSS via `webFrame.removeInsertedCSS(key)`. Injects CSS into the current web page and returns a unique key for the inserted @@ -107,24 +124,31 @@ stylesheet. ### `webFrame.removeInsertedCSS(key)` -* `key` String +* `key` string Removes the inserted CSS from the current web page. The stylesheet is identified by its key, which is returned from `webFrame.insertCSS(css)`. ### `webFrame.insertText(text)` -* `text` String +* `text` string Inserts `text` to the focused element. -### `webFrame.executeJavaScript(code[, userGesture])` +### `webFrame.executeJavaScript(code[, userGesture, callback])` -* `code` String -* `userGesture` Boolean (optional) - Default is `false`. +* `code` string +* `userGesture` boolean (optional) - Default is `false`. +* `callback` Function (optional) - Called after script has been executed. Unless + the frame is suspended (e.g. showing a modal alert), execution will be + synchronous and the callback will be invoked before the method returns. For + compatibility with an older version of this method, the error parameter is + second. + * `result` Any + * `error` Error -Returns `Promise<any>` - A promise that resolves with the result of the executed code -or is rejected if the result of the code is a rejected promise. +Returns `Promise<any>` - A promise that resolves with the result of the executed +code or is rejected if execution throws or results in a rejected promise. Evaluates `code` in page. @@ -132,23 +156,38 @@ In the browser window some HTML APIs like `requestFullScreen` can only be invoked by a gesture from the user. Setting `userGesture` to `true` will remove this limitation. -### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture])` +### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])` -* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here. +* `worldId` Integer - The ID of the world to run the javascript + in, `0` is the default main world (where content runs), `999` is the + world used by Electron's `contextIsolation` feature. Accepts values + in the range 1..536870911. * `scripts` [WebSource[]](structures/web-source.md) -* `userGesture` Boolean (optional) - Default is `false`. - -Returns `Promise<any>` - A promise that resolves with the result of the executed code -or is rejected if the result of the code is a rejected promise. +* `userGesture` boolean (optional) - Default is `false`. +* `callback` Function (optional) - Called after script has been executed. Unless + the frame is suspended (e.g. showing a modal alert), execution will be + synchronous and the callback will be invoked before the method returns. For + compatibility with an older version of this method, the error parameter is + second. + * `result` Any + * `error` Error + +Returns `Promise<any>` - A promise that resolves with the result of the executed +code or is rejected if execution could not start. Works like `executeJavaScript` but evaluates `scripts` in an isolated context. +Note that when the execution of script fails, the returned promise will not +reject and the `result` would be `undefined`. This is because Chromium does not +dispatch errors of isolated worlds to foreign worlds. + ### `webFrame.setIsolatedWorldInfo(worldId, info)` + * `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. Chrome extensions reserve the range of IDs in `[1 << 20, 1 << 29)`. You can provide any integer here. * `info` Object - * `securityOrigin` String (optional) - Security origin for the isolated world. - * `csp` String (optional) - Content Security Policy for the isolated world. - * `name` String (optional) - Name for isolated world. Useful in devtools. + * `securityOrigin` string (optional) - Security origin for the isolated world. + * `csp` string (optional) - Content Security Policy for the isolated world. + * `name` string (optional) - Name for isolated world. Useful in devtools. Set the security origin, content security policy and name of the isolated world. Note: If the `csp` is specified, then the `securityOrigin` also has to be specified. @@ -203,7 +242,7 @@ and intend to stay there). ### `webFrame.getFrameForSelector(selector)` -* `selector` String - CSS selector for a frame element. +* `selector` string - CSS selector for a frame element. Returns `WebFrame` - The frame element in `webFrame's` document selected by `selector`, `null` would be returned if `selector` does not select a frame or @@ -211,7 +250,7 @@ if the frame is not in the current renderer process. ### `webFrame.findFrameByName(name)` -* `name` String +* `name` string Returns `WebFrame` - A child of `webFrame` with the supplied `name`, `null` would be returned if there's no such frame or if the frame is not in the current @@ -226,6 +265,20 @@ renderer process. Returns `WebFrame` - that has the supplied `routingId`, `null` if not found. +### `webFrame.isWordMisspelled(word)` + +* `word` string - The word to be spellchecked. + +Returns `boolean` - True if the word is misspelled according to the built in +spellchecker, false otherwise. If no dictionary is loaded, always return false. + +### `webFrame.getWordSuggestions(word)` + +* `word` string - The misspelled word. + +Returns `string[]` - A list of suggested words for a given word. If the word +is spelled correctly, the result will be empty. + ## Properties ### `webFrame.top` _Readonly_ diff --git a/docs/api/web-request.md b/docs/api/web-request.md index 502da81301d5b..9f3e4347ccf5d 100644 --- a/docs/api/web-request.md +++ b/docs/api/web-request.md @@ -2,7 +2,8 @@ > Intercept and modify the contents of a request at various stages of its lifetime. -Process: [Main](../glossary.md#main-process) +Process: [Main](../glossary.md#main-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ Instances of the `WebRequest` class are accessed by using the `webRequest` property of a `Session`. @@ -42,23 +43,23 @@ The following methods are available on instances of `WebRequest`: #### `webRequest.onBeforeRequest([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double * `uploadData` [UploadData[]](structures/upload-data.md) * `callback` Function * `response` Object - * `cancel` Boolean (optional) - * `redirectURL` String (optional) - The original request is prevented from + * `cancel` boolean (optional) + * `redirectURL` string (optional) - The original request is prevented from being sent or completed and is instead redirected to the given URL. The `listener` will be called with `listener(details, callback)` when a request @@ -85,44 +86,45 @@ Some examples of valid `urls`: #### `webRequest.onBeforeSendHeaders([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double + * `uploadData` [UploadData[]](structures/upload-data.md) (optional) * `requestHeaders` Record<string, string> * `callback` Function - * `response` Object - * `cancel` Boolean (optional) - * `requestHeaders` Record<string, string> (optional) - When provided, request will be made + * `beforeSendResponse` Object + * `cancel` boolean (optional) + * `requestHeaders` Record<string, string | string[]> (optional) - When provided, request will be made with these headers. The `listener` will be called with `listener(details, callback)` before sending an HTTP request, once the request headers are available. This may occur after a TCP connection is made to the server, but before any http data is sent. -The `callback` has to be called with an `response` object. +The `callback` has to be called with a `response` object. #### `webRequest.onSendHeaders([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double * `requestHeaders` Record<string, string> @@ -132,54 +134,54 @@ response are visible by the time this listener is fired. #### `webRequest.onHeadersReceived([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double - * `statusLine` String + * `statusLine` string * `statusCode` Integer - * `responseHeaders` Record<string, string> (optional) + * `responseHeaders` Record<string, string[]> (optional) * `callback` Function - * `response` Object - * `cancel` Boolean (optional) - * `responseHeaders` Record<string, string> (optional) - When provided, the server is assumed + * `headersReceivedResponse` Object + * `cancel` boolean (optional) + * `responseHeaders` Record<string, string | string[]> (optional) - When provided, the server is assumed to have responded with these headers. - * `statusLine` String (optional) - Should be provided when overriding + * `statusLine` string (optional) - Should be provided when overriding `responseHeaders` to change header status otherwise original response header's status will be used. The `listener` will be called with `listener(details, callback)` when HTTP response headers of a request have been received. -The `callback` has to be called with an `response` object. +The `callback` has to be called with a `response` object. #### `webRequest.onResponseStarted([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double - * `responseHeaders` Record<string, string> (optional) - * `fromCache` Boolean - Indicates whether the response was fetched from disk + * `responseHeaders` Record<string, string[]> (optional) + * `fromCache` boolean - Indicates whether the response was fetched from disk cache. * `statusCode` Integer - * `statusLine` String + * `statusLine` string The `listener` will be called with `listener(details)` when first byte of the response body is received. For HTTP requests, this means that the status line @@ -187,65 +189,67 @@ and response headers are available. #### `webRequest.onBeforeRedirect([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double - * `redirectURL` String + * `redirectURL` string * `statusCode` Integer - * `ip` String (optional) - The server IP address that the request was + * `statusLine` string + * `ip` string (optional) - The server IP address that the request was actually sent to. - * `fromCache` Boolean - * `responseHeaders` Record<string, string> (optional) + * `fromCache` boolean + * `responseHeaders` Record<string, string[]> (optional) The `listener` will be called with `listener(details)` when a server initiated redirect is about to occur. #### `webRequest.onCompleted([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double - * `responseHeaders` Record<string, string> (optional) - * `fromCache` Boolean + * `responseHeaders` Record<string, string[]> (optional) + * `fromCache` boolean * `statusCode` Integer - * `statusLine` String + * `statusLine` string + * `error` string The `listener` will be called with `listener(details)` when a request is completed. #### `webRequest.onErrorOccurred([filter, ]listener)` -* `filter` Object (optional) - * `urls` String[] - Array of URL patterns that will be used to filter out the - requests that do not match the URL patterns. +* `filter` [WebRequestFilter](structures/web-request-filter.md) (optional) * `listener` Function | null * `details` Object * `id` Integer - * `url` String - * `method` String + * `url` string + * `method` string * `webContentsId` Integer (optional) - * `resourceType` String - * `referrer` String + * `webContents` WebContents (optional) + * `frame` WebFrameMain (optional) + * `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`. + * `referrer` string * `timestamp` Double - * `fromCache` Boolean - * `error` String - The error description. + * `fromCache` boolean + * `error` string - The error description. The `listener` will be called with `listener(details)` when an error occurs. diff --git a/docs/api/webview-tag.md b/docs/api/webview-tag.md index 39d5472e8ad4b..fd90e8b5d5660 100644 --- a/docs/api/webview-tag.md +++ b/docs/api/webview-tag.md @@ -5,7 +5,7 @@ Electron's `webview` tag is based on [Chromium's `webview`][chrome-webview], which is undergoing dramatic architectural changes. This impacts the stability of `webviews`, including rendering, navigation, and event routing. We currently recommend to not -use the `webview` tag and to consider alternatives, like `iframe`, Electron's `BrowserView`, +use the `webview` tag and to consider alternatives, like `iframe`, [Electron's `BrowserView`](browser-view.md), or an architecture that avoids embedded content altogether. ## Enabling @@ -18,7 +18,8 @@ more information see the [BrowserWindow constructor docs](browser-window.md). > Display external web content in an isolated frame and process. -Process: [Renderer](../glossary.md#renderer-process) +Process: [Renderer](../glossary.md#renderer-process)<br /> +_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._ Use the `webview` tag to embed 'guest' content (such as web pages) in your Electron app. The guest content is contained within the `webview` container. @@ -100,7 +101,7 @@ The `webview` tag has the following attributes: <webview src="https://www.github.com/"></webview> ``` -A `String` representing the visible URL. Writing to this attribute initiates top-level +A `string` representing the visible URL. Writing to this attribute initiates top-level navigation. Assigning `src` its own value will reload the current page. @@ -114,7 +115,7 @@ The `src` attribute can also accept data URLs, such as <webview src="http://www.google.com/" nodeintegration></webview> ``` -A `Boolean`. When this attribute is present the guest page in `webview` will have node +A `boolean`. When this attribute is present the guest page in `webview` will have node integration and can use node APIs like `require` and `process` to access low level system resources. Node integration is disabled by default in the guest page. @@ -125,38 +126,33 @@ page. <webview src="http://www.google.com/" nodeintegrationinsubframes></webview> ``` -A `Boolean` for the experimental option for enabling NodeJS support in sub-frames such as iframes +A `boolean` for the experimental option for enabling NodeJS support in sub-frames such as iframes inside the `webview`. All your preloads will load for every iframe, you can use `process.isMainFrame` to determine if you are in the main frame or not. This option is disabled by default in the guest page. -### `enableremotemodule` - -```html -<webview src="http://www.google.com/" enableremotemodule="false"></webview> -``` - -A `Boolean`. When this attribute is `false` the guest page in `webview` will not have access -to the [`remote`](remote.md) module. The remote module is available by default. - ### `plugins` ```html <webview src="https://www.github.com/" plugins></webview> ``` -A `Boolean`. When this attribute is present the guest page in `webview` will be able to use +A `boolean`. When this attribute is present the guest page in `webview` will be able to use browser plugins. Plugins are disabled by default. ### `preload` ```html +<!-- from a file --> <webview src="https://www.github.com/" preload="./test.js"></webview> +<!-- or if you want to load from an asar archive --> +<webview src="https://www.github.com/" preload="./app.asar/test.js"></webview> ``` -A `String` that specifies a script that will be loaded before other scripts run in the guest -page. The protocol of script's URL must be either `file:` or `asar:`, because it -will be loaded by `require` in guest page under the hood. +A `string` that specifies a script that will be loaded before other scripts run in the guest +page. The protocol of script's URL must be `file:` (even when using `asar:` archives) because +it will be loaded by Node's `require` under the hood, which treats `asar:` archives as virtual +directories. When the guest page doesn't have node integration this script will still have access to all Node APIs, but global objects injected by Node will be deleted @@ -171,7 +167,7 @@ the `webPreferences` specified to the `will-attach-webview` event. <webview src="https://www.github.com/" httpreferrer="http://cheng.guru"></webview> ``` -A `String` that sets the referrer URL for the guest page. +A `string` that sets the referrer URL for the guest page. ### `useragent` @@ -179,7 +175,7 @@ A `String` that sets the referrer URL for the guest page. <webview src="https://www.github.com/" useragent="Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko"></webview> ``` -A `String` that sets the user agent for the guest page before the page is navigated to. Once the +A `string` that sets the user agent for the guest page before the page is navigated to. Once the page is loaded, use the `setUserAgent` method to change the user agent. ### `disablewebsecurity` @@ -188,7 +184,7 @@ page is loaded, use the `setUserAgent` method to change the user agent. <webview src="https://www.github.com/" disablewebsecurity></webview> ``` -A `Boolean`. When this attribute is present the guest page will have web security disabled. +A `boolean`. When this attribute is present the guest page will have web security disabled. Web security is enabled by default. ### `partition` @@ -198,7 +194,7 @@ Web security is enabled by default. <webview src="https://electronjs.org" partition="electron"></webview> ``` -A `String` that sets the session used by the page. If `partition` starts with `persist:`, the +A `string` that sets the session used by the page. If `partition` starts with `persist:`, the page will use a persistent session available to all pages in the app with the same `partition`. if there is no `persist:` prefix, the page will use an in-memory session. By assigning the same `partition`, multiple pages can share @@ -215,7 +211,7 @@ value will fail with a DOM exception. <webview src="https://www.github.com/" allowpopups></webview> ``` -A `Boolean`. When this attribute is present the guest page will be allowed to open new +A `boolean`. When this attribute is present the guest page will be allowed to open new windows. Popups are disabled by default. ### `webpreferences` @@ -224,7 +220,7 @@ windows. Popups are disabled by default. <webview src="https://github.com" webpreferences="allowRunningInsecureContent, javascript=no"></webview> ``` -A `String` which is a comma separated list of strings which specifies the web preferences to be set on the webview. +A `string` which is a comma separated list of strings which specifies the web preferences to be set on the webview. The full list of supported preference strings can be found in [BrowserWindow](browser-window.md#new-browserwindowoptions). The string follows the same format as the features string in `window.open`. @@ -238,7 +234,7 @@ Special values `yes` and `1` are interpreted as `true`, while `no` and `0` are i <webview src="https://www.github.com/" enableblinkfeatures="PreciseMemoryInfo, CSSVariables"></webview> ``` -A `String` which is a list of strings which specifies the blink features to be enabled separated by `,`. +A `string` which is a list of strings which specifies the blink features to be enabled separated by `,`. The full list of supported feature strings can be found in the [RuntimeEnabledFeatures.json5][runtime-enabled-features] file. @@ -248,7 +244,7 @@ The full list of supported feature strings can be found in the <webview src="https://www.github.com/" disableblinkfeatures="PreciseMemoryInfo, CSSVariables"></webview> ``` -A `String` which is a list of strings which specifies the blink features to be disabled separated by `,`. +A `string` which is a list of strings which specifies the blink features to be disabled separated by `,`. The full list of supported feature strings can be found in the [RuntimeEnabledFeatures.json5][runtime-enabled-features] file. @@ -271,11 +267,11 @@ webview.addEventListener('dom-ready', () => { * `url` URL * `options` Object (optional) - * `httpReferrer` (String | [Referrer](structures/referrer.md)) (optional) - An HTTP Referrer url. - * `userAgent` String (optional) - A user agent originating the request. - * `extraHeaders` String (optional) - Extra headers separated by "\n" - * `postData` ([UploadRawData[]](structures/upload-raw-data.md) | [UploadFile[]](structures/upload-file.md) | [UploadBlob[]](structures/upload-blob.md)) (optional) - * `baseURLForDataURL` String (optional) - Base url (with trailing path separator) for files to be loaded by the data url. This is needed only if the specified `url` is a data url and needs to load other files. + * `httpReferrer` (string | [Referrer](structures/referrer.md)) (optional) - An HTTP Referrer url. + * `userAgent` string (optional) - A user agent originating the request. + * `extraHeaders` string (optional) - Extra headers separated by "\n" + * `postData` ([UploadRawData](structures/upload-raw-data.md) | [UploadFile](structures/upload-file.md))[] (optional) + * `baseURLForDataURL` string (optional) - Base url (with trailing path separator) for files to be loaded by the data url. This is needed only if the specified `url` is a data url and needs to load other files. Returns `Promise<void>` - The promise will resolve when the page has finished loading (see [`did-finish-load`](webview-tag.md#event-did-finish-load)), and rejects @@ -287,30 +283,30 @@ e.g. the `http://` or `file://`. ### `<webview>.downloadURL(url)` -* `url` String +* `url` string Initiates a download of the resource at `url` without navigating. ### `<webview>.getURL()` -Returns `String` - The URL of guest page. +Returns `string` - The URL of guest page. ### `<webview>.getTitle()` -Returns `String` - The title of guest page. +Returns `string` - The title of guest page. ### `<webview>.isLoading()` -Returns `Boolean` - Whether guest page is still loading resources. +Returns `boolean` - Whether guest page is still loading resources. ### `<webview>.isLoadingMainFrame()` -Returns `Boolean` - Whether the main frame (and not just iframes or frames within it) is +Returns `boolean` - Whether the main frame (and not just iframes or frames within it) is still loading. ### `<webview>.isWaitingForResponse()` -Returns `Boolean` - Whether the guest page is waiting for a first-response for the +Returns `boolean` - Whether the guest page is waiting for a first-response for the main resource of the page. ### `<webview>.stop()` @@ -327,17 +323,17 @@ Reloads the guest page and ignores cache. ### `<webview>.canGoBack()` -Returns `Boolean` - Whether the guest page can go back. +Returns `boolean` - Whether the guest page can go back. ### `<webview>.canGoForward()` -Returns `Boolean` - Whether the guest page can go forward. +Returns `boolean` - Whether the guest page can go forward. ### `<webview>.canGoToOffset(offset)` * `offset` Integer -Returns `Boolean` - Whether the guest page can go to `offset`. +Returns `boolean` - Whether the guest page can go to `offset`. ### `<webview>.clearHistory()` @@ -365,23 +361,23 @@ Navigates to the specified offset from the "current entry". ### `<webview>.isCrashed()` -Returns `Boolean` - Whether the renderer process has crashed. +Returns `boolean` - Whether the renderer process has crashed. ### `<webview>.setUserAgent(userAgent)` -* `userAgent` String +* `userAgent` string Overrides the user agent for the guest page. ### `<webview>.getUserAgent()` -Returns `String` - The user agent for guest page. +Returns `string` - The user agent for guest page. ### `<webview>.insertCSS(css)` -* `css` String +* `css` string -Returns `Promise<String>` - A promise that resolves with a key for the inserted +Returns `Promise<string>` - A promise that resolves with a key for the inserted CSS that can later be used to remove the CSS via `<webview>.removeInsertedCSS(key)`. @@ -390,7 +386,7 @@ stylesheet. ### `<webview>.removeInsertedCSS(key)` -* `key` String +* `key` string Returns `Promise<void>` - Resolves if the removal was successful. @@ -399,8 +395,8 @@ by its key, which is returned from `<webview>.insertCSS(css)`. ### `<webview>.executeJavaScript(code[, userGesture])` -* `code` String -* `userGesture` Boolean (optional) - Default `false`. +* `code` string +* `userGesture` boolean (optional) - Default `false`. Returns `Promise<any>` - A promise that resolves with the result of the executed code or is rejected if the result of the code is a rejected promise. @@ -419,11 +415,11 @@ Closes the DevTools window of guest page. ### `<webview>.isDevToolsOpened()` -Returns `Boolean` - Whether guest page has a DevTools window attached. +Returns `boolean` - Whether guest page has a DevTools window attached. ### `<webview>.isDevToolsFocused()` -Returns `Boolean` - Whether DevTools window of guest page is focused. +Returns `boolean` - Whether DevTools window of guest page is focused. ### `<webview>.inspectElement(x, y)` @@ -442,17 +438,17 @@ Opens the DevTools for the service worker context present in the guest page. ### `<webview>.setAudioMuted(muted)` -* `muted` Boolean +* `muted` boolean Set guest page muted. ### `<webview>.isAudioMuted()` -Returns `Boolean` - Whether guest page has been muted. +Returns `boolean` - Whether guest page has been muted. ### `<webview>.isCurrentlyAudible()` -Returns `Boolean` - Whether audio is currently playing. +Returns `boolean` - Whether audio is currently playing. ### `<webview>.undo()` @@ -492,19 +488,19 @@ Executes editing command `unselect` in page. ### `<webview>.replace(text)` -* `text` String +* `text` string Executes editing command `replace` in page. ### `<webview>.replaceMisspelling(text)` -* `text` String +* `text` string Executes editing command `replaceMisspelling` in page. ### `<webview>.insertText(text)` -* `text` String +* `text` string Returns `Promise<void>` @@ -512,19 +508,12 @@ Inserts `text` to the focused element. ### `<webview>.findInPage(text[, options])` -* `text` String - Content to be searched, must not be empty. +* `text` string - Content to be searched, must not be empty. * `options` Object (optional) - * `forward` Boolean (optional) - Whether to search forward or backward, defaults to `true`. - * `findNext` Boolean (optional) - Whether the operation is first request or a follow up, - defaults to `false`. - * `matchCase` Boolean (optional) - Whether search should be case-sensitive, - defaults to `false`. - * `wordStart` Boolean (optional) - Whether to look only at the start of words. + * `forward` boolean (optional) - Whether to search forward or backward, defaults to `true`. + * `findNext` boolean (optional) - Whether to begin a new text finding session with this request. Should be `true` for initial requests, and `false` for follow-up requests. Defaults to `false`. + * `matchCase` boolean (optional) - Whether search should be case-sensitive, defaults to `false`. - * `medialCapitalAsWordStart` Boolean (optional) - When combined with `wordStart`, - accepts a match in the middle of a word if the match begins with an - uppercase letter followed by a lowercase or non-letter. - Accepts several other intra-word matches, defaults to `false`. Returns `Integer` - The request id used for the request. @@ -533,7 +522,7 @@ can be obtained by subscribing to [`found-in-page`](webview-tag.md#event-found-i ### `<webview>.stopFindInPage(action)` -* `action` String - Specifies the action to take place when ending +* `action` string - Specifies the action to take place when ending [`<webview>.findInPage`](#webviewfindinpagetext-options) request. * `clearSelection` - Clear the selection. * `keepSelection` - Translate the selection into a normal selection. @@ -544,10 +533,33 @@ Stops any `findInPage` request for the `webview` with the provided `action`. ### `<webview>.print([options])` * `options` Object (optional) - * `silent` Boolean (optional) - Don't ask user for print settings. Default is `false`. - * `printBackground` Boolean (optional) - Also prints the background color and image of + * `silent` boolean (optional) - Don't ask user for print settings. Default is `false`. + * `printBackground` boolean (optional) - Prints the background color and image of the web page. Default is `false`. - * `deviceName` String (optional) - Set the printer device name to use. Default is `''`. + * `deviceName` string (optional) - Set the printer device name to use. Must be the system-defined name and not the 'friendly' name, e.g 'Brother_QL_820NWB' and not 'Brother QL-820NWB'. + * `color` boolean (optional) - Set whether the printed web page will be in color or grayscale. Default is `true`. + * `margins` Object (optional) + * `marginType` string (optional) - Can be `default`, `none`, `printableArea`, or `custom`. If `custom` is chosen, you will also need to specify `top`, `bottom`, `left`, and `right`. + * `top` number (optional) - The top margin of the printed web page, in pixels. + * `bottom` number (optional) - The bottom margin of the printed web page, in pixels. + * `left` number (optional) - The left margin of the printed web page, in pixels. + * `right` number (optional) - The right margin of the printed web page, in pixels. + * `landscape` boolean (optional) - Whether the web page should be printed in landscape mode. Default is `false`. + * `scaleFactor` number (optional) - The scale factor of the web page. + * `pagesPerSheet` number (optional) - The number of pages to print per page sheet. + * `collate` boolean (optional) - Whether the web page should be collated. + * `copies` number (optional) - The number of copies of the web page to print. + * `pageRanges` Object[] (optional) - The page range to print. + * `from` number - Index of the first page to print (0-based). + * `to` number - Index of the last page to print (inclusive) (0-based). + * `duplexMode` string (optional) - Set the duplex mode of the printed web page. Can be `simplex`, `shortEdge`, or `longEdge`. + * `dpi` Record<string, number> (optional) + * `horizontal` number (optional) - The horizontal dpi. + * `vertical` number (optional) - The vertical dpi. + * `header` string (optional) - string to be printed as page header. + * `footer` string (optional) - string to be printed as page footer. + * `pageSize` string | Size (optional) - Specify page size of the printed document. Can be `A3`, + `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height`. Returns `Promise<void>` @@ -556,16 +568,23 @@ Prints `webview`'s web page. Same as `webContents.print([options])`. ### `<webview>.printToPDF(options)` * `options` Object + * `headerFooter` Record<string, string> (optional) - the header and footer for the PDF. + * `title` string - The title for the PDF header. + * `url` string - the url for the PDF footer. + * `landscape` boolean (optional) - `true` for landscape, `false` for portrait. * `marginsType` Integer (optional) - Specifies the type of margins to use. Uses 0 for default margin, 1 for no margin, and 2 for minimum margin. - * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`, - `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height` and `width` in microns. - * `printBackground` Boolean (optional) - Whether to print CSS backgrounds. - * `printSelectionOnly` Boolean (optional) - Whether to print selection only. - * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait. + * `scaleFactor` number (optional) - The scale factor of the web page. Can range from 0 to 100. + * `pageRanges` Record<string, number> (optional) - The page range to print. On macOS, only the first range is honored. + * `from` number - Index of the first page to print (0-based). + * `to` number - Index of the last page to print (inclusive) (0-based). + * `pageSize` string | Size (optional) - Specify page size of the generated PDF. Can be `A3`, + `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height` + * `printBackground` boolean (optional) - Whether to print CSS backgrounds. + * `printSelectionOnly` boolean (optional) - Whether to print selection only. -Returns `Promise<Buffer>` - Resolves with the generated PDF data. +Returns `Promise<Uint8Array>` - Resolves with the generated PDF data. Prints `webview`'s web page as PDF, Same as `webContents.printToPDF(options)`. @@ -579,7 +598,7 @@ Captures a snapshot of the page within `rect`. Omitting `rect` will capture the ### `<webview>.send(channel, ...args)` -* `channel` String +* `channel` string * `...args` any[] Returns `Promise<void>` @@ -591,9 +610,24 @@ listening to the `channel` event with the [`ipcRenderer`](ipc-renderer.md) modul See [webContents.send](web-contents.md#contentssendchannel-args) for examples. +### `<webview>.sendToFrame(frameId, channel, ...args)` + +* `frameId` [number, number] - `[processId, frameId]` +* `channel` string +* `...args` any[] + +Returns `Promise<void>` + +Send an asynchronous message to renderer process via `channel`, you can also +send arbitrary arguments. The renderer process can handle the message by +listening to the `channel` event with the [`ipcRenderer`](ipc-renderer.md) module. + +See [webContents.sendToFrame](web-contents.md#contentssendtoframeframeid-channel-args) for +examples. + ### `<webview>.sendInputEvent(event)` -* `event` Object +* `event` [MouseInputEvent](structures/mouse-input-event.md) | [MouseWheelInputEvent](structures/mouse-wheel-input-event.md) | [KeyboardInputEvent](structures/keyboard-input-event.md) Returns `Promise<void>` @@ -604,61 +638,48 @@ for detailed description of `event` object. ### `<webview>.setZoomFactor(factor)` -* `factor` Number - Zoom factor. +* `factor` number - Zoom factor. Changes the zoom factor to the specified factor. Zoom factor is zoom percent divided by 100, so 300% = 3.0. ### `<webview>.setZoomLevel(level)` -* `level` Number - Zoom level. +* `level` number - Zoom level. Changes the zoom level to the specified level. The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively. The formula for this is `scale := 1.2 ^ level`. +> **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that the +> zoom level for a specific domain propagates across all instances of windows with +> the same domain. Differentiating the window URLs will make zoom work per-window. + ### `<webview>.getZoomFactor()` -Returns `Number` - the current zoom factor. +Returns `number` - the current zoom factor. ### `<webview>.getZoomLevel()` -Returns `Number` - the current zoom level. +Returns `number` - the current zoom level. ### `<webview>.setVisualZoomLevelLimits(minimumLevel, maximumLevel)` -* `minimumLevel` Number -* `maximumLevel` Number +* `minimumLevel` number +* `maximumLevel` number Returns `Promise<void>` Sets the maximum and minimum pinch-to-zoom level. -### `<webview>.setLayoutZoomLevelLimits(minimumLevel, maximumLevel)` - -* `minimumLevel` Number -* `maximumLevel` Number - -Returns `Promise<void>` - -Sets the maximum and minimum layout-based (i.e. non-visual) zoom level. - ### `<webview>.showDefinitionForSelection()` _macOS_ Shows pop-up dictionary that searches the selected word on the page. -### `<webview>.getWebContents()` - -Returns [`WebContents`](web-contents.md) - The web contents associated with -this `webview`. - -It depends on the [`remote`](remote.md) module, -it is therefore not available when this module is disabled. - ### `<webview>.getWebContentsId()` -Returns `Number` - The WebContents ID of this `webview`. +Returns `number` - The WebContents ID of this `webview`. ## DOM Events @@ -668,8 +689,8 @@ The following DOM events are available to the `webview` tag: Returns: -* `url` String -* `isMainFrame` Boolean +* `url` string +* `isMainFrame` boolean Fired when a load has committed. This includes navigation within the current document as well as subframe document-level loads, but does not include @@ -685,9 +706,9 @@ spinning, and the `onload` event is dispatched. Returns: * `errorCode` Integer -* `errorDescription` String -* `validatedURL` String -* `isMainFrame` Boolean +* `errorDescription` string +* `validatedURL` string +* `isMainFrame` boolean This event is like `did-finish-load`, but fired when the load failed or was cancelled, e.g. `window.stop()` is invoked. @@ -696,7 +717,7 @@ cancelled, e.g. `window.stop()` is invoked. Returns: -* `isMainFrame` Boolean +* `isMainFrame` boolean Fired when a frame has done navigation. @@ -708,6 +729,10 @@ Corresponds to the points in time when the spinner of the tab starts spinning. Corresponds to the points in time when the spinner of the tab stops spinning. +### Event: 'did-attach' + +Fired when attached to the embedder web contents. + ### Event: 'dom-ready' Fired when document in the given frame is loaded. @@ -716,8 +741,8 @@ Fired when document in the given frame is loaded. Returns: -* `title` String -* `explicitSet` Boolean +* `title` string +* `explicitSet` boolean Fired when page title is set during navigation. `explicitSet` is false when title is synthesized from file url. @@ -726,7 +751,7 @@ title is synthesized from file url. Returns: -* `favicons` String[] - Array of URLs. +* `favicons` string[] - Array of URLs. Fired when page receives favicon urls. @@ -742,10 +767,10 @@ Fired when page leaves fullscreen triggered by HTML API. Returns: -* `level` Integer -* `message` String -* `line` Integer -* `sourceId` String +* `level` Integer - The log level, from 0 to 3. In order it matches `verbose`, `info`, `warning` and `error`. +* `message` string - The actual console message +* `line` Integer - The line number of the source that triggered this console message +* `sourceId` string Fired when the guest window logs a console message. @@ -767,8 +792,8 @@ Returns: * `requestId` Integer * `activeMatchOrdinal` Integer - Position of the active match. * `matches` Integer - Number of Matches. - * `selectionArea` Object - Coordinates of first match region. - * `finalUpdate` Boolean + * `selectionArea` Rectangle - Coordinates of first match region. + * `finalUpdate` boolean Fired when a result is available for [`webview.findInPage`](#webviewfindinpagetext-options) request. @@ -787,11 +812,11 @@ console.log(requestId) Returns: -* `url` String -* `frameName` String -* `disposition` String - Can be `default`, `foreground-tab`, `background-tab`, +* `url` string +* `frameName` string +* `disposition` string - Can be `default`, `foreground-tab`, `background-tab`, `new-window`, `save-to-disk` and `other`. -* `options` Object - The options which should be used for creating the new +* `options` BrowserWindowConstructorOptions - The options which should be used for creating the new [`BrowserWindow`](browser-window.md). Fired when the guest page attempts to open a new browser window. @@ -803,7 +828,7 @@ const { shell } = require('electron') const webview = document.querySelector('webview') webview.addEventListener('new-window', async (e) => { - const protocol = require('url').parse(e.url).protocol + const protocol = (new URL(e.url)).protocol if (protocol === 'http:' || protocol === 'https:') { await shell.openExternal(e.url) } @@ -814,7 +839,7 @@ webview.addEventListener('new-window', async (e) => { Returns: -* `url` String +* `url` string Emitted when a user or the page wants to start navigation. It can happen when the `window.location` object is changed or a user clicks a link in the page. @@ -828,11 +853,37 @@ this purpose. Calling `event.preventDefault()` does __NOT__ have any effect. +### Event: 'did-start-navigation' + +Returns: + +* `url` string +* `isInPlace` boolean +* `isMainFrame` boolean +* `frameProcessId` Integer +* `frameRoutingId` Integer + +Emitted when any frame (including main) starts navigating. `isInPlace` will be +`true` for in-page navigations. + +### Event: 'did-redirect-navigation' + +Returns: + +* `url` string +* `isInPlace` boolean +* `isMainFrame` boolean +* `frameProcessId` Integer +* `frameRoutingId` Integer + +Emitted after a server side redirect occurs during navigation. For example a 302 +redirect. + ### Event: 'did-navigate' Returns: -* `url` String +* `url` string Emitted when a navigation is done. @@ -840,12 +891,29 @@ This event is not emitted for in-page navigations, such as clicking anchor links or updating the `window.location.hash`. Use `did-navigate-in-page` event for this purpose. +### Event: 'did-frame-navigate' + +Returns: + +* `url` string +* `httpResponseCode` Integer - -1 for non HTTP navigations +* `httpStatusText` string - empty for non HTTP navigations, +* `isMainFrame` boolean +* `frameProcessId` Integer +* `frameRoutingId` Integer + +Emitted when any frame navigation is done. + +This event is not emitted for in-page navigations, such as clicking anchor links +or updating the `window.location.hash`. Use `did-navigate-in-page` event for +this purpose. + ### Event: 'did-navigate-in-page' Returns: -* `isMainFrame` Boolean -* `url` String +* `isMainFrame` boolean +* `url` string Emitted when an in-page navigation happened. @@ -871,8 +939,9 @@ webview.addEventListener('close', () => { Returns: -* `channel` String -* `args` Array +* `frameId` [number, number] - pair of `[processId, frameId]`. +* `channel` string +* `args` any[] Fired when the guest page has sent an asynchronous message to embedder page. @@ -905,8 +974,8 @@ Fired when the renderer process is crashed. Returns: -* `name` String -* `version` String +* `name` string +* `version` string Fired when a plugin process is crashed. @@ -926,7 +995,7 @@ Emitted when media is paused or done playing. Returns: -* `themeColor` String +* `themeColor` string Emitted when a page's theme color changes. This is usually due to encountering a meta tag: @@ -938,7 +1007,7 @@ Emitted when a page's theme color changes. This is usually due to encountering a Returns: -* `url` String +* `url` string Emitted when mouse moves over a link or the keyboard moves the focus to a link. @@ -955,4 +1024,79 @@ Emitted when DevTools is closed. Emitted when DevTools is focused / opened. [runtime-enabled-features]: https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/runtime_enabled_features.json5?l=70 -[chrome-webview]: https://developer.chrome.com/apps/tags/webview +[chrome-webview]: https://developer.chrome.com/docs/extensions/reference/webviewTag/ + +### Event: 'context-menu' + +Returns: + +* `params` Object + * `x` Integer - x coordinate. + * `y` Integer - y coordinate. + * `linkURL` string - URL of the link that encloses the node the context menu + was invoked on. + * `linkText` string - Text associated with the link. May be an empty + string if the contents of the link are an image. + * `pageURL` string - URL of the top level page that the context menu was + invoked on. + * `frameURL` string - URL of the subframe that the context menu was invoked + on. + * `srcURL` string - Source URL for the element that the context menu + was invoked on. Elements with source URLs are images, audio and video. + * `mediaType` string - Type of the node the context menu was invoked on. Can + be `none`, `image`, `audio`, `video`, `canvas`, `file` or `plugin`. + * `hasImageContents` boolean - Whether the context menu was invoked on an image + which has non-empty contents. + * `isEditable` boolean - Whether the context is editable. + * `selectionText` string - Text of the selection that the context menu was + invoked on. + * `titleText` string - Title text of the selection that the context menu was + invoked on. + * `altText` string - Alt text of the selection that the context menu was + invoked on. + * `suggestedFilename` string - Suggested filename to be used when saving file through 'Save + Link As' option of context menu. + * `selectionRect` [Rectangle](structures/rectangle.md) - Rect representing the coordinates in the document space of the selection. + * `selectionStartOffset` number - Start position of the selection text. + * `referrerPolicy` [Referrer](structures/referrer.md) - The referrer policy of the frame on which the menu is invoked. + * `misspelledWord` string - The misspelled word under the cursor, if any. + * `dictionarySuggestions` string[] - An array of suggested words to show the + user to replace the `misspelledWord`. Only available if there is a misspelled + word and spellchecker is enabled. + * `frameCharset` string - The character encoding of the frame on which the + menu was invoked. + * `inputFieldType` string - If the context menu was invoked on an input + field, the type of that field. Possible values are `none`, `plainText`, + `password`, `other`. + * `spellcheckEnabled` boolean - If the context is editable, whether or not spellchecking is enabled. + * `menuSourceType` string - Input source that invoked the context menu. + Can be `none`, `mouse`, `keyboard`, `touch`, `touchMenu`, `longPress`, `longTap`, `touchHandle`, `stylus`, `adjustSelection`, or `adjustSelectionReset`. + * `mediaFlags` Object - The flags for the media element the context menu was + invoked on. + * `inError` boolean - Whether the media element has crashed. + * `isPaused` boolean - Whether the media element is paused. + * `isMuted` boolean - Whether the media element is muted. + * `hasAudio` boolean - Whether the media element has audio. + * `isLooping` boolean - Whether the media element is looping. + * `isControlsVisible` boolean - Whether the media element's controls are + visible. + * `canToggleControls` boolean - Whether the media element's controls are + toggleable. + * `canPrint` boolean - Whether the media element can be printed. + * `canSave` boolean - Whether or not the media element can be downloaded. + * `canShowPictureInPicture` boolean - Whether the media element can show picture-in-picture. + * `isShowingPictureInPicture` boolean - Whether the media element is currently showing picture-in-picture. + * `canRotate` boolean - Whether the media element can be rotated. + * `canLoop` boolean - Whether the media element can be looped. + * `editFlags` Object - These flags indicate whether the renderer believes it + is able to perform the corresponding action. + * `canUndo` boolean - Whether the renderer believes it can undo. + * `canRedo` boolean - Whether the renderer believes it can redo. + * `canCut` boolean - Whether the renderer believes it can cut. + * `canCopy` boolean - Whether the renderer believes it can copy. + * `canPaste` boolean - Whether the renderer believes it can paste. + * `canDelete` boolean - Whether the renderer believes it can delete. + * `canSelectAll` boolean - Whether the renderer believes it can select all. + * `canEditRichly` boolean - Whether the renderer believes it can edit text richly. + +Emitted when there is a new context menu that needs to be handled. diff --git a/docs/api/window-open.md b/docs/api/window-open.md index 631455b113c42..a2cb76a2a569e 100644 --- a/docs/api/window-open.md +++ b/docs/api/window-open.md @@ -1,34 +1,51 @@ -# `window.open` Function +# Opening windows from the renderer -> Open a new window and load a URL. +There are several ways to control how windows are created from trusted or +untrusted content within a renderer. Windows can be created from the renderer in two ways: -When `window.open` is called to create a new window in a web page, a new instance -of [`BrowserWindow`](browser-window.md) will be created for the `url` and a proxy will be returned -to `window.open` to let the page have limited control over it. +* clicking on links or submitting forms adorned with `target=_blank` +* JavaScript calling `window.open()` -The proxy has limited standard functionality implemented to be -compatible with traditional web pages. For full control of the new window -you should create a `BrowserWindow` directly. +For same-origin content, the new window is created within the same process, +enabling the parent to access the child window directly. This can be very +useful for app sub-windows that act as preference panels, or similar, as the +parent can render to the sub-window directly, as if it were a `div` in the +parent. This is the same behavior as in the browser. -The newly created `BrowserWindow` will inherit the parent window's options by -default. To override inherited options you can set them in the `features` -string. +Electron pairs this native Chrome `Window` with a BrowserWindow under the hood. +You can take advantage of all the customization available when creating a +BrowserWindow in the main process by using `webContents.setWindowOpenHandler()` +for renderer-created windows. + +BrowserWindow constructor options are set by, in increasing precedence +order: parsed options from the `features` string from `window.open()`, +security-related webPreferences inherited from the parent, and options given by +[`webContents.setWindowOpenHandler`](web-contents.md#contentssetwindowopenhandlerhandler). +Note that `webContents.setWindowOpenHandler` has final say and full privilege +because it is invoked in the main process. ### `window.open(url[, frameName][, features])` -* `url` String -* `frameName` String (optional) -* `features` String (optional) +* `url` string +* `frameName` string (optional) +* `features` string (optional) + +Returns [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) | null -Returns [`BrowserWindowProxy`](browser-window-proxy.md) - Creates a new window -and returns an instance of `BrowserWindowProxy` class. +`features` is a comma-separated key-value list, following the standard format of +the browser. Electron will parse `BrowserWindowConstructorOptions` out of this +list where possible, for convenience. For full control and better ergonomics, +consider using `webContents.setWindowOpenHandler` to customize the +BrowserWindow creation. -The `features` string follows the format of standard browser, but each feature -has to be a field of `BrowserWindow`'s options. These are the features you can set via `features` string: `zoomFactor`, `nodeIntegration`, `preload`, `javascript`, `contextIsolation`, `webviewTag`. +A subset of `WebPreferences` can be set directly, +unnested, from the features string: `zoomFactor`, `nodeIntegration`, `preload`, +`javascript`, `contextIsolation`, and `webviewTag`. For example: + ```js -window.open('https://github.com', '_blank', 'nodeIntegration=no') +window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIntegration=no') ``` **Notes:** @@ -40,60 +57,50 @@ window.open('https://github.com', '_blank', 'nodeIntegration=no') * JavaScript will always be disabled in the opened `window` if it is disabled on the parent window. * Non-standard features (that are not handled by Chromium or Electron) given in - `features` will be passed to any registered `webContent`'s `new-window` event - handler in the `additionalFeatures` argument. - -### `window.opener.postMessage(message, targetOrigin)` - -* `message` String -* `targetOrigin` String - -Sends a message to the parent window with the specified origin or `*` for no -origin preference. - -### Using Chrome's `window.open()` implementation - -If you want to use Chrome's built-in `window.open()` implementation, set -`nativeWindowOpen` to `true` in the `webPreferences` options object. - -Native `window.open()` allows synchronous access to opened windows so it is -convenient choice if you need to open a dialog or a preferences window. - -This option can also be set on `<webview>` tags as well: - -```html -<webview webpreferences="nativeWindowOpen=yes"></webview> -``` - -The creation of the `BrowserWindow` is customizable via `WebContents`'s -`new-window` event. + `features` will be passed to any registered `webContents`'s + `did-create-window` event handler in the `options` argument. +* `frameName` follows the specification of `windowName` located in the [native documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#parameters). +* When opening `about:blank`, the child window's `WebPreferences` will be copied + from the parent window, and there is no way to override it because Chromium + skips browser side navigation in this case. + +To customize or cancel the creation of the window, you can optionally set an +override handler with `webContents.setWindowOpenHandler()` from the main +process. Returning `{ action: 'deny' }` cancels the window. Returning `{ +action: 'allow', overrideBrowserWindowOptions: { ... } }` will allow opening +the window and setting the `BrowserWindowConstructorOptions` to be used when +creating the window. Note that this is more powerful than passing options +through the feature string, as the renderer has more limited privileges in +deciding security preferences than the main process. + +### Native `Window` example ```javascript -// main process -const mainWindow = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - nativeWindowOpen: true - } -}) -mainWindow.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => { - if (frameName === 'modal') { - // open window as modal - event.preventDefault() - Object.assign(options, { - modal: true, - parent: mainWindow, - width: 100, - height: 100 - }) - event.newGuest = new BrowserWindow(options) +// main.js +const mainWindow = new BrowserWindow() + +// In this example, only windows with the `about:blank` url will be created. +// All other urls will be blocked. +mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url === 'about:blank') { + return { + action: 'allow', + overrideBrowserWindowOptions: { + frame: false, + fullscreenable: false, + backgroundColor: 'black', + webPreferences: { + preload: 'my-child-window-preload-script.js' + } + } + } } + return { action: 'deny' } }) ``` ```javascript // renderer process (mainWindow) -let modal = window.open('', 'modal') -modal.document.write('<h1>Hello</h1>') +const childWindow = window.open('', 'modal') +childWindow.document.write('<h1>Hello</h1>') ``` diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md new file mode 100644 index 0000000000000..c448a5303836e --- /dev/null +++ b/docs/breaking-changes.md @@ -0,0 +1,1480 @@ +# Breaking Changes + +Breaking changes will be documented here, and deprecation warnings added to JS code where possible, at least [one major version](tutorial/electron-versioning.md#semver) before the change is made. + +### Types of Breaking Changes + +This document uses the following convention to categorize breaking changes: + +* **API Changed:** An API was changed in such a way that code that has not been updated is guaranteed to throw an exception. +* **Behavior Changed:** The behavior of Electron has changed, but not in such a way that an exception will necessarily be thrown. +* **Default Changed:** Code depending on the old default may break, not necessarily throwing an exception. The old behavior can be restored by explicitly specifying the value. +* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release. +* **Removed:** An API or feature was removed, and is no longer supported by Electron. + +## Planned Breaking API Changes (20.0) + +### Default Changed: renderers without `nodeIntegration: true` are sandboxed by default + +Previously, renderers that specified a preload script defaulted to being +unsandboxed. This meant that by default, preload scripts had access to Node.js. +In Electron 20, this default has changed. Beginning in Electron 20, renderers +will be sandboxed by default, unless `nodeIntegration: true` or `sandbox: false` +is specified. + +If your preload scripts do not depend on Node, no action is needed. If your +preload scripts _do_ depend on Node, either refactor them to remove Node usage +from the renderer, or explicitly specify `sandbox: false` for the relevant +renderers. + +### Removed: `skipTaskbar` on Linux + +On X11, `skipTaskbar` sends a `_NET_WM_STATE_SKIP_TASKBAR` message to the X11 +window manager. There is not a direct equivalent for Wayland, and the known +workarounds have unacceptable tradeoffs (e.g. Window.is_skip_taskbar in GNOME +requires unsafe mode), so Electron is unable to support this feature on Linux. + +## Planned Breaking API Changes (19.0) + +*None (yet)* + +## Planned Breaking API Changes (18.0) + +### Removed: `nativeWindowOpen` + +Prior to Electron 15, `window.open` was by default shimmed to use +`BrowserWindowProxy`. This meant that `window.open('about:blank')` did not work +to open synchronously scriptable child windows, among other incompatibilities. +Since Electron 15, `nativeWindowOpen` has been enabled by default. + +See the documentation for [window.open in Electron](api/window-open.md) +for more details. + +## Planned Breaking API Changes (17.0) + +### Removed: `desktopCapturer.getSources` in the renderer + +The `desktopCapturer.getSources` API is now only available in the main process. +This has been changed in order to improve the default security of Electron +apps. + +If you need this functionality, it can be replaced as follows: + +```js +// Main process +const { ipcMain, desktopCapturer } = require('electron') + +ipcMain.handle( + 'DESKTOP_CAPTURER_GET_SOURCES', + (event, opts) => desktopCapturer.getSources(opts) +) +``` + +```js +// Renderer process +const { ipcRenderer } = require('electron') + +const desktopCapturer = { + getSources: (opts) => ipcRenderer.invoke('DESKTOP_CAPTURER_GET_SOURCES', opts) +} +``` + +However, you should consider further restricting the information returned to +the renderer; for instance, displaying a source selector to the user and only +returning the selected source. + +### Deprecated: `nativeWindowOpen` + +Prior to Electron 15, `window.open` was by default shimmed to use +`BrowserWindowProxy`. This meant that `window.open('about:blank')` did not work +to open synchronously scriptable child windows, among other incompatibilities. +Since Electron 15, `nativeWindowOpen` has been enabled by default. + +See the documentation for [window.open in Electron](api/window-open.md) +for more details. + +## Planned Breaking API Changes (16.0) + +### Behavior Changed: `crashReporter` implementation switched to Crashpad on Linux + +The underlying implementation of the `crashReporter` API on Linux has changed +from Breakpad to Crashpad, bringing it in line with Windows and Mac. As a +result of this, child processes are now automatically monitored, and calling +`process.crashReporter.start` in Node child processes is no longer needed (and +is not advisable, as it will start a second instance of the Crashpad reporter). + +There are also some subtle changes to how annotations will be reported on +Linux, including that long values will no longer be split between annotations +appended with `__1`, `__2` and so on, and instead will be truncated at the +(new, longer) annotation value limit. + +### Deprecated: `desktopCapturer.getSources` in the renderer + +Usage of the `desktopCapturer.getSources` API in the renderer has been +deprecated and will be removed. This change improves the default security of +Electron apps. + +See [here](#removed-desktopcapturergetsources-in-the-renderer) for details on +how to replace this API in your app. + +## Planned Breaking API Changes (15.0) + +### Default Changed: `nativeWindowOpen` defaults to `true` + +Prior to Electron 15, `window.open` was by default shimmed to use +`BrowserWindowProxy`. This meant that `window.open('about:blank')` did not work +to open synchronously scriptable child windows, among other incompatibilities. +`nativeWindowOpen` is no longer experimental, and is now the default. + +See the documentation for [window.open in Electron](api/window-open.md) +for more details. + +## Planned Breaking API Changes (14.0) + +### Removed: `remote` module + +The `remote` module was deprecated in Electron 12, and will be removed in +Electron 14. It is replaced by the +[`@electron/remote`](https://github.com/electron/remote) module. + +```js +// Deprecated in Electron 12: +const { BrowserWindow } = require('electron').remote +``` + +```js +// Replace with: +const { BrowserWindow } = require('@electron/remote') + +// In the main process: +require('@electron/remote/main').initialize() +``` + +### Removed: `app.allowRendererProcessReuse` + +The `app.allowRendererProcessReuse` property will be removed as part of our plan to +more closely align with Chromium's process model for security, performance and maintainability. + +For more detailed information see [#18397](https://github.com/electron/electron/issues/18397). + +### Removed: Browser Window Affinity + +The `affinity` option when constructing a new `BrowserWindow` will be removed +as part of our plan to more closely align with Chromium's process model for security, +performance and maintainability. + +For more detailed information see [#18397](https://github.com/electron/electron/issues/18397). + +### API Changed: `window.open()` + +The optional parameter `frameName` will no longer set the title of the window. This now follows the specification described by the [native documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#parameters) under the corresponding parameter `windowName`. + +If you were using this parameter to set the title of a window, you can instead use [win.setTitle(title)](api/browser-window.md#winsettitletitle). + +### Removed: `worldSafeExecuteJavaScript` + +In Electron 14, `worldSafeExecuteJavaScript` will be removed. There is no alternative, please +ensure your code works with this property enabled. It has been enabled by default since Electron +12. + +You will be affected by this change if you use either `webFrame.executeJavaScript` or `webFrame.executeJavaScriptInIsolatedWorld`. You will need to ensure that values returned by either of those methods are supported by the [Context Bridge API](api/context-bridge.md#parameter--error--return-type-support) as these methods use the same value passing semantics. + +### Removed: BrowserWindowConstructorOptions inheriting from parent windows + +Prior to Electron 14, windows opened with `window.open` would inherit +BrowserWindow constructor options such as `transparent` and `resizable` from +their parent window. Beginning with Electron 14, this behavior is removed, and +windows will not inherit any BrowserWindow constructor options from their +parents. + +Instead, explicitly set options for the new window with `setWindowOpenHandler`: + +```js +webContents.setWindowOpenHandler((details) => { + return { + action: 'allow', + overrideBrowserWindowOptions: { + // ... + } + } +}) +``` + +### Removed: `additionalFeatures` + +The deprecated `additionalFeatures` property in the `new-window` and +`did-create-window` events of WebContents has been removed. Since `new-window` +uses positional arguments, the argument is still present, but will always be +the empty array `[]`. (Though note, the `new-window` event itself is +deprecated, and is replaced by `setWindowOpenHandler`.) Bare keys in window +features will now present as keys with the value `true` in the options object. + +```js +// Removed in Electron 14 +// Triggered by window.open('...', '', 'my-key') +webContents.on('did-create-window', (window, details) => { + if (details.additionalFeatures.includes('my-key')) { + // ... + } +}) + +// Replace with +webContents.on('did-create-window', (window, details) => { + if (details.options['my-key']) { + // ... + } +}) +``` + +## Planned Breaking API Changes (13.0) + +### API Changed: `session.setPermissionCheckHandler(handler)` + +The `handler` methods first parameter was previously always a `webContents`, it can now sometimes be `null`. You should use the `requestingOrigin`, `embeddingOrigin` and `securityOrigin` properties to respond to the permission check correctly. As the `webContents` can be `null` it can no longer be relied on. + +```js +// Old code +session.setPermissionCheckHandler((webContents, permission) => { + if (webContents.getURL().startsWith('https://google.com/') && permission === 'notification') { + return true + } + return false +}) + +// Replace with +session.setPermissionCheckHandler((webContents, permission, requestingOrigin) => { + if (new URL(requestingOrigin).hostname === 'google.com' && permission === 'notification') { + return true + } + return false +}) +``` + +### Removed: `shell.moveItemToTrash()` + +The deprecated synchronous `shell.moveItemToTrash()` API has been removed. Use +the asynchronous `shell.trashItem()` instead. + +```js +// Removed in Electron 13 +shell.moveItemToTrash(path) +// Replace with +shell.trashItem(path).then(/* ... */) +``` + +### Removed: `BrowserWindow` extension APIs + +The deprecated extension APIs have been removed: + +* `BrowserWindow.addExtension(path)` +* `BrowserWindow.addDevToolsExtension(path)` +* `BrowserWindow.removeExtension(name)` +* `BrowserWindow.removeDevToolsExtension(name)` +* `BrowserWindow.getExtensions()` +* `BrowserWindow.getDevToolsExtensions()` + +Use the session APIs instead: + +* `ses.loadExtension(path)` +* `ses.removeExtension(extension_id)` +* `ses.getAllExtensions()` + +```js +// Removed in Electron 13 +BrowserWindow.addExtension(path) +BrowserWindow.addDevToolsExtension(path) +// Replace with +session.defaultSession.loadExtension(path) +``` + +```js +// Removed in Electron 13 +BrowserWindow.removeExtension(name) +BrowserWindow.removeDevToolsExtension(name) +// Replace with +session.defaultSession.removeExtension(extension_id) +``` + +```js +// Removed in Electron 13 +BrowserWindow.getExtensions() +BrowserWindow.getDevToolsExtensions() +// Replace with +session.defaultSession.getAllExtensions() +``` + +### Removed: methods in `systemPreferences` + +The following `systemPreferences` methods have been deprecated: + +* `systemPreferences.isDarkMode()` +* `systemPreferences.isInvertedColorScheme()` +* `systemPreferences.isHighContrastColorScheme()` + +Use the following `nativeTheme` properties instead: + +* `nativeTheme.shouldUseDarkColors` +* `nativeTheme.shouldUseInvertedColorScheme` +* `nativeTheme.shouldUseHighContrastColors` + +```js +// Removed in Electron 13 +systemPreferences.isDarkMode() +// Replace with +nativeTheme.shouldUseDarkColors + +// Removed in Electron 13 +systemPreferences.isInvertedColorScheme() +// Replace with +nativeTheme.shouldUseInvertedColorScheme + +// Removed in Electron 13 +systemPreferences.isHighContrastColorScheme() +// Replace with +nativeTheme.shouldUseHighContrastColors +``` + +### Deprecated: WebContents `new-window` event + +The `new-window` event of WebContents has been deprecated. It is replaced by [`webContents.setWindowOpenHandler()`](api/web-contents.md#contentssetwindowopenhandlerhandler). + +```js +// Deprecated in Electron 13 +webContents.on('new-window', (event) => { + event.preventDefault() +}) + +// Replace with +webContents.setWindowOpenHandler((details) => { + return { action: 'deny' } +}) +``` + +## Planned Breaking API Changes (12.0) + +### Removed: Pepper Flash support + +Chromium has removed support for Flash, and so we must follow suit. See +Chromium's [Flash Roadmap](https://www.chromium.org/flash-roadmap) for more +details. + +### Default Changed: `worldSafeExecuteJavaScript` defaults to `true` + +In Electron 12, `worldSafeExecuteJavaScript` will be enabled by default. To restore +the previous behavior, `worldSafeExecuteJavaScript: false` must be specified in WebPreferences. +Please note that setting this option to `false` is **insecure**. + +This option will be removed in Electron 14 so please migrate your code to support the default +value. + +### Default Changed: `contextIsolation` defaults to `true` + +In Electron 12, `contextIsolation` will be enabled by default. To restore +the previous behavior, `contextIsolation: false` must be specified in WebPreferences. + +We [recommend having contextIsolation enabled](tutorial/security.md#3-enable-context-isolation-for-remote-content) for the security of your application. + +Another implication is that `require()` cannot be used in the renderer process unless +`nodeIntegration` is `true` and `contextIsolation` is `false`. + +For more details see: https://github.com/electron/electron/issues/23506 + +### Removed: `crashReporter.getCrashesDirectory()` + +The `crashReporter.getCrashesDirectory` method has been removed. Usage +should be replaced by `app.getPath('crashDumps')`. + +```js +// Removed in Electron 12 +crashReporter.getCrashesDirectory() +// Replace with +app.getPath('crashDumps') +``` + +### Removed: `crashReporter` methods in the renderer process + +The following `crashReporter` methods are no longer available in the renderer +process: + +* `crashReporter.start` +* `crashReporter.getLastCrashReport` +* `crashReporter.getUploadedReports` +* `crashReporter.getUploadToServer` +* `crashReporter.setUploadToServer` +* `crashReporter.getCrashesDirectory` + +They should be called only from the main process. + +See [#23265](https://github.com/electron/electron/pull/23265) for more details. + +### Default Changed: `crashReporter.start({ compress: true })` + +The default value of the `compress` option to `crashReporter.start` has changed +from `false` to `true`. This means that crash dumps will be uploaded to the +crash ingestion server with the `Content-Encoding: gzip` header, and the body +will be compressed. + +If your crash ingestion server does not support compressed payloads, you can +turn off compression by specifying `{ compress: false }` in the crash reporter +options. + +### Deprecated: `remote` module + +The `remote` module is deprecated in Electron 12, and will be removed in +Electron 14. It is replaced by the +[`@electron/remote`](https://github.com/electron/remote) module. + +```js +// Deprecated in Electron 12: +const { BrowserWindow } = require('electron').remote +``` + +```js +// Replace with: +const { BrowserWindow } = require('@electron/remote') + +// In the main process: +require('@electron/remote/main').initialize() +``` + +### Deprecated: `shell.moveItemToTrash()` + +The synchronous `shell.moveItemToTrash()` has been replaced by the new, +asynchronous `shell.trashItem()`. + +```js +// Deprecated in Electron 12 +shell.moveItemToTrash(path) +// Replace with +shell.trashItem(path).then(/* ... */) +``` + +## Planned Breaking API Changes (11.0) + +### Removed: `BrowserView.{destroy, fromId, fromWebContents, getAllViews}` and `id` property of `BrowserView` + +The experimental APIs `BrowserView.{destroy, fromId, fromWebContents, getAllViews}` +have now been removed. Additionally, the `id` property of `BrowserView` +has also been removed. + +For more detailed information, see [#23578](https://github.com/electron/electron/pull/23578). + +## Planned Breaking API Changes (10.0) + +### Deprecated: `companyName` argument to `crashReporter.start()` + +The `companyName` argument to `crashReporter.start()`, which was previously +required, is now optional, and further, is deprecated. To get the same +behavior in a non-deprecated way, you can pass a `companyName` value in +`globalExtra`. + +```js +// Deprecated in Electron 10 +crashReporter.start({ companyName: 'Umbrella Corporation' }) +// Replace with +crashReporter.start({ globalExtra: { _companyName: 'Umbrella Corporation' } }) +``` + +### Deprecated: `crashReporter.getCrashesDirectory()` + +The `crashReporter.getCrashesDirectory` method has been deprecated. Usage +should be replaced by `app.getPath('crashDumps')`. + +```js +// Deprecated in Electron 10 +crashReporter.getCrashesDirectory() +// Replace with +app.getPath('crashDumps') +``` + +### Deprecated: `crashReporter` methods in the renderer process + +Calling the following `crashReporter` methods from the renderer process is +deprecated: + +* `crashReporter.start` +* `crashReporter.getLastCrashReport` +* `crashReporter.getUploadedReports` +* `crashReporter.getUploadToServer` +* `crashReporter.setUploadToServer` +* `crashReporter.getCrashesDirectory` + +The only non-deprecated methods remaining in the `crashReporter` module in the +renderer are `addExtraParameter`, `removeExtraParameter` and `getParameters`. + +All above methods remain non-deprecated when called from the main process. + +See [#23265](https://github.com/electron/electron/pull/23265) for more details. + +### Deprecated: `crashReporter.start({ compress: false })` + +Setting `{ compress: false }` in `crashReporter.start` is deprecated. Nearly +all crash ingestion servers support gzip compression. This option will be +removed in a future version of Electron. + +### Default Changed: `enableRemoteModule` defaults to `false` + +In Electron 9, using the remote module without explicitly enabling it via the +`enableRemoteModule` WebPreferences option began emitting a warning. In +Electron 10, the remote module is now disabled by default. To use the remote +module, `enableRemoteModule: true` must be specified in WebPreferences: + +```js +const w = new BrowserWindow({ + webPreferences: { + enableRemoteModule: true + } +}) +``` + +We [recommend moving away from the remote +module](https://medium.com/@nornagon/electrons-remote-module-considered-harmful-70d69500f31). + +### `protocol.unregisterProtocol` + +### `protocol.uninterceptProtocol` + +The APIs are now synchronous and the optional callback is no longer needed. + +```javascript +// Deprecated +protocol.unregisterProtocol(scheme, () => { /* ... */ }) +// Replace with +protocol.unregisterProtocol(scheme) +``` + +### `protocol.registerFileProtocol` + +### `protocol.registerBufferProtocol` + +### `protocol.registerStringProtocol` + +### `protocol.registerHttpProtocol` + +### `protocol.registerStreamProtocol` + +### `protocol.interceptFileProtocol` + +### `protocol.interceptStringProtocol` + +### `protocol.interceptBufferProtocol` + +### `protocol.interceptHttpProtocol` + +### `protocol.interceptStreamProtocol` + +The APIs are now synchronous and the optional callback is no longer needed. + +```javascript +// Deprecated +protocol.registerFileProtocol(scheme, handler, () => { /* ... */ }) +// Replace with +protocol.registerFileProtocol(scheme, handler) +``` + +The registered or intercepted protocol does not have effect on current page +until navigation happens. + +### `protocol.isProtocolHandled` + +This API is deprecated and users should use `protocol.isProtocolRegistered` +and `protocol.isProtocolIntercepted` instead. + +```javascript +// Deprecated +protocol.isProtocolHandled(scheme).then(() => { /* ... */ }) +// Replace with +const isRegistered = protocol.isProtocolRegistered(scheme) +const isIntercepted = protocol.isProtocolIntercepted(scheme) +``` + +## Planned Breaking API Changes (9.0) + +### Default Changed: Loading non-context-aware native modules in the renderer process is disabled by default + +As of Electron 9 we do not allow loading of non-context-aware native modules in +the renderer process. This is to improve security, performance and maintainability +of Electron as a project. + +If this impacts you, you can temporarily set `app.allowRendererProcessReuse` to `false` +to revert to the old behavior. This flag will only be an option until Electron 11 so +you should plan to update your native modules to be context aware. + +For more detailed information see [#18397](https://github.com/electron/electron/issues/18397). + +### Deprecated: `BrowserWindow` extension APIs + +The following extension APIs have been deprecated: + +* `BrowserWindow.addExtension(path)` +* `BrowserWindow.addDevToolsExtension(path)` +* `BrowserWindow.removeExtension(name)` +* `BrowserWindow.removeDevToolsExtension(name)` +* `BrowserWindow.getExtensions()` +* `BrowserWindow.getDevToolsExtensions()` + +Use the session APIs instead: + +* `ses.loadExtension(path)` +* `ses.removeExtension(extension_id)` +* `ses.getAllExtensions()` + +```js +// Deprecated in Electron 9 +BrowserWindow.addExtension(path) +BrowserWindow.addDevToolsExtension(path) +// Replace with +session.defaultSession.loadExtension(path) +``` + +```js +// Deprecated in Electron 9 +BrowserWindow.removeExtension(name) +BrowserWindow.removeDevToolsExtension(name) +// Replace with +session.defaultSession.removeExtension(extension_id) +``` + +```js +// Deprecated in Electron 9 +BrowserWindow.getExtensions() +BrowserWindow.getDevToolsExtensions() +// Replace with +session.defaultSession.getAllExtensions() +``` + +### Removed: `<webview>.getWebContents()` + +This API, which was deprecated in Electron 8.0, is now removed. + +```js +// Removed in Electron 9.0 +webview.getWebContents() +// Replace with +const { remote } = require('electron') +remote.webContents.fromId(webview.getWebContentsId()) +``` + +### Removed: `webFrame.setLayoutZoomLevelLimits()` + +Chromium has removed support for changing the layout zoom level limits, and it +is beyond Electron's capacity to maintain it. The function was deprecated in +Electron 8.x, and has been removed in Electron 9.x. The layout zoom level limits +are now fixed at a minimum of 0.25 and a maximum of 5.0, as defined +[here](https://chromium.googlesource.com/chromium/src/+/938b37a6d2886bf8335fc7db792f1eb46c65b2ae/third_party/blink/common/page/page_zoom.cc#11). + +### Behavior Changed: Sending non-JS objects over IPC now throws an exception + +In Electron 8.0, IPC was changed to use the Structured Clone Algorithm, +bringing significant performance improvements. To help ease the transition, the +old IPC serialization algorithm was kept and used for some objects that aren't +serializable with Structured Clone. In particular, DOM objects (e.g. `Element`, +`Location` and `DOMMatrix`), Node.js objects backed by C++ classes (e.g. +`process.env`, some members of `Stream`), and Electron objects backed by C++ +classes (e.g. `WebContents`, `BrowserWindow` and `WebFrame`) are not +serializable with Structured Clone. Whenever the old algorithm was invoked, a +deprecation warning was printed. + +In Electron 9.0, the old serialization algorithm has been removed, and sending +such non-serializable objects will now throw an "object could not be cloned" +error. + +### API Changed: `shell.openItem` is now `shell.openPath` + +The `shell.openItem` API has been replaced with an asynchronous `shell.openPath` API. +You can see the original API proposal and reasoning [here](https://github.com/electron/governance/blob/main/wg-api/spec-documents/shell-openitem.md). + +## Planned Breaking API Changes (8.0) + +### Behavior Changed: Values sent over IPC are now serialized with Structured Clone Algorithm + +The algorithm used to serialize objects sent over IPC (through +`ipcRenderer.send`, `ipcRenderer.sendSync`, `WebContents.send` and related +methods) has been switched from a custom algorithm to V8's built-in [Structured +Clone Algorithm][SCA], the same algorithm used to serialize messages for +`postMessage`. This brings about a 2x performance improvement for large +messages, but also brings some breaking changes in behavior. + +* Sending Functions, Promises, WeakMaps, WeakSets, or objects containing any + such values, over IPC will now throw an exception, instead of silently + converting the functions to `undefined`. + +```js +// Previously: +ipcRenderer.send('channel', { value: 3, someFunction: () => {} }) +// => results in { value: 3 } arriving in the main process + +// From Electron 8: +ipcRenderer.send('channel', { value: 3, someFunction: () => {} }) +// => throws Error("() => {} could not be cloned.") +``` + +* `NaN`, `Infinity` and `-Infinity` will now be correctly serialized, instead + of being converted to `null`. +* Objects containing cyclic references will now be correctly serialized, + instead of being converted to `null`. +* `Set`, `Map`, `Error` and `RegExp` values will be correctly serialized, + instead of being converted to `{}`. +* `BigInt` values will be correctly serialized, instead of being converted to + `null`. +* Sparse arrays will be serialized as such, instead of being converted to dense + arrays with `null`s. +* `Date` objects will be transferred as `Date` objects, instead of being + converted to their ISO string representation. +* Typed Arrays (such as `Uint8Array`, `Uint16Array`, `Uint32Array` and so on) + will be transferred as such, instead of being converted to Node.js `Buffer`. +* Node.js `Buffer` objects will be transferred as `Uint8Array`s. You can + convert a `Uint8Array` back to a Node.js `Buffer` by wrapping the underlying + `ArrayBuffer`: + +```js +Buffer.from(value.buffer, value.byteOffset, value.byteLength) +``` + +Sending any objects that aren't native JS types, such as DOM objects (e.g. +`Element`, `Location`, `DOMMatrix`), Node.js objects (e.g. `process.env`, +`Stream`), or Electron objects (e.g. `WebContents`, `BrowserWindow`, +`WebFrame`) is deprecated. In Electron 8, these objects will be serialized as +before with a DeprecationWarning message, but starting in Electron 9, sending +these kinds of objects will throw a 'could not be cloned' error. + +[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm + +### Deprecated: `<webview>.getWebContents()` + +This API is implemented using the `remote` module, which has both performance +and security implications. Therefore its usage should be explicit. + +```js +// Deprecated +webview.getWebContents() +// Replace with +const { remote } = require('electron') +remote.webContents.fromId(webview.getWebContentsId()) +``` + +However, it is recommended to avoid using the `remote` module altogether. + +```js +// main +const { ipcMain, webContents } = require('electron') + +const getGuestForWebContents = (webContentsId, contents) => { + const guest = webContents.fromId(webContentsId) + if (!guest) { + throw new Error(`Invalid webContentsId: ${webContentsId}`) + } + if (guest.hostWebContents !== contents) { + throw new Error('Access denied to webContents') + } + return guest +} + +ipcMain.handle('openDevTools', (event, webContentsId) => { + const guest = getGuestForWebContents(webContentsId, event.sender) + guest.openDevTools() +}) + +// renderer +const { ipcRenderer } = require('electron') + +ipcRenderer.invoke('openDevTools', webview.getWebContentsId()) +``` + +### Deprecated: `webFrame.setLayoutZoomLevelLimits()` + +Chromium has removed support for changing the layout zoom level limits, and it +is beyond Electron's capacity to maintain it. The function will emit a warning +in Electron 8.x, and cease to exist in Electron 9.x. The layout zoom level +limits are now fixed at a minimum of 0.25 and a maximum of 5.0, as defined +[here](https://chromium.googlesource.com/chromium/src/+/938b37a6d2886bf8335fc7db792f1eb46c65b2ae/third_party/blink/common/page/page_zoom.cc#11). + +### Deprecated events in `systemPreferences` + +The following `systemPreferences` events have been deprecated: + +* `inverted-color-scheme-changed` +* `high-contrast-color-scheme-changed` + +Use the new `updated` event on the `nativeTheme` module instead. + +```js +// Deprecated +systemPreferences.on('inverted-color-scheme-changed', () => { /* ... */ }) +systemPreferences.on('high-contrast-color-scheme-changed', () => { /* ... */ }) + +// Replace with +nativeTheme.on('updated', () => { /* ... */ }) +``` + +### Deprecated: methods in `systemPreferences` + +The following `systemPreferences` methods have been deprecated: + +* `systemPreferences.isDarkMode()` +* `systemPreferences.isInvertedColorScheme()` +* `systemPreferences.isHighContrastColorScheme()` + +Use the following `nativeTheme` properties instead: + +* `nativeTheme.shouldUseDarkColors` +* `nativeTheme.shouldUseInvertedColorScheme` +* `nativeTheme.shouldUseHighContrastColors` + +```js +// Deprecated +systemPreferences.isDarkMode() +// Replace with +nativeTheme.shouldUseDarkColors + +// Deprecated +systemPreferences.isInvertedColorScheme() +// Replace with +nativeTheme.shouldUseInvertedColorScheme + +// Deprecated +systemPreferences.isHighContrastColorScheme() +// Replace with +nativeTheme.shouldUseHighContrastColors +``` + +## Planned Breaking API Changes (7.0) + +### Deprecated: Atom.io Node Headers URL + +This is the URL specified as `disturl` in a `.npmrc` file or as the `--dist-url` +command line flag when building native Node modules. Both will be supported for +the foreseeable future but it is recommended that you switch. + +Deprecated: https://atom.io/download/electron + +Replace with: https://electronjs.org/headers + +### API Changed: `session.clearAuthCache()` no longer accepts options + +The `session.clearAuthCache` API no longer accepts options for what to clear, and instead unconditionally clears the whole cache. + +```js +// Deprecated +session.clearAuthCache({ type: 'password' }) +// Replace with +session.clearAuthCache() +``` + +### API Changed: `powerMonitor.querySystemIdleState` is now `powerMonitor.getSystemIdleState` + +```js +// Removed in Electron 7.0 +powerMonitor.querySystemIdleState(threshold, callback) +// Replace with synchronous API +const idleState = powerMonitor.getSystemIdleState(threshold) +``` + +### API Changed: `powerMonitor.querySystemIdleTime` is now `powerMonitor.getSystemIdleTime` + +```js +// Removed in Electron 7.0 +powerMonitor.querySystemIdleTime(callback) +// Replace with synchronous API +const idleTime = powerMonitor.getSystemIdleTime() +``` + +### API Changed: `webFrame.setIsolatedWorldInfo` replaces separate methods + +```js +// Removed in Electron 7.0 +webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp) +webFrame.setIsolatedWorldHumanReadableName(worldId, name) +webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin) +// Replace with +webFrame.setIsolatedWorldInfo( + worldId, + { + securityOrigin: 'some_origin', + name: 'human_readable_name', + csp: 'content_security_policy' + }) +``` + +### Removed: `marked` property on `getBlinkMemoryInfo` + +This property was removed in Chromium 77, and as such is no longer available. + +### Behavior Changed: `webkitdirectory` attribute for `<input type="file"/>` now lists directory contents + +The `webkitdirectory` property on HTML file inputs allows them to select folders. +Previous versions of Electron had an incorrect implementation where the `event.target.files` +of the input returned a `FileList` that returned one `File` corresponding to the selected folder. + +As of Electron 7, that `FileList` is now list of all files contained within +the folder, similarly to Chrome, Firefox, and Edge +([link to MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory)). + +As an illustration, take a folder with this structure: + +```console +folder +├── file1 +├── file2 +└── file3 +``` + +In Electron <=6, this would return a `FileList` with a `File` object for: + +```console +path/to/folder +``` + +In Electron 7, this now returns a `FileList` with a `File` object for: + +```console +/path/to/folder/file3 +/path/to/folder/file2 +/path/to/folder/file1 +``` + +Note that `webkitdirectory` no longer exposes the path to the selected folder. +If you require the path to the selected folder rather than the folder contents, +see the `dialog.showOpenDialog` API ([link](api/dialog.md#dialogshowopendialogbrowserwindow-options)). + +### API Changed: Callback-based versions of promisified APIs + +Electron 5 and Electron 6 introduced Promise-based versions of existing +asynchronous APIs and deprecated their older, callback-based counterparts. +In Electron 7, all deprecated callback-based APIs are now removed. + +These functions now only return Promises: + +* `app.getFileIcon()` [#15742](https://github.com/electron/electron/pull/15742) +* `app.dock.show()` [#16904](https://github.com/electron/electron/pull/16904) +* `contentTracing.getCategories()` [#16583](https://github.com/electron/electron/pull/16583) +* `contentTracing.getTraceBufferUsage()` [#16600](https://github.com/electron/electron/pull/16600) +* `contentTracing.startRecording()` [#16584](https://github.com/electron/electron/pull/16584) +* `contentTracing.stopRecording()` [#16584](https://github.com/electron/electron/pull/16584) +* `contents.executeJavaScript()` [#17312](https://github.com/electron/electron/pull/17312) +* `cookies.flushStore()` [#16464](https://github.com/electron/electron/pull/16464) +* `cookies.get()` [#16464](https://github.com/electron/electron/pull/16464) +* `cookies.remove()` [#16464](https://github.com/electron/electron/pull/16464) +* `cookies.set()` [#16464](https://github.com/electron/electron/pull/16464) +* `debugger.sendCommand()` [#16861](https://github.com/electron/electron/pull/16861) +* `dialog.showCertificateTrustDialog()` [#17181](https://github.com/electron/electron/pull/17181) +* `inAppPurchase.getProducts()` [#17355](https://github.com/electron/electron/pull/17355) +* `inAppPurchase.purchaseProduct()`[#17355](https://github.com/electron/electron/pull/17355) +* `netLog.stopLogging()` [#16862](https://github.com/electron/electron/pull/16862) +* `session.clearAuthCache()` [#17259](https://github.com/electron/electron/pull/17259) +* `session.clearCache()` [#17185](https://github.com/electron/electron/pull/17185) +* `session.clearHostResolverCache()` [#17229](https://github.com/electron/electron/pull/17229) +* `session.clearStorageData()` [#17249](https://github.com/electron/electron/pull/17249) +* `session.getBlobData()` [#17303](https://github.com/electron/electron/pull/17303) +* `session.getCacheSize()` [#17185](https://github.com/electron/electron/pull/17185) +* `session.resolveProxy()` [#17222](https://github.com/electron/electron/pull/17222) +* `session.setProxy()` [#17222](https://github.com/electron/electron/pull/17222) +* `shell.openExternal()` [#16176](https://github.com/electron/electron/pull/16176) +* `webContents.loadFile()` [#15855](https://github.com/electron/electron/pull/15855) +* `webContents.loadURL()` [#15855](https://github.com/electron/electron/pull/15855) +* `webContents.hasServiceWorker()` [#16535](https://github.com/electron/electron/pull/16535) +* `webContents.printToPDF()` [#16795](https://github.com/electron/electron/pull/16795) +* `webContents.savePage()` [#16742](https://github.com/electron/electron/pull/16742) +* `webFrame.executeJavaScript()` [#17312](https://github.com/electron/electron/pull/17312) +* `webFrame.executeJavaScriptInIsolatedWorld()` [#17312](https://github.com/electron/electron/pull/17312) +* `webviewTag.executeJavaScript()` [#17312](https://github.com/electron/electron/pull/17312) +* `win.capturePage()` [#15743](https://github.com/electron/electron/pull/15743) + +These functions now have two forms, synchronous and Promise-based asynchronous: + +* `dialog.showMessageBox()`/`dialog.showMessageBoxSync()` [#17298](https://github.com/electron/electron/pull/17298) +* `dialog.showOpenDialog()`/`dialog.showOpenDialogSync()` [#16973](https://github.com/electron/electron/pull/16973) +* `dialog.showSaveDialog()`/`dialog.showSaveDialogSync()` [#17054](https://github.com/electron/electron/pull/17054) + +## Planned Breaking API Changes (6.0) + +### API Changed: `win.setMenu(null)` is now `win.removeMenu()` + +```js +// Deprecated +win.setMenu(null) +// Replace with +win.removeMenu() +``` + +### API Changed: `electron.screen` in the renderer process should be accessed via `remote` + +```js +// Deprecated +require('electron').screen +// Replace with +require('electron').remote.screen +``` + +### API Changed: `require()`ing node builtins in sandboxed renderers no longer implicitly loads the `remote` version + +```js +// Deprecated +require('child_process') +// Replace with +require('electron').remote.require('child_process') + +// Deprecated +require('fs') +// Replace with +require('electron').remote.require('fs') + +// Deprecated +require('os') +// Replace with +require('electron').remote.require('os') + +// Deprecated +require('path') +// Replace with +require('electron').remote.require('path') +``` + +### Deprecated: `powerMonitor.querySystemIdleState` replaced with `powerMonitor.getSystemIdleState` + +```js +// Deprecated +powerMonitor.querySystemIdleState(threshold, callback) +// Replace with synchronous API +const idleState = powerMonitor.getSystemIdleState(threshold) +``` + +### Deprecated: `powerMonitor.querySystemIdleTime` replaced with `powerMonitor.getSystemIdleTime` + +```js +// Deprecated +powerMonitor.querySystemIdleTime(callback) +// Replace with synchronous API +const idleTime = powerMonitor.getSystemIdleTime() +``` + +### Deprecated: `app.enableMixedSandbox()` is no longer needed + +```js +// Deprecated +app.enableMixedSandbox() +``` + +Mixed-sandbox mode is now enabled by default. + +### Deprecated: `Tray.setHighlightMode` + +Under macOS Catalina our former Tray implementation breaks. +Apple's native substitute doesn't support changing the highlighting behavior. + +```js +// Deprecated +tray.setHighlightMode(mode) +// API will be removed in v7.0 without replacement. +``` + +## Planned Breaking API Changes (5.0) + +### Default Changed: `nodeIntegration` and `webviewTag` default to false, `contextIsolation` defaults to true + +The following `webPreferences` option default values are deprecated in favor of the new defaults listed below. + +| Property | Deprecated Default | New Default | +|----------|--------------------|-------------| +| `contextIsolation` | `false` | `true` | +| `nodeIntegration` | `true` | `false` | +| `webviewTag` | `nodeIntegration` if set else `true` | `false` | + +E.g. Re-enabling the webviewTag + +```js +const w = new BrowserWindow({ + webPreferences: { + webviewTag: true + } +}) +``` + +### Behavior Changed: `nodeIntegration` in child windows opened via `nativeWindowOpen` + +Child windows opened with the `nativeWindowOpen` option will always have Node.js integration disabled, unless `nodeIntegrationInSubFrames` is `true`. + +### API Changed: Registering privileged schemes must now be done before app ready + +Renderer process APIs `webFrame.registerURLSchemeAsPrivileged` and `webFrame.registerURLSchemeAsBypassingCSP` as well as browser process API `protocol.registerStandardSchemes` have been removed. +A new API, `protocol.registerSchemesAsPrivileged` has been added and should be used for registering custom schemes with the required privileges. Custom schemes are required to be registered before app ready. + +### Deprecated: `webFrame.setIsolatedWorld*` replaced with `webFrame.setIsolatedWorldInfo` + +```js +// Deprecated +webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp) +webFrame.setIsolatedWorldHumanReadableName(worldId, name) +webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin) +// Replace with +webFrame.setIsolatedWorldInfo( + worldId, + { + securityOrigin: 'some_origin', + name: 'human_readable_name', + csp: 'content_security_policy' + }) +``` + +### API Changed: `webFrame.setSpellCheckProvider` now takes an asynchronous callback + +The `spellCheck` callback is now asynchronous, and `autoCorrectWord` parameter has been removed. + +```js +// Deprecated +webFrame.setSpellCheckProvider('en-US', true, { + spellCheck: (text) => { + return !spellchecker.isMisspelled(text) + } +}) +// Replace with +webFrame.setSpellCheckProvider('en-US', { + spellCheck: (words, callback) => { + callback(words.filter(text => spellchecker.isMisspelled(text))) + } +}) +``` + +### API Changed: `webContents.getZoomLevel` and `webContents.getZoomFactor` are now synchronous + +`webContents.getZoomLevel` and `webContents.getZoomFactor` no longer take callback parameters, +instead directly returning their number values. + +```js +// Deprecated +webContents.getZoomLevel((level) => { + console.log(level) +}) +// Replace with +const level = webContents.getZoomLevel() +console.log(level) +``` + +```js +// Deprecated +webContents.getZoomFactor((factor) => { + console.log(factor) +}) +// Replace with +const factor = webContents.getZoomFactor() +console.log(factor) +``` + +## Planned Breaking API Changes (4.0) + +The following list includes the breaking API changes made in Electron 4.0. + +### `app.makeSingleInstance` + +```js +// Deprecated +app.makeSingleInstance((argv, cwd) => { + /* ... */ +}) +// Replace with +app.requestSingleInstanceLock() +app.on('second-instance', (event, argv, cwd) => { + /* ... */ +}) +``` + +### `app.releaseSingleInstance` + +```js +// Deprecated +app.releaseSingleInstance() +// Replace with +app.releaseSingleInstanceLock() +``` + +### `app.getGPUInfo` + +```js +app.getGPUInfo('complete') +// Now behaves the same with `basic` on macOS +app.getGPUInfo('basic') +``` + +### `win_delay_load_hook` + +When building native modules for windows, the `win_delay_load_hook` variable in +the module's `binding.gyp` must be true (which is the default). If this hook is +not present, then the native module will fail to load on Windows, with an error +message like `Cannot find module`. See the [native module +guide](/docs/tutorial/using-native-node-modules.md) for more. + +## Breaking API Changes (3.0) + +The following list includes the breaking API changes in Electron 3.0. + +### `app` + +```js +// Deprecated +app.getAppMemoryInfo() +// Replace with +app.getAppMetrics() + +// Deprecated +const metrics = app.getAppMetrics() +const { memory } = metrics[0] // Deprecated property +``` + +### `BrowserWindow` + +```js +// Deprecated +const optionsA = { webPreferences: { blinkFeatures: '' } } +const windowA = new BrowserWindow(optionsA) +// Replace with +const optionsB = { webPreferences: { enableBlinkFeatures: '' } } +const windowB = new BrowserWindow(optionsB) + +// Deprecated +window.on('app-command', (e, cmd) => { + if (cmd === 'media-play_pause') { + // do something + } +}) +// Replace with +window.on('app-command', (e, cmd) => { + if (cmd === 'media-play-pause') { + // do something + } +}) +``` + +### `clipboard` + +```js +// Deprecated +clipboard.readRtf() +// Replace with +clipboard.readRTF() + +// Deprecated +clipboard.writeRtf() +// Replace with +clipboard.writeRTF() + +// Deprecated +clipboard.readHtml() +// Replace with +clipboard.readHTML() + +// Deprecated +clipboard.writeHtml() +// Replace with +clipboard.writeHTML() +``` + +### `crashReporter` + +```js +// Deprecated +crashReporter.start({ + companyName: 'Crashly', + submitURL: 'https://crash.server.com', + autoSubmit: true +}) +// Replace with +crashReporter.start({ + companyName: 'Crashly', + submitURL: 'https://crash.server.com', + uploadToServer: true +}) +``` + +### `nativeImage` + +```js +// Deprecated +nativeImage.createFromBuffer(buffer, 1.0) +// Replace with +nativeImage.createFromBuffer(buffer, { + scaleFactor: 1.0 +}) +``` + +### `process` + +```js +// Deprecated +const info = process.getProcessMemoryInfo() +``` + +### `screen` + +```js +// Deprecated +screen.getMenuBarHeight() +// Replace with +screen.getPrimaryDisplay().workArea +``` + +### `session` + +```js +// Deprecated +ses.setCertificateVerifyProc((hostname, certificate, callback) => { + callback(true) +}) +// Replace with +ses.setCertificateVerifyProc((request, callback) => { + callback(0) +}) +``` + +### `Tray` + +```js +// Deprecated +tray.setHighlightMode(true) +// Replace with +tray.setHighlightMode('on') + +// Deprecated +tray.setHighlightMode(false) +// Replace with +tray.setHighlightMode('off') +``` + +### `webContents` + +```js +// Deprecated +webContents.openDevTools({ detach: true }) +// Replace with +webContents.openDevTools({ mode: 'detach' }) + +// Removed +webContents.setSize(options) +// There is no replacement for this API +``` + +### `webFrame` + +```js +// Deprecated +webFrame.registerURLSchemeAsSecure('app') +// Replace with +protocol.registerStandardSchemes(['app'], { secure: true }) + +// Deprecated +webFrame.registerURLSchemeAsPrivileged('app', { secure: true }) +// Replace with +protocol.registerStandardSchemes(['app'], { secure: true }) +``` + +### `<webview>` + +```js +// Removed +webview.setAttribute('disableguestresize', '') +// There is no replacement for this API + +// Removed +webview.setAttribute('guestinstance', instanceId) +// There is no replacement for this API + +// Keyboard listeners no longer work on webview tag +webview.onkeydown = () => { /* handler */ } +webview.onkeyup = () => { /* handler */ } +``` + +### Node Headers URL + +This is the URL specified as `disturl` in a `.npmrc` file or as the `--dist-url` +command line flag when building native Node modules. + +Deprecated: https://atom.io/download/atom-shell + +Replace with: https://atom.io/download/electron + +## Breaking API Changes (2.0) + +The following list includes the breaking API changes made in Electron 2.0. + +### `BrowserWindow` + +```js +// Deprecated +const optionsA = { titleBarStyle: 'hidden-inset' } +const windowA = new BrowserWindow(optionsA) +// Replace with +const optionsB = { titleBarStyle: 'hiddenInset' } +const windowB = new BrowserWindow(optionsB) +``` + +### `menu` + +```js +// Removed +menu.popup(browserWindow, 100, 200, 2) +// Replaced with +menu.popup(browserWindow, { x: 100, y: 200, positioningItem: 2 }) +``` + +### `nativeImage` + +```js +// Removed +nativeImage.toPng() +// Replaced with +nativeImage.toPNG() + +// Removed +nativeImage.toJpeg() +// Replaced with +nativeImage.toJPEG() +``` + +### `process` + +* `process.versions.electron` and `process.version.chrome` will be made + read-only properties for consistency with the other `process.versions` + properties set by Node. + +### `webContents` + +```js +// Removed +webContents.setZoomLevelLimits(1, 2) +// Replaced with +webContents.setVisualZoomLevelLimits(1, 2) +``` + +### `webFrame` + +```js +// Removed +webFrame.setZoomLevelLimits(1, 2) +// Replaced with +webFrame.setVisualZoomLevelLimits(1, 2) +``` + +### `<webview>` + +```js +// Removed +webview.setZoomLevelLimits(1, 2) +// Replaced with +webview.setVisualZoomLevelLimits(1, 2) +``` + +### Duplicate ARM Assets + +Each Electron release includes two identical ARM builds with slightly different +filenames, like `electron-v1.7.3-linux-arm.zip` and +`electron-v1.7.3-linux-armv7l.zip`. The asset with the `v7l` prefix was added +to clarify to users which ARM version it supports, and to disambiguate it from +future armv6l and arm64 assets that may be produced. + +The file _without the prefix_ is still being published to avoid breaking any +setups that may be consuming it. Starting at 2.0, the unprefixed file will +no longer be published. + +For details, see +[6986](https://github.com/electron/electron/pull/6986) +and +[7189](https://github.com/electron/electron/pull/7189). diff --git a/docs/development/README.md b/docs/development/README.md index cef714f2eeeea..6ddbbcdbae800 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -4,24 +4,77 @@ These guides are intended for people working on the Electron project itself. For guides on Electron app development, see [/docs/README.md](../README.md#guides-and-tutorials). -* [Code of Conduct](../../CODE_OF_CONDUCT.md) -* [Contributing to Electron](../../CONTRIBUTING.md) +## Table of Contents + * [Issues](issues.md) * [Pull Requests](pull-requests.md) * [Documentation Styleguide](coding-style.md#documentation) * [Source Code Directory Structure](source-code-directory-structure.md) * [Coding Style](coding-style.md) -* [Using clang-format on C++ Code](clang-format.md) -* [Build System Overview](build-system-overview.md) -* [Build Instructions (macOS)](build-instructions-macos.md) -* [Build Instructions (Windows)](build-instructions-windows.md) -* [Build Instructions (Linux)](build-instructions-linux.md) +* [Using clang-tidy on C++ Code](clang-tidy.md) +* [Build Instructions](build-instructions-gn.md) + * [macOS](build-instructions-macos.md) + * [Windows](build-instructions-windows.md) + * [Linux](build-instructions-linux.md) * [Chromium Development](chromium-development.md) * [V8 Development](v8-development.md) * [Testing](testing.md) -* [Debugging on Windows](debug-instructions-windows.md) -* [Debugging on macOS](debugging-instructions-macos.md) -* [Setting Up Symbol Server in Debugger](setting-up-symbol-server.md) -* [Upgrading Chromium](upgrading-chromium.md) -* [Upgrading Crashpad](upgrading-crashpad.md) -* [Upgrading Node](upgrading-node.md) +* [Debugging](debugging.md) +* [Patches](patches.md) + +## Getting Started + +In order to contribute to Electron, the first thing you'll want to do is get the code. + +[Electron's `build-tools`](https://github.com/electron/build-tools) automate much of the setup for compiling Electron from source with different configurations and build targets. + +If you would prefer to build Electron manually, see the [build instructions](build-instructions-gn.md). + +Once you've checked out and built the code, you may want to take a look around the source tree to get a better idea +of what each directory is responsible for. The [source code directory structure](source-code-directory-structure.md) gives a good overview of the purpose of each directory. + +## Opening Issues on Electron + +For any issue, there are generally three ways an individual can contribute: + +1. By opening the issue for discussion + * If you believe that you have found a new bug in Electron, you should report it by creating a new issue in + the [`electron/electron` issue tracker](https://github.com/electron/electron/issues). +2. By helping to triage the issue + * You can do this either by providing assistive details (a reproducible test case that demonstrates a bug) or by providing suggestions to address the issue. +3. By helping to resolve the issue + * This can be done by demonstrating that the issue is not a bug or is fixed; + but more often, by opening a pull request that changes the source in `electron/electron` + in a concrete and reviewable manner. + +See [issues](issues.md) for more information. + +## Making a Pull Request to Electron + +Most pull requests opened against the `electron/electron` repository include +changes to either the C/C++ code in the `shell/` folder, +the TypeScript code in the `lib/` folder, the documentation in `docs/`, +or tests in the `spec/` and `spec-main/` folders. + +See [pull requests](pull-requests.md) for more information. + +If you want to add a new API module to Electron, you'll want to look in [creating API](creating-api.md). + +## Governance + +Electron has a fully-fledged governance system that oversees activity in Electron and whose working groups are responsible for areas like APIs, releases, and upgrades to Electron's dependencies including Chromium and Node.js. Depending on how frequently and to what end you want to contribute, you may want to consider joining a working group. + +Details about each group and their reponsibilities can be found in the [governance repo](https://github.com/electron/governance). + +## Patches in Electron + +Electron is built on two major upstream projects: Chromium and Node.js. Each of these projects has several of their own dependencies, too. We try our best to use these dependencies exactly as they are but sometimes we can't achieve our goals without patching those upstream dependencies to fit our use cases. + +As such, we maintain a collection of patches as part of our source tree. The process for adding or altering one of these patches to Electron's source tree via a pull request can be found in [patches](patches.md). + +## Debugging + +There are many different approaches to debugging issues and bugs in Electron, many of which +are platform specific. + +For an overview of information related to debugging Electron itself (and not an app _built with Electron_), see [debugging](debugging.md). diff --git a/docs/development/atom-shell-vs-node-webkit.md b/docs/development/atom-shell-vs-node-webkit.md deleted file mode 100644 index a419caea10dcb..0000000000000 --- a/docs/development/atom-shell-vs-node-webkit.md +++ /dev/null @@ -1,52 +0,0 @@ -# Technical Differences Between Electron and NW.js (formerly node-webkit) - -__Note: Electron was previously named Atom Shell.__ - -Like NW.js, Electron provides a platform to write desktop applications -with JavaScript and HTML and has Node integration to grant access to the low -level system from web pages. - -But there are also fundamental differences between the two projects that make -Electron a completely separate product from NW.js: - -__1. Entry of Application__ - -In NW.js the main entry point of an application is a web page or a JS script. You specify a -html or js file in the `package.json` and it is opened in a browser window as -the application's main window (in case of an html entrypoint) or the script is executed. - -In Electron, the entry point is a JavaScript script. Instead of -providing a URL directly, you manually create a browser window and load -an HTML file using the API. You also need to listen to window events -to decide when to quit the application. - -Electron works more like the Node.js runtime. Electron's APIs are lower level -so you can use it for browser testing in place of [PhantomJS](http://phantomjs.org/). - -__2. Build System__ - -In order to avoid the complexity of building all of Chromium, Electron uses [`libchromiumcontent`](https://github.com/electron/libchromiumcontent) to access -Chromium's Content API. `libchromiumcontent` is a single shared library that -includes the Chromium Content module and all of its dependencies. Users don't -need a powerful machine to build Electron. - -__3. Node Integration__ - -In NW.js, the Node integration in web pages requires patching Chromium to -work, while in Electron we chose a different way to integrate the libuv loop -with each platform's message loop to avoid hacking Chromium. See the -[`node_bindings`][node-bindings] code for how that was done. - -__4. Multi-context__ - -If you are an experienced NW.js user, you should be familiar with the -concept of Node context and web context. These concepts were invented because -of how NW.js was implemented. - -By using the [multi-context](https://github.com/nodejs/node-v0.x-archive/commit/756b622) -feature of Node, Electron doesn't introduce a new JavaScript context in web -pages. - -Note: NW.js has optionally supported multi-context since 0.13. - -[node-bindings]: https://github.com/electron/electron/tree/master/atom/common diff --git a/docs/development/azure-vm-setup.md b/docs/development/azure-vm-setup.md index d7ce7225e0347..477e9ee781de0 100644 --- a/docs/development/azure-vm-setup.md +++ b/docs/development/azure-vm-setup.md @@ -8,7 +8,7 @@ Example Use Case: * We need `VS15.9` and we have `VS15.7` installed; this would require us to update an Azure image. 1. Identify the image you wish to modify. - * In [appveyor.yml](https://github.com/electron/electron/blob/master/appveyor.yml), the image is identified by the property *image*. + * In [appveyor.yml](https://github.com/electron/electron/blob/main/appveyor.yml), the image is identified by the property *image*. * The names used correspond to the *"images"* defined for a build cloud, eg the [libcc-20 cloud](https://windows-ci.electronjs.org/build-clouds/8). * Find the image you wish to modify in the build cloud and make note of the **VHD Blob Path** for that image, which is the value for that corresponding key. * You will need this URI path to copy into a new image. @@ -49,7 +49,7 @@ Example Use Case: * Master VHD URI - use URI obtained @ end of previous step * Location use `East US` -6. Log back into Azure and find the VM you just created in Homee < Virtual Machines < `$YOUR_NEW_VM` +6. Log back into Azure and find the VM you just created in Home < Virtual Machines < `$YOUR_NEW_VM` * You can download a RDP (Remote Desktop) file to access the VM. 7. Using Microsoft Remote Desktop, click `Connect` to connect to the VM. diff --git a/docs/development/build-instructions-gn.md b/docs/development/build-instructions-gn.md index 2a5c6d55ffedc..70b5c48ed857c 100644 --- a/docs/development/build-instructions-gn.md +++ b/docs/development/build-instructions-gn.md @@ -1,14 +1,33 @@ # Build Instructions -Follow the guidelines below for building Electron. +Follow the guidelines below for building **Electron itself**, for the purposes of creating custom Electron binaries. For bundling and distributing your app code with the prebuilt Electron binaries, see the [application distribution][application-distribution] guide. + +[application-distribution]: ../tutorial/application-distribution.md ## Platform prerequisites Check the build prerequisites for your platform before proceeding - * [macOS](build-instructions-macos.md#prerequisites) - * [Linux](build-instructions-linux.md#prerequisites) - * [Windows](build-instructions-windows.md#prerequisites) +* [macOS](build-instructions-macos.md#prerequisites) +* [Linux](build-instructions-linux.md#prerequisites) +* [Windows](build-instructions-windows.md#prerequisites) + +## Build Tools + +[Electron's Build Tools](https://github.com/electron/build-tools) automate much of the setup for compiling Electron from source with different configurations and build targets. If you wish to set up the environment manually, the instructions are listed below. + +Electron uses [GN](https://gn.googlesource.com/gn) for project generation and +[ninja](https://ninja-build.org/) for building. Project configurations can +be found in the `.gn` and `.gni` files. + +## GN Files + +The following `gn` files contain the main rules for building Electron: + +* `BUILD.gn` defines how Electron itself is built and + includes the default configurations for linking with Chromium. +* `build/args/{testing,release,all}.gn` contain the default build arguments for + building Electron. ## GN prerequisites @@ -22,15 +41,14 @@ Security` → `System` → `Advanced system settings` and add a system variable your locally installed version of Visual Studio (by default, `depot_tools` will try to download a Google-internal version that only Googlers have access to). -[depot-tools]: http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up - -## Cached builds (optional step) +[depot-tools]: https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up -### GIT\_CACHE\_PATH +### Setting up the git cache -If you plan on building Electron more than once, adding a git cache will -speed up subsequent calls to `gclient`. To do this, set a `GIT_CACHE_PATH` -environment variable: +If you plan on checking out Electron more than once (for example, to have +multiple parallel directories checked out to different branches), using the git +cache will speed up subsequent calls to `gclient`. To do this, set a +`GIT_CACHE_PATH` environment variable: ```sh $ export GIT_CACHE_PATH="${HOME}/.git_cache" @@ -38,22 +56,10 @@ $ mkdir -p "${GIT_CACHE_PATH}" # This will use about 16G. ``` -### sccache - -Thousands of files must be compiled to build Chromium and Electron. -You can avoid much of the wait by reusing Electron CI's build output via -[sccache](https://github.com/mozilla/sccache). This requires some -optional steps (listed below) and these two environment variables: - -```sh -export SCCACHE_BUCKET="electronjs-sccache" -export SCCACHE_TWO_TIER=true -``` - ## Getting the code ```sh -$ mkdir electron-gn && cd electron-gn +$ mkdir electron && cd electron $ gclient config --name "src/electron" --unmanaged https://github.com/electron/electron $ gclient sync --with_branch_heads --with_tags # This will take a while, go get a coffee. @@ -62,7 +68,7 @@ $ gclient sync --with_branch_heads --with_tags > Instead of `https://github.com/electron/electron`, you can use your own fork > here (something like `https://github.com/<username>/electron`). -#### A note on pulling/pushing +### A note on pulling/pushing If you intend to `git pull` or `git push` from the official `electron` repository in the future, you now need to update the respective folder's @@ -72,7 +78,8 @@ origin URLs. $ cd src/electron $ git remote remove origin $ git remote add origin https://github.com/electron/electron -$ git branch --set-upstream-to=origin/master +$ git checkout main +$ git branch --set-upstream-to=origin/main $ cd - ``` @@ -82,6 +89,7 @@ Running `gclient sync -f` ensures that all dependencies required to build Electron match that file. So, in order to pull, you'd run the following commands: + ```sh $ cd src/electron $ git pull @@ -90,53 +98,49 @@ $ gclient sync -f ## Building +**Set the environment variable for chromium build tools** + +On Linux & MacOS + ```sh $ cd src $ export CHROMIUM_BUILDTOOLS_PATH=`pwd`/buildtools -# this next line is needed only if building with sccache -$ export GN_EXTRA_ARGS="${GN_EXTRA_ARGS} cc_wrapper=\"${PWD}/electron/external_binaries/sccache\"" -$ gn gen out/Debug --args="import(\"//electron/build/args/debug.gn\") $GN_EXTRA_ARGS" ``` -Or on Windows (without the optional argument): +On Windows: + ```sh $ cd src $ set CHROMIUM_BUILDTOOLS_PATH=%cd%\buildtools -$ gn gen out/Debug --args="import(\"//electron/build/args/debug.gn\")" ``` -This will generate a build directory `out/Debug` under `src/` with -debug build configuration. You can replace `Debug` with another name, -but it should be a subdirectory of `out`. -Also you shouldn't have to run `gn gen` again—if you want to change the -build arguments, you can run `gn args out/Debug` to bring up an editor. - -To see the list of available build configuration options, run `gn args -out/Debug --list`. - -**For generating Debug (aka "component" or "shared") build config of -Electron:** +**To generate Testing build config of Electron:** ```sh -$ gn gen out/Debug --args="import(\"//electron/build/args/debug.gn\") $GN_EXTRA_ARGS" +$ gn gen out/Testing --args="import(\"//electron/build/args/testing.gn\")" ``` -**For generating Release (aka "non-component" or "static") build config of -Electron:** +**To generate Release build config of Electron:** ```sh -$ gn gen out/Release --args="import(\"//electron/build/args/release.gn\") $GN_EXTRA_ARGS" +$ gn gen out/Release --args="import(\"//electron/build/args/release.gn\")" ``` +**Note:** This will generate a `out/Testing` or `out/Release` build directory under `src/` with the testing or release build depending upon the configuration passed above. You can replace `Testing|Release` with another names, but it should be a subdirectory of `out`. + +Also you shouldn't have to run `gn gen` again—if you want to change the build arguments, you can run `gn args out/Testing` to bring up an editor. To see the list of available build configuration options, run `gn args out/Testing --list`. + **To build, run `ninja` with the `electron` target:** -Nota Bene: This will also take a while and probably heat up your lap. +Note: This will also take a while and probably heat up your lap. + +For the testing configuration: -For the debug configuration: ```sh -$ ninja -C out/Debug electron +$ ninja -C out/Testing electron ``` For the release configuration: + ```sh $ ninja -C out/Release electron ``` @@ -145,32 +149,28 @@ This will build all of what was previously 'libchromiumcontent' (i.e. the `content/` directory of `chromium` and its dependencies, incl. WebKit and V8), so it will take a while. -To speed up subsequent builds, you can use [sccache][sccache]. Add the GN arg -`cc_wrapper = "sccache"` by running `gn args out/Debug` to bring up an -editor and adding a line to the end of the file. - -[sccache]: https://github.com/mozilla/sccache - -The built executable will be under `./out/Debug`: +The built executable will be under `./out/Testing`: ```sh -$ ./out/Debug/Electron.app/Contents/MacOS/Electron +$ ./out/Testing/Electron.app/Contents/MacOS/Electron # or, on Windows -$ ./out/Debug/electron.exe +$ ./out/Testing/electron.exe # or, on Linux -$ ./out/Debug/electron +$ ./out/Testing/electron ``` ### Packaging On linux, first strip the debugging and symbol information: + ```sh -electron/script/strip-binaries.py -d out/Release +$ electron/script/strip-binaries.py -d out/Release ``` To package the electron build as a distributable zip file: + ```sh -ninja -C out/Release electron:electron_dist_zip +$ ninja -C out/Release electron:electron_dist_zip ``` ### Cross-compiling @@ -180,17 +180,16 @@ set the `target_cpu` and `target_os` GN arguments. For example, to compile an x86 target from an x64 host, specify `target_cpu = "x86"` in `gn args`. ```sh -$ gn gen out/Debug-x86 --args='... target_cpu = "x86"' +$ gn gen out/Testing-x86 --args='... target_cpu = "x86"' ``` Not all combinations of source and target CPU/OS are supported by Chromium. -<table> -<tr><th>Host</th><th>Target</th><th>Status</th></tr> -<tr><td>Windows x64</td><td>Windows arm64</td><td>Experimental</td> -<tr><td>Windows x64</td><td>Windows x86</td><td>Automatically tested</td></tr> -<tr><td>Linux x64</td><td>Linux x86</td><td>Automatically tested</td></tr> -</table> +| Host | Target | Status | +|-------------|---------------|----------------------| +| Windows x64 | Windows arm64 | Experimental | +| Windows x64 | Windows x86 | Automatically tested | +| Linux x64 | Linux x86 | Automatically tested | If you test other combinations and find them to work, please update this document :) @@ -201,6 +200,7 @@ and [`target_cpu`][target_cpu values]. [target_cpu values]: https://gn.googlesource.com/gn/+/master/docs/reference.md#built_in-predefined-variables-target_cpu_the-desired-cpu-architecture-for-the-build-possible-values #### Windows on Arm (experimental) + To cross-compile for Windows on Arm, [follow Chromium's guide](https://chromium.googlesource.com/chromium/src/+/refs/heads/master/docs/windows_build_instructions.md#Visual-Studio) to get the necessary dependencies, SDK and libraries, then build with `ELECTRON_BUILDING_WOA=1` in your environment before running `gclient sync`. ```bat @@ -209,6 +209,7 @@ gclient sync -f --with_branch_heads --with_tags ``` Or (if using PowerShell): + ```powershell $env:ELECTRON_BUILDING_WOA=1 gclient sync -f --with_branch_heads --with_tags @@ -216,7 +217,6 @@ gclient sync -f --with_branch_heads --with_tags Next, run `gn gen` as above with `target_cpu="arm64"`. - ## Tests To run the tests, you'll first need to build the test modules against the @@ -225,28 +225,17 @@ generate build headers for the modules to compile against, run the following under `src/` directory. ```sh -$ ninja -C out/Debug third_party/electron_node:headers -# Install the test modules with the generated headers -$ (cd electron/spec && npm i --nodedir=../../out/Debug/gen/node_headers) +$ ninja -C out/Testing third_party/electron_node:headers ``` -Then, run Electron with `electron/spec` as the argument: - -```sh -# on Mac: -$ ./out/Debug/Electron.app/Contents/MacOS/Electron electron/spec -# on Windows: -$ ./out/Debug/electron.exe electron/spec -# on Linux: -$ ./out/Debug/electron electron/spec -``` +You can now [run the tests](testing.md#unit-tests). If you're debugging something, it can be helpful to pass some extra flags to the Electron binary: ```sh -$ ./out/Debug/Electron.app/Contents/MacOS/Electron electron/spec \ - --ci --enable-logging -g 'BrowserWindow module' +$ npm run test -- \ + --enable-logging -g 'BrowserWindow module' ``` ## Sharing the git cache between multiple machines @@ -273,12 +262,27 @@ New-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\Lanmanworkstatio ## Troubleshooting -### Stale locks in the git cache -If `gclient sync` is interrupted while using the git cache, it will leave -the cache locked. To remove the lock, pass the `--break_repo_locks` argument to -`gclient sync`. +### gclient sync complains about rebase + +If `gclient sync` is interrupted the git tree may be left in a bad state, leading to a cryptic message when running `gclient sync` in the future: + +```plaintext +2> Conflict while rebasing this branch. +2> Fix the conflict and run gclient again. +2> See man git-rebase for details. +``` + +If there are no git conflicts or rebases in `src/electron`, you may need to abort a `git am` in `src`: + +```sh +$ cd ../ +$ git am --abort +$ cd electron +$ gclient sync -f +``` ### I'm being asked for a username/password for chromium-internal.googlesource.com + If you see a prompt for `Username for 'https://chrome-internal.googlesource.com':` when running `gclient sync` on Windows, it's probably because the `DEPOT_TOOLS_WIN_TOOLCHAIN` environment variable is not set to 0. Open `Control Panel` → `System and Security` → `System` → `Advanced system settings` and add a system variable `DEPOT_TOOLS_WIN_TOOLCHAIN` with value `0`. This tells `depot_tools` to use your locally installed version of Visual Studio (by default, `depot_tools` will diff --git a/docs/development/build-instructions-linux.md b/docs/development/build-instructions-linux.md index 01b8f931915ae..3bdd96863ceba 100644 --- a/docs/development/build-instructions-linux.md +++ b/docs/development/build-instructions-linux.md @@ -1,33 +1,31 @@ # Build Instructions (Linux) -Follow the guidelines below for building Electron on Linux. +Follow the guidelines below for building **Electron itself** on Linux, for the purposes of creating custom Electron binaries. For bundling and distributing your app code with the prebuilt Electron binaries, see the [application distribution][application-distribution] guide. + +[application-distribution]: ../tutorial/application-distribution.md ## Prerequisites * At least 25GB disk space and 8GB RAM. -* Python 2.7.x. Some distributions like CentOS 6.x still use Python 2.6.x - so you may need to check your Python version with `python -V`. - - Please also ensure that your system and Python version support at least TLS 1.2. - For a quick test, run the following script: - - ```sh - $ npx @electron/check-python-tls - ``` - - If the script returns that your configuration is using an outdated security - protocol, use your system's package manager to update Python to the latest - version in the 2.7.x branch. Alternatively, visit https://www.python.org/downloads/ - for detailed instructions. - +* Python >= 3.7. * Node.js. There are various ways to install Node. You can download source code from [nodejs.org](https://nodejs.org) and compile it. Doing so permits installing Node on your own home directory as a standard user. Or try repositories such as [NodeSource](https://nodesource.com/blog/nodejs-v012-iojs-and-the-nodesource-linux-repositories). * [clang](https://clang.llvm.org/get_started.html) 3.4 or later. -* Development headers of GTK+ and libnotify. +* Development headers of GTK 3 and libnotify. -On Ubuntu, install the following libraries: +On Ubuntu >= 20.04, install the following libraries: + +```sh +$ sudo apt-get install build-essential clang libdbus-1-dev libgtk-3-dev \ + libnotify-dev libasound2-dev libcap-dev \ + libcups2-dev libxtst-dev \ + libxss1 libnss3-dev gcc-multilib g++-multilib curl \ + gperf bison python3-dbusmock openjdk-8-jre +``` + +On Ubuntu < 20.04, install the following libraries: ```sh $ sudo apt-get install build-essential clang libdbus-1-dev libgtk-3-dev \ @@ -55,6 +53,15 @@ $ sudo dnf install clang dbus-devel gtk3-devel libnotify-devel \ nss-devel python-dbusmock openjdk-8-jre ``` +On Arch Linux / Manjaro, install the following libraries: + +```sh +$ sudo pacman -Syu base-devel clang libdbus gtk2 libnotify \ + libgnome-keyring alsa-lib libcap libcups libxtst \ + libxss nss gcc-multilib curl gperf bison \ + python2 python-dbusmock jdk8-openjdk +``` + Other distributions may offer similar packages for installation via package managers such as pacman. Or one can compile from source code. @@ -79,7 +86,7 @@ And to cross-compile for `arm` or `ia32` targets, you should pass the `target_cpu` parameter to `gn gen`: ```sh -$ gn gen out/Debug --args='import(...) target_cpu="arm"' +$ gn gen out/Testing --args='import(...) target_cpu="arm"' ``` ## Building @@ -114,7 +121,7 @@ GN args. For example if you installed `clang` under `/usr/local/bin/clang`: ```sh -$ gn gen out/Debug --args='import("//electron/build/args/debug.gn") clang_base_path = "/usr/local/bin"' +$ gn gen out/Testing --args='import("//electron/build/args/testing.gn") clang_base_path = "/usr/local/bin"' ``` ### Using compilers other than `clang` diff --git a/docs/development/build-instructions-macos.md b/docs/development/build-instructions-macos.md index 88912e51c041f..fb4ef30497727 100644 --- a/docs/development/build-instructions-macos.md +++ b/docs/development/build-instructions-macos.md @@ -1,48 +1,17 @@ # Build Instructions (macOS) -Follow the guidelines below for building Electron on macOS. +Follow the guidelines below for building **Electron itself** on macOS, for the purposes of creating custom Electron binaries. For bundling and distributing your app code with the prebuilt Electron binaries, see the [application distribution][application-distribution] guide. + +[application-distribution]: ../tutorial/application-distribution.md ## Prerequisites -* macOS >= 10.11.6 -* [Xcode](https://developer.apple.com/technologies/tools/) >= 9.0.0 +* macOS >= 11.6.0 +* [Xcode](https://developer.apple.com/technologies/tools/). The exact version + needed depends on what branch you are building, but the latest version of + Xcode is generally a good bet for building `main`. * [node.js](https://nodejs.org) (external) -* Python 2.7 with support for TLS 1.2 - -## Python - -Please also ensure that your system and Python version support at least TLS 1.2. -This depends on both your version of macOS and Python. For a quick test, run: - -```sh -$ npx @electron/check-python-tls -``` - -If the script returns that your configuration is using an outdated security -protocol, you can either update macOS to High Sierra or install a new version -of Python 2.7.x. To upgrade Python, use [Homebrew](https://brew.sh/): - -```sh -$ brew install python@2 && brew link python@2 --force -``` - -If you are using Python as provided by Homebrew, you also need to install -the following Python modules: - -* [pyobjc](https://pypi.org/project/pyobjc/#description) - -You can use `pip` to install it: - -```sh -$ pip install pyobjc -``` - -## macOS SDK - -If you're developing Electron and don't plan to redistribute your -custom Electron build, you may skip this section. - -Official Electron builds are built with [Xcode 9.4.1](http://adcdownload.apple.com/Developer_Tools/Xcode_9.4.1/Xcode_9.4.1.xip), and the MacOS 10.13 SDK. Building with a newer SDK works too, but the releases currently use the 10.13 SDK. +* Python >= 3.7 ## Building Electron diff --git a/docs/development/build-instructions-windows.md b/docs/development/build-instructions-windows.md index 62c27a7843ee0..49e436197d851 100644 --- a/docs/development/build-instructions-windows.md +++ b/docs/development/build-instructions-windows.md @@ -1,29 +1,30 @@ # Build Instructions (Windows) -Follow the guidelines below for building Electron on Windows. +Follow the guidelines below for building **Electron itself** on Windows, for the purposes of creating custom Electron binaries. For bundling and distributing your app code with the prebuilt Electron binaries, see the [application distribution][application-distribution] guide. + +[application-distribution]: ../tutorial/application-distribution.md ## Prerequisites * Windows 10 / Server 2012 R2 or higher -* Visual Studio 2017 15.7.2 or higher - [download VS 2017 Community Edition for +* Visual Studio 2017 15.7.2 or higher - [download VS 2019 Community Edition for free](https://www.visualstudio.com/vs/) -* [Python 2.7.10 or higher](http://www.python.org/download/releases/2.7/) - * Contrary to the `depot_tools` setup instructions linked below, you will need - to use your locally installed Python with at least version 2.7.10 (with - support for TLS 1.2). To do so, make sure that in **PATH**, your locally - installed Python comes before the `depot_tools` folder. Right now - `depot_tools` still comes with Python 2.7.6, which will cause the `gclient` - command to fail (see https://crbug.com/868864). + * See [the Chromium build documentation](https://chromium.googlesource.com/chromium/src/+/master/docs/windows_build_instructions.md#visual-studio) for more details on which Visual Studio + components are required. + * If your Visual Studio is installed in a directory other than the default, you'll need to + set a few environment variables to point the toolchains to your installation path. + * `vs2019_install = DRIVE:\path\to\Microsoft Visual Studio\2019\Community`, replacing `2019` and `Community` with your installed versions and replacing `DRIVE:` with the drive that Visual Studio is on. Often, this will be `C:`. + * `WINDOWSSDKDIR = DRIVE:\path\to\Windows Kits\10`, replacing `DRIVE:` with the drive that Windows Kits is on. Often, this will be `C:`. * [Python for Windows (pywin32) Extensions](https://pypi.org/project/pywin32/#files) is also needed in order to run the build process. * [Node.js](https://nodejs.org/download/) -* [Git](http://git-scm.com) +* [Git](https://git-scm.com) * Debugging Tools for Windows of Windows SDK 10.0.15063.468 if you plan on creating a full distribution since `symstore.exe` is used for creating a symbol store from `.pdb` files. * Different versions of the SDK can be installed side by side. To install the SDK, open Visual Studio Installer, select - `Change` → `Individual Components`, scroll down and select the appropriate + `Modify` → `Individual Components`, scroll down and select the appropriate Windows SDK to install. Another option would be to look at the [Windows SDK and emulator archive](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive) and download the standalone version of the SDK respectively. @@ -44,6 +45,13 @@ building with Visual Studio will come in the future. **Note:** Even though Visual Studio is not used for building, it's still **required** because we need the build toolchains it provides. +## Exclude source tree from Windows Security + +Windows Security doesn't like one of the files in the Chromium source code +(see https://crbug.com/441184), so it will constantly delete it, causing `gclient sync` issues. +You can exclude the source tree from being monitored by Windows Security by +[following these instructions](https://support.microsoft.com/en-us/windows/add-an-exclusion-to-windows-security-811816c0-4dfd-af4a-47e4-c301afe13b26). + ## Building See [Build Instructions: GN](build-instructions-gn.md) @@ -67,7 +75,7 @@ To generate a Visual Studio project, you can pass the `--ide=vs2017` parameter to `gn gen`: ```powershell -$ gn gen out/Debug --ide=vs2017 +$ gn gen out/Testing --ide=vs2017 ``` ## Troubleshooting @@ -113,3 +121,10 @@ This can happen during build, when Debugging Tools for Windows has been installe ### ImportError: No module named win32file Make sure you have installed `pywin32` with `pip install pywin32`. + +### Build Scripts Hang Until Keypress + +This bug is a "feature" of Windows' command prompt. It happens when clicking inside the prompt window with +`QuickEdit` enabled and is intended to allow selecting and copying output text easily. +Since each accidental click will pause the build process, you might want to disable this +feature in the command prompt properties. diff --git a/docs/development/build-system-overview.md b/docs/development/build-system-overview.md deleted file mode 100644 index 881e4cc653fd4..0000000000000 --- a/docs/development/build-system-overview.md +++ /dev/null @@ -1,80 +0,0 @@ -# Build System Overview - -Electron uses [GN](https://gn.googlesource.com/gn) for project generation and -[ninja](https://ninja-build.org/) for building. Project configurations can -be found in the `.gn` and `.gni` files. - -## GN Files - -The following `gn` files contain the main rules for building Electron: - -* `BUILD.gn` defines how Electron itself is built and - includes the default configurations for linking with Chromium. -* `build/args/{debug,release,all}.gn` contain the default build arguments for - building Electron. - -## Component Build - -Since Chromium is quite a large project, the final linking stage can take -quite a few minutes, which makes it hard for development. In order to solve -this, Chromium introduced the "component build", which builds each component as -a separate shared library, making linking very quick but sacrificing file size -and performance. - -Electron inherits this build option from Chromium. In `Debug` builds, the -binary will be linked to a shared library version of Chromium's components to -achieve fast linking time; for `Release` builds, the binary will be linked to -the static library versions, so we can have the best possible binary size and -performance. - -## Tests - -**NB** _this section is out of date and contains information that is no longer -relevant to the GN-built electron._ - -Test your changes conform to the project coding style using: - -```sh -$ npm run lint -``` - -Test functionality using: - -```sh -$ npm test -``` - -Whenever you make changes to Electron source code, you'll need to re-run the -build before the tests: - -```sh -$ npm run build && npm test -``` - -You can make the test suite run faster by isolating the specific test or block -you're currently working on using Mocha's -[exclusive tests](https://mochajs.org/#exclusive-tests) feature. Append -`.only` to any `describe` or `it` function call: - -```js -describe.only('some feature', function () { - // ... only tests in this block will be run -}) -``` - -Alternatively, you can use mocha's `grep` option to only run tests matching the -given regular expression pattern: - -```sh -$ npm test -- --grep child_process -``` - -Tests that include native modules (e.g. `runas`) can't be executed with the -debug build (see [#2558](https://github.com/electron/electron/issues/2558) for -details), but they will work with the release build. - -To run the tests with the release build use: - -```sh -$ npm test -- -R -``` diff --git a/docs/development/chromium-development.md b/docs/development/chromium-development.md index 1892ef9a29a8c..6ad450dfbcb55 100644 --- a/docs/development/chromium-development.md +++ b/docs/development/chromium-development.md @@ -1,13 +1,39 @@ # Chromium Development -> A collection of resources for learning about Chromium and tracking its development - -- [@ChromiumDev](https://twitter.com/ChromiumDev) on Twitter -- [@googlechrome](https://twitter.com/googlechrome) on Twitter -- [Blog](https://blog.chromium.org) -- [Code Search](https://cs.chromium.org/) -- [Source Code](https://cs.chromium.org/chromium/src/) -- [Development Calendar and Release Info](https://www.chromium.org/developers/calendar) -- [Discussion Groups](http://www.chromium.org/developers/discussion-groups) +> A collection of resources for learning about Chromium and tracking its development. See also [V8 Development](v8-development.md) + +## Contributing to Chromium + +- [Checking Out and Building](https://chromium.googlesource.com/chromium/src/+/main/docs/#checking-out-and-building) + - [Windows](https://chromium.googlesource.com/chromium/src/+/main/docs/windows_build_instructions.md) + - [macOS](https://chromium.googlesource.com/chromium/src/+/main/docs/mac_build_instructions.md) + - [Linux](https://chromium.googlesource.com/chromium/src/+/main/docs/linux/build_instructions.md) + +- [Contributing](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/contributing.md) - This document outlines the process of getting a code change merged to the Chromium source tree. + - Assumes a working Chromium checkout and build. + +## Resources for Chromium Development + +### Code Resources + +- [Code Search](https://cs.chromium.org/) - Indexed and searchable source code for Chromium and associated projects. +- [Source Code](https://cs.chromium.org/chromium/src/) - The source code for Chromium itself. +- [Chromium Review](https://chromium-review.googlesource.com) - The searchable code host which facilitates code reviews for Chromium and related projects. + +### Informational Resources + +- [Chromium Dash](https://chromiumdash.appspot.com/home) - Chromium Dash ties together multiple data sources in order to present a consolidated view of what's going on in Chromium and Chrome, plus related projects like V8, WebRTC & Skia. + - [Schedule](https://chromiumdash.appspot.com/schedule) - Review upcoming Chromium release schedule. + - [Branches](https://chromiumdash.appspot.com/branches) - Look up which branch corresponds to which milestone. + - [Releases](https://chromiumdash.appspot.com/releases) - See what version of Chromium is shipping to each release channel and look up changes between each version. + - [Commits](https://chromiumdash.appspot.com/commits) - See and search for commits to the Chromium source tree by commit SHA or committer username. +- [Discussion Groups](https://www.chromium.org/developers/discussion-groups) - Subscribe to the following groups to get project updates and discuss the Chromium projects, and to get help in developing for Chromium-based browsers. +- [Chromium Slack](https://www.chromium.org/developers/slack) - a virtual meeting place where Chromium ecosystem developers can foster community and coordinate work. + +## Social Links + +- [Blog](https://blog.chromium.org) - News and developments from Chromium. +- [@ChromiumDev](https://twitter.com/ChromiumDev) - Twitter account containing news & guidance for developers from the Google Chrome Developer Relations team. +- [@googlechrome](https://twitter.com/googlechrome) - Official Twitter account for the Google Chrome browser. diff --git a/docs/development/clang-format.md b/docs/development/clang-format.md deleted file mode 100644 index d5f1b45b2b536..0000000000000 --- a/docs/development/clang-format.md +++ /dev/null @@ -1,35 +0,0 @@ -# Using clang-format on C++ Code - -[`clang-format`](http://clang.llvm.org/docs/ClangFormat.html) is a tool to -automatically format C/C++/Objective-C code, so that developers don't need to -worry about style issues during code reviews. - -It is highly recommended to format your changed C++ code before opening pull -requests, which will save you and the reviewers' time. - -You can install `clang-format` and `git-clang-format` via -`npm install -g clang-format`. - -To automatically format a file according to Electron C++ code style, run -`clang-format -i path/to/electron/file.cc`. It should work on macOS/Linux/Windows. - -The workflow to format your changed code: - -1. Make codes changes in Electron repository. -2. Run `git add your_changed_file.cc`. -3. Run `git-clang-format`, and you will probably see modifications in - `your_changed_file.cc`, these modifications are generated from `clang-format`. -4. Run `git add your_changed_file.cc`, and commit your change. -5. Now the branch is ready to be opened as a pull request. - -If you want to format the changed code on your latest git commit (HEAD), you can -run `git-clang-format HEAD~1`. See `git-clang-format -h` for more details. - -## Editor Integration - -You can also integrate `clang-format` directly into your favorite editors. -For further guidance on setting up editor integration, see these pages: - - * [Atom](https://atom.io/packages/clang-format) - * [Vim & Emacs](http://clang.llvm.org/docs/ClangFormat.html#vim-integration) - * [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=xaver.clang-format) diff --git a/docs/development/clang-tidy.md b/docs/development/clang-tidy.md new file mode 100644 index 0000000000000..b529d91b889b1 --- /dev/null +++ b/docs/development/clang-tidy.md @@ -0,0 +1,37 @@ +# Using clang-tidy on C++ Code + +[`clang-tidy`](https://clang.llvm.org/extra/clang-tidy/) is a tool to +automatically check C/C++/Objective-C code for style violations, programming +errors, and best practices. + +Electron's `clang-tidy` integration is provided as a linter script which can +be run with `npm run lint:clang-tidy`. While `clang-tidy` checks your on-disk +files, you need to have built Electron so that it knows which compiler flags +were used. There is one required option for the script `--output-dir`, which +tells the script which build directory to pull the compilation information +from. A typical usage would be: +`npm run lint:clang-tidy --out-dir ../out/Testing` + +With no filenames provided, all C/C++/Objective-C files will be checked. +You can provide a list of files to be checked by passing the filenames after +the options: +`npm run lint:clang-tidy --out-dir ../out/Testing shell/browser/api/electron_api_app.cc` + +While `clang-tidy` has a +[long list](https://clang.llvm.org/extra/clang-tidy/checks/list.html) +of possible checks, in Electron only a few are enabled by default. At the +moment Electron doesn't have a `.clang-tidy` config, so `clang-tidy` will +find the one from Chromium at `src/.clang-tidy` and use the checks which +Chromium has enabled. You can change which checks are run by using the +`--checks=` option. This is passed straight through to `clang-tidy`, so see +its documentation for full details. Wildcards can be used, and checks can +be disabled by prefixing a `-`. By default any checks listed are added to +those in `.clang-tidy`, so if you'd like to limit the checks to specific +ones you should first exclude all checks then add back what you want, like +`--checks=-*,performance*`. + +Running `clang-tidy` is rather slow - internally it compiles each file and +then runs the checks so it will always be some factor slower than compilation. +While you can use parallel runs to speed it up using the `--jobs|-j` option, +`clang-tidy` also uses a lot of memory during its checks, so it can easily +run into out-of-memory errors. As such the default number of jobs is one. diff --git a/docs/development/coding-style.md b/docs/development/coding-style.md index 0abaea2417d7e..e7e66a357d9cf 100644 --- a/docs/development/coding-style.md +++ b/docs/development/coding-style.md @@ -25,11 +25,10 @@ You can run `npm run lint` to show any style issues detected by `cpplint` and ## C++ and Python For C++ and Python, we follow Chromium's [Coding -Style](https://www.chromium.org/developers/coding-style). You can use -[clang-format](clang-format.md) to format the C++ code automatically. There is -also a script `script/cpplint.py` to check whether all files conform. +Style](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/styleguide/styleguide.md). +There is also a script `script/cpplint.py` to check whether all files conform. -The Python version we are using now is Python 2.7. +The Python version we are using now is Python 3.9. The C++ code uses a lot of Chromium's abstractions and types, so it's recommended to get acquainted with them. A good place to start is @@ -47,14 +46,14 @@ formatted correctly. ## JavaScript -* Write [standard](https://npm.im/standard) JavaScript style. +* Write [standard](https://www.npmjs.com/package/standard) JavaScript style. * File names should be concatenated with `-` instead of `_`, e.g. `file-name.js` rather than `file_name.js`, because in [github/atom](https://github.com/github/atom) module names are usually in the `module-name` form. This rule only applies to `.js` files. * Use newer ES6/ES2015 syntax where appropriate * [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) - for requires and other constants + for requires and other constants. If the value is a primitive, use uppercase naming (eg `const NUMBER_OF_RETRIES = 5`). * [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) for defining variables * [Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) @@ -66,11 +65,11 @@ formatted correctly. Electron APIs uses the same capitalization scheme as Node.js: -- When the module itself is a class like `BrowserWindow`, use `PascalCase`. -- When the module is a set of APIs, like `globalShortcut`, use `camelCase`. -- When the API is a property of object, and it is complex enough to be in a +* When the module itself is a class like `BrowserWindow`, use `PascalCase`. +* When the module is a set of APIs, like `globalShortcut`, use `camelCase`. +* When the API is a property of object, and it is complex enough to be in a separate chapter like `win.webContents`, use `mixedCase`. -- For other non-module APIs, use natural titles, like `<webview> Tag` or +* For other non-module APIs, use natural titles, like `<webview> Tag` or `Process Object`. When creating a new API, it is preferred to use getters and setters instead of diff --git a/docs/development/creating-api.md b/docs/development/creating-api.md new file mode 100644 index 0000000000000..6b6527c66c424 --- /dev/null +++ b/docs/development/creating-api.md @@ -0,0 +1,171 @@ +# Creating a New Electron Browser Module + +Welcome to the Electron API guide! If you are unfamiliar with creating a new Electron API module within the [`browser`](https://github.com/electron/electron/tree/main/shell/browser) directory, this guide serves as a checklist for some of the necessary steps that you will need to implement. + +This is not a comprehensive end-all guide to creating an Electron Browser API, rather an outline documenting some of the more unintuitive steps. + +## Add your files to Electron's project configuration + +Electron uses [GN](https://gn.googlesource.com/gn) as a meta build system to generate files for its compiler, [Ninja](https://ninja-build.org/). This means that in order to tell Electron to compile your code, we have to add your API's code and header file names into [`filenames.gni`](https://github.com/electron/electron/blob/main/filenames.gni). + +You will need to append your API file names alphabetically into the appropriate files like so: + +```cpp title='filenames.gni' +lib_sources = [ + "path/to/api/api_name.cc", + "path/to/api/api_name.h", +] + +lib_sources_mac = [ + "path/to/api/api_name_mac.h", + "path/to/api/api_name_mac.mm", +] + +lib_sources_win = [ + "path/to/api/api_name_win.cc", + "path/to/api/api_name_win.h", +] + +lib_sources_linux = [ + "path/to/api/api_name_linux.cc", + "path/to/api/api_name_linux.h", +] +``` + +Note that the Windows, macOS and Linux array additions are optional and should only be added if your API has specific platform implementations. + +## Create API documentation + +Type definitions are generated by Electron using [`@electron/docs-parser`](https://github.com/electron/docs-parser) and [`@electron/typescript-definitions`](https://github.com/electron/typescript-definitions). This step is necessary to ensure consistency across Electron's API documentation. This means that for your API type definition to appear in the `electron.d.ts` file, we must create a `.md` file. Examples can be found in [this folder](https://github.com/electron/electron/tree/main/docs/api). + +## Set up `ObjectTemplateBuilder` and `Wrappable` + +Electron constructs its modules using [`object_template_builder`](https://www.electronjs.org/blog/from-native-to-js#mateobjecttemplatebuilder). + +[`wrappable`](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/gin/wrappable.h) is a base class for C++ objects that have corresponding v8 wrapper objects. + +Here is a basic example of code that you may need to add, in order to incorporate `object_template_builder` and `wrappable` into your API. For further reference, you can find more implementations [here](https://github.com/electron/electron/tree/main/shell/browser/api). + +In your `api_name.h` file: + +```cpp title='api_name.h' + +#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_{API_NAME}_H_ +#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_{API_NAME}_H_ + +#include "gin/handle.h" +#include "gin/wrappable.h" + +namespace electron { + +namespace api { + +class ApiName : public gin::Wrappable<ApiName> { + public: + static gin::Handle<ApiName> Create(v8::Isolate* isolate); + + // gin::Wrappable + static gin::WrapperInfo kWrapperInfo; + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override; + const char* GetTypeName() override; +} // namespace api +} // namespace electron +``` + +In your `api_name.cc` file: + +```cpp title='api_name.cc' +#include "shell/browser/api/electron_api_safe_storage.h" + +#include "shell/browser/browser.h" +#include "shell/common/gin_converters/base_converter.h" +#include "shell/common/gin_converters/callback_converter.h" +#include "shell/common/gin_helper/dictionary.h" +#include "shell/common/gin_helper/object_template_builder.h" +#include "shell/common/node_includes.h" +#include "shell/common/platform_util.h" + +namespace electron { + +namespace api { + +gin::WrapperInfo ApiName::kWrapperInfo = {gin::kEmbedderNativeGin}; + +gin::ObjectTemplateBuilder ApiName::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return gin::ObjectTemplateBuilder(isolate) + .SetMethod("methodName", &ApiName::methodName); +} + +const char* ApiName::GetTypeName() { + return "ApiName"; +} + +// static +gin::Handle<ApiName> ApiName::Create(v8::Isolate* isolate) { + return gin::CreateHandle(isolate, new ApiName()); +} + +} // namespace api + +} // namespace electron + +namespace { + +void Initialize(v8::Local<v8::Object> exports, + v8::Local<v8::Value> unused, + v8::Local<v8::Context> context, + void* priv) { + v8::Isolate* isolate = context->GetIsolate(); + gin_helper::Dictionary dict(isolate, exports); + dict.Set("apiName", electron::api::ApiName::Create(isolate)); +} + +} // namespace +``` + +## Link your Electron API with Node + +In the [`typings/internal-ambient.d.ts`](https://github.com/electron/electron/blob/main/typings/internal-ambient.d.ts) file, we need to append a new property onto the `Process` interface like so: + +```ts title='typings/internal-ambient.d.ts' +interface Process { + _linkedBinding(name: 'electron_browser_{api_name}', Electron.ApiName); +} +``` + +At the very bottom of your `api_name.cc` file: + +```cpp title='api_name.cc' +NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_{api_name},Initialize) +``` + +In your [`shell/common/node_bindings.cc`](https://github.com/electron/electron/blob/main/shell/common/node_bindings.cc) file, add your node binding name to Electron's built-in modules. + +```cpp title='shell/common/node_bindings.cc' +#define ELECTRON_BUILTIN_MODULES(V) \ + V(electron_browser_{api_name}) +``` + +> Note: More technical details on how Node links with Electron can be found on [our blog](https://www.electronjs.org/blog/electron-internals-using-node-as-a-library#link-node-with-electron). + +## Expose your API to TypeScript + +### Export your API as a module + +We will need to create a new TypeScript file in the path that follows: + +`"lib/browser/api/{electron_browser_{api_name}}.ts"` + +An example of the contents of this file can be found [here](https://github.com/electron/electron/blob/main/lib/browser/api/native-image.ts). + +### Expose your module to TypeScript + +Add your module to the module list found at `"lib/browser/api/module-list.ts"` like so: + +```typescript title='lib/browser/api/module-list.ts' +export const browserModuleList: ElectronInternal.ModuleEntry[] = [ + { name: 'apiName', loader: () => require('./api-name') }, +]; +``` diff --git a/docs/development/debug-instructions-windows.md b/docs/development/debug-instructions-windows.md deleted file mode 100644 index c2d79003cec78..0000000000000 --- a/docs/development/debug-instructions-windows.md +++ /dev/null @@ -1,93 +0,0 @@ -# Debugging on Windows - -If you experience crashes or issues in Electron that you believe are not caused -by your JavaScript application, but instead by Electron itself, debugging can -be a little bit tricky, especially for developers not used to native/C++ -debugging. However, using Visual Studio, GitHub's hosted Electron Symbol Server, -and the Electron source code, you can enable step-through debugging -with breakpoints inside Electron's source code. - -**See also**: There's a wealth of information on debugging Chromium, much of which also applies to Electron, on the Chromium developers site: [Debugging Chromium on Windows](https://www.chromium.org/developers/how-tos/debugging-on-windows). - -## Requirements - -* **A debug build of Electron**: The easiest way is usually building it - yourself, using the tools and prerequisites listed in the - [build instructions for Windows](build-instructions-windows.md). While you can - attach to and debug Electron as you can download it directly, you will - find that it is heavily optimized, making debugging substantially more - difficult: The debugger will not be able to show you the content of all - variables and the execution path can seem strange because of inlining, - tail calls, and other compiler optimizations. - -* **Visual Studio with C++ Tools**: The free community editions of Visual - Studio 2013 and Visual Studio 2015 both work. Once installed, - [configure Visual Studio to use GitHub's Electron Symbol server](setting-up-symbol-server.md). - It will enable Visual Studio to gain a better understanding of what happens - inside Electron, making it easier to present variables in a human-readable - format. - -* **ProcMon**: The [free SysInternals tool][sys-internals] allows you to inspect - a processes parameters, file handles, and registry operations. - -## Attaching to and Debugging Electron - -To start a debugging session, open up PowerShell/CMD and execute your debug -build of Electron, using the application to open as a parameter. - -```powershell -$ ./out/Debug/electron.exe ~/my-electron-app/ -``` - -### Setting Breakpoints - -Then, open up Visual Studio. Electron is not built with Visual Studio and hence -does not contain a project file - you can however open up the source code files -"As File", meaning that Visual Studio will open them up by themselves. You can -still set breakpoints - Visual Studio will automatically figure out that the -source code matches the code running in the attached process and break -accordingly. - -Relevant code files can be found in `./atom/`. - -### Attaching - -You can attach the Visual Studio debugger to a running process on a local or -remote computer. After the process is running, click Debug / Attach to Process -(or press `CTRL+ALT+P`) to open the "Attach to Process" dialog box. You can use -this capability to debug apps that are running on a local or remote computer, -debug multiple processes simultaneously. - -If Electron is running under a different user account, select the -`Show processes from all users` check box. Notice that depending on how many -BrowserWindows your app opened, you will see multiple processes. A typical -one-window app will result in Visual Studio presenting you with two -`Electron.exe` entries - one for the main process and one for the renderer -process. Since the list only gives you names, there's currently no reliable -way of figuring out which is which. - -### Which Process Should I Attach to? - -Code executed within the main process (that is, code found in or eventually run -by your main JavaScript file) as well as code called using the remote -(`require('electron').remote`) will run inside the main process, while other -code will execute inside its respective renderer process. - -You can be attached to multiple programs when you are debugging, but only one -program is active in the debugger at any time. You can set the active program -in the `Debug Location` toolbar or the `Processes window`. - -## Using ProcMon to Observe a Process - -While Visual Studio is fantastic for inspecting specific code paths, ProcMon's -strength is really in observing everything your application is doing with the -operating system - it captures File, Registry, Network, Process, and Profiling -details of processes. It attempts to log **all** events occurring and can be -quite overwhelming, but if you seek to understand what and how your application -is doing to the operating system, it can be a valuable resource. - -For an introduction to ProcMon's basic and advanced debugging features, go check -out [this video tutorial][procmon-instructions] provided by Microsoft. - -[sys-internals]: https://technet.microsoft.com/en-us/sysinternals/processmonitor.aspx -[procmon-instructions]: https://channel9.msdn.com/shows/defrag-tools/defrag-tools-4-process-monitor diff --git a/docs/development/debugging-instructions-macos-xcode.md b/docs/development/debugging-instructions-macos-xcode.md deleted file mode 100644 index 13da10df29c2a..0000000000000 --- a/docs/development/debugging-instructions-macos-xcode.md +++ /dev/null @@ -1,27 +0,0 @@ -## Debugging with XCode - -### Generate xcode project for debugging sources (cannot build code from xcode) -Run `gn gen` with the --ide=xcode argument. -```sh -$ gn gen out/Debug --ide=xcode -``` -This will generate the electron.ninja.xcworkspace. You will have to open this workspace -to set breakpoints and inspect. - -See `gn help gen` for more information on generating IDE projects with GN. - -### Debugging and breakpoints - -Launch Electron app after build. -You can now open the xcode workspace created above and attach to the Electron process -through the Debug > Attach To Process > Electron debug menu. [Note: If you want to debug -the renderer process, you need to attach to the Electron Helper as well.] - -You can now set breakpoints in any of the indexed files. However, you will not be able -to set breakpoints directly in the Chromium source. -To set break points in the Chromium source, you can choose Debug > Breakpoints > Create -Symbolic Breakpoint and set any function name as the symbol. This will set the breakpoint -for all functions with that name, from all the classes if there are more than one. -You can also do this step of setting break points prior to attaching the debugger, -however, actual breakpoints for symbolic breakpoint functions may not show up until the -debugger is attached to the app. diff --git a/docs/development/debugging-instructions-macos.md b/docs/development/debugging-instructions-macos.md deleted file mode 100644 index 630ab12113871..0000000000000 --- a/docs/development/debugging-instructions-macos.md +++ /dev/null @@ -1,125 +0,0 @@ -# Debugging on macOS - -If you experience crashes or issues in Electron that you believe are not caused -by your JavaScript application, but instead by Electron itself, debugging can -be a little bit tricky, especially for developers not used to native/C++ -debugging. However, using lldb, and the Electron source code, you can enable -step-through debugging with breakpoints inside Electron's source code. -You can also use [XCode for debugging](debugging-instructions-macos-xcode.md) if -you prefer a graphical interface. - -## Requirements - -* **A debug build of Electron**: The easiest way is usually building it - yourself, using the tools and prerequisites listed in the - [build instructions for macOS](build-instructions-macos.md). While you can - attach to and debug Electron as you can download it directly, you will - find that it is heavily optimized, making debugging substantially more - difficult: The debugger will not be able to show you the content of all - variables and the execution path can seem strange because of inlining, - tail calls, and other compiler optimizations. - -* **Xcode**: In addition to Xcode, also install the Xcode command line tools. - They include LLDB, the default debugger in Xcode on Mac OS X. It supports - debugging C, Objective-C and C++ on the desktop and iOS devices and simulator. - -## Attaching to and Debugging Electron - -To start a debugging session, open up Terminal and start `lldb`, passing a debug -build of Electron as a parameter. - -```sh -$ lldb ./out/Debug/Electron.app -(lldb) target create "./out/Debug/Electron.app" -Current executable set to './out/Debug/Electron.app' (x86_64). -``` - -### Setting Breakpoints - -LLDB is a powerful tool and supports multiple strategies for code inspection. For -this basic introduction, let's assume that you're calling a command from JavaScript -that isn't behaving correctly - so you'd like to break on that command's C++ -counterpart inside the Electron source. - -Relevant code files can be found in `./atom/`. - -Let's assume that you want to debug `app.setName()`, which is defined in `browser.cc` -as `Browser::SetName()`. Set the breakpoint using the `breakpoint` command, specifying -file and line to break on: - -```sh -(lldb) breakpoint set --file browser.cc --line 117 -Breakpoint 1: where = Electron Framework`atom::Browser::SetName(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) + 20 at browser.cc:118, address = 0x000000000015fdb4 -``` - -Then, start Electron: - -```sh -(lldb) run -``` - -The app will immediately be paused, since Electron sets the app's name on launch: - -```sh -(lldb) run -Process 25244 launched: '/Users/fr/Code/electron/out/Debug/Electron.app/Contents/MacOS/Electron' (x86_64) -Process 25244 stopped -* thread #1: tid = 0x839a4c, 0x0000000100162db4 Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 20 at browser.cc:118, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 - frame #0: 0x0000000100162db4 Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 20 at browser.cc:118 - 115 } - 116 - 117 void Browser::SetName(const std::string& name) { --> 118 name_override_ = name; - 119 } - 120 - 121 int Browser::GetBadgeCount() { -(lldb) -``` - -To show the arguments and local variables for the current frame, run `frame variable` (or `fr v`), -which will show you that the app is currently setting the name to "Electron". - -```sh -(lldb) frame variable -(atom::Browser *) this = 0x0000000108b14f20 -(const string &) name = "Electron": { - [...] -} -``` - -To do a source level single step in the currently selected thread, execute `step` (or `s`). -This would take you into `name_override_.empty()`. To proceed and do a step over, -run `next` (or `n`). - -```sh -(lldb) step -Process 25244 stopped -* thread #1: tid = 0x839a4c, 0x0000000100162dcc Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 44 at browser.cc:119, queue = 'com.apple.main-thread', stop reason = step in - frame #0: 0x0000000100162dcc Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 44 at browser.cc:119 - 116 - 117 void Browser::SetName(const std::string& name) { - 118 name_override_ = name; --> 119 } - 120 - 121 int Browser::GetBadgeCount() { - 122 return badge_count_; -``` - -To finish debugging at this point, run `process continue`. You can also continue until a certain -line is hit in this thread (`thread until 100`). This command will run the thread in the current -frame till it reaches line 100 in this frame or stops if it leaves the current frame. - -Now, if you open up Electron's developer tools and call `setName`, you will once again hit the -breakpoint. - -### Further Reading -LLDB is a powerful tool with a great documentation. To learn more about it, consider -Apple's debugging documentation, for instance the [LLDB Command Structure Reference][lldb-command-structure] -or the introduction to [Using LLDB as a Standalone Debugger][lldb-standalone]. - -You can also check out LLDB's fantastic [manual and tutorial][lldb-tutorial], which -will explain more complex debugging scenarios. - -[lldb-command-structure]: https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-basics.html#//apple_ref/doc/uid/TP40012917-CH2-SW2 -[lldb-standalone]: https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-terminal-workflow-tutorial.html -[lldb-tutorial]: http://lldb.llvm.org/tutorial.html diff --git a/docs/development/debugging-on-macos.md b/docs/development/debugging-on-macos.md new file mode 100644 index 0000000000000..163df8e5aaf3a --- /dev/null +++ b/docs/development/debugging-on-macos.md @@ -0,0 +1,133 @@ +# Debugging on macOS + +If you experience crashes or issues in Electron that you believe are not caused +by your JavaScript application, but instead by Electron itself, debugging can +be a little bit tricky especially for developers not used to native/C++ +debugging. However, using `lldb` and the Electron source code, you can enable +step-through debugging with breakpoints inside Electron's source code. +You can also use [XCode for debugging](debugging-with-xcode.md) if you prefer a graphical interface. + +## Requirements + +* **A testing build of Electron**: The easiest way is usually to build it from source, + which you can do by following the instructions in the [build instructions](./build-instructions-macos.md). While you can attach to and debug Electron as you can download it directly, you will + find that it is heavily optimized, making debugging substantially more difficult. + In this case the debugger will not be able to show you the content of all + variables and the execution path can seem strange because of inlining, + tail calls, and other compiler optimizations. + +* **Xcode**: In addition to Xcode, you should also install the Xcode command line tools. + They include [LLDB](https://lldb.llvm.org/), the default debugger in Xcode on macOS. It supports + debugging C, Objective-C and C++ on the desktop and iOS devices and simulator. + +* **.lldbinit**: Create or edit `~/.lldbinit` to allow Chromium code to be properly source-mapped. + + ```text + # e.g: ['~/electron/src/tools/lldb'] + script sys.path[:0] = ['<...path/to/electron/src/tools/lldb>'] + script import lldbinit + ``` + +## Attaching to and Debugging Electron + +To start a debugging session, open up Terminal and start `lldb`, passing a non-release +build of Electron as a parameter. + +```sh +$ lldb ./out/Testing/Electron.app +(lldb) target create "./out/Testing/Electron.app" +Current executable set to './out/Testing/Electron.app' (x86_64). +``` + +### Setting Breakpoints + +LLDB is a powerful tool and supports multiple strategies for code inspection. For +this basic introduction, let's assume that you're calling a command from JavaScript +that isn't behaving correctly - so you'd like to break on that command's C++ +counterpart inside the Electron source. + +Relevant code files can be found in `./shell/`. + +Let's assume that you want to debug `app.setName()`, which is defined in `browser.cc` +as `Browser::SetName()`. Set the breakpoint using the `breakpoint` command, specifying +file and line to break on: + +```sh +(lldb) breakpoint set --file browser.cc --line 117 +Breakpoint 1: where = Electron Framework`atom::Browser::SetName(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) + 20 at browser.cc:118, address = 0x000000000015fdb4 +``` + +Then, start Electron: + +```sh +(lldb) run +``` + +The app will immediately be paused, since Electron sets the app's name on launch: + +```sh +(lldb) run +Process 25244 launched: '/Users/fr/Code/electron/out/Testing/Electron.app/Contents/MacOS/Electron' (x86_64) +Process 25244 stopped +* thread #1: tid = 0x839a4c, 0x0000000100162db4 Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 20 at browser.cc:118, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 + frame #0: 0x0000000100162db4 Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 20 at browser.cc:118 + 115 } + 116 + 117 void Browser::SetName(const std::string& name) { +-> 118 name_override_ = name; + 119 } + 120 + 121 int Browser::GetBadgeCount() { +(lldb) +``` + +To show the arguments and local variables for the current frame, run `frame variable` (or `fr v`), +which will show you that the app is currently setting the name to "Electron". + +```sh +(lldb) frame variable +(atom::Browser *) this = 0x0000000108b14f20 +(const string &) name = "Electron": { + [...] +} +``` + +To do a source level single step in the currently selected thread, execute `step` (or `s`). +This would take you into `name_override_.empty()`. To proceed and do a step over, +run `next` (or `n`). + +```sh +(lldb) step +Process 25244 stopped +* thread #1: tid = 0x839a4c, 0x0000000100162dcc Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 44 at browser.cc:119, queue = 'com.apple.main-thread', stop reason = step in + frame #0: 0x0000000100162dcc Electron Framework`atom::Browser::SetName(this=0x0000000108b14f20, name="Electron") + 44 at browser.cc:119 + 116 + 117 void Browser::SetName(const std::string& name) { + 118 name_override_ = name; +-> 119 } + 120 + 121 int Browser::GetBadgeCount() { + 122 return badge_count_; +``` + +**NOTE:** If you don't see source code when you think you should, you may not have added the `~/.lldbinit` file above. + +To finish debugging at this point, run `process continue`. You can also continue until a certain +line is hit in this thread (`thread until 100`). This command will run the thread in the current +frame till it reaches line 100 in this frame or stops if it leaves the current frame. + +Now, if you open up Electron's developer tools and call `setName`, you will once again hit the +breakpoint. + +### Further Reading + +LLDB is a powerful tool with a great documentation. To learn more about it, consider +Apple's debugging documentation, for instance the [LLDB Command Structure Reference][lldb-command-structure] +or the introduction to [Using LLDB as a Standalone Debugger][lldb-standalone]. + +You can also check out LLDB's fantastic [manual and tutorial][lldb-tutorial], which +will explain more complex debugging scenarios. + +[lldb-command-structure]: https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-basics.html#//apple_ref/doc/uid/TP40012917-CH2-SW2 +[lldb-standalone]: https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-terminal-workflow-tutorial.html +[lldb-tutorial]: https://lldb.llvm.org/tutorial.html diff --git a/docs/development/debugging-on-windows.md b/docs/development/debugging-on-windows.md new file mode 100644 index 0000000000000..aa047bf6803ef --- /dev/null +++ b/docs/development/debugging-on-windows.md @@ -0,0 +1,109 @@ +# Debugging on Windows + +If you experience crashes or issues in Electron that you believe are not caused +by your JavaScript application, but instead by Electron itself, debugging can +be a little bit tricky, especially for developers not used to native/C++ +debugging. However, using Visual Studio, Electron's hosted Symbol Server, +and the Electron source code, you can enable step-through debugging +with breakpoints inside Electron's source code. + +**See also**: There's a wealth of information on debugging Chromium, much of which also applies to Electron, on the Chromium developers site: [Debugging Chromium on Windows](https://www.chromium.org/developers/how-tos/debugging-on-windows). + +## Requirements + +* **A debug build of Electron**: The easiest way is usually building it + yourself, using the tools and prerequisites listed in the + [build instructions for Windows](build-instructions-windows.md). While you can + attach to and debug Electron as you can download it directly, you will + find that it is heavily optimized, making debugging substantially more + difficult: The debugger will not be able to show you the content of all + variables and the execution path can seem strange because of inlining, + tail calls, and other compiler optimizations. + +* **Visual Studio with C++ Tools**: The free community editions of Visual + Studio 2013 and Visual Studio 2015 both work. Once installed, + [configure Visual Studio to use Electron's Symbol server](debugging-with-symbol-server.md). + It will enable Visual Studio to gain a better understanding of what happens + inside Electron, making it easier to present variables in a human-readable + format. + +* **ProcMon**: The [free SysInternals tool][sys-internals] allows you to inspect + a processes parameters, file handles, and registry operations. + +## Attaching to and Debugging Electron + +To start a debugging session, open up PowerShell/CMD and execute your debug +build of Electron, using the application to open as a parameter. + +```powershell +$ ./out/Testing/electron.exe ~/my-electron-app/ +``` + +### Setting Breakpoints + +Then, open up Visual Studio. Electron is not built with Visual Studio and hence +does not contain a project file - you can however open up the source code files +"As File", meaning that Visual Studio will open them up by themselves. You can +still set breakpoints - Visual Studio will automatically figure out that the +source code matches the code running in the attached process and break +accordingly. + +Relevant code files can be found in `./shell/`. + +### Attaching + +You can attach the Visual Studio debugger to a running process on a local or +remote computer. After the process is running, click Debug / Attach to Process +(or press `CTRL+ALT+P`) to open the "Attach to Process" dialog box. You can use +this capability to debug apps that are running on a local or remote computer, +debug multiple processes simultaneously. + +If Electron is running under a different user account, select the +`Show processes from all users` check box. Notice that depending on how many +BrowserWindows your app opened, you will see multiple processes. A typical +one-window app will result in Visual Studio presenting you with two +`Electron.exe` entries - one for the main process and one for the renderer +process. Since the list only gives you names, there's currently no reliable +way of figuring out which is which. + +### Which Process Should I Attach to? + +Code executed within the main process (that is, code found in or eventually run +by your main JavaScript file) will run inside the main process, while other +code will execute inside its respective renderer process. + +You can be attached to multiple programs when you are debugging, but only one +program is active in the debugger at any time. You can set the active program +in the `Debug Location` toolbar or the `Processes window`. + +## Using ProcMon to Observe a Process + +While Visual Studio is fantastic for inspecting specific code paths, ProcMon's +strength is really in observing everything your application is doing with the +operating system - it captures File, Registry, Network, Process, and Profiling +details of processes. It attempts to log **all** events occurring and can be +quite overwhelming, but if you seek to understand what and how your application +is doing to the operating system, it can be a valuable resource. + +For an introduction to ProcMon's basic and advanced debugging features, go check +out [this video tutorial][procmon-instructions] provided by Microsoft. + +[sys-internals]: https://technet.microsoft.com/en-us/sysinternals/processmonitor.aspx +[procmon-instructions]: https://channel9.msdn.com/shows/defrag-tools/defrag-tools-4-process-monitor + +## Using WinDbg +<!-- TODO(@codebytere): add images and more information here? --> + +It's possible to debug crashes and issues in the Renderer process with [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/getting-started-with-windbg). + +To attach to a debug a process with WinDbg: + +1. Add `--renderer-startup-dialog` as a command line flag to Electron. +2. Launch the app you are intending to debug. +3. A dialog box will appear with a pid: “Renderer starting with pid: 1234”. +4. Launch WinDbg and choose “File > Attach to process” in the application menu. +5. Enter in pid from the dialog box in Step 3. +6. See that the debugger will be in a paused state, and that there is a command line in the app to enter text into. +7. Type “g” into the above command line to start the debuggee. +8. Press the enter key to continue the program. +9. Go back to the dialog box and press “ok”. diff --git a/docs/development/debugging-with-symbol-server.md b/docs/development/debugging-with-symbol-server.md new file mode 100644 index 0000000000000..336eb7a54f3f9 --- /dev/null +++ b/docs/development/debugging-with-symbol-server.md @@ -0,0 +1,57 @@ +# Setting Up Symbol Server in Debugger + +Debug symbols allow you to have better debugging sessions. They have information +about the functions contained in executables and dynamic libraries and provide +you with information to get clean call stacks. A Symbol Server allows the +debugger to load the correct symbols, binaries and sources automatically without +forcing users to download large debugging files. The server functions like +[Microsoft's symbol server](https://support.microsoft.com/kb/311503) so the +documentation there can be useful. + +Note that because released Electron builds are heavily optimized, debugging is +not always easy. The debugger will not be able to show you the content of all +variables and the execution path can seem strange because of inlining, tail +calls, and other compiler optimizations. The only workaround is to build an +unoptimized local build. + +The official symbol server URL for Electron is +https://symbols.electronjs.org. +You cannot visit this URL directly, you must add it to the symbol path of your +debugging tool. In the examples below, a local cache directory is used to avoid +repeatedly fetching the PDB from the server. Replace `c:\code\symbols` with an +appropriate cache directory on your machine. + +## Using the Symbol Server in Windbg + +The Windbg symbol path is configured with a string value delimited with asterisk +characters. To use only the Electron symbol server, add the following entry to +your symbol path (**Note:** you can replace `c:\code\symbols` with any writable +directory on your computer, if you'd prefer a different location for downloaded +symbols): + +```powershell +SRV*c:\code\symbols\*https://symbols.electronjs.org +``` + +Set this string as `_NT_SYMBOL_PATH` in the environment, using the Windbg menus, +or by typing the `.sympath` command. If you would like to get symbols from +Microsoft's symbol server as well, you should list that first: + +```powershell +SRV*c:\code\symbols\*https://msdl.microsoft.com/download/symbols;SRV*c:\code\symbols\*https://symbols.electronjs.org +``` + +## Using the symbol server in Visual Studio + +![Tools -> Options](../images/vs-tools-options.png) + +![Symbols Settings](../images/vs-options-debugging-symbols.png) + +## Troubleshooting: Symbols will not load + +Type the following commands in Windbg to print why symbols are not loading: + +```powershell +> !sym noisy +> .reload /f electron.exe +``` diff --git a/docs/development/debugging-with-xcode.md b/docs/development/debugging-with-xcode.md new file mode 100644 index 0000000000000..7e88a92bba755 --- /dev/null +++ b/docs/development/debugging-with-xcode.md @@ -0,0 +1,30 @@ +## Debugging with XCode + +### Generate xcode project for debugging sources (cannot build code from xcode) + +Run `gn gen` with the --ide=xcode argument. + +```sh +$ gn gen out/Testing --ide=xcode +``` + +This will generate the electron.ninja.xcworkspace. You will have to open this workspace +to set breakpoints and inspect. + +See `gn help gen` for more information on generating IDE projects with GN. + +### Debugging and breakpoints + +Launch Electron app after build. +You can now open the xcode workspace created above and attach to the Electron process +through the Debug > Attach To Process > Electron debug menu. [Note: If you want to debug +the renderer process, you need to attach to the Electron Helper as well.] + +You can now set breakpoints in any of the indexed files. However, you will not be able +to set breakpoints directly in the Chromium source. +To set break points in the Chromium source, you can choose Debug > Breakpoints > Create +Symbolic Breakpoint and set any function name as the symbol. This will set the breakpoint +for all functions with that name, from all the classes if there are more than one. +You can also do this step of setting break points prior to attaching the debugger, +however, actual breakpoints for symbolic breakpoint functions may not show up until the +debugger is attached to the app. diff --git a/docs/development/debugging.md b/docs/development/debugging.md new file mode 100644 index 0000000000000..775a31199b95e --- /dev/null +++ b/docs/development/debugging.md @@ -0,0 +1,71 @@ +# Electron Debugging + +There are many different approaches to debugging issues and bugs in Electron, many of which +are platform specific. + +Some of the more common approaches are outlined below. + +## Generic Debugging + +Chromium contains logging macros which can aid debugging by printing information to console in C++ and Objective-C++. + +You might use this to print out variable values, function names, and line numbers, amonst other things. + +Some examples: + +```cpp +LOG(INFO) << "bitmap.width(): " << bitmap.width(); + +LOG(INFO, bitmap.width() > 10) << "bitmap.width() is greater than 10!"; +``` + +There are also different levels of logging severity: `INFO`, `WARN`, and `ERROR`. + +See [logging.h](https://chromium.googlesource.com/chromium/src/base/+/refs/heads/main/logging.h) in Chromium's source tree for more information and examples. + +## Printing Stacktraces + +Chromium contains a helper to print stack traces to console without interrrupting the program. + +```cpp +#include "base/debug/stack_trace.h" +... +base::debug::StackTrace().Print(); +``` + +This will allow you to observe call chains and identify potential issue areas. + +## Breakpoint Debugging + +> Note that this will increase the size of the build significantly, taking up around 50G of disk space + +Write the following file to `electron/.git/info/exclude/debug.gn` + +```gn +import("//electron/build/args/testing.gn") +is_debug = true +symbol_level = 2 +forbid_non_component_debug_builds = false +``` + +Then execute: + +```sh +$ gn gen out/Debug --args="import(\"//electron/.git/info/exclude/debug.gn\") $GN_EXTRA_ARGS" +$ ninja -C out/Debug electron +``` + +Now you can use `LLDB` for breakpoint debugging. + +## Platform-Specific Debugging +<!-- TODO(@codebytere): add debugging file for Linux--> + +- [macOS Debugging](debugging-on-macos.md) + - [Debugging with Xcode](debugging-with-xcode.md) +- [Windows Debugging](debugging-on-windows.md) + +## Debugging with the Symbol Server + +Debug symbols allow you to have better debugging sessions. They have information about the functions contained in executables and dynamic libraries and provide you with information to get clean call stacks. A Symbol Server allows the debugger to load the correct symbols, binaries and sources automatically without forcing users to download large debugging files. + +For more information about how to set up a symbol server for Electron, see [debugging with a symbol server](debugging-with-symbol-server.md). diff --git a/docs/development/goma.md b/docs/development/goma.md new file mode 100644 index 0000000000000..f0d2f37d1c34b --- /dev/null +++ b/docs/development/goma.md @@ -0,0 +1,56 @@ +# Goma + +> Goma is a distributed compiler service for open-source projects such as +> Chromium and Android. + +Electron has a deployment of a custom Goma Backend that we make available to +all Electron Maintainers. See the [Access](#access) section below for details +on authentication. There is also a `cache-only` Goma endpoint that will be +used by default if you do not have credentials. Requests to the cache-only +Goma will not hit our cluster, but will read from our cache and should result +in significantly faster build times. + +## Enabling Goma + +Currently the only supported way to use Goma is to use our [Build Tools](https://github.com/electron/build-tools). +Goma configuration is automatically included when you set up `build-tools`. + +If you are a maintainer and have access to our cluster, please ensure that you run +`e init` with `--goma=cluster` in order to configure `build-tools` to use +the Goma cluster. If you have an existing config, you can just set `"goma": "cluster"` +in your config file. + +## Building with Goma + +When you are using Goma you can run `ninja` with a substantially higher `j` +value than would normally be supported by your machine. + +Please do not set a value higher than **200**. We monitor Goma system usage, and users +found to be abusing it with unreasonable concurrency will be de-activated. + +```bash +ninja -C out/Testing electron -j 200 +``` + +If you're using `build-tools`, appropriate `-j` values will automatically be used for you. + +## Monitoring Goma + +If you access [http://localhost:8088](http://localhost:8088) on your local +machine you can monitor compile jobs as they flow through the goma system. + +## Access + +For security and cost reasons, access to Electron's Goma cluster is currently restricted +to Electron Maintainers. If you want access please head to `#access-requests` in +Slack and ping `@goma-squad` to ask for access. Please be aware that being a +maintainer does not *automatically* grant access and access is determined on a +case by case basis. + +## Uptime / Support + +We have automated monitoring of our Goma cluster and cache at https://status.notgoma.com + +We do not provide support for usage of Goma and any issues raised asking for help / having +issues will _probably_ be closed without much reason, we do not have the capacity to handle +that kind of support. diff --git a/docs/development/issues.md b/docs/development/issues.md index 7a3157a74597b..ec0ad5dc04f36 100644 --- a/docs/development/issues.md +++ b/docs/development/issues.md @@ -1,19 +1,19 @@ # Issues In Electron -* [How to Contribute in Issues](#how-to-contribute-in-issues) +* [How to Contribute to Issues](#how-to-contribute-to-issues) * [Asking for General Help](#asking-for-general-help) * [Submitting a Bug Report](#submitting-a-bug-report) * [Triaging a Bug Report](#triaging-a-bug-report) * [Resolving a Bug Report](#resolving-a-bug-report) -## How to Contribute in Issues +## How to Contribute to Issues For any issue, there are fundamentally three ways an individual can contribute: 1. By opening the issue for discussion: If you believe that you have found a new bug in Electron, you should report it by creating a new issue in - the `electron/electron` issue tracker. + the [`electron/electron` issue tracker](https://github.com/electron/electron/issues). 2. By helping to triage the issue: You can do this either by providing assistive details (a reproducible test case that demonstrates a bug) or by providing suggestions to address the issue. @@ -30,52 +30,16 @@ contributing, and more. Please use the issue tracker for bugs only! ## Submitting a Bug Report -When opening a new issue in the `electron/electron` issue tracker, users -will be presented with a template that should be filled in. - -```markdown -<!-- -Thanks for opening an issue! A few things to keep in mind: - -- The issue tracker is only for bugs and feature requests. -- Before reporting a bug, please try reproducing your issue against - the latest version of Electron. -- If you need general advice, join our Slack: http://atom-slack.herokuapp.com ---> - -* Electron version: -* Operating system: - -### Expected behavior - -<!-- What do you think should happen? --> - -### Actual behavior - -<!-- What actually happens? --> +To submit a bug report: -### How to reproduce - -<!-- - -Your best chance of getting this bug looked at quickly is to provide a REPOSITORY that can be cloned and run. - -You can fork https://github.com/electron/electron-quick-start and include a link to the branch with your changes. - -If you provide a URL, please list the commands required to clone/setup/run your repo e.g. - - $ git clone $YOUR_URL -b $BRANCH - $ npm install - $ npm start || electron . - ---> -``` +When opening a new issue in the [`electron/electron` issue tracker](https://github.com/electron/electron/issues/new/choose), users +will be presented with a template that should be filled in. -If you believe that you have found a bug in Electron, please fill out this -form to the best of your ability. +If you believe that you have found a bug in Electron, please fill out the template +to the best of your ability. The two most important pieces of information needed to evaluate the report are -a description of the bug and a simple test case to recreate it. It easier to fix +a description of the bug and a simple test case to recreate it. It is easier to fix a bug if it can be reproduced. See [How to create a Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve). @@ -92,7 +56,7 @@ are not helpful or professional. To many, such responses are annoying and unfriendly. Contributors are encouraged to solve issues collaboratively and help one -another make progress. If encounter an issue that you feel is invalid, or +another make progress. If you encounter an issue that you feel is invalid, or which contains incorrect information, explain *why* you feel that way with additional supporting context, and be willing to be convinced that you may be wrong. By doing so, we can often reach the correct outcome faster. diff --git a/docs/development/patches.md b/docs/development/patches.md new file mode 100644 index 0000000000000..634a35c355a87 --- /dev/null +++ b/docs/development/patches.md @@ -0,0 +1,93 @@ +# Patches in Electron + +Electron is built on two major upstream projects: Chromium and Node.js. Each of these projects has several of their own dependencies, too. We try our best to use these dependencies exactly as they are but sometimes we can't achieve our goals without patching those upstream dependencies to fit our use cases. + +## Patch justification + +Every patch in Electron is a maintenance burden. When upstream code changes, patches can break—sometimes without even a patch conflict or a compilation error. It's an ongoing effort to keep our patch set up-to-date and effective. So we strive to keep our patch count at a minimum. To that end, every patch must describe its reason for existence in its commit message. That reason must be one of the following: + +1. The patch is temporary, and is intended to be (or has been) committed upstream or otherwise eventually removed. Include a link to an upstream PR or code review if available, or a procedure for verifying whether the patch is still needed at a later date. +2. The patch allows the code to compile in the Electron environment, but cannot be upstreamed because it's Electron-specific (e.g. patching out references to Chrome's `Profile`). Include reasoning about why the change cannot be implemented without a patch (e.g. by subclassing or copying the code). +3. The patch makes Electron-specific changes in functionality which are fundamentally incompatible with upstream. + +In general, all the upstream projects we work with are friendly folks and are often happy to accept refactorings that allow the code in question to be compatible with both Electron and the upstream project. (See e.g. [this](https://chromium-review.googlesource.com/c/chromium/src/+/1637040) change in Chromium, which allowed us to remove a patch that did the same thing, or [this](https://github.com/nodejs/node/pull/22110) change in Node, which was a no-op for Node but fixed a bug in Electron.) **We should aim to upstream changes whenever we can, and avoid indefinite-lifetime patches**. + +## Patch system + +If you find yourself in the unfortunate position of having to make a change which can only be made through patching an upstream project, you'll need to know how to manage patches in Electron. + +All patches to upstream projects in Electron are contained in the `patches/` directory. Each subdirectory of `patches/` contains several patch files, along with a `.patches` file which lists the order in which the patches should be applied. Think of these files as making up a series of git commits that are applied on top of the upstream project after we check it out. + +```text +patches +├── config.json <-- this describes which patchset directory is applied to what project +├── chromium +│   ├── .patches +│   ├── accelerator.patch +│   ├── add_contentgpuclient_precreatemessageloop_callback.patch +│ ⋮ +├── node +│   ├── .patches +│   ├── add_openssl_is_boringssl_guard_to_oaep_hash_check.patch +│   ├── build_add_gn_build_files.patch +│   ⋮ +⋮ +``` + +To help manage these patch sets, we provide two tools: `git-import-patches` and `git-export-patches`. `git-import-patches` imports a set of patch files into a git repository by applying each patch in the correct order and creating a commit for each one. `git-export-patches` does the reverse; it exports a series of git commits in a repository into a set of files in a directory and an accompanying `.patches` file. + +> Side note: the reason we use a `.patches` file to maintain the order of applied patches, rather than prepending a number like `001-` to each file, is because it reduces conflicts related to patch ordering. It prevents the situation where two PRs both add a patch at the end of the series with the same numbering and end up both getting merged resulting in a duplicate identifier, and it also reduces churn when a patch is added or deleted in the middle of the series. + +### Usage + +#### Adding a new patch + +```bash +$ cd src/third_party/electron_node +$ vim some/code/file.cc +$ git commit +$ ../../electron/script/git-export-patches -o ../../electron/patches/node +``` + +> **NOTE**: `git-export-patches` ignores any uncommitted files, so you must create a commit if you want your changes to be exported. The subject line of the commit message will be used to derive the patch file name, and the body of the commit message should include the reason for the patch's existence. + +Re-exporting patches will sometimes cause shasums in unrelated patches to change. This is generally harmless and can be ignored (but go ahead and add those changes to your PR, it'll stop them from showing up for other people). + +#### Editing an existing patch + +```bash +$ cd src/v8 +$ vim some/code/file.cc +$ git log +# Find the commit sha of the patch you want to edit. +$ git commit --fixup [COMMIT_SHA] +$ git rebase --autosquash -i [COMMIT_SHA]^ +$ ../electron/script/git-export-patches -o ../electron/patches/v8 +``` + +#### Removing a patch + +```bash +$ vim src/electron/patches/node/.patches +# Delete the line with the name of the patch you want to remove +$ cd src/third_party/electron_node +$ git reset --hard refs/patches/upstream-head +$ ../../electron/script/git-import-patches ../../electron/patches/node +$ ../../electron/script/git-export-patches -o ../../electron/patches/node +``` + +Note that `git-import-patches` will mark the commit that was `HEAD` when it was run as `refs/patches/upstream-head`. This lets you keep track of which commits are from Electron patches (those that come after `refs/patches/upstream-head`) and which commits are in upstream (those before `refs/patches/upstream-head`). + +#### Resolving conflicts + +When updating an upstream dependency, patches may fail to apply cleanly. Often, the conflict can be resolved automatically by git with a 3-way merge. You can instruct `git-import-patches` to use the 3-way merge algorithm by passing the `-3` argument: + +```bash +$ cd src/third_party/electron_node +# If the patch application failed midway through, you can reset it with: +$ git am --abort +# And then retry with 3-way merge: +$ ../../electron/script/git-import-patches -3 ../../electron/patches/node +``` + +If `git-import-patches -3` encounters a merge conflict that it can't resolve automatically, it will pause and allow you to resolve the conflict manually. Once you have resolved the conflict, `git add` the resolved files and continue to apply the rest of the patches by running `git am --continue`. diff --git a/docs/development/pull-requests.md b/docs/development/pull-requests.md index 19960889ff956..fa4b6dfbdf19b 100644 --- a/docs/development/pull-requests.md +++ b/docs/development/pull-requests.md @@ -35,19 +35,20 @@ $ git fetch upstream Build steps and dependencies differ slightly depending on your operating system. See these detailed guides on building Electron locally: -* [Building on MacOS](https://electronjs.org/docs/development/build-instructions-macos) -* [Building on Linux](https://electronjs.org/docs/development/build-instructions-linux) -* [Building on Windows](https://electronjs.org/docs/development/build-instructions-windows) + +* [Building on macOS](build-instructions-macos.md) +* [Building on Linux](build-instructions-linux.md) +* [Building on Windows](build-instructions-windows.md) Once you've built the project locally, you're ready to start making changes! ### Step 3: Branch To keep your development environment organized, create local branches to -hold your work. These should be branched directly off of the `master` branch. +hold your work. These should be branched directly off of the `main` branch. ```sh -$ git checkout -b my-branch -t upstream/master +$ git checkout -b my-branch -t upstream/main ``` ## Making Changes @@ -55,14 +56,14 @@ $ git checkout -b my-branch -t upstream/master ### Step 4: Code Most pull requests opened against the `electron/electron` repository include -changes to either the C/C++ code in the `atom/` folder, +changes to either the C/C++ code in the `shell/` folder, the JavaScript code in the `lib/` folder, the documentation in `docs/api/` or tests in the `spec/` folder. Please be sure to run `npm run lint` from time to time on any code changes to ensure that they follow the project's code style. -See [coding style](https://electronjs.org/docs/development/coding-style) for +See [coding style](coding-style.md) for more information about best practice when modifying code in different parts of the project. @@ -78,7 +79,7 @@ $ git add my/changed/files $ git commit ``` -Note that multiple commits often get squashed when they are landed. +Note that multiple commits get squashed when they are landed. #### Commit message guidelines @@ -90,29 +91,28 @@ Before a pull request can be merged, it **must** have a pull request title with Examples of commit messages with semantic prefixes: -- `fix: don't overwrite prevent_default if default wasn't prevented` -- `feat: add app.isPackaged() method` -- `docs: app.isDefaultProtocolClient is now available on Linux` +* `fix: don't overwrite prevent_default if default wasn't prevented` +* `feat: add app.isPackaged() method` +* `docs: app.isDefaultProtocolClient is now available on Linux` Common prefixes: - - fix: A bug fix - - feat: A new feature - - docs: Documentation changes - - test: Adding missing tests or correcting existing tests - - build: Changes that affect the build system - - ci: Changes to our CI configuration files and scripts - - perf: A code change that improves performance - - refactor: A code change that neither fixes a bug nor adds a feature - - style: Changes that do not affect the meaning of the code (linting) - - vendor: Bumping a dependency like libchromiumcontent or node +* fix: A bug fix +* feat: A new feature +* docs: Documentation changes +* test: Adding missing tests or correcting existing tests +* build: Changes that affect the build system +* ci: Changes to our CI configuration files and scripts +* perf: A code change that improves performance +* refactor: A code change that neither fixes a bug nor adds a feature +* style: Changes that do not affect the meaning of the code (linting) Other things to keep in mind when writing a commit message: 1. The first line should: - - contain a short description of the change (preferably 50 characters or less, + * contain a short description of the change (preferably 50 characters or less, and no more than 72 characters) - - be entirely in lowercase with the exception of proper nouns, acronyms, and + * be entirely in lowercase with the exception of proper nouns, acronyms, and the words that refer to code, like function/variable names 2. Keep the second line blank. 3. Wrap all other lines at 72 columns. @@ -134,16 +134,16 @@ Once you have committed your changes, it is a good idea to use `git rebase` ```sh $ git fetch upstream -$ git rebase upstream/master +$ git rebase upstream/main ``` This ensures that your working branch has the latest changes from `electron/electron` -master. +main. ### Step 7: Test Bug fixes and features should always come with tests. A -[testing guide](https://electronjs.org/docs/development/testing) has been +[testing guide](testing.md) has been provided to make the process easier. Looking at other tests to see how they should be structured can also help. @@ -180,18 +180,10 @@ $ git push origin my-branch ### Step 9: Opening the Pull Request From within GitHub, opening a new pull request will present you with a template -that should be filled out: - -```markdown -<!-- -Thank you for your pull request. Please provide a description above and review -the requirements below. +that should be filled out. It can be found [here](https://github.com/electron/electron/blob/main/.github/PULL_REQUEST_TEMPLATE.md). -Bug fixes and new features should include tests and possibly benchmarks. - -Contributors guide: https://github.com/electron/electron/blob/master/CONTRIBUTING.md ---> -``` +If you do not adequately complete this template, your PR may be delayed in being merged as maintainers +seek more information or clarify ambiguities. ### Step 10: Discuss and update @@ -221,18 +213,19 @@ seem unfamiliar, refer to this #### Approval and Request Changes Workflow -All pull requests require approval from a [Code Owner](https://github.com/orgs/electron/teams/code-owners) of the area you -modified in order to land. Whenever a maintainer reviews a pull request they -may request changes. These may be small, such as fixing a typo, or may involve -substantive changes. Such requests are intended to be helpful, but at times -may come across as abrupt or unhelpful, especially if they do not include +All pull requests require approval from a +[Code Owner](https://github.com/electron/electron/blob/main/.github/CODEOWNERS) +of the area you modified in order to land. Whenever a maintainer reviews a pull +request they may request changes. These may be small, such as fixing a typo, or +may involve substantive changes. Such requests are intended to be helpful, but +at times may come across as abrupt or unhelpful, especially if they do not include concrete suggestions on *how* to change them. Try not to be discouraged. If you feel that a review is unfair, say so or seek the input of another project contributor. Often such comments are the result of a reviewer having taken insufficient time to review and are not ill-intended. Such difficulties can often be resolved with a bit of patience. That said, -reviewers should be expected to provide helpful feeback. +reviewers should be expected to provide helpful feedback. ### Step 11: Landing @@ -254,7 +247,5 @@ platforms or for so-called "flaky" tests to fail ("be red"). Each CI failure must be manually inspected to determine the cause. CI starts automatically when you open a pull request, but only -[Releasers](https://github.com/orgs/electron/teams/releasers/members) -can restart a CI run. If you believe CI is giving a false negative, -ask a Releaser to restart the tests. - +core maintainers can restart a CI run. If you believe CI is giving a +false negative, ask a maintainer to restart the tests. diff --git a/docs/development/setting-up-symbol-server.md b/docs/development/setting-up-symbol-server.md deleted file mode 100644 index 0f5030cbf5179..0000000000000 --- a/docs/development/setting-up-symbol-server.md +++ /dev/null @@ -1,56 +0,0 @@ -# Setting Up Symbol Server in Debugger - -Debug symbols allow you to have better debugging sessions. They have information -about the functions contained in executables and dynamic libraries and provide -you with information to get clean call stacks. A Symbol Server allows the -debugger to load the correct symbols, binaries and sources automatically without -forcing users to download large debugging files. The server functions like -[Microsoft's symbol server](https://support.microsoft.com/kb/311503) so the -documentation there can be useful. - -Note that because released Electron builds are heavily optimized, debugging is -not always easy. The debugger will not be able to show you the content of all -variables and the execution path can seem strange because of inlining, tail -calls, and other compiler optimizations. The only workaround is to build an -unoptimized local build. - -The official symbol server URL for Electron is -https://electron-symbols.githubapp.com. -You cannot visit this URL directly, you must add it to the symbol path of your -debugging tool. In the examples below, a local cache directory is used to avoid -repeatedly fetching the PDB from the server. Replace `c:\code\symbols` with an -appropriate cache directory on your machine. - -## Using the Symbol Server in Windbg - -The Windbg symbol path is configured with a string value delimited with asterisk -characters. To use only the Electron symbol server, add the following entry to -your symbol path (**Note:** you can replace `c:\code\symbols` with any writable -directory on your computer, if you'd prefer a different location for downloaded -symbols): - -```powershell -SRV*c:\code\symbols\*https://electron-symbols.githubapp.com -``` - -Set this string as `_NT_SYMBOL_PATH` in the environment, using the Windbg menus, -or by typing the `.sympath` command. If you would like to get symbols from -Microsoft's symbol server as well, you should list that first: - -```powershell -SRV*c:\code\symbols\*https://msdl.microsoft.com/download/symbols;SRV*c:\code\symbols\*https://electron-symbols.githubapp.com -``` - -## Using the symbol server in Visual Studio - -<img src='https://mdn.mozillademos.org/files/733/symbol-server-vc8express-menu.jpg'> -<img src='https://mdn.mozillademos.org/files/2497/2005_options.gif'> - -## Troubleshooting: Symbols will not load - -Type the following commands in Windbg to print why symbols are not loading: - -```powershell -> !sym noisy -> .reload /f electron.exe -``` diff --git a/docs/development/source-code-directory-structure.md b/docs/development/source-code-directory-structure.md index 5a0369c4a32e4..a7d0105f7dcb6 100644 --- a/docs/development/source-code-directory-structure.md +++ b/docs/development/source-code-directory-structure.md @@ -11,7 +11,48 @@ to understand the source code better. ```diff Electron -├── atom/ - C++ source code. +├── build/ - Build configuration files needed to build with GN. +├── buildflags/ - Determines the set of features that can be conditionally built. +├── chromium_src/ - Source code copied from Chromium that isn't part of the content layer. +├── default_app/ - A default app run when Electron is started without +| providing a consumer app. +├── docs/ - Electron's documentation. +| ├── api/ - Documentation for Electron's externally-facing modules and APIs. +| ├── development/ - Documentation to aid in developing for and with Electron. +| ├── fiddles/ - A set of code snippets one can run in Electron Fiddle. +| ├── images/ - Images used in documentation. +| └── tutorial/ - Tutorial documents for various aspects of Electron. +├── lib/ - JavaScript/TypeScript source code. +| ├── browser/ - Main process initialization code. +| | ├── api/ - API implementation for main process modules. +| | └── remote/ - Code related to the remote module as it is +| | used in the main process. +| ├── common/ - Relating to logic needed by both main and renderer processes. +| | └── api/ - API implementation for modules that can be used in +| | both the main and renderer processes +| ├── isolated_renderer/ - Handles creation of isolated renderer processes when +| | contextIsolation is enabled. +| ├── renderer/ - Renderer process initialization code. +| | ├── api/ - API implementation for renderer process modules. +| | ├── extension/ - Code related to use of Chrome Extensions +| | | in Electron's renderer process. +| | ├── remote/ - Logic that handles use of the remote module in +| | | the main process. +| | └── web-view/ - Logic that handles the use of webviews in the +| | renderer process. +| ├── sandboxed_renderer/ - Logic that handles creation of sandboxed renderer +| | | processes. +| | └── api/ - API implementation for sandboxed renderer processes. +| └── worker/ - Logic that handles proper functionality of Node.js +| environments in Web Workers. +├── patches/ - Patches applied on top of Electron's core dependencies +| | in order to handle differences between our use cases and +| | default functionality. +| ├── boringssl/ - Patches applied to Google's fork of OpenSSL, BoringSSL. +| ├── chromium/ - Patches applied to Chromium. +| ├── node/ - Patches applied on top of Node.js. +| └── v8/ - Patches applied on top of Google's V8 engine. +├── shell/ - C++ source code. | ├── app/ - System entry code. | ├── browser/ - The frontend including the main window, UI, and all of the | | | main process things. This talks to the renderer to manage web @@ -31,77 +72,30 @@ Electron | | message loop into Chromium's message loop. | └── api/ - The implementation of common APIs, and foundations of | Electron's built-in modules. -├── chromium_src/ - Source code copied from Chromium. See below. -├── default_app/ - The default page to show when Electron is started without -| providing an app. -├── docs/ - Documentations. -├── lib/ - JavaScript source code. -| ├── browser/ - Javascript main process initialization code. -| | └── api/ - Javascript API implementation. -| ├── common/ - JavaScript used by both the main and renderer processes -| | └── api/ - Javascript API implementation. -| └── renderer/ - Javascript renderer process initialization code. -| └── api/ - Javascript API implementation. -├── native_mate/ - A fork of Chromium's gin library that makes it easier to marshal -| types between C++ and JavaScript. -├── spec/ - Automatic tests. +├── spec/ - Components of Electron's test suite run in the renderer process. +├── spec-main/ - Components of Electron's test suite run in the main process. └── BUILD.gn - Building rules of Electron. ``` -## `/chromium_src` - -The files in `/chromium_src` tend to be pieces of Chromium that aren't part of -the content layer. For example to implement Pepper API, we need some wiring -similar to what official Chrome does. We could have built the relevant -sources as a part of [libcc](../glossary.md#libchromiumcontent) but most -often we don't require all the features (some tend to be proprietary, -analytics stuff) so we took parts of the code. These could have easily -been patches in libcc, but at the time when these were written the goal of -libcc was to maintain very minimal patches and chromium_src changes tend to be -big ones. Also, note that these patches can never be upstreamed unlike other -libcc patches we maintain now. - ## Structure of Other Directories -* **script** - Scripts used for development purpose like building, packaging, - testing, etc. -* **tools** - Helper scripts used by GN files, unlike `script`, scripts put - here should never be invoked by users directly. -* **vendor** - Source code of third party dependencies, we didn't use - `third_party` as name because it would confuse it with the same directory in - Chromium's source code tree. -* **node_modules** - Third party node modules used for building. -* **out** - Temporary output directory of `ninja`. +* **.circleci** - Config file for CI with CircleCI. +* **.github** - GitHub-specific config files including issues templates and CODEOWNERS. * **dist** - Temporary directory created by `script/create-dist.py` script when creating a distribution. -* **external_binaries** - Downloaded binaries of third-party frameworks which - do not support building with `gn`. - -## Keeping Git Submodules Up to Date - -The Electron repository has a few vendored dependencies, found in the -[/vendor][vendor] directory. Occasionally you might see a message like this -when running `git status`: - -```sh -$ git status - - modified: vendor/depot_tools (new commits) - modified: vendor/boto (new commits) -``` - -To update these vendored dependencies, run the following command: - -```sh -git submodule update --init --recursive -``` - -If you find yourself running this command often, you can create an alias for it -in your `~/.gitconfig` file: +* **node_modules** - Third party node modules used for building. +* **npm** - Logic for installation of Electron via npm. +* **out** - Temporary output directory of `ninja`. +* **script** - Scripts used for development purpose like building, packaging, + testing, etc. -```sh -[alias] - su = submodule update --init --recursive +```diff +script/ - The set of all scripts Electron runs for a variety of purposes. +├── codesign/ - Fakes codesigning for Electron apps; used for testing. +├── lib/ - Miscellaneous python utility scripts. +└── release/ - Scripts run during Electron's release process. + ├── notes/ - Generates release notes for new Electron versions. + └── uploaders/ - Uploads various release-related files during release. ``` -[vendor]: https://github.com/electron/electron/tree/master/vendor +* **typings** - TypeScript typings for Electron's internal code. diff --git a/docs/development/testing.md b/docs/development/testing.md index 2b6651c7e3531..f170dc6c8540e 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -11,40 +11,90 @@ as well as unit and integration tests. To learn more about Electron's coding style, please see the [coding-style](coding-style.md) document. ## Linting -To ensure that your JavaScript is in compliance with the Electron coding -style, run `npm run lint-js`, which will run `standard` against both -Electron itself as well as the unit tests. If you are using an editor -with a plugin/addon system, you might want to use one of the many -[StandardJS addons][standard-addons] to be informed of coding style -violations before you ever commit them. -To run `standard` with parameters, run `npm run lint-js --` followed by -arguments you want passed to `standard`. +To ensure that your changes are in compliance with the Electron coding +style, run `npm run lint`, which will run a variety of linting checks +against your changes depending on which areas of the code they touch. -To ensure that your C++ is in compliance with the Electron coding style, -run `npm run lint-cpp`, which runs a `cpplint` script. We recommend that -you use `clang-format` and prepared [a short tutorial](clang-format.md). - -There is not a lot of Python in this repository, but it too is governed -by coding style rules. `npm run lint-py` will check all Python, using -`pylint` to do so. +Many of these checks are included as precommit hooks, so it's likely +you error would be caught at commit time. ## Unit Tests +If you are not using [build-tools](https://github.com/electron/build-tools), +ensure that the name you have configured for your +local build of Electron is one of `Testing`, `Release`, `Default`, or +you have set `process.env.ELECTRON_OUT_DIR`. Without these set, Electron will fail +to perform some pre-testing steps. + To run all unit tests, run `npm run test`. The unit tests are an Electron app (surprise!) that can be found in the `spec` folder. Note that it has its own `package.json` and that its dependencies are therefore not defined in the top-level `package.json`. +To run only tests in a specific process, run `npm run test --runners=PROCESS` +where `PROCESS` is one of `main` or `remote`. + To run only specific tests matching a pattern, run `npm run test -- -g=PATTERN`, replacing the `PATTERN` with a regex that matches the tests you would like to run. As an example: If you want to run only IPC tests, you would run `npm run test -- -g ipc`. -[standard-addons]: https://standardjs.com/#are-there-text-editor-plugins +## Node.js Smoke Tests + +If you've made changes that might affect the way Node.js is embedded into Electron, +we have a test runner that runs all of the tests from Node.js, using Electron's custom fork +of Node.js. + +To run all of the Node.js tests: + +```bash +$ node script/node-spec-runner.js +``` + +To run a single Node.js test: + +```bash +$ node script/node-spec-runner.js parallel/test-crypto-keygen +``` + +where the argument passed to the runner is the path to the test in +the Node.js source tree. ### Testing on Windows 10 devices + +#### Extra steps to run the unit test: + +1. Visual Studio 2019 must be installed. +2. Node headers have to be compiled for your configuration. + + ```powershell + ninja -C out\Testing third_party\electron_node:headers + ``` + +3. The electron.lib has to be copied as node.lib. + + ```powershell + cd out\Testing + mkdir gen\node_headers\Release + copy electron.lib gen\node_headers\Release\node.lib + ``` + +#### Missing fonts + [Some Windows 10 devices](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list) do not ship with the Meiryo font installed, which may cause a font fallback test to fail. To install Meiryo: + 1. Push the Windows key and search for _Manage optional features_. -1. Click _Add a feature_. -1. Select _Japanese Supplemental Fonts_ and click _Install_. +2. Click _Add a feature_. +3. Select _Japanese Supplemental Fonts_ and click _Install_. + +#### Pixel measurements + +Some tests which rely on precise pixel measurements may not work correctly on +devices with Hi-DPI screen settings due to floating point precision errors. +To run these tests correctly, make sure the device is set to 100% scaling. + +To configure display scaling: + +1. Push the Windows key and search for _Display settings_. +2. Under _Scale and layout_, make sure that the device is set to 100%. diff --git a/docs/development/upgrading-chromium.md b/docs/development/upgrading-chromium.md deleted file mode 100644 index bf75a36701cf7..0000000000000 --- a/docs/development/upgrading-chromium.md +++ /dev/null @@ -1,166 +0,0 @@ -# Upgrading Chromium - -This is an overview of the steps needed to upgrade Chromium in Electron. - -- Upgrade libcc to a new Chromium version -- Make Electron code compatible with the new libcc -- Update Electron dependencies (crashpad, NodeJS, etc.) if needed -- Make internal builds of libcc and electron -- Update Electron docs if necessary - - -## Upgrade `libcc` to a new Chromium version - -1. Get the code and initialize the project: - ```sh - $ git clone git@github.com:electron/libchromiumcontent.git - $ cd libchromiumcontent - $ ./script/bootstrap -v - ``` -2. Update the Chromium snapshot - - Choose a version number from [OmahaProxy](https://omahaproxy.appspot.com/) - and update the `VERSION` file with it - - This can be done manually by visiting OmahaProxy in a browser, or automatically: - - One-liner for the latest stable mac version: `curl -so- https://omahaproxy.appspot.com/mac > VERSION` - - One-liner for the latest win64 beta version: `curl -so- https://omahaproxy.appspot.com/all | grep "win64,beta" | awk -F, 'NR==1{print $3}' > VERSION` - - run `$ ./script/update` - - Brew some tea -- this may run for 30m or more. - - It will probably fail applying patches. -3. Fix `*.patch` files in the `patches/` and `patches-mas/` folders. -4. (Optional) `script/update` applies patches, but if multiple tries are needed - you can manually run the same script that `update` calls: - `$ ./script/apply-patches` - - There is a second script, `script/patch.py` that may be useful. - Read `./script/patch.py -h` for more information. -5. Run the build when all patches can be applied without errors - - `$ ./script/build` - - If some patches are no longer compatible with the Chromium code, - fix compilation errors. -6. When the build succeeds, create a `dist` for Electron - - `$ ./script/create-dist --no_zip` - - It will create a `dist/main` folder in the libcc repo's root. - You will need this to build Electron. -7. (Optional) Update script contents if there are errors resulting from files - that were removed or renamed. (`--no_zip` prevents script from create `dist` - archives. You don't need them.) - - -## Update Electron's code - -1. Get the code: - ```sh - $ git clone git@github.com:electron/electron.git - $ cd electron - ``` -2. If you have libcc built on your machine in its own repo, - tell Electron to use it: - ```sh - $ ./script/bootstrap.py -v \ - --libcc_source_path <libcc_folder>/src \ - --libcc_shared_library_path <libcc_folder>/shared_library \ - --libcc_static_library_path <libcc_folder>/static_library - ``` -3. If you haven't yet built libcc but it's already supposed to be upgraded - to a new Chromium, bootstrap Electron as usual - `$ ./script/bootstrap.py -v` - - Ensure that libcc submodule (`vendor/libchromiumcontent`) points to the - right revision - -4. Set `CLANG_REVISION` in `script/update-clang.sh` to match the version - Chromium is using. - - Located in `electron/libchromiumcontent/src/tools/clang/scripts/update.py` - -5. Checkout Chromium if you haven't already: - - https://chromium.googlesource.com/chromium/src.git/+/{VERSION}/tools/clang/scripts/update.py - - (Replace the `{VERSION}` placeholder in the url above to the Chromium - version libcc uses.) -6. Build Electron. - - Try to build Debug version first: `$ ./script/build.py -c D` - - You will need it to run tests -7. Fix compilation and linking errors -8. Ensure that Release build can be built too - - `$ ./script/build.py -c R` - - Often the Release build will have different linking errors that you'll - need to fix. - - Some compilation and linking errors are caused by missing source/object - files in the libcc `dist` -9. Update `./script/create-dist` in the libcc repo, recreate a `dist`, and - run Electron bootstrap script once again. - -### Tips for fixing compilation errors -- Fix build config errors first -- Fix fatal errors first, like missing files and errors related to compiler - flags or defines -- Try to identify complex errors as soon as possible. - - Ask for help if you're not sure how to fix them -- Disable all Electron features, fix the build, then enable them one by one -- Add more build flags to disable features in build-time. - -When a Debug build of Electron succeeds, run the tests: -`$ npm run test` -Fix the failing tests. - -Follow all the steps above to fix Electron code on all supported platforms. - - -## Updating Crashpad - -If there are any compilation errors related to the Crashpad, it probably means -you need to update the fork to a newer revision. See -[Upgrading Crashpad](upgrading-crashpad.md) -for instructions on how to do that. - - -## Updating NodeJS - -Upgrade `vendor/node` to the Node release that corresponds to the v8 version -used in the new Chromium release. See the v8 versions in Node on - -See [Upgrading Node](upgrading-node.md) -for instructions on this. - -## Verify ffmpeg support - -Electron ships with a version of `ffmpeg` that includes proprietary codecs by -default. A version without these codecs is built and distributed with each -release as well. Each Chrome upgrade should verify that switching this version -is still supported. - -You can verify Electron's support for multiple `ffmpeg` builds by loading the -following page. It should work with the default `ffmpeg` library distributed -with Electron and not work with the `ffmpeg` library built without proprietary -codecs. - -```html -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <title>Proprietary Codec Check - - -

Checking if Electron is using proprietary codecs by loading video from http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4

-

- - - - -``` - -## Useful links - -- [Chrome Release Schedule](https://www.chromium.org/developers/calendar) -- [OmahaProxy](http://omahaproxy.appspot.com) -- [Chromium Issue Tracker](https://bugs.chromium.org/p/chromium) diff --git a/docs/development/upgrading-crashpad.md b/docs/development/upgrading-crashpad.md deleted file mode 100644 index 1fd65d5f22a15..0000000000000 --- a/docs/development/upgrading-crashpad.md +++ /dev/null @@ -1,39 +0,0 @@ -# Upgrading Crashpad - -1. Get the version of crashpad that we're going to use. - - `libcc/src/third_party/crashpad/README.chromium` will have a line `Revision:` with a checksum - - We need to check out the corresponding branch. - - Clone Google's crashpad (https://chromium.googlesource.com/crashpad/crashpad) - - `git clone https://chromium.googlesource.com/crashpad/crashpad` - - Check out the branch with the revision checksum: - - `git checkout ` - - Add electron's crashpad fork as a remote - - `git remote add electron https://github.com/electron/crashpad` - - Check out a new branch for the update - - `git checkout -b electron-crashpad-vA.B.C.D` - - `A.B.C.D` is the Chromium version found in `libcc/VERSION` - and will be something like `62.0.3202.94` - -2. Make a checklist of the Electron patches that need to be applied - with `git log --oneline` - - Or view https://github.com/electron/crashpad/commits/previous-branch-name - -3. For each patch: - - In `electron-crashpad-vA.B.C.D`, cherry-pick the patch's checksum - - `git cherry-pick ` - - Resolve any conflicts - - Make sure it builds then add, commit, and push work to electron's crashpad fork - - `git push electron electron-crashpad-vA.B.C.D` - -4. Update Electron to build the new crashpad: - - `cd vendor/crashpad` - - `git fetch` - - `git checkout electron-crashpad-v62.0.3202.94` -5. Regenerate Ninja files against both targets - - From Electron root's root, run `script/update.py` - - `script/build.py -c D --target=crashpad_client` - - `script/build.py -c D --target=crashpad_handler` - - Both should build with no errors -6. Push changes to submodule reference - - (From electron root) `git add vendor/crashpad` - - `git push origin upgrade-to-chromium-62` diff --git a/docs/development/upgrading-node.md b/docs/development/upgrading-node.md deleted file mode 100644 index 7b5dca06ba0be..0000000000000 --- a/docs/development/upgrading-node.md +++ /dev/null @@ -1,121 +0,0 @@ -# Upgrading Node - -## Discussion - -Chromium and Node.js both depend on V8, and Electron contains -only a single copy of V8, so it's important to ensure that the -version of V8 chosen is compatible with the build's version of -Node.js and Chromium. - -Upgrading Node is much easier than upgrading Chromium, so fewer -conflicts arise if one upgrades Chromium first, then chooses the -upstream Node release whose version of V8 is closest to the one -Chromium contains. - -Electron has its own [Node fork](https://github.com/electron/node) -with modifications for the V8 build details mentioned above -and for exposing API needed by Electron. Once an upstream Node -release is chosen, it's placed in a branch in Electron's Node fork -and any Electron Node patches are applied there. - -Another factor is that the Node project patches its version of V8. -As mentioned above, Electron builds everything with a single copy -of V8, so Node's V8 patches must be ported to that copy. - -Once all of Electron's dependencies are building and using the same -copy of V8, the next step is to fix any Electron code issues caused -by the Node upgrade. - -[FIXME] something about a Node debugger in Atom that we (e.g. deepak) -use and need to confirm doesn't break with the Node upgrade? - -So in short, the primary steps are: - -1. Update Electron's Node fork to the desired version -2. Backport Node's V8 patches to our copy of V8 -3. Update the GN build files, porting changes from node's GYP files -4. Update Electron's DEPS to use new version of Node - -## Updating Electron's Node [fork](https://github.com/electron/node) - -1. Ensure that `master` on `electron/node` has updated release tags from `nodejs/node` -2. Create a branch in https://github.com/electron/node: `electron-node-vX.X.X` where the base that you're branching from is the tag for the desired update - - `vX.X.X` Must use a version of Node compatible with our current version of Chromium -3. Re-apply our commits from the previous version of Node we were using (`vY.Y.Y`) to `v.X.X.X` - - Check release tag and select the range of commits we need to re-apply - - Cherry-pick commit range: - 1. Checkout both `vY.Y.Y` & `v.X.X.X` - 2. `git cherry-pick FIRST_COMMIT_HASH..LAST_COMMIT_HASH` - - Resolve merge conflicts in each file encountered, then: - 1. `git add ` - 2. `git cherry-pick --continue` - 3. Repeat until finished - - -## Updating [V8](https://github.com/electron/node/src/V8) Patches - -We need to generate a patch file from each patch that Node -applies to V8. - -```sh -$ cd third_party/electron_node -$ CURRENT_NODE_VERSION=vX.Y.Z -# Find the last commit with the message "deps: update V8 to " -# This commit corresponds to Node resetting V8 to its pristine upstream -# state at the stated version. -$ LAST_V8_UPDATE="$(git log --grep='^deps: update V8' --format='%H' -1 deps/v8)" -# This creates a patch file containing all changes in deps/v8 from -# $LAST_V8_UPDATE up to the current Node version, formatted in a way that -# it will apply cleanly to the V8 repository (i.e. with `deps/v8` -# stripped off the path and excluding the v8/gypfiles directory, which -# isn't present in V8. -$ git format-patch \ - --relative=deps/v8 \ - $LAST_V8_UPDATE..$CURRENT_NODE_VERSION \ - deps/v8 \ - ':(exclude)deps/v8/gypfiles' \ - --stdout \ - > ../../electron/common/patches/v8/node_v8_patches.patch -``` - -This list of patches will probably include one that claims to -make the V8 API backwards-compatible with a previous version of -V8. Unfortunately, those patches almost always change the V8 API -in a way that is incompatible with Chromium. - -It's usually easier to update Node to work without the -compatibility patch than to update Chromium to work with the -compatibility patch, so it's recommended to revert the -compatibility patch and fix any errors that arise when compiling -Node. - -## Update Electron's `DEPS` file - -Update the `DEPS` file in the root of -[electron/electron](https://github.com/electron/electron) to -point to the git hash of the updated Node. - -## Notes - -- Node maintains its own fork of V8 - - They backport a small amount of things as needed - - Documentation in Node about how [they work with V8](https://nodejs.org/api/v8.html) -- We update code such that we only use one copy of V8 across all of Electron - - E.g Electron, Chromium, and Node.js -- We don’t track upstream closely due to logistics: - - Upstream uses multiple repos and so merging into a single repo - would result in lost history. So we only update when we’re planning - a Node version bump in Electron. -- Chromium is large and time-consuming to update, so we typically - choose the Node version based on which of its releases has a version - of V8 that’s closest to the version in Chromium that we’re using. - - We sometimes have to wait for the next periodic Node release - because it will sync more closely with the version of V8 in the new Chromium - - Electron keeps all its patches in the repo because it’s simpler than - maintaining different repos for patches for each upstream project. - - Crashpad, Node.js, Chromium, Skia etc. patches are all kept in the same place - - Building Node: - - We maintain our own GN build files for Node.js to make it easier to ensure - that eevrything is built with the same compiler flags. - This means that every time we upgrade Node.js we have to do a modest amount of - work to synchronize the GN files with the upstream GYP files. diff --git a/docs/development/v8-development.md b/docs/development/v8-development.md index 76d13299ca7e5..5bc62244fcfc6 100644 --- a/docs/development/v8-development.md +++ b/docs/development/v8-development.md @@ -2,10 +2,10 @@ > A collection of resources for learning and using V8 -* [V8 Tracing](https://github.com/v8/v8/wiki/Tracing-V8) -* [V8 Profiler](https://github.com/v8/v8/wiki/V8-Profiler) - Profiler combinations which are useful for profiling: `--prof`, `--trace-ic`, `--trace-opt`, `--trace-deopt`, `--print-bytecode`, `--print-opt-code` +* [V8 Tracing](https://v8.dev/docs/trace) +* [V8 Profiler](https://v8.dev/docs/profile) - Profiler combinations which are useful for profiling: `--prof`, `--trace-ic`, `--trace-opt`, `--trace-deopt`, `--print-bytecode`, `--print-opt-code` * [V8 Interpreter Design](https://docs.google.com/document/d/11T2CRex9hXxoJwbYqVQ32yIPMh0uouUZLdyrtmMoL44/edit?ts=56f27d9d#heading=h.6jz9dj3bnr8t) -* [Optimizing compiler](https://github.com/v8/v8/wiki/TurboFan) -* [V8 GDB Debugging](https://github.com/v8/v8/wiki/GDB-JIT-Interface) +* [Optimizing compiler](https://v8.dev/docs/turbofan) +* [V8 GDB Debugging](https://v8.dev/docs/gdb-jit) See also [Chromium Development](chromium-development.md) diff --git a/docs/experimental.md b/docs/experimental.md new file mode 100644 index 0000000000000..b9bc620ea83a8 --- /dev/null +++ b/docs/experimental.md @@ -0,0 +1,23 @@ +# Experimental APIs + +Some of Electrons APIs are tagged with `_Experimental_` in the documentation. +This tag indicates that the API may not be considered stable and the API may +be removed or modified more frequently than other APIs with less warning. + +## Conditions for an API to be tagged as Experimental + +Anyone can request an API be tagged as experimental in a feature PR, disagreements +on the experimental nature of a feature can be discussed in the API WG if they +can't be resolved in the PR. + +## Process for removing the Experimental tag + +Once an API has been stable and in at least two major stable release lines it +can be nominated to have its experimental tag removed. This discussion should +happen at an API WG meeting. Things to consider when discussing / nominating: + +* The above "two major stables release lines" condition must have been met +* During that time no major bugs / issues should have been caused by the adoption of this feature +* The API is stable enough and hasn't been heavily impacted by Chromium upgrades +* Is anyone using the API? +* Is the API fulfilling the original proposed usecases, does it have any gaps? diff --git a/docs/faq.md b/docs/faq.md index d708222cd5a57..174104aee9b0c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -43,30 +43,18 @@ use HTML5 APIs which are already available in browsers. Good candidates are [Storage API][storage], [`localStorage`][local-storage], [`sessionStorage`][session-storage], and [IndexedDB][indexed-db]. -Or you can use the IPC system, which is specific to Electron, to store objects -in the main process as a global variable, and then to access them from the -renderers through the `remote` property of `electron` module: - -```javascript -// In the main process. -global.sharedObject = { - someProperty: 'default value' -} -``` - -```javascript -// In page 1. -require('electron').remote.getGlobal('sharedObject').someProperty = 'new value' -``` - -```javascript -// In page 2. -console.log(require('electron').remote.getGlobal('sharedObject').someProperty) -``` - -## My app's window/tray disappeared after a few minutes. - -This happens when the variable which is used to store the window/tray gets +Alternatively, you can use the IPC primitives that are provided by Electron. To +share data between the main and renderer processes, you can use the +[`ipcMain`](api/ipc-main.md) and [`ipcRenderer`](api/ipc-renderer.md) modules. +To communicate directly between web pages, you can send a +[`MessagePort`][message-port] from one to the other, possibly via the main process +using [`ipcRenderer.postMessage()`](api/ipc-renderer.md#ipcrendererpostmessagechannel-message-transfer). +Subsequent communication over message ports is direct and does not detour through +the main process. + +## My app's tray disappeared after a few minutes. + +This happens when the variable which is used to store the tray gets garbage collected. If you encounter this problem, the following articles may prove helpful: @@ -79,7 +67,7 @@ code from this: ```javascript const { app, Tray } = require('electron') -app.on('ready', () => { +app.whenReady().then(() => { const tray = new Tray('/path/to/icon.png') tray.setTitle('hello world') }) @@ -90,7 +78,7 @@ to this: ```javascript const { app, Tray } = require('electron') let tray = null -app.on('ready', () => { +app.whenReady().then(() => { tray = new Tray('/path/to/icon.png') tray.setTitle('hello world') }) @@ -107,7 +95,7 @@ To solve this, you can turn off node integration in Electron: ```javascript // In the main process. const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ +const win = new BrowserWindow({ webPreferences: { nodeIntegration: false } @@ -139,38 +127,13 @@ When using Electron's built-in module you might encounter an error like this: Uncaught TypeError: Cannot read property 'setZoomLevel' of undefined ``` -This is because you have the [npm `electron` module][electron-module] installed -either locally or globally, which overrides Electron's built-in module. - -To verify whether you are using the correct built-in module, you can print the -path of the `electron` module: - -```javascript -console.log(require.resolve('electron')) -``` - -and then check if it is in the following form: - -```sh -"/path/to/Electron.app/Contents/Resources/atom.asar/renderer/api/lib/exports/electron.js" -``` - -If it is something like `node_modules/electron/index.js`, then you have to -either remove the npm `electron` module, or rename it. - -```sh -npm uninstall electron -npm uninstall -g electron -``` - -However if you are using the built-in module but still getting this error, it -is very likely you are using the module in the wrong process. For example +It is very likely you are using the module in the wrong process. For example `electron.app` can only be used in the main process, while `electron.webFrame` is only available in renderer processes. ## The font looks blurry, what is this and what can I do? -If [sub-pixel anti-aliasing](http://alienryderflex.com/sub_pixel/) is deactivated, then fonts on LCD screens can look blurry. Example: +If [sub-pixel anti-aliasing](https://alienryderflex.com/sub_pixel/) is deactivated, then fonts on LCD screens can look blurry. Example: ![subpixel rendering example] @@ -180,7 +143,7 @@ To achieve this goal, set the background in the constructor for [BrowserWindow][ ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow({ +const win = new BrowserWindow({ backgroundColor: '#fff' }) ``` @@ -196,5 +159,6 @@ Notice that just setting the background in the CSS does not have the desired eff [local-storage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage [session-storage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage [indexed-db]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API +[message-port]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort [browser-window]: api/browser-window.md [subpixel rendering example]: images/subpixel-rendering-screenshot.gif diff --git a/spec/fixtures/api/execute-javascript.html b/docs/fiddles/communication/two-processes/.keep similarity index 100% rename from spec/fixtures/api/execute-javascript.html rename to docs/fiddles/communication/two-processes/.keep diff --git a/docs/fiddles/communication/two-processes/asynchronous-messages/index.html b/docs/fiddles/communication/two-processes/asynchronous-messages/index.html new file mode 100644 index 0000000000000..43d23a29087f2 --- /dev/null +++ b/docs/fiddles/communication/two-processes/asynchronous-messages/index.html @@ -0,0 +1,27 @@ + + + + + + +
+
+

Asynchronous messages

+ Supports: Win, macOS, Linux | Process: Both +
+
+ + +
+

Using ipc to send messages between processes asynchronously is the preferred method since it will return when finished without blocking other operations in the same process.

+ +

This example sends a "ping" from this process (renderer) to the main process. The main process then replies with "pong".

+
+
+
+ + + diff --git a/docs/fiddles/communication/two-processes/asynchronous-messages/main.js b/docs/fiddles/communication/two-processes/asynchronous-messages/main.js new file mode 100644 index 0000000000000..942f7022f8590 --- /dev/null +++ b/docs/fiddles/communication/two-processes/asynchronous-messages/main.js @@ -0,0 +1,29 @@ +const { app, BrowserWindow, ipcMain } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Asynchronous messages', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) + +ipcMain.on('asynchronous-message', (event, arg) => { + event.sender.send('asynchronous-reply', 'pong') +}) diff --git a/docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js b/docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js new file mode 100644 index 0000000000000..40ed596201ad2 --- /dev/null +++ b/docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js @@ -0,0 +1,12 @@ +const { ipcRenderer } = require('electron') + +const asyncMsgBtn = document.getElementById('async-msg') + +asyncMsgBtn.addEventListener('click', () => { + ipcRenderer.send('asynchronous-message', 'ping') +}) + +ipcRenderer.on('asynchronous-reply', (event, arg) => { + const message = `Asynchronous message reply: ${arg}` + document.getElementById('async-reply').innerHTML = message +}) diff --git a/docs/fiddles/communication/two-processes/synchronous-messages/index.html b/docs/fiddles/communication/two-processes/synchronous-messages/index.html new file mode 100644 index 0000000000000..055fcf3473ce1 --- /dev/null +++ b/docs/fiddles/communication/two-processes/synchronous-messages/index.html @@ -0,0 +1,27 @@ + + + + + + +
+
+

Synchronous messages

+ Supports: Win, macOS, Linux | Process: Both +
+
+ + +
+

You can use the ipc module to send synchronous messages between processes as well, but note that the synchronous nature of this method means that it will block other operations while completing its task.

+ +

This example sends a synchronous message, "ping", from this process (renderer) to the main process. The main process then replies with "pong".

+
+
+
+ + + \ No newline at end of file diff --git a/docs/fiddles/communication/two-processes/synchronous-messages/main.js b/docs/fiddles/communication/two-processes/synchronous-messages/main.js new file mode 100644 index 0000000000000..1adb7c02c9f11 --- /dev/null +++ b/docs/fiddles/communication/two-processes/synchronous-messages/main.js @@ -0,0 +1,29 @@ +const { app, BrowserWindow, ipcMain } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Synchronous Messages', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) + +ipcMain.on('synchronous-message', (event, arg) => { + event.returnValue = 'pong' +}) \ No newline at end of file diff --git a/docs/fiddles/communication/two-processes/synchronous-messages/renderer.js b/docs/fiddles/communication/two-processes/synchronous-messages/renderer.js new file mode 100644 index 0000000000000..4769b6f97f714 --- /dev/null +++ b/docs/fiddles/communication/two-processes/synchronous-messages/renderer.js @@ -0,0 +1,9 @@ +const { ipcRenderer } = require('electron') + +const syncMsgBtn = document.getElementById('sync-msg') + +syncMsgBtn.addEventListener('click', () => { + const reply = ipcRenderer.sendSync('synchronous-message', 'ping') + const message = `Synchronous message reply: ${reply}` + document.getElementById('sync-reply').innerHTML = message +}) \ No newline at end of file diff --git a/docs/fiddles/features/drag-and-drop/index.html b/docs/fiddles/features/drag-and-drop/index.html new file mode 100644 index 0000000000000..7541c174b86fd --- /dev/null +++ b/docs/fiddles/features/drag-and-drop/index.html @@ -0,0 +1,15 @@ + + + + + Hello World! + + + +

Hello World!

+

Drag the boxes below to somewhere in your OS (Finder/Explorer, Desktop, etc.) to copy an example markdown file.

+
Drag me - File 1
+
Drag me - File 2
+ + + diff --git a/docs/fiddles/features/drag-and-drop/main.js b/docs/fiddles/features/drag-and-drop/main.js new file mode 100644 index 0000000000000..9ad158beafd5e --- /dev/null +++ b/docs/fiddles/features/drag-and-drop/main.js @@ -0,0 +1,48 @@ +const { app, BrowserWindow, ipcMain, nativeImage, NativeImage } = require('electron') +const path = require('path') +const fs = require('fs') +const https = require('https') + +function createWindow() { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + win.loadFile('index.html') +} + +const iconName = path.join(__dirname, 'iconForDragAndDrop.png'); +const icon = fs.createWriteStream(iconName); + +// Create a new file to copy - you can also copy existing files. +fs.writeFileSync(path.join(__dirname, 'drag-and-drop-1.md'), '# First file to test drag and drop') +fs.writeFileSync(path.join(__dirname, 'drag-and-drop-2.md'), '# Second file to test drag and drop') + +https.get('https://img.icons8.com/ios/452/drag-and-drop.png', (response) => { + response.pipe(icon); +}); + +app.whenReady().then(createWindow) + +ipcMain.on('ondragstart', (event, filePath) => { + event.sender.startDrag({ + file: path.join(__dirname, filePath), + icon: iconName, + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/drag-and-drop/preload.js b/docs/fiddles/features/drag-and-drop/preload.js new file mode 100644 index 0000000000000..4609e12c75528 --- /dev/null +++ b/docs/fiddles/features/drag-and-drop/preload.js @@ -0,0 +1,8 @@ +const { contextBridge, ipcRenderer } = require('electron') +const path = require('path') + +contextBridge.exposeInMainWorld('electron', { + startDrag: (fileName) => { + ipcRenderer.send('ondragstart', fileName) + } +}) diff --git a/docs/fiddles/features/drag-and-drop/renderer.js b/docs/fiddles/features/drag-and-drop/renderer.js new file mode 100644 index 0000000000000..b402fa3929258 --- /dev/null +++ b/docs/fiddles/features/drag-and-drop/renderer.js @@ -0,0 +1,9 @@ +document.getElementById('drag1').ondragstart = (event) => { + event.preventDefault() + window.electron.startDrag('drag-and-drop-1.md') +} + +document.getElementById('drag2').ondragstart = (event) => { + event.preventDefault() + window.electron.startDrag('drag-and-drop-2.md') +} diff --git a/docs/fiddles/features/keyboard-shortcuts/global/index.html b/docs/fiddles/features/keyboard-shortcuts/global/index.html new file mode 100644 index 0000000000000..fbe7e6323c996 --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/global/index.html @@ -0,0 +1,12 @@ + + + + + Hello World! + + + +

Hello World!

+

Hit Alt+Ctrl+I on Windows or Opt+Cmd+I on Mac to see a message printed to the console.

+ + diff --git a/docs/fiddles/features/keyboard-shortcuts/global/main.js b/docs/fiddles/features/keyboard-shortcuts/global/main.js new file mode 100644 index 0000000000000..24b2d343fbaaa --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/global/main.js @@ -0,0 +1,28 @@ +const { app, BrowserWindow, globalShortcut } = require('electron') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600, + }) + + win.loadFile('index.html') +} + +app.whenReady().then(() => { + globalShortcut.register('Alt+CommandOrControl+I', () => { + console.log('Electron loves global shortcuts!') + }) +}).then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/keyboard-shortcuts/interception-from-main/index.html b/docs/fiddles/features/keyboard-shortcuts/interception-from-main/index.html new file mode 100644 index 0000000000000..ff4540a3c9b2e --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/interception-from-main/index.html @@ -0,0 +1,12 @@ + + + + + Hello World! + + + +

Hello World!

+

Hit Ctrl+I to see a message printed to the console.

+ + diff --git a/docs/fiddles/features/keyboard-shortcuts/interception-from-main/main.js b/docs/fiddles/features/keyboard-shortcuts/interception-from-main/main.js new file mode 100644 index 0000000000000..80e4012c812d1 --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/interception-from-main/main.js @@ -0,0 +1,13 @@ +const { app, BrowserWindow } = require('electron') + +app.whenReady().then(() => { + const win = new BrowserWindow({ width: 800, height: 600 }) + + win.loadFile('index.html') + win.webContents.on('before-input-event', (event, input) => { + if (input.control && input.key.toLowerCase() === 'i') { + console.log('Pressed Control+I') + event.preventDefault() + } + }) +}) diff --git a/docs/fiddles/features/keyboard-shortcuts/local/index.html b/docs/fiddles/features/keyboard-shortcuts/local/index.html new file mode 100644 index 0000000000000..3aeae635b41d2 --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/local/index.html @@ -0,0 +1,12 @@ + + + + + Hello World! + + + +

Hello World!

+

Hit Alt+Shift+I on Windows, or Opt+Cmd+I on mac to see a message printed to the console.

+ + diff --git a/docs/fiddles/features/keyboard-shortcuts/local/main.js b/docs/fiddles/features/keyboard-shortcuts/local/main.js new file mode 100644 index 0000000000000..c583469df4820 --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/local/main.js @@ -0,0 +1,36 @@ +const { app, BrowserWindow, Menu, MenuItem } = require('electron') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600, + }) + + win.loadFile('index.html') +} + +const menu = new Menu() +menu.append(new MenuItem({ + label: 'Electron', + submenu: [{ + role: 'help', + accelerator: process.platform === 'darwin' ? 'Alt+Cmd+I' : 'Alt+Shift+I', + click: () => { console.log('Electron rocks!') } + }] +})) + +Menu.setApplicationMenu(menu) + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/keyboard-shortcuts/web-apis/index.html b/docs/fiddles/features/keyboard-shortcuts/web-apis/index.html new file mode 100644 index 0000000000000..b19f3e92fd070 --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/web-apis/index.html @@ -0,0 +1,17 @@ + + + + + + + + Hello World! + + +

Hello World!

+ +

Hit any key with this window focused to see it captured here.

+
Last Key Pressed:
+ + + diff --git a/docs/fiddles/features/keyboard-shortcuts/web-apis/main.js b/docs/fiddles/features/keyboard-shortcuts/web-apis/main.js new file mode 100644 index 0000000000000..5944f55c83f15 --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/web-apis/main.js @@ -0,0 +1,35 @@ +// Modules to control application life and create native browser window +const {app, BrowserWindow} = require('electron') +const path = require('path') + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/features/keyboard-shortcuts/web-apis/renderer.js b/docs/fiddles/features/keyboard-shortcuts/web-apis/renderer.js new file mode 100644 index 0000000000000..7f7e406c4b2cc --- /dev/null +++ b/docs/fiddles/features/keyboard-shortcuts/web-apis/renderer.js @@ -0,0 +1,7 @@ +function handleKeyPress (event) { + // You can put code here to handle the keypress. + document.getElementById("last-keypress").innerText = event.key + console.log(`You pressed ${event.key}`) +} + +window.addEventListener('keyup', handleKeyPress, true) diff --git a/docs/fiddles/features/macos-dark-mode/index.html b/docs/fiddles/features/macos-dark-mode/index.html new file mode 100644 index 0000000000000..dfd1e075fd705 --- /dev/null +++ b/docs/fiddles/features/macos-dark-mode/index.html @@ -0,0 +1,19 @@ + + + + + Hello World! + + + + +

Hello World!

+

Current theme source: System

+ + + + + + + + diff --git a/docs/fiddles/features/macos-dark-mode/main.js b/docs/fiddles/features/macos-dark-mode/main.js new file mode 100644 index 0000000000000..9503efb5f9a92 --- /dev/null +++ b/docs/fiddles/features/macos-dark-mode/main.js @@ -0,0 +1,43 @@ +const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron') +const path = require('path') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + win.loadFile('index.html') + + ipcMain.handle('dark-mode:toggle', () => { + if (nativeTheme.shouldUseDarkColors) { + nativeTheme.themeSource = 'light' + } else { + nativeTheme.themeSource = 'dark' + } + return nativeTheme.shouldUseDarkColors + }) + + ipcMain.handle('dark-mode:system', () => { + nativeTheme.themeSource = 'system' + }) +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) diff --git a/docs/fiddles/features/macos-dark-mode/preload.js b/docs/fiddles/features/macos-dark-mode/preload.js new file mode 100644 index 0000000000000..3def9e06ed8ea --- /dev/null +++ b/docs/fiddles/features/macos-dark-mode/preload.js @@ -0,0 +1,6 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('darkMode', { + toggle: () => ipcRenderer.invoke('dark-mode:toggle'), + system: () => ipcRenderer.invoke('dark-mode:system') +}) diff --git a/docs/fiddles/features/macos-dark-mode/renderer.js b/docs/fiddles/features/macos-dark-mode/renderer.js new file mode 100644 index 0000000000000..637f714c22406 --- /dev/null +++ b/docs/fiddles/features/macos-dark-mode/renderer.js @@ -0,0 +1,9 @@ +document.getElementById('toggle-dark-mode').addEventListener('click', async () => { + const isDarkMode = await window.darkMode.toggle() + document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light' +}) + +document.getElementById('reset-to-system').addEventListener('click', async () => { + await window.darkMode.system() + document.getElementById('theme-source').innerHTML = 'System' +}) diff --git a/docs/fiddles/features/macos-dark-mode/styles.css b/docs/fiddles/features/macos-dark-mode/styles.css new file mode 100644 index 0000000000000..eb6dd2f243449 --- /dev/null +++ b/docs/fiddles/features/macos-dark-mode/styles.css @@ -0,0 +1,7 @@ +@media (prefers-color-scheme: dark) { + body { background: #333; color: white; } +} + +@media (prefers-color-scheme: light) { + body { background: #ddd; color: black; } +} diff --git a/docs/fiddles/features/macos-dock-menu/index.html b/docs/fiddles/features/macos-dock-menu/index.html new file mode 100644 index 0000000000000..02eb6e015a9c6 --- /dev/null +++ b/docs/fiddles/features/macos-dock-menu/index.html @@ -0,0 +1,12 @@ + + + + + Hello World! + + + +

Hello World!

+

Right click the dock icon to see the custom menu options.

+ + diff --git a/docs/fiddles/features/macos-dock-menu/main.js b/docs/fiddles/features/macos-dock-menu/main.js new file mode 100644 index 0000000000000..f7f86a2361a80 --- /dev/null +++ b/docs/fiddles/features/macos-dock-menu/main.js @@ -0,0 +1,42 @@ +const { app, BrowserWindow, Menu } = require('electron') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600, + }) + + win.loadFile('index.html') +} + +const dockMenu = Menu.buildFromTemplate([ + { + label: 'New Window', + click () { console.log('New Window') } + }, { + label: 'New Window with Settings', + submenu: [ + { label: 'Basic' }, + { label: 'Pro' } + ] + }, + { label: 'New Command...' } +]) + +app.whenReady().then(() => { + if (process.platform === 'darwin') { + app.dock.setMenu(dockMenu) + } +}).then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/notifications/main/index.html b/docs/fiddles/features/notifications/main/index.html new file mode 100644 index 0000000000000..3c23f9066d9c1 --- /dev/null +++ b/docs/fiddles/features/notifications/main/index.html @@ -0,0 +1,12 @@ + + + + + Hello World! + + + +

Hello World!

+

After launching this application, you should see the system notification.

+ + diff --git a/docs/fiddles/features/notifications/main/main.js b/docs/fiddles/features/notifications/main/main.js new file mode 100644 index 0000000000000..f6e6f867ccc88 --- /dev/null +++ b/docs/fiddles/features/notifications/main/main.js @@ -0,0 +1,31 @@ +const { app, BrowserWindow, Notification } = require('electron') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadFile('index.html') +} + +const NOTIFICATION_TITLE = 'Basic Notification' +const NOTIFICATION_BODY = 'Notification from the Main process' + +function showNotification () { + new Notification({ title: NOTIFICATION_TITLE, body: NOTIFICATION_BODY }).show() +} + +app.whenReady().then(createWindow).then(showNotification) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/notifications/renderer/index.html b/docs/fiddles/features/notifications/renderer/index.html new file mode 100644 index 0000000000000..206eadb3a3dd6 --- /dev/null +++ b/docs/fiddles/features/notifications/renderer/index.html @@ -0,0 +1,15 @@ + + + + + Hello World! + + + +

Hello World!

+

After launching this application, you should see the system notification.

+

Click it to see the effect in this interface.

+ + + + diff --git a/docs/fiddles/features/notifications/renderer/main.js b/docs/fiddles/features/notifications/renderer/main.js new file mode 100644 index 0000000000000..e24a66dd52b8f --- /dev/null +++ b/docs/fiddles/features/notifications/renderer/main.js @@ -0,0 +1,24 @@ +const { app, BrowserWindow } = require('electron') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadFile('index.html') +} + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/notifications/renderer/renderer.js b/docs/fiddles/features/notifications/renderer/renderer.js new file mode 100644 index 0000000000000..a6c88f9a79464 --- /dev/null +++ b/docs/fiddles/features/notifications/renderer/renderer.js @@ -0,0 +1,6 @@ +const NOTIFICATION_TITLE = 'Title' +const NOTIFICATION_BODY = 'Notification from the Renderer process. Click to log to console.' +const CLICK_MESSAGE = 'Notification clicked!' + +new Notification(NOTIFICATION_TITLE, { body: NOTIFICATION_BODY }) + .onclick = () => document.getElementById("output").innerText = CLICK_MESSAGE diff --git a/docs/fiddles/features/offscreen-rendering/main.js b/docs/fiddles/features/offscreen-rendering/main.js new file mode 100644 index 0000000000000..10ad35d3d9a80 --- /dev/null +++ b/docs/fiddles/features/offscreen-rendering/main.js @@ -0,0 +1,29 @@ +const { app, BrowserWindow } = require('electron') +const fs = require('fs') +const path = require('path') + +app.disableHardwareAcceleration() + +let win + +app.whenReady().then(() => { + win = new BrowserWindow({ webPreferences: { offscreen: true } }) + win.loadURL('https://github.com') + win.webContents.on('paint', (event, dirty, image) => { + fs.writeFileSync('ex.png', image.toPNG()) + }) + win.webContents.setFrameRate(60) + console.log(`The screenshot has been successfully saved to ${path.join(process.cwd(), 'ex.png')}`) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/online-detection/index.html b/docs/fiddles/features/online-detection/index.html new file mode 100644 index 0000000000000..372e5a963f770 --- /dev/null +++ b/docs/fiddles/features/online-detection/index.html @@ -0,0 +1,13 @@ + + + + + Hello World! + + + +

Connection status:

+ + + + diff --git a/docs/fiddles/features/online-detection/main.js b/docs/fiddles/features/online-detection/main.js new file mode 100644 index 0000000000000..7bc42d7725670 --- /dev/null +++ b/docs/fiddles/features/online-detection/main.js @@ -0,0 +1,26 @@ +const { app, BrowserWindow } = require('electron') + +function createWindow () { + const onlineStatusWindow = new BrowserWindow({ + width: 300, + height: 200 + }) + + onlineStatusWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) diff --git a/docs/fiddles/features/online-detection/renderer.js b/docs/fiddles/features/online-detection/renderer.js new file mode 100644 index 0000000000000..223a517ae1677 --- /dev/null +++ b/docs/fiddles/features/online-detection/renderer.js @@ -0,0 +1,8 @@ +function onlineStatusIndicator () { + document.getElementById('status').innerHTML = navigator.onLine ? 'online' : 'offline' +} + +window.addEventListener('online', onlineStatusIndicator) +window.addEventListener('offline', onlineStatusIndicator) + +onlineStatusIndicator() diff --git a/docs/fiddles/features/progress-bar/index.html b/docs/fiddles/features/progress-bar/index.html new file mode 100644 index 0000000000000..d68c5129a6c2b --- /dev/null +++ b/docs/fiddles/features/progress-bar/index.html @@ -0,0 +1,15 @@ + + + + + Hello World! + + + +

Hello World!

+

Keep an eye on the dock (Mac) or taskbar (Windows, Unity) for this application!

+

It should indicate a progress that advances from 0 to 100%.

+

It should then show indeterminate (Windows) or pin at 100% (other operating systems) + briefly and then loop.

+ + diff --git a/docs/fiddles/features/progress-bar/main.js b/docs/fiddles/features/progress-bar/main.js new file mode 100644 index 0000000000000..c400638359011 --- /dev/null +++ b/docs/fiddles/features/progress-bar/main.js @@ -0,0 +1,48 @@ +const { app, BrowserWindow } = require('electron') + +let progressInterval + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadFile('index.html') + + const INCREMENT = 0.03 + const INTERVAL_DELAY = 100 // ms + + let c = 0 + progressInterval = setInterval(() => { + // update progress bar to next value + // values between 0 and 1 will show progress, >1 will show indeterminate or stick at 100% + win.setProgressBar(c) + + // increment or reset progress bar + if (c < 2) { + c += INCREMENT + } else { + c = (-INCREMENT * 5) // reset to a bit less than 0 to show reset state + } + }, INTERVAL_DELAY) +} + +app.whenReady().then(createWindow) + +// before the app is terminated, clear both timers +app.on('before-quit', () => { + clearInterval(progressInterval) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/recent-documents/index.html b/docs/fiddles/features/recent-documents/index.html new file mode 100644 index 0000000000000..62aae8f8a25c3 --- /dev/null +++ b/docs/fiddles/features/recent-documents/index.html @@ -0,0 +1,15 @@ + + + + + Recent Documents + + + +

Recent Documents

+

+ Right click on the app icon to see recent documents. + You should see `recently-used.md` added to the list of recent files +

+ + diff --git a/docs/fiddles/features/recent-documents/main.js b/docs/fiddles/features/recent-documents/main.js new file mode 100644 index 0000000000000..d11a5bcc6705a --- /dev/null +++ b/docs/fiddles/features/recent-documents/main.js @@ -0,0 +1,32 @@ +const { app, BrowserWindow } = require('electron') +const fs = require('fs') +const path = require('path') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadFile('index.html') +} + +const fileName = 'recently-used.md' +fs.writeFile(fileName, 'Lorem Ipsum', () => { + app.addRecentDocument(path.join(__dirname, fileName)) +}) + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + app.clearRecentDocuments() + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/represented-file/index.html b/docs/fiddles/features/represented-file/index.html new file mode 100644 index 0000000000000..67583b9d9ddd0 --- /dev/null +++ b/docs/fiddles/features/represented-file/index.html @@ -0,0 +1,17 @@ + + + + + Hello World! + + + + +

Hello World!

+

+ Click on the title with the

Command
or
Control
key pressed. + You should see a popup with the represented file at the top. +

+ + + diff --git a/docs/fiddles/features/represented-file/main.js b/docs/fiddles/features/represented-file/main.js new file mode 100644 index 0000000000000..204a3fc4586eb --- /dev/null +++ b/docs/fiddles/features/represented-file/main.js @@ -0,0 +1,30 @@ +const { app, BrowserWindow } = require('electron') +const os = require('os'); + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadFile('index.html') +} + +app.whenReady().then(() => { + const win = new BrowserWindow() + + win.setRepresentedFilename(os.homedir()) + win.setDocumentEdited(true) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) diff --git a/docs/fiddles/features/web-bluetooth/index.html b/docs/fiddles/features/web-bluetooth/index.html new file mode 100644 index 0000000000000..b2be53d400a6a --- /dev/null +++ b/docs/fiddles/features/web-bluetooth/index.html @@ -0,0 +1,17 @@ + + + + + + Web Bluetooth API + + +

Web Bluetooth API

+ + + +

Currently selected bluetooth device:

+ + + + diff --git a/docs/fiddles/features/web-bluetooth/main.js b/docs/fiddles/features/web-bluetooth/main.js new file mode 100644 index 0000000000000..b3cc55a438198 --- /dev/null +++ b/docs/fiddles/features/web-bluetooth/main.js @@ -0,0 +1,30 @@ +const {app, BrowserWindow} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + width: 800, + height: 600 + }) + + mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => { + event.preventDefault() + if (deviceList && deviceList.length > 0) { + callback(deviceList[0].deviceId) + } + }) + + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/features/web-bluetooth/renderer.js b/docs/fiddles/features/web-bluetooth/renderer.js new file mode 100644 index 0000000000000..e5830955599af --- /dev/null +++ b/docs/fiddles/features/web-bluetooth/renderer.js @@ -0,0 +1,8 @@ +async function testIt() { + const device = await navigator.bluetooth.requestDevice({ + acceptAllDevices: true + }) + document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}` +} + +document.getElementById('clickme').addEventListener('click',testIt) \ No newline at end of file diff --git a/docs/fiddles/features/web-hid/index.html b/docs/fiddles/features/web-hid/index.html new file mode 100644 index 0000000000000..659b5bc12395e --- /dev/null +++ b/docs/fiddles/features/web-hid/index.html @@ -0,0 +1,21 @@ + + + + + + WebHID API + + +

WebHID API

+ + + +

HID devices automatically granted access via setDevicePermissionHandler

+
+ +

HID devices automatically granted access via select-hid-device

+
+ + + + diff --git a/docs/fiddles/features/web-hid/main.js b/docs/fiddles/features/web-hid/main.js new file mode 100644 index 0000000000000..cb61e188a0fd0 --- /dev/null +++ b/docs/fiddles/features/web-hid/main.js @@ -0,0 +1,50 @@ +const {app, BrowserWindow} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + width: 800, + height: 600 + }) + + mainWindow.webContents.session.on('select-hid-device', (event, details, callback) => { + event.preventDefault() + if (details.deviceList && details.deviceList.length > 0) { + callback(details.deviceList[0].deviceId) + } + }) + + mainWindow.webContents.session.on('hid-device-added', (event, device) => { + console.log('hid-device-added FIRED WITH', device) + }) + + mainWindow.webContents.session.on('hid-device-removed', (event, device) => { + console.log('hid-device-removed FIRED WITH', device) + }) + + mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'hid' && details.securityOrigin === 'file:///') { + return true + } + }) + + mainWindow.webContents.session.setDevicePermissionHandler((details) => { + if (details.deviceType === 'hid' && details.origin === 'file://') { + return true + } + }) + + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/features/web-hid/renderer.js b/docs/fiddles/features/web-hid/renderer.js new file mode 100644 index 0000000000000..54bda3f977106 --- /dev/null +++ b/docs/fiddles/features/web-hid/renderer.js @@ -0,0 +1,19 @@ +async function testIt() { + const grantedDevices = await navigator.hid.getDevices() + let grantedDeviceList = '' + grantedDevices.forEach(device => { + grantedDeviceList += `
${device.productName}` + }) + document.getElementById('granted-devices').innerHTML = grantedDeviceList + const grantedDevices2 = await navigator.hid.requestDevice({ + filters: [] + }) + + grantedDeviceList = '' + grantedDevices2.forEach(device => { + grantedDeviceList += `
${device.productName}` + }) + document.getElementById('granted-devices2').innerHTML = grantedDeviceList +} + +document.getElementById('clickme').addEventListener('click',testIt) diff --git a/docs/fiddles/features/web-serial/index.html b/docs/fiddles/features/web-serial/index.html new file mode 100644 index 0000000000000..013718c2931fd --- /dev/null +++ b/docs/fiddles/features/web-serial/index.html @@ -0,0 +1,16 @@ + + + + + + Web Serial API + +

Web Serial API

+ + + +

Matching Arduino Uno device:

+ + + + \ No newline at end of file diff --git a/docs/fiddles/features/web-serial/main.js b/docs/fiddles/features/web-serial/main.js new file mode 100644 index 0000000000000..c6bd996724d2e --- /dev/null +++ b/docs/fiddles/features/web-serial/main.js @@ -0,0 +1,54 @@ +const {app, BrowserWindow} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + width: 800, + height: 600 + }) + + mainWindow.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + event.preventDefault() + if (portList && portList.length > 0) { + callback(portList[0].portId) + } else { + callback('') //Could not find any matching devices + } + }) + + mainWindow.webContents.session.on('serial-port-added', (event, port) => { + console.log('serial-port-added FIRED WITH', port) + }) + + mainWindow.webContents.session.on('serial-port-removed', (event, port) => { + console.log('serial-port-removed FIRED WITH', port) + }) + + mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'serial' && details.securityOrigin === 'file:///') { + return true + } + }) + + mainWindow.webContents.session.setDevicePermissionHandler((details) => { + if (details.deviceType === 'serial' && details.origin === 'file://') { + return true + } + }) + + mainWindow.loadFile('index.html') + + mainWindow.webContents.openDevTools() +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/features/web-serial/renderer.js b/docs/fiddles/features/web-serial/renderer.js new file mode 100644 index 0000000000000..1d684d219252e --- /dev/null +++ b/docs/fiddles/features/web-serial/renderer.js @@ -0,0 +1,19 @@ +async function testIt() { + const filters = [ + { usbVendorId: 0x2341, usbProductId: 0x0043 }, + { usbVendorId: 0x2341, usbProductId: 0x0001 } + ]; + try { + const port = await navigator.serial.requestPort({filters}); + const portInfo = port.getInfo(); + document.getElementById('device-name').innerHTML = `vendorId: ${portInfo.usbVendorId} | productId: ${portInfo.usbProductId} ` + } catch (ex) { + if (ex.name === 'NotFoundError') { + document.getElementById('device-name').innerHTML = 'Device NOT found' + } else { + document.getElementById('device-name').innerHTML = ex + } + } +} + +document.getElementById('clickme').addEventListener('click',testIt) diff --git a/docs/fiddles/ipc/pattern-1/index.html b/docs/fiddles/ipc/pattern-1/index.html new file mode 100644 index 0000000000000..28c1e42cd8b17 --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/index.html @@ -0,0 +1,14 @@ + + + + + + + Hello World! + + + Title: + + + + diff --git a/docs/fiddles/ipc/pattern-1/main.js b/docs/fiddles/ipc/pattern-1/main.js new file mode 100644 index 0000000000000..2e9e97edb81b0 --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/main.js @@ -0,0 +1,30 @@ +const {app, BrowserWindow, ipcMain} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + ipcMain.on('set-title', (event, title) => { + const webContents = event.sender + const win = BrowserWindow.fromWebContents(webContents) + win.setTitle(title) + }) + + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/ipc/pattern-1/preload.js b/docs/fiddles/ipc/pattern-1/preload.js new file mode 100644 index 0000000000000..822f4ed51622b --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + setTitle: (title) => ipcRenderer.send('set-title', title) +}) diff --git a/docs/fiddles/ipc/pattern-1/renderer.js b/docs/fiddles/ipc/pattern-1/renderer.js new file mode 100644 index 0000000000000..c44f59a8df08c --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/renderer.js @@ -0,0 +1,6 @@ +const setButton = document.getElementById('btn') +const titleInput = document.getElementById('title') +setButton.addEventListener('click', () => { + const title = titleInput.value + window.electronAPI.setTitle(title) +}); diff --git a/docs/fiddles/ipc/pattern-2/index.html b/docs/fiddles/ipc/pattern-2/index.html new file mode 100644 index 0000000000000..06e928c8ef13d --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/index.html @@ -0,0 +1,14 @@ + + + + + + + Dialog + + + + File path: + + + diff --git a/docs/fiddles/ipc/pattern-2/main.js b/docs/fiddles/ipc/pattern-2/main.js new file mode 100644 index 0000000000000..5492a7d159297 --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/main.js @@ -0,0 +1,32 @@ +const {app, BrowserWindow, ipcMain, dialog} = require('electron') +const path = require('path') + +async function handleFileOpen() { + const { canceled, filePaths } = await dialog.showOpenDialog() + if (canceled) { + return + } else { + return filePaths[0] + } +} + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + ipcMain.handle('dialog:openFile', handleFileOpen) + createWindow() + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/ipc/pattern-2/preload.js b/docs/fiddles/ipc/pattern-2/preload.js new file mode 100644 index 0000000000000..cb78f84230f8b --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI',{ + openFile: () => ipcRenderer.invoke('dialog:openFile') +}) diff --git a/docs/fiddles/ipc/pattern-2/renderer.js b/docs/fiddles/ipc/pattern-2/renderer.js new file mode 100644 index 0000000000000..47712eefe7df1 --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/renderer.js @@ -0,0 +1,7 @@ +const btn = document.getElementById('btn') +const filePathElement = document.getElementById('filePath') + +btn.addEventListener('click', async () => { + const filePath = await window.electronAPI.openFile() + filePathElement.innerText = filePath +}) diff --git a/docs/fiddles/ipc/pattern-3/index.html b/docs/fiddles/ipc/pattern-3/index.html new file mode 100644 index 0000000000000..18d2598986271 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/index.html @@ -0,0 +1,13 @@ + + + + + + + Menu Counter + + + Current value: 0 + + + diff --git a/docs/fiddles/ipc/pattern-3/main.js b/docs/fiddles/ipc/pattern-3/main.js new file mode 100644 index 0000000000000..357aa64e0fb43 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/main.js @@ -0,0 +1,48 @@ +const {app, BrowserWindow, Menu, ipcMain} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + const menu = Menu.buildFromTemplate([ + { + label: app.name, + submenu: [ + { + click: () => mainWindow.webContents.send('update-counter', 1), + label: 'Increment', + }, + { + click: () => mainWindow.webContents.send('update-counter', -1), + label: 'Decrement', + } + ] + } + + ]) + + Menu.setApplicationMenu(menu) + mainWindow.loadFile('index.html') + + // Open the DevTools. + mainWindow.webContents.openDevTools() +} + +app.whenReady().then(() => { + ipcMain.on('counter-value', (_event, value) => { + console.log(value) // will print value to Node console + }) + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/ipc/pattern-3/preload.js b/docs/fiddles/ipc/pattern-3/preload.js new file mode 100644 index 0000000000000..ad4dd27f1f9b2 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + handleCounter: (callback) => ipcRenderer.on('update-counter', callback) +}) diff --git a/docs/fiddles/ipc/pattern-3/renderer.js b/docs/fiddles/ipc/pattern-3/renderer.js new file mode 100644 index 0000000000000..04fd4bca890a3 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/renderer.js @@ -0,0 +1,8 @@ +const counter = document.getElementById('counter') + +window.electronAPI.handleCounter((event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue + event.sender.send('counter-value', newValue) +}) diff --git a/docs/fiddles/media/screenshot/.keep b/docs/fiddles/media/screenshot/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/media/screenshot/take-screenshot/index.html b/docs/fiddles/media/screenshot/take-screenshot/index.html new file mode 100644 index 0000000000000..264899abddeac --- /dev/null +++ b/docs/fiddles/media/screenshot/take-screenshot/index.html @@ -0,0 +1,25 @@ + + + + + + +
+

Take a Screenshot

+ Supports: Win, macOS, Linux | Process: Renderer +
+
+
+ + +
+

This demo uses the desktopCapturer module to gather screens in use and select the entire screen and take a snapshot of what is visible.

+

Clicking the demo button will take a screenshot of your current screen and open it in your default viewer.

+
+
+ + + diff --git a/docs/fiddles/media/screenshot/take-screenshot/main.js b/docs/fiddles/media/screenshot/take-screenshot/main.js new file mode 100644 index 0000000000000..be8ed98328b8d --- /dev/null +++ b/docs/fiddles/media/screenshot/take-screenshot/main.js @@ -0,0 +1,29 @@ +const { BrowserWindow, app, screen, ipcMain } = require('electron') + +let mainWindow = null + +ipcMain.handle('get-screen-size', () => { + return screen.getPrimaryDisplay().workAreaSize +}) + +function createWindow () { + const windowOptions = { + width: 600, + height: 300, + title: 'Take a Screenshot', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/media/screenshot/take-screenshot/renderer.js b/docs/fiddles/media/screenshot/take-screenshot/renderer.js new file mode 100644 index 0000000000000..e7988f5067d18 --- /dev/null +++ b/docs/fiddles/media/screenshot/take-screenshot/renderer.js @@ -0,0 +1,42 @@ +const { desktopCapturer, shell, ipcRenderer } = require('electron') + +const fs = require('fs') +const os = require('os') +const path = require('path') + +const screenshot = document.getElementById('screen-shot') +const screenshotMsg = document.getElementById('screenshot-path') + +screenshot.addEventListener('click', async (event) => { + screenshotMsg.textContent = 'Gathering screens...' + const thumbSize = await determineScreenShotSize() + const options = { types: ['screen'], thumbnailSize: thumbSize } + + desktopCapturer.getSources(options, (error, sources) => { + if (error) return console.log(error) + + sources.forEach((source) => { + const sourceName = source.name.toLowerCase() + if (sourceName === 'entire screen' || sourceName === 'screen 1') { + const screenshotPath = path.join(os.tmpdir(), 'screenshot.png') + + fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => { + if (error) return console.log(error) + shell.openExternal(`file://${screenshotPath}`) + + const message = `Saved screenshot to: ${screenshotPath}` + screenshotMsg.textContent = message + }) + } + }) + }) +}) + +async function determineScreenShotSize () { + const screenSize = await ipcRenderer.invoke('get-screen-size') + const maxDimension = Math.max(screenSize.width, screenSize.height) + return { + width: maxDimension * window.devicePixelRatio, + height: maxDimension * window.devicePixelRatio + } +} diff --git a/docs/fiddles/menus/customize-menus/.keep b/docs/fiddles/menus/customize-menus/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/menus/customize-menus/index.html b/docs/fiddles/menus/customize-menus/index.html new file mode 100644 index 0000000000000..e8b354f2a1865 --- /dev/null +++ b/docs/fiddles/menus/customize-menus/index.html @@ -0,0 +1,128 @@ + + + + + Customize Menus + + + +
+

Customize Menus

+ +

+ The Menu and MenuItem modules can be used to + create custom native menus. +

+ +

+ There are two kinds of menus: the application (top) menu and context + (right-click) menu. +

+ +

+ Open the + full API documentation(opens in new window) + in your browser. +

+
+ +
+

Create an application menu

+
+
+

+ The Menu and MenuItem modules allow you to + customize your application menu. If you don't set any menu, Electron + will generate a minimal menu for your app by default. +

+ +

+ If you click the 'View' option in the application menu and then the + 'App Menu Demo', you'll see an information box displayed. +

+ +
+

ProTip

+ Know operating system menu differences. +

+ When designing an app for multiple operating systems it's + important to be mindful of the ways application menu conventions + differ on each operating system. +

+

+ For instance, on Windows, accelerators are set with an + &. Naming conventions also vary, like between + "Settings" or "Preferences". Below are resources for learning + operating system specific standards. +

+ +
+
+
+
+ +
+

Create a context menu

+
+
+
+ +
+

+ A context, or right-click, menu can be created with the + Menu and MenuItem modules as well. You can + right-click anywhere in this app or click the demo button to see an + example context menu. +

+ +

+ In this demo we use the ipcRenderer module to show the + context menu when explicitly calling it from the renderer process. +

+

+ See the full + context-menu event documentation + for all the available properties. +

+
+
+
+ + + + diff --git a/docs/fiddles/menus/customize-menus/main.js b/docs/fiddles/menus/customize-menus/main.js new file mode 100644 index 0000000000000..db56e3f5fb519 --- /dev/null +++ b/docs/fiddles/menus/customize-menus/main.js @@ -0,0 +1,360 @@ +// Modules to control application life and create native browser window +const { + BrowserWindow, + Menu, + MenuItem, + ipcMain, + app, + shell, + dialog +} = require('electron') + +const menu = new Menu() +menu.append(new MenuItem({ label: 'Hello' })) +menu.append(new MenuItem({ type: 'separator' })) +menu.append( + new MenuItem({ label: 'Electron', type: 'checkbox', checked: true }) +) + +const template = [ + { + label: 'Edit', + submenu: [ + { + label: 'Undo', + accelerator: 'CmdOrCtrl+Z', + role: 'undo' + }, + { + label: 'Redo', + accelerator: 'Shift+CmdOrCtrl+Z', + role: 'redo' + }, + { + type: 'separator' + }, + { + label: 'Cut', + accelerator: 'CmdOrCtrl+X', + role: 'cut' + }, + { + label: 'Copy', + accelerator: 'CmdOrCtrl+C', + role: 'copy' + }, + { + label: 'Paste', + accelerator: 'CmdOrCtrl+V', + role: 'paste' + }, + { + label: 'Select All', + accelerator: 'CmdOrCtrl+A', + role: 'selectall' + } + ] + }, + { + label: 'View', + submenu: [ + { + label: 'Reload', + accelerator: 'CmdOrCtrl+R', + click: (item, focusedWindow) => { + if (focusedWindow) { + // on reload, start fresh and close any old + // open secondary windows + if (focusedWindow.id === 1) { + BrowserWindow.getAllWindows().forEach(win => { + if (win.id > 1) win.close() + }) + } + focusedWindow.reload() + } + } + }, + { + label: 'Toggle Full Screen', + accelerator: (() => { + if (process.platform === 'darwin') { + return 'Ctrl+Command+F' + } else { + return 'F11' + } + })(), + click: (item, focusedWindow) => { + if (focusedWindow) { + focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) + } + } + }, + { + label: 'Toggle Developer Tools', + accelerator: (() => { + if (process.platform === 'darwin') { + return 'Alt+Command+I' + } else { + return 'Ctrl+Shift+I' + } + })(), + click: (item, focusedWindow) => { + if (focusedWindow) { + focusedWindow.toggleDevTools() + } + } + }, + { + type: 'separator' + }, + { + label: 'App Menu Demo', + click: function (item, focusedWindow) { + if (focusedWindow) { + const options = { + type: 'info', + title: 'Application Menu Demo', + buttons: ['Ok'], + message: + 'This demo is for the Menu section, showing how to create a clickable menu item in the application menu.' + } + dialog.showMessageBox(focusedWindow, options, function () {}) + } + } + } + ] + }, + { + label: 'Window', + role: 'window', + submenu: [ + { + label: 'Minimize', + accelerator: 'CmdOrCtrl+M', + role: 'minimize' + }, + { + label: 'Close', + accelerator: 'CmdOrCtrl+W', + role: 'close' + }, + { + type: 'separator' + }, + { + label: 'Reopen Window', + accelerator: 'CmdOrCtrl+Shift+T', + enabled: false, + key: 'reopenMenuItem', + click: () => { + app.emit('activate') + } + } + ] + }, + { + label: 'Help', + role: 'help', + submenu: [ + { + label: 'Learn More', + click: () => { + shell.openExternal('https://electronjs.org') + } + } + ] + } +] + +function addUpdateMenuItems (items, position) { + if (process.mas) return + + const version = app.getVersion() + const updateItems = [ + { + label: `Version ${version}`, + enabled: false + }, + { + label: 'Checking for Update', + enabled: false, + key: 'checkingForUpdate' + }, + { + label: 'Check for Update', + visible: false, + key: 'checkForUpdate', + click: () => { + require('electron').autoUpdater.checkForUpdates() + } + }, + { + label: 'Restart and Install Update', + enabled: true, + visible: false, + key: 'restartToUpdate', + click: () => { + require('electron').autoUpdater.quitAndInstall() + } + } + ] + + items.splice.apply(items, [position, 0].concat(updateItems)) +} + +function findReopenMenuItem () { + const menu = Menu.getApplicationMenu() + if (!menu) return + + let reopenMenuItem + menu.items.forEach(item => { + if (item.submenu) { + item.submenu.items.forEach(item => { + if (item.key === 'reopenMenuItem') { + reopenMenuItem = item + } + }) + } + }) + return reopenMenuItem +} + +if (process.platform === 'darwin') { + const name = app.getName() + template.unshift({ + label: name, + submenu: [ + { + label: `About ${name}`, + role: 'about' + }, + { + type: 'separator' + }, + { + label: 'Services', + role: 'services', + submenu: [] + }, + { + type: 'separator' + }, + { + label: `Hide ${name}`, + accelerator: 'Command+H', + role: 'hide' + }, + { + label: 'Hide Others', + accelerator: 'Command+Alt+H', + role: 'hideothers' + }, + { + label: 'Show All', + role: 'unhide' + }, + { + type: 'separator' + }, + { + label: 'Quit', + accelerator: 'Command+Q', + click: () => { + app.quit() + } + } + ] + }) + + // Window menu. + template[3].submenu.push( + { + type: 'separator' + }, + { + label: 'Bring All to Front', + role: 'front' + } + ) + + addUpdateMenuItems(template[0].submenu, 1) +} + +if (process.platform === 'win32') { + const helpMenu = template[template.length - 1].submenu + addUpdateMenuItems(helpMenu, 0) +} +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + createWindow() + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) +}) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + const reopenMenuItem = findReopenMenuItem() + if (reopenMenuItem) reopenMenuItem.enabled = true + + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +app.on('browser-window-created', (event, win) => { + const reopenMenuItem = findReopenMenuItem() + if (reopenMenuItem) reopenMenuItem.enabled = false + + win.webContents.on('context-menu', (e, params) => { + menu.popup(win, params.x, params.y) + }) +}) + +ipcMain.on('show-context-menu', event => { + const win = BrowserWindow.fromWebContents(event.sender) + menu.popup(win) +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/menus/customize-menus/renderer.js b/docs/fiddles/menus/customize-menus/renderer.js new file mode 100644 index 0000000000000..5527e1f20008d --- /dev/null +++ b/docs/fiddles/menus/customize-menus/renderer.js @@ -0,0 +1,8 @@ +const { ipcRenderer } = require('electron') + +// Tell main process to show the menu when demo button is clicked +const contextMenuBtn = document.getElementById('context-menu') + +contextMenuBtn.addEventListener('click', () => { + ipcRenderer.send('show-context-menu') +}) diff --git a/docs/fiddles/menus/shortcuts/.keep b/docs/fiddles/menus/shortcuts/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/menus/shortcuts/index.html b/docs/fiddles/menus/shortcuts/index.html new file mode 100644 index 0000000000000..6851357980c72 --- /dev/null +++ b/docs/fiddles/menus/shortcuts/index.html @@ -0,0 +1,73 @@ + + + + + + Keyboard Shortcuts + + + +
+

Keyboard Shortcuts

+ +

The globalShortcut and Menu modules can be used to define keyboard shortcuts.

+ +

+ In Electron, keyboard shortcuts are called accelerators. + They can be assigned to actions in your application's Menu, + or they can be assigned globally so they'll be triggered even when + your app doesn't have keyboard focus. +

+ +

+ Open the full documentation for the + Menu, + Accelerator, + and + globalShortcut + APIs in your browser. +

+ +
+ +
+
+
+

+ To try this demo, press CommandOrControl+Alt+K on your + keyboard. +

+ +

+ Global shortcuts are detected even when the app doesn't have + keyboard focus, and they must be registered after the app's + `ready` event is emitted. +

+ +
+

ProTip

+ Avoid overriding system-wide keyboard shortcuts. +

+ When registering global shortcuts, it's important to be aware of + existing defaults in the target operating system, so as not to + override any existing behaviors. For an overview of each + operating system's keyboard shortcuts, view these documents: +

+ + +
+ +
+
+
+ + + diff --git a/docs/fiddles/menus/shortcuts/main.js b/docs/fiddles/menus/shortcuts/main.js new file mode 100644 index 0000000000000..ee3708bf565a7 --- /dev/null +++ b/docs/fiddles/menus/shortcuts/main.js @@ -0,0 +1,69 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, globalShortcut, dialog } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + globalShortcut.register('CommandOrControl+Alt+K', () => { + dialog.showMessageBox({ + type: 'info', + message: 'Success!', + detail: 'You pressed the registered global shortcut keybinding.', + buttons: ['OK'] + }) + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +app.on('will-quit', function () { + globalShortcut.unregisterAll() +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/dialogs/.keep b/docs/fiddles/native-ui/dialogs/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/native-ui/dialogs/error-dialog/index.html b/docs/fiddles/native-ui/dialogs/error-dialog/index.html new file mode 100644 index 0000000000000..2d516c28b683e --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/error-dialog/index.html @@ -0,0 +1,81 @@ + + + + + Error Dialog + + + +
+

Use system dialogs

+ +

+ The dialog module in Electron allows you to use native + system dialogs for opening files or directories, saving a file or + displaying informational messages. +

+ +

+ This is a main process module because this process is more efficient + with native utilities and it allows the call to happen without + interrupting the visible elements in your page's renderer process. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Error Dialog

+
+
+ +
+

+ In this demo, the ipc module is used to send a message + from the renderer process instructing the main process to launch the + error dialog. +

+ +

+ You can use an error dialog before the app's + ready event, which is useful for showing errors upon + startup. +

+
Renderer Process
+
+          
+const {ipcRenderer} = require('electron')
+
+const errorBtn = document.getElementById('error-dialog')
+
+errorBtn.addEventListener('click', (event) => {
+  ipcRenderer.send('open-error-dialog')
+})
+          
+
Main Process
+
+          
+const {ipcMain, dialog} = require('electron')
+
+ipcMain.on('open-error-dialog', (event) => {
+  dialog.showErrorBox('An Error Message', 'Demonstrating an error message.')
+})
+          
+        
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/dialogs/error-dialog/main.js b/docs/fiddles/native-ui/dialogs/error-dialog/main.js new file mode 100644 index 0000000000000..7567aa411bda2 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/error-dialog/main.js @@ -0,0 +1,95 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain, dialog } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +ipcMain.on('open-error-dialog', event => { + dialog.showErrorBox('An Error Message', 'Demonstrating an error message.') +}) + +ipcMain.on('open-information-dialog', event => { + const options = { + type: 'info', + title: 'Information', + message: "This is an information dialog. Isn't it nice?", + buttons: ['Yes', 'No'] + } + dialog.showMessageBox(options, index => { + event.sender.send('information-dialog-selection', index) + }) +}) + +ipcMain.on('open-file-dialog', event => { + dialog.showOpenDialog( + { + properties: ['openFile', 'openDirectory'] + }, + files => { + if (files) { + event.sender.send('selected-directory', files) + } + } + ) +}) + +ipcMain.on('save-dialog', event => { + const options = { + title: 'Save an Image', + filters: [{ name: 'Images', extensions: ['jpg', 'png', 'gif'] }] + } + dialog.showSaveDialog(options, filename => { + event.sender.send('saved-file', filename) + }) +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/dialogs/error-dialog/renderer.js b/docs/fiddles/native-ui/dialogs/error-dialog/renderer.js new file mode 100644 index 0000000000000..4011066587dcb --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/error-dialog/renderer.js @@ -0,0 +1,18 @@ +const { ipcRenderer, shell } = require('electron') + +const links = document.querySelectorAll('a[href]') +const errorBtn = document.getElementById('error-dialog') + +errorBtn.addEventListener('click', event => { + ipcRenderer.send('open-error-dialog') +}) + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) \ No newline at end of file diff --git a/docs/fiddles/native-ui/dialogs/information-dialog/index.html b/docs/fiddles/native-ui/dialogs/information-dialog/index.html new file mode 100644 index 0000000000000..5600b653874ad --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/information-dialog/index.html @@ -0,0 +1,104 @@ + + + + + Information Dialog + + + +
+

Use system dialogs

+ +

+ The dialog module in Electron allows you to use native + system dialogs for opening files or directories, saving a file or + displaying informational messages. +

+ +

+ This is a main process module because this process is more efficient + with native utilities and it allows the call to happen without + interrupting the visible elements in your page's renderer process. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Information Dialog

+
+
+ + +
+

+ In this demo, the ipc module is used to send a message + from the renderer process instructing the main process to launch the + information dialog. Options may be provided for responses which can + then be relayed back to the renderer process. +

+ +

+ Note: The title property is not displayed in macOS. +

+ +

+ An information dialog can contain an icon, your choice of buttons, + title and message. +

+
Renderer Process
+
+            
+const {ipcRenderer} = require('electron')
+
+const informationBtn = document.getElementById('information-dialog')
+
+informationBtn.addEventListener('click', (event) => {
+  ipcRenderer.send('open-information-dialog')
+})
+
+ipcRenderer.on('information-dialog-selection', (event, index) => {
+  let message = 'You selected '
+  if (index === 0) message += 'yes.'
+  else message += 'no.'
+  document.getElementById('info-selection').innerHTML = message
+})
+            
+          
+
Main Process
+
+            
+const {ipcMain, dialog} = require('electron')
+
+ipcMain.on('open-information-dialog', (event) => {
+  const options = {
+    type: 'info',
+    title: 'Information',
+    message: "This is an information dialog. Isn't it nice?",
+    buttons: ['Yes', 'No']
+  }
+  dialog.showMessageBox(options, (index) => {
+    event.sender.send('information-dialog-selection', index)
+  })
+})
+            
+          
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/dialogs/information-dialog/main.js b/docs/fiddles/native-ui/dialogs/information-dialog/main.js new file mode 100644 index 0000000000000..3e81a5782076f --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/information-dialog/main.js @@ -0,0 +1,70 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain, dialog } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + + +ipcMain.on('open-information-dialog', event => { + const options = { + type: 'info', + title: 'Information', + message: "This is an information dialog. Isn't it nice?", + buttons: ['Yes', 'No'] + } + dialog.showMessageBox(options, index => { + event.sender.send('information-dialog-selection', index) + }) +}) + + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/dialogs/information-dialog/renderer.js b/docs/fiddles/native-ui/dialogs/information-dialog/renderer.js new file mode 100644 index 0000000000000..69ea9cdb1a2f2 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/information-dialog/renderer.js @@ -0,0 +1,25 @@ +const { ipcRenderer, shell } = require('electron') + +const informationBtn = document.getElementById('information-dialog') +const links = document.querySelectorAll('a[href]') + +informationBtn.addEventListener('click', event => { + ipcRenderer.send('open-information-dialog') +}) + +ipcRenderer.on('information-dialog-selection', (event, index) => { + let message = 'You selected ' + if (index === 0) message += 'yes.' + else message += 'no.' + document.getElementById('info-selection').innerHTML = message +}) + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) \ No newline at end of file diff --git a/docs/fiddles/native-ui/dialogs/open-file-or-directory/index.html b/docs/fiddles/native-ui/dialogs/open-file-or-directory/index.html new file mode 100644 index 0000000000000..df96f2e810812 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/open-file-or-directory/index.html @@ -0,0 +1,108 @@ + + + + + Open File or Directory + + + +
+

Use system dialogs

+ +

+ The dialog module in Electron allows you to use native + system dialogs for opening files or directories, saving a file or + displaying informational messages. +

+ +

+ This is a main process module because this process is more efficient + with native utilities and it allows the call to happen without + interrupting the visible elements in your page's renderer process. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Open a File or Directory

+
+
+ + +
+

+ In this demo, the ipc module is used to send a message + from the renderer process instructing the main process to launch the + open file (or directory) dialog. If a file is selected, the main + process can send that information back to the renderer process. +

+
Renderer Process
+
+          
+const {ipcRenderer} = require('electron')
+
+const selectDirBtn = document.getElementById('select-directory')
+
+selectDirBtn.addEventListener('click', (event) => {
+  ipcRenderer.send('open-file-dialog')
+})
+
+ipcRenderer.on('selected-directory', (event, path) => {
+  document.getElementById('selected-file').innerHTML = `You selected: ${path}`
+})
+          
+        
+
Main Process
+
+          
+const {ipcMain, dialog} = require('electron')
+
+ipcMain.on('open-file-dialog', (event) => {
+  dialog.showOpenDialog({
+    properties: ['openFile', 'openDirectory']
+  }, (files) => {
+    if (files) {
+      event.sender.send('selected-directory', files)
+    }
+  })
+})
+          
+        
+ +
+

ProTip

+ The sheet-style dialog on macOS. +

+ On macOS you can choose between a "sheet" dialog or a default + dialog. The sheet version descends from the top of the window. To + use sheet version, pass the window as the first + argument in the dialog method. +

+
const ipc = require('electron').ipcMain
+const dialog = require('electron').dialog
+const BrowserWindow = require('electron').BrowserWindow
+
+
+ipc.on('open-file-dialog-sheet', function (event) {
+  const window = BrowserWindow.fromWebContents(event.sender)
+  const files = dialog.showOpenDialog(window, { properties: [ 'openFile' ]})
+})
+
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/dialogs/open-file-or-directory/main.js b/docs/fiddles/native-ui/dialogs/open-file-or-directory/main.js new file mode 100644 index 0000000000000..24b9164dab3a9 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/open-file-or-directory/main.js @@ -0,0 +1,70 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain, dialog } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + + +ipcMain.on('open-file-dialog', event => { + dialog.showOpenDialog( + { + properties: ['openFile', 'openDirectory'] + }, + files => { + if (files) { + event.sender.send('selected-directory', files) + } + } + ) +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/dialogs/open-file-or-directory/renderer.js b/docs/fiddles/native-ui/dialogs/open-file-or-directory/renderer.js new file mode 100644 index 0000000000000..5389ea50709a0 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/open-file-or-directory/renderer.js @@ -0,0 +1,22 @@ +const { ipcRenderer, shell } = require('electron') + +const selectDirBtn = document.getElementById('select-directory') +const links = document.querySelectorAll('a[href]') + +selectDirBtn.addEventListener('click', event => { + ipcRenderer.send('open-file-dialog') +}) + +ipcRenderer.on('selected-directory', (event, path) => { + document.getElementById('selected-file').innerHTML = `You selected: ${path}` +}) + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) diff --git a/docs/fiddles/native-ui/dialogs/save-dialog/index.html b/docs/fiddles/native-ui/dialogs/save-dialog/index.html new file mode 100644 index 0000000000000..b7ceaee7b1202 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/save-dialog/index.html @@ -0,0 +1,91 @@ + + + + + Save Dialog + + + +
+

Use system dialogs

+ +

+ The dialog module in Electron allows you to use native + system dialogs for opening files or directories, saving a file or + displaying informational messages. +

+ +

+ This is a main process module because this process is more efficient + with native utilities and it allows the call to happen without + interrupting the visible elements in your page's renderer process. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Save Dialog

+
+
+ + +
+

+ In this demo, the ipc module is used to send a message + from the renderer process instructing the main process to launch the + save dialog. It returns the path selected by the user which can be + relayed back to the renderer process. +

+
Renderer Process
+
+            
+const {ipcRenderer} = require('electron')
+
+const saveBtn = document.getElementById('save-dialog')
+
+saveBtn.addEventListener('click', (event) => {
+  ipcRenderer.send('save-dialog')
+})
+
+ipcRenderer.on('saved-file', (event, path) => {
+  if (!path) path = 'No path'
+  document.getElementById('file-saved').innerHTML = `Path selected: ${path}`
+})
+            
+          
+
Main Process
+
+            
+const {ipcMain, dialog} = require('electron')
+
+ipcMain.on('save-dialog', (event) => {
+  const options = {
+    title: 'Save an Image',
+    filters: [
+      { name: 'Images', extensions: ['jpg', 'png', 'gif'] }
+    ]
+  }
+  dialog.showSaveDialog(options, (filename) => {
+    event.sender.send('saved-file', filename)
+  })
+})
+            
+          
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/dialogs/save-dialog/main.js b/docs/fiddles/native-ui/dialogs/save-dialog/main.js new file mode 100644 index 0000000000000..b6e6ec1331be9 --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/save-dialog/main.js @@ -0,0 +1,66 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain, dialog } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +ipcMain.on('save-dialog', event => { + const options = { + title: 'Save an Image', + filters: [{ name: 'Images', extensions: ['jpg', 'png', 'gif'] }] + } + dialog.showSaveDialog(options, filename => { + event.sender.send('saved-file', filename) + }) +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/dialogs/save-dialog/renderer.js b/docs/fiddles/native-ui/dialogs/save-dialog/renderer.js new file mode 100644 index 0000000000000..9f6da5546946d --- /dev/null +++ b/docs/fiddles/native-ui/dialogs/save-dialog/renderer.js @@ -0,0 +1,23 @@ +const { ipcRenderer, shell } = require('electron') + +const saveBtn = document.getElementById('save-dialog') +const links = document.querySelectorAll('a[href]') + +saveBtn.addEventListener('click', event => { + ipcRenderer.send('save-dialog') +}) + +ipcRenderer.on('saved-file', (event, path) => { + if (!path) path = 'No path' + document.getElementById('file-saved').innerHTML = `Path selected: ${path}` +}) + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) \ No newline at end of file diff --git a/docs/fiddles/native-ui/drag-and-drop/.keep b/docs/fiddles/native-ui/drag-and-drop/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/native-ui/drag-and-drop/index.html b/docs/fiddles/native-ui/drag-and-drop/index.html new file mode 100644 index 0000000000000..40f2733cd266d --- /dev/null +++ b/docs/fiddles/native-ui/drag-and-drop/index.html @@ -0,0 +1,76 @@ + + + + + Drag and drop files + + + +
+

Drag and drop files

+
Supports: Win, macOS, Linux | Process: Both
+

+ Electron supports dragging files and content out from web content into + the operating system's world. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Dragging files

+
+
+ Drag Demo +
+

+ Click and drag the link above to copy the renderer process + javascript file on to your machine. +

+ +

+ In this demo, the webContents.startDrag() API is called + in response to the ondragstart event. +

+
Renderer Process
+

+const {ipcRenderer} = require('electron')
+
+const dragFileLink = document.getElementById('drag-file-link')
+
+dragFileLink.addEventListener('dragstart', (event) => {
+  event.preventDefault()
+  ipcRenderer.send('ondragstart', __filename)
+})
+        
+
Main Process
+
+            
+const {ipcMain} = require('electron')
+const path = require('path')
+
+ipcMain.on('ondragstart', (event, filepath) => {
+  const iconName = 'codeIcon.png'
+  event.sender.startDrag({
+    file: filepath,
+    icon: path.join(__dirname, iconName)
+  })
+})
+            
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/drag-and-drop/main.js b/docs/fiddles/native-ui/drag-and-drop/main.js new file mode 100644 index 0000000000000..b7093b59a868b --- /dev/null +++ b/docs/fiddles/native-ui/drag-and-drop/main.js @@ -0,0 +1,64 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain, nativeImage } = require('electron') +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +ipcMain.on('ondragstart', (event, filepath) => { + const icon = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAACsZJREFUWAmtWFlsXFcZ/u82++Jt7IyT2Em6ZFHTpAtWIzspEgjEUhA8VNAiIYEQUvuABBIUwUMkQIVKPCIoEiABLShISEBbhFJwIGRpIKRpbNeJ7bh2HHvssR3PPnPnLnzfmRlju6EQqUc+c++c8y/fv54z1uQOh+/7Glh0TD59TE/TND7lnfa4/64OKsM071QoeZpA/y9WWvk/B4XCC06TUC+Xyw8HTXNQ1+Ww6PpOrMebewXxvBueJ6/XHOdMJBL5J9Y97m2R0SS/wweE6JxkGx5dilWr1S/7dXsEa2o4+LyFmcFcaL5zbX3Y9gh5hpeWYpSB9XV5/H678V89BGYDXnHJlCsWn4gHrGc1K9CXxferOdvPOOKUfF8cH7nUyCtklQZXih/VNNlmirk3GdBSoIcRswW7/vVkLPYi5W2Uze8bh7J+4wLfh4dViFx5/nmrUi7/MhGNvrCkBfpeWqnW/7BUdadqntQ8zwr6vhUV34xpYnDynWvcmwQNaclDXsqgLMqkocPDw7fNx7d5qIX+/PmJxKGD6VdDkeh7ztyqOFfrokGCEWiiZ1mp0uITnuKAosaT7+pNxMYTyefutcQfbA+b1XLpH5fnF97/yD335Fu6mqTqsclDINBVmI4fDxw80KPAvJSt1MZtMcLiGxYUu83p4UkgnJZlqcl3LAj3WnTkIS9lUBYNPJjueVWgg7qocyOgliFqjZsg8gq5tRdiieQTf1gq15Y8CUbRZtyWOzZwc8lEqS3PTCtgqd13ieO68BQ2uNl64tXAewktrFuX2mPdkWAxn3sxnmx7sqUTJGqso8MGS9tbXFz8DMH8bblUX3T9QARVi8RV8qljfcJy0zRlaf6mzHEuzEtmekqCoZB4rqp0OmudHtUnlEWZlE0d1EWd1N3EozourcO65pw4eTIZQTW9VazJtbqvw9XwKVFQMsKDBuNhtp4uvGGFI+IDgKnpMjYyIis3ZsQMBIR7pONsIaMsyqRs6ohY1rPUSd3EQFDqo+kdZ3Fh4aupbdu+99uFQr2A1CBs4uEAjZjIFUMHi4dVxMXzCdCXQj4vBrwVCofl0ulTcv/DAxJJJBUPc8mpoyI2JDw7bFyT+ifTcSubyXytJ51+roWBxwG9Q73WWjZ7eSUU3//nXM0NI+x0PBGrTSgsLS9JFuFxHFrvSqIrJV279gi6tjiVspTza3JjZhY+0CQZj0mlWJSeHTslCro6eFqymCcVVN77kkGjs1p4sy2VOoSlOrFwT+XR+PjkgGaZ+ycKVbRTYUdVrmaImCvzk1dlFCEJdHRJ284+ie/ol0h7p7jFvExcvCCXzp2Rqem3pAMAiqWS6JGYhFI9Mjo6KjevXVUyKEuFHrKpY6JQ8TXT3D8+OTkAHBw6o6LCFo9ag3o4JtlCyTHEt5AxKvS6YUi5kJeZG3Py0NAxlLcJ9xti+K7Mjo/JfGZRuvv6Ze+9+yWEhDZAvzg3JyhX2d6/S7q6e+TimdOS7ElLKBZDwqvmj6rztayr1fVI1IoXi4PAcYZY1tPEEO1wEVlXgRFBDcmIXTqJsS+XyhKLJ5A/OpIVXXptWUYv/UvaenfIocEhMQ2EzHHErlXFCgQl3paU1eVl6QAY8sQTCSmVihKJx1V/ogvgIYF/pACdcMBhqONoHhF88/2d+bojyA6cRvje2IdFjoSjUSnBS8hgyS9lZOzKFdmPxO3o6gQIGzwuDn1dVSCtCKPy1pZXlATXqUsVYMLRmKo87vP4Y1ioqwCdCegmMYx3W/VPn8RrSDwwIMMbcEjkYo29JZVOy+ybI7K4eksODx1VSqvligpReSVLgySM/FI5h2q062jNyL3s7FtoAyGJIlx1225UmwJF6aJRJ3XzHXO9bWvsJa3jQFlBJkz6iuXdu32HzM7MyP0PPNgAU6ko4Qzp6b+flr8MD9OYJg9CwtzL5+T65ITs2bsP3mGxN/ZbBcOn0sk20gAkLQ+huXpFi8vkoY9AoyDjxTR1mbo6Ltt275HpN0dlNxQE40mVM8Ajjxx9VAGhAvQR1akZFCq799ADysMuQqOxh2FNmamEaz51ItGLfFD9+oUJoZkLowHoFA2mljUacqOMflKuVmHpfmnfvlMuvXZeStmMBIMhcWEdjgFJtrUjXI0KchAuAg0ilxLJNoRVBxhIBm0TjjKAuqjTqTs3CQZ6QUUMGFW7eiWMUg6w+yo8YMW7DqtqlZLkUDV2ISfd29KyDwk9MjYmMyOXxQIIKuShqo4VGFNBEgeDQYqVam5N5tEePFQgURIUBCsd1EWd1XrtDUUMLARD9bKaK5ytQ2Gb75g8WMiEP6VkfnZGevv6UF1vSBW5E0PFDAweFRvlfun8WVmamhDNrkmweQ0pwaPt6M4m8mgKTTFXqcrV0ZH1FKBg6qAu6qTuJiCV1Cp2Q0NDr9Uq5Ym+oMEDlSewsoRwrVBEaij7AJ4s7zrOpumxEdm15y6558GHJVe1Zezy6zJx6aJkpq5JFB4z6zVZmBiX1VWUP0IY4CFMYcpQdZ3xqIs6oftCE5DHKwd0q/tzOV8svdDb3nk8VnG9qmgQC0ZURz8Ur91alXgSByZ6ES9kZZTr/PR16UOCh+7dq0CWyyXJ4xqCQ0nKt9YQSlPue2gAeYZzD7yNLk0wmqAreb2WYSxAJ8Dget64wxtEBlDaqVOn/K5dB67t6+t5MhoMJuc8w8UPKiQ9CQR9JK5czhZAQxPt7TKF3OiAIisUViAD2Lg5d0P2HDgoKeRaW0enyqVwBJcO5fFG5dqa7h406qaeX8384uTZL5w9+UqxhYHFp0YLIYA9ddfu3T+4UJF6Rg+YAc9D0+RoIGP1ULhpWspr10evyK7+ftWTrk9PS/++A9KZSm26cih2mMOErem6n/ZsZwA2TM/MPHXs2LEftnSTbh0Q36mIIbx44cLvOnu3f+xUwbWLmoHTCUlF6g2jBQo/GnFrnGNqSHdvr+rIKGMW1KahwEBdzHft98aNwMr8zd8/NDDwccihc0hLi3GubRjY0Bm6H19fPvnZI4c/fHd7PJ2peXYZ+WQ26JufZELjQ6lbAQtnWre0d3apY8TFIdtAo+Qri6mupsB49lBMC+QXF0YefObZT8j0eKWlswVjEyCCOXHihPGb575VCvVuf3lvetsH9rXF0rla3cnhpoIGjgsUPhR3I4TMKYJQV1Z6WO02aEjHa5mNe3OPW3OPRHVrbXFh9Ocvv/KR1372owx1Pf3005uc35Ddgtd8rsf06IdS5777zZ+mUqmPzjm6TPpmvayZOq4LyATeCzkanmiy4qEuC/yXiO8CSMRzvLs1x9phepLNZl868sy3Pyen/5hd1/EfRvWmuvSWNeaRS/RkPDI4+NjE1NSXEoXlpaNB1zqo20abi59/vu/UfM2pie7WUDVq8l3wTwnskeZ+zTbIQ17KoCzKpGzq2KqX32/roRbh8ePHdUzl0s9/5Rv9n/7go19MxCKfCkZiu3V06wrO5gocxL7Dgd/IEobEMH6rejg+auXidL5Y/vWv/vTX53/y/e/MkGajTH7fOt4RUJOY1df4RdtY6ICFRzqTySOhUOA+3Ai3o31H1ZbnlXBruFmt2iMrudy5xx9//BzWV7nXDBGN2xpjbt/5oGUEdhtO3iD47xZOvm8a5CHvpsV38wsUaMwBWsz3rbK5xr0mzdv2t9Jv/f5vhsF4J+Q63IUAAAAASUVORK5CYII=') + + event.sender.startDrag({ + file: filepath, + icon + }) +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/drag-and-drop/renderer.js b/docs/fiddles/native-ui/drag-and-drop/renderer.js new file mode 100644 index 0000000000000..67f35d61ee1b7 --- /dev/null +++ b/docs/fiddles/native-ui/drag-and-drop/renderer.js @@ -0,0 +1,21 @@ +const { ipcRenderer } = require('electron') +const shell = require('electron').shell + +const links = document.querySelectorAll('a[href]') + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) + +const dragFileLink = document.getElementById('drag-file-link') + +dragFileLink.addEventListener('dragstart', event => { + event.preventDefault() + ipcRenderer.send('ondragstart', __filename) +}) diff --git a/docs/fiddles/native-ui/external-links-file-manager/.keep b/docs/fiddles/native-ui/external-links-file-manager/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/native-ui/external-links-file-manager/external-links/index.html b/docs/fiddles/native-ui/external-links-file-manager/external-links/index.html new file mode 100644 index 0000000000000..f96fae00f79c6 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/external-links/index.html @@ -0,0 +1,67 @@ + + + + + Open external links + + +
+
+
+
+ +
+

+ If you do not want your app to open website links + within the app, you can use the shell module + to open them externally. When clicked, the links will open outside + of your app and in the user's default web browser. +

+

+ When the demo button is clicked, the electron website will open in + your browser. +

+

+
Renderer Process
+

+                const { shell } = require('electron')
+                const exLinksBtn = document.getElementById('open-ex-links')
+                exLinksBtn.addEventListener('click', (event) => {
+                shell.openExternal('https://electronjs.org')
+                }) 
+            
+ +
+

ProTip

+ Open all outbound links externally. +

+ You may want to open all http and + https links outside of your app. To do this, query + the document and loop through each link and add a listener. This + app uses the code below which is located in + assets/ex-links.js. +

+
Renderer Process
+

+                const { shell } = require('electron')
+                const links = document.querySelectorAll('a[href]')
+                Array.prototype.forEach.call(links, (link) => {
+                    const url = link.getAttribute('href')
+                    if (url.indexOf('http') === 0) {
+                    link.addEventListener('click', (e) => {
+                        e.preventDefault()
+                        shell.openExternal(url)
+                    })
+                }})
+            
+
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/external-links-file-manager/external-links/main.js b/docs/fiddles/native-ui/external-links-file-manager/external-links/main.js new file mode 100644 index 0000000000000..8c60edf69f8b3 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/external-links/main.js @@ -0,0 +1,25 @@ +const { app, BrowserWindow } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Open External Links', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/native-ui/external-links-file-manager/external-links/renderer.js b/docs/fiddles/native-ui/external-links-file-manager/external-links/renderer.js new file mode 100644 index 0000000000000..14ed2d979f628 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/external-links/renderer.js @@ -0,0 +1,21 @@ +const { shell } = require('electron') + +const exLinksBtn = document.getElementById('open-ex-links') + +exLinksBtn.addEventListener('click', (event) => { + shell.openExternal('https://electronjs.org') +}) + +const OpenAllOutboundLinks = () => { + const links = document.querySelectorAll('a[href]') + + Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } + }) +} diff --git a/docs/fiddles/native-ui/external-links-file-manager/index.html b/docs/fiddles/native-ui/external-links-file-manager/index.html new file mode 100644 index 0000000000000..c4b41b9905556 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/index.html @@ -0,0 +1,104 @@ + + + + + Open external links and the file manager + + +
+

+ Open external links and the file manager +

+

+ The shell module in Electron allows you to access certain + native elements like the file manager and default web browser. +

+ +

This module works in both the main and renderer process.

+

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Open Path in File Manager

+
+
+ +
+

+ This demonstrates using the shell module to open the + system file manager at a particular location. +

+

+ Clicking the demo button will open your file manager at the root. +

+
+
+
+ +
+
+

Open External Links

+
+
+ +
+

+ If you do not want your app to open website links + within the app, you can use the shell module + to open them externally. When clicked, the links will open outside + of your app and in the user's default web browser. +

+

+ When the demo button is clicked, the electron website will open in + your browser. +

+

+ +
+

ProTip

+ Open all outbound links externally. +

+ You may want to open all http and + https links outside of your app. To do this, query + the document and loop through each link and add a listener. This + app uses the code below which is located in + assets/ex-links.js. +

+
Renderer Process
+
+                
+const shell = require('electron').shell
+
+const links = document.querySelectorAll('a[href]')
+
+Array.prototype.forEach.call(links, (link) => {
+  const url = link.getAttribute('href')
+  if (url.indexOf('http') === 0) {
+    link.addEventListener('click', (e) => {
+      e.preventDefault()
+      shell.openExternal(url)
+    })
+  }
+})
+                
+              
+
+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/external-links-file-manager/main.js b/docs/fiddles/native-ui/external-links-file-manager/main.js new file mode 100644 index 0000000000000..6291dcad9c993 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/main.js @@ -0,0 +1,56 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/index.html b/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/index.html new file mode 100644 index 0000000000000..be6a38c9fa167 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/index.html @@ -0,0 +1,24 @@ + + + + + + +
+
+

Open Path in File Manager

+ Supports: Win, macOS, Linux | Process: Both +
+

This demonstrates using the shell module to open the system file manager at a particular location.

+

Clicking the demo button will open your file manager at the root.

+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/main.js b/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/main.js new file mode 100644 index 0000000000000..9246b95bb242c --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/main.js @@ -0,0 +1,25 @@ +const { app, BrowserWindow } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Open Path in File Manager', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/renderer.js b/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/renderer.js new file mode 100644 index 0000000000000..d49754cdb7311 --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/path-in-file-manager/renderer.js @@ -0,0 +1,8 @@ +const { shell } = require('electron') +const os = require('os') + +const fileManagerBtn = document.getElementById('open-file-manager') + +fileManagerBtn.addEventListener('click', (event) => { + shell.showItemInFolder(os.homedir()) +}) diff --git a/docs/fiddles/native-ui/external-links-file-manager/renderer.js b/docs/fiddles/native-ui/external-links-file-manager/renderer.js new file mode 100644 index 0000000000000..5ce5bae2da3aa --- /dev/null +++ b/docs/fiddles/native-ui/external-links-file-manager/renderer.js @@ -0,0 +1,13 @@ +const { shell } = require('electron') +const os = require('os') + +const exLinksBtn = document.getElementById('open-ex-links') +const fileManagerBtn = document.getElementById('open-file-manager') + +fileManagerBtn.addEventListener('click', (event) => { + shell.showItemInFolder(os.homedir()) +}) + +exLinksBtn.addEventListener('click', (event) => { + shell.openExternal('https://electronjs.org') +}) diff --git a/docs/fiddles/native-ui/notifications/.keep b/docs/fiddles/native-ui/notifications/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/native-ui/notifications/basic-notification/index.html b/docs/fiddles/native-ui/notifications/basic-notification/index.html new file mode 100644 index 0000000000000..2ffd45453701a --- /dev/null +++ b/docs/fiddles/native-ui/notifications/basic-notification/index.html @@ -0,0 +1,22 @@ + + + + + + +
+

Basic notification

+ Supports: Win 7+, macOS, Linux (that supports libnotify)| Process: Renderer +
+
+ +
+

This demo demonstrates a basic notification. Text only.

+
+
+ + + diff --git a/docs/fiddles/native-ui/notifications/basic-notification/main.js b/docs/fiddles/native-ui/notifications/basic-notification/main.js new file mode 100644 index 0000000000000..b05ea9cbcc6e5 --- /dev/null +++ b/docs/fiddles/native-ui/notifications/basic-notification/main.js @@ -0,0 +1,25 @@ +const { BrowserWindow, app } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 300, + title: 'Basic Notification', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/native-ui/notifications/basic-notification/renderer.js b/docs/fiddles/native-ui/notifications/basic-notification/renderer.js new file mode 100644 index 0000000000000..a46583c683dea --- /dev/null +++ b/docs/fiddles/native-ui/notifications/basic-notification/renderer.js @@ -0,0 +1,14 @@ +const notification = { + title: 'Basic Notification', + body: 'Short message part' +} + +const notificationButton = document.getElementById('basic-noti') + +notificationButton.addEventListener('click', () => { + const myNotification = new window.Notification(notification.title, notification) + + myNotification.onclick = () => { + console.log('Notification clicked') + } +}) diff --git a/docs/fiddles/native-ui/notifications/index.html b/docs/fiddles/native-ui/notifications/index.html new file mode 100644 index 0000000000000..2848ad6d552d3 --- /dev/null +++ b/docs/fiddles/native-ui/notifications/index.html @@ -0,0 +1,67 @@ + + + + + Desktop notifications + + +
+

Desktop notifications

+

+ The notification module in Electron allows you to add basic + desktop notifications. +

+ +

+ Electron conveniently allows developers to send notifications with the + HTML5 Notification API, + using the currently running operating system’s native notification + APIs to display it. +

+ +

+ Note: Since this is an HTML5 API it is only available in the + renderer process. +

+ +

+ Open the + + full API documentation(opens in new window) + + in your browser. +

+
+ +
+
+

Basic notification

+
+
+ +
+

This demo demonstrates a basic notification. Text only.

+
+
+
+ +
+
+

Notification with image

+
+
+ +
+

+ This demo demonstrates a basic notification. Both text and a image +

+
+
+
+ + + + diff --git a/docs/fiddles/native-ui/notifications/main.js b/docs/fiddles/native-ui/notifications/main.js new file mode 100644 index 0000000000000..6291dcad9c993 --- /dev/null +++ b/docs/fiddles/native-ui/notifications/main.js @@ -0,0 +1,56 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow } = require('electron') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/native-ui/notifications/notification-with-image/index.html b/docs/fiddles/native-ui/notifications/notification-with-image/index.html new file mode 100644 index 0000000000000..5b9df4e78ecb5 --- /dev/null +++ b/docs/fiddles/native-ui/notifications/notification-with-image/index.html @@ -0,0 +1,22 @@ + + + + + + +
+

Notification with image

+ Supports: Win 7+, macOS, Linux (that supports libnotify)| Process: Renderer +
+
+ +
+

This demo demonstrates an advanced notification. Both text and image.

+
+
+ + + diff --git a/docs/fiddles/native-ui/notifications/notification-with-image/main.js b/docs/fiddles/native-ui/notifications/notification-with-image/main.js new file mode 100644 index 0000000000000..40c6001a99ecd --- /dev/null +++ b/docs/fiddles/native-ui/notifications/notification-with-image/main.js @@ -0,0 +1,25 @@ +const { BrowserWindow, app } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 300, + title: 'Advanced Notification', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/native-ui/notifications/notification-with-image/renderer.js b/docs/fiddles/native-ui/notifications/notification-with-image/renderer.js new file mode 100644 index 0000000000000..84c43d2e111be --- /dev/null +++ b/docs/fiddles/native-ui/notifications/notification-with-image/renderer.js @@ -0,0 +1,14 @@ +const notification = { + title: 'Notification with image', + body: 'Short message plus a custom image', + icon: 'https://raw.githubusercontent.com/electron/electron-api-demos/v2.0.2/assets/img/programming.png' +} +const notificationButton = document.getElementById('advanced-noti') + +notificationButton.addEventListener('click', () => { + const myNotification = new window.Notification(notification.title, notification) + + myNotification.onclick = () => { + console.log('Notification clicked') + } +}) diff --git a/docs/fiddles/native-ui/notifications/renderer.js b/docs/fiddles/native-ui/notifications/renderer.js new file mode 100644 index 0000000000000..9a97f7a869e03 --- /dev/null +++ b/docs/fiddles/native-ui/notifications/renderer.js @@ -0,0 +1,29 @@ +const basicNotification = { + title: 'Basic Notification', + body: 'Short message part' +} + +const notification = { + title: 'Notification with image', + body: 'Short message plus a custom image', + icon: 'https://via.placeholder.com/150' +} + +const basicNotificationButton = document.getElementById('basic-noti') +const notificationButton = document.getElementById('advanced-noti') + +notificationButton.addEventListener('click', () => { + const myNotification = new window.Notification(notification.title, notification) + + myNotification.onclick = () => { + console.log('Notification clicked') + } +}) + +basicNotificationButton.addEventListener('click', () => { + const myNotification = new window.Notification(basicNotification.title, basicNotification) + + myNotification.onclick = () => { + console.log('Notification clicked') + } +}) diff --git a/docs/fiddles/native-ui/tray/.keep b/docs/fiddles/native-ui/tray/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/native-ui/tray/index.html b/docs/fiddles/native-ui/tray/index.html new file mode 100644 index 0000000000000..81a25e1a864ef --- /dev/null +++ b/docs/fiddles/native-ui/tray/index.html @@ -0,0 +1,47 @@ + + + + + Tray + + +
+

Tray

+

+ The tray module allows you to create an icon in the + operating system's notification area. +

+

This icon can also have a context menu attached.

+ +

+ Open the + + full API documentation + + in your browser. +

+
+
+
+

ProTip

+ Tray support in Linux. +

+ On Linux distributions that only have app indicator support, users + will need to install libappindicator1 to make the + tray icon work. See the + + full API documentation + + for more details about using Tray on Linux. +

+
+
+
+ + + + + diff --git a/docs/fiddles/native-ui/tray/main.js b/docs/fiddles/native-ui/tray/main.js new file mode 100644 index 0000000000000..3d5ce65e02a43 --- /dev/null +++ b/docs/fiddles/native-ui/tray/main.js @@ -0,0 +1,18 @@ +const { app, Tray, Menu, nativeImage } = require('electron') + +let tray + +app.whenReady().then(() => { + const icon = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAACsZJREFUWAmtWFlsXFcZ/u82++Jt7IyT2Em6ZFHTpAtWIzspEgjEUhA8VNAiIYEQUvuABBIUwUMkQIVKPCIoEiABLShISEBbhFJwIGRpIKRpbNeJ7bh2HHvssR3PPnPnLnzfmRlju6EQqUc+c++c8y/fv54z1uQOh+/7Glh0TD59TE/TND7lnfa4/64OKsM071QoeZpA/y9WWvk/B4XCC06TUC+Xyw8HTXNQ1+Ww6PpOrMebewXxvBueJ6/XHOdMJBL5J9Y97m2R0SS/wweE6JxkGx5dilWr1S/7dXsEa2o4+LyFmcFcaL5zbX3Y9gh5hpeWYpSB9XV5/H678V89BGYDXnHJlCsWn4gHrGc1K9CXxferOdvPOOKUfF8cH7nUyCtklQZXih/VNNlmirk3GdBSoIcRswW7/vVkLPYi5W2Uze8bh7J+4wLfh4dViFx5/nmrUi7/MhGNvrCkBfpeWqnW/7BUdadqntQ8zwr6vhUV34xpYnDynWvcmwQNaclDXsqgLMqkocPDw7fNx7d5qIX+/PmJxKGD6VdDkeh7ztyqOFfrokGCEWiiZ1mp0uITnuKAosaT7+pNxMYTyefutcQfbA+b1XLpH5fnF97/yD335Fu6mqTqsclDINBVmI4fDxw80KPAvJSt1MZtMcLiGxYUu83p4UkgnJZlqcl3LAj3WnTkIS9lUBYNPJjueVWgg7qocyOgliFqjZsg8gq5tRdiieQTf1gq15Y8CUbRZtyWOzZwc8lEqS3PTCtgqd13ieO68BQ2uNl64tXAewktrFuX2mPdkWAxn3sxnmx7sqUTJGqso8MGS9tbXFz8DMH8bblUX3T9QARVi8RV8qljfcJy0zRlaf6mzHEuzEtmekqCoZB4rqp0OmudHtUnlEWZlE0d1EWd1N3EozourcO65pw4eTIZQTW9VazJtbqvw9XwKVFQMsKDBuNhtp4uvGGFI+IDgKnpMjYyIis3ZsQMBIR7pONsIaMsyqRs6ohY1rPUSd3EQFDqo+kdZ3Fh4aupbdu+99uFQr2A1CBs4uEAjZjIFUMHi4dVxMXzCdCXQj4vBrwVCofl0ulTcv/DAxJJJBUPc8mpoyI2JDw7bFyT+ifTcSubyXytJ51+roWBxwG9Q73WWjZ7eSUU3//nXM0NI+x0PBGrTSgsLS9JFuFxHFrvSqIrJV279gi6tjiVspTza3JjZhY+0CQZj0mlWJSeHTslCro6eFqymCcVVN77kkGjs1p4sy2VOoSlOrFwT+XR+PjkgGaZ+ycKVbRTYUdVrmaImCvzk1dlFCEJdHRJ284+ie/ol0h7p7jFvExcvCCXzp2Rqem3pAMAiqWS6JGYhFI9Mjo6KjevXVUyKEuFHrKpY6JQ8TXT3D8+OTkAHBw6o6LCFo9ag3o4JtlCyTHEt5AxKvS6YUi5kJeZG3Py0NAxlLcJ9xti+K7Mjo/JfGZRuvv6Ze+9+yWEhDZAvzg3JyhX2d6/S7q6e+TimdOS7ElLKBZDwqvmj6rztayr1fVI1IoXi4PAcYZY1tPEEO1wEVlXgRFBDcmIXTqJsS+XyhKLJ5A/OpIVXXptWUYv/UvaenfIocEhMQ2EzHHErlXFCgQl3paU1eVl6QAY8sQTCSmVihKJx1V/ogvgIYF/pACdcMBhqONoHhF88/2d+bojyA6cRvje2IdFjoSjUSnBS8hgyS9lZOzKFdmPxO3o6gQIGzwuDn1dVSCtCKPy1pZXlATXqUsVYMLRmKo87vP4Y1ioqwCdCegmMYx3W/VPn8RrSDwwIMMbcEjkYo29JZVOy+ybI7K4eksODx1VSqvligpReSVLgySM/FI5h2q062jNyL3s7FtoAyGJIlx1225UmwJF6aJRJ3XzHXO9bWvsJa3jQFlBJkz6iuXdu32HzM7MyP0PPNgAU6ko4Qzp6b+flr8MD9OYJg9CwtzL5+T65ITs2bsP3mGxN/ZbBcOn0sk20gAkLQ+huXpFi8vkoY9AoyDjxTR1mbo6Ltt275HpN0dlNxQE40mVM8Ajjxx9VAGhAvQR1akZFCq799ADysMuQqOxh2FNmamEaz51ItGLfFD9+oUJoZkLowHoFA2mljUacqOMflKuVmHpfmnfvlMuvXZeStmMBIMhcWEdjgFJtrUjXI0KchAuAg0ilxLJNoRVBxhIBm0TjjKAuqjTqTs3CQZ6QUUMGFW7eiWMUg6w+yo8YMW7DqtqlZLkUDV2ISfd29KyDwk9MjYmMyOXxQIIKuShqo4VGFNBEgeDQYqVam5N5tEePFQgURIUBCsd1EWd1XrtDUUMLARD9bKaK5ytQ2Gb75g8WMiEP6VkfnZGevv6UF1vSBW5E0PFDAweFRvlfun8WVmamhDNrkmweQ0pwaPt6M4m8mgKTTFXqcrV0ZH1FKBg6qAu6qTuJiCV1Cp2Q0NDr9Uq5Ym+oMEDlSewsoRwrVBEaij7AJ4s7zrOpumxEdm15y6558GHJVe1Zezy6zJx6aJkpq5JFB4z6zVZmBiX1VWUP0IY4CFMYcpQdZ3xqIs6oftCE5DHKwd0q/tzOV8svdDb3nk8VnG9qmgQC0ZURz8Ur91alXgSByZ6ES9kZZTr/PR16UOCh+7dq0CWyyXJ4xqCQ0nKt9YQSlPue2gAeYZzD7yNLk0wmqAreb2WYSxAJ8Dget64wxtEBlDaqVOn/K5dB67t6+t5MhoMJuc8w8UPKiQ9CQR9JK5czhZAQxPt7TKF3OiAIisUViAD2Lg5d0P2HDgoKeRaW0enyqVwBJcO5fFG5dqa7h406qaeX8384uTZL5w9+UqxhYHFp0YLIYA9ddfu3T+4UJF6Rg+YAc9D0+RoIGP1ULhpWspr10evyK7+ftWTrk9PS/++A9KZSm26cih2mMOErem6n/ZsZwA2TM/MPHXs2LEftnSTbh0Q36mIIbx44cLvOnu3f+xUwbWLmoHTCUlF6g2jBQo/GnFrnGNqSHdvr+rIKGMW1KahwEBdzHft98aNwMr8zd8/NDDwccihc0hLi3GubRjY0Bm6H19fPvnZI4c/fHd7PJ2peXYZ+WQ26JufZELjQ6lbAQtnWre0d3apY8TFIdtAo+Qri6mupsB49lBMC+QXF0YefObZT8j0eKWlswVjEyCCOXHihPGb575VCvVuf3lvetsH9rXF0rla3cnhpoIGjgsUPhR3I4TMKYJQV1Z6WO02aEjHa5mNe3OPW3OPRHVrbXFh9Ocvv/KR1372owx1Pf3005uc35Ddgtd8rsf06IdS5777zZ+mUqmPzjm6TPpmvayZOq4LyATeCzkanmiy4qEuC/yXiO8CSMRzvLs1x9phepLNZl868sy3Pyen/5hd1/EfRvWmuvSWNeaRS/RkPDI4+NjE1NSXEoXlpaNB1zqo20abi59/vu/UfM2pie7WUDVq8l3wTwnskeZ+zTbIQ17KoCzKpGzq2KqX32/roRbh8ePHdUzl0s9/5Rv9n/7go19MxCKfCkZiu3V06wrO5gocxL7Dgd/IEobEMH6rejg+auXidL5Y/vWv/vTX53/y/e/MkGajTH7fOt4RUJOY1df4RdtY6ICFRzqTySOhUOA+3Ai3o31H1ZbnlXBruFmt2iMrudy5xx9//BzWV7nXDBGN2xpjbt/5oGUEdhtO3iD47xZOvm8a5CHvpsV38wsUaMwBWsz3rbK5xr0mzdv2t9Jv/f5vhsF4J+Q63IUAAAAASUVORK5CYII=') + tray = new Tray(icon) + + const contextMenu = Menu.buildFromTemplate([ + { label: 'Item1', type: 'radio' }, + { label: 'Item2', type: 'radio' }, + { label: 'Item3', type: 'radio', checked: true }, + { label: 'Item4', type: 'radio' } + ]) + + tray.setToolTip('This is my application.') + tray.setContextMenu(contextMenu) +}) diff --git a/docs/fiddles/quick-start/index.html b/docs/fiddles/quick-start/index.html new file mode 100644 index 0000000000000..f008d867a0f89 --- /dev/null +++ b/docs/fiddles/quick-start/index.html @@ -0,0 +1,16 @@ + + + + + Hello World! + + + +

Hello World!

+

+ We are using Node.js , + Chromium , + and Electron . +

+ + diff --git a/docs/fiddles/quick-start/main.js b/docs/fiddles/quick-start/main.js new file mode 100644 index 0000000000000..519a67947cdbb --- /dev/null +++ b/docs/fiddles/quick-start/main.js @@ -0,0 +1,31 @@ +const { app, BrowserWindow } = require('electron') +const path = require('path') + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + win.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + diff --git a/docs/fiddles/quick-start/preload.js b/docs/fiddles/quick-start/preload.js new file mode 100644 index 0000000000000..7674d012240c4 --- /dev/null +++ b/docs/fiddles/quick-start/preload.js @@ -0,0 +1,11 @@ +window.addEventListener('DOMContentLoaded', () => { + const replaceText = (selector, text) => { + const element = document.getElementById(selector) + if (element) element.innerText = text + } + + for (const type of ['chrome', 'node', 'electron']) { + replaceText(`${type}-version`, process.versions[type]) + } +}) + diff --git a/docs/fiddles/screen/fit-screen/main.js b/docs/fiddles/screen/fit-screen/main.js new file mode 100644 index 0000000000000..8fbaabcc5b850 --- /dev/null +++ b/docs/fiddles/screen/fit-screen/main.js @@ -0,0 +1,20 @@ +// Retrieve information about screen size, displays, cursor position, etc. +// +// For more info, see: +// https://electronjs.org/docs/api/screen + +const { app, BrowserWindow } = require('electron') + +let mainWindow = null + +app.whenReady().then(() => { + // We cannot require the screen module until the app is ready. + const { screen } = require('electron') + + // Create a window that fills the screen's available work area. + const primaryDisplay = screen.getPrimaryDisplay() + const { width, height } = primaryDisplay.workAreaSize + + mainWindow = new BrowserWindow({ width, height }) + mainWindow.loadURL('https://electronjs.org') +}) diff --git a/docs/fiddles/system/clipboard/.keep b/docs/fiddles/system/clipboard/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/system/clipboard/copy/index.html b/docs/fiddles/system/clipboard/copy/index.html new file mode 100644 index 0000000000000..2bf063a2f516b --- /dev/null +++ b/docs/fiddles/system/clipboard/copy/index.html @@ -0,0 +1,24 @@ + + + + + + +
+
+

Clipboard copy

+ Supports: Win, macOS, Linux | Process: Both +
+
+ + +
+

In this example we copy a phrase to the clipboard. After clicking 'Copy' use the text area to paste (CMD + V or CTRL + V) the phrase from the clipboard.

+
+
+
+ + + diff --git a/docs/fiddles/system/clipboard/copy/main.js b/docs/fiddles/system/clipboard/copy/main.js new file mode 100644 index 0000000000000..36ad14197f6b7 --- /dev/null +++ b/docs/fiddles/system/clipboard/copy/main.js @@ -0,0 +1,25 @@ +const { app, BrowserWindow } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Clipboard copy', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/system/clipboard/copy/renderer.js b/docs/fiddles/system/clipboard/copy/renderer.js new file mode 100644 index 0000000000000..75e204136e09a --- /dev/null +++ b/docs/fiddles/system/clipboard/copy/renderer.js @@ -0,0 +1,10 @@ +const { clipboard } = require('electron') + +const copyBtn = document.getElementById('copy-to') +const copyInput = document.getElementById('copy-to-input') + +copyBtn.addEventListener('click', () => { + if (copyInput.value !== '') copyInput.value = '' + copyInput.placeholder = 'Copied! Paste here to see.' + clipboard.writeText('Electron Demo!') +}) diff --git a/docs/fiddles/system/clipboard/paste/index.html b/docs/fiddles/system/clipboard/paste/index.html new file mode 100644 index 0000000000000..9cc2ac5ba7ce8 --- /dev/null +++ b/docs/fiddles/system/clipboard/paste/index.html @@ -0,0 +1,24 @@ + + + + + + +
+
+

Clipboard paste

+ Supports: Win, macOS, Linux | Process: Both +
+
+ + +
+

In this example we copy a string to the clipboard and then paste the results into a message above.

+
+
+
+ + + diff --git a/docs/fiddles/system/clipboard/paste/main.js b/docs/fiddles/system/clipboard/paste/main.js new file mode 100644 index 0000000000000..b0883e6f4e56f --- /dev/null +++ b/docs/fiddles/system/clipboard/paste/main.js @@ -0,0 +1,25 @@ +const { app, BrowserWindow } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Clipboard paste', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/system/clipboard/paste/renderer.js b/docs/fiddles/system/clipboard/paste/renderer.js new file mode 100644 index 0000000000000..27a52422cf0da --- /dev/null +++ b/docs/fiddles/system/clipboard/paste/renderer.js @@ -0,0 +1,9 @@ +const { clipboard } = require('electron') + +const pasteBtn = document.getElementById('paste-to') + +pasteBtn.addEventListener('click', () => { + clipboard.writeText('What a demo!') + const message = `Clipboard contents: ${clipboard.readText()}` + document.getElementById('paste-from').innerHTML = message +}) diff --git a/docs/fiddles/system/protocol-handler/.keep b/docs/fiddles/system/protocol-handler/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/index.html b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/index.html new file mode 100644 index 0000000000000..a3ddd1b933fc0 --- /dev/null +++ b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/index.html @@ -0,0 +1,81 @@ + + + + + + + + + app.setAsDefaultProtocol Demo + + + +

App Default Protocol Demo

+ +

The protocol API allows us to register a custom protocol and intercept existing protocol requests.

+

These methods allow you to set and unset the protocols your app should be the default app for. Similar to when a + browser asks to be your default for viewing web pages.

+ +

Open the full protocol API documentation in your + browser.

+ + ----- + +

Demo

+

+ First: Launch current page in browser + +

+ +

+ Then: Launch the app from a web link! + Click here to launch the app +

+ + ---- + +

You can set your app as the default app to open for a specific protocol. For instance, in this demo we set this app + as the default for electron-fiddle://. The demo button above will launch a page in your default + browser with a link. Click that link and it will re-launch this app.

+ + +

Packaging

+

This feature will only work on macOS when your app is packaged. It will not work when you're launching it in + development from the command-line. When you package your app you'll need to make sure the macOS plist + for the app is updated to include the new protocol handler. If you're using electron-packager then you + can add the flag --extend-info with a path to the plist you've created. The one for this + app is below:

+ +

+

macOS plist
+

+    <?xml version="1.0" encoding="UTF-8"?>
+    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+            <plist version="1.0">
+                <dict>
+                    <key>CFBundleURLTypes</key>
+                    <array>
+                        <dict>
+                            <key>CFBundleURLSchemes</key>
+                            <array>
+                                <string>electron-api-demos</string>
+                            </array>
+                            <key>CFBundleURLName</key>
+                            <string>Electron API Demos Protocol</string>
+                        </dict>
+                    </array>
+                    <key>ElectronTeamID</key>
+                    <string>VEKTX9H2N7</string>
+                </dict>
+            </plist>
+        
+    
+

+ + + + + + \ No newline at end of file diff --git a/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/main.js b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/main.js new file mode 100644 index 0000000000000..5a3b553612de3 --- /dev/null +++ b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/main.js @@ -0,0 +1,63 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain, shell, dialog } = require('electron') +const path = require('path') + +let mainWindow; + +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient('electron-fiddle', process.execPath, [path.resolve(process.argv[1])]) + } +} else { + app.setAsDefaultProtocolClient('electron-fiddle') +} + +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + app.quit() +} else { + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + }) + + // Create mainWindow, load the rest of the app, etc... + app.whenReady().then(() => { + createWindow() + }) + + app.on('open-url', (event, url) => { + dialog.showErrorBox('Welcome Back', `You arrived from: ${url}`) + }) +} + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + } + }) + + mainWindow.loadFile('index.html') +} + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) + +// Handle window controls via IPC +ipcMain.on('shell:open', () => { + const pageDirectory = __dirname.replace('app.asar', 'app.asar.unpacked') + const pagePath = path.join('file://', pageDirectory, 'index.html') + shell.openExternal(pagePath) +}) diff --git a/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/preload.js b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/preload.js new file mode 100644 index 0000000000000..1eebf784ad672 --- /dev/null +++ b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/preload.js @@ -0,0 +1,11 @@ +// All of the Node.js APIs are available in the preload process. +// It has the same sandbox as a Chrome extension. +const { contextBridge, ipcRenderer } = require('electron') + +// Set up context bridge between the renderer process and the main process +contextBridge.exposeInMainWorld( + 'shell', + { + open: () => ipcRenderer.send('shell:open'), + } +) \ No newline at end of file diff --git a/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/renderer.js b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/renderer.js new file mode 100644 index 0000000000000..525f25ff2e76e --- /dev/null +++ b/docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app/renderer.js @@ -0,0 +1,8 @@ +// This file is required by the index.html file and will +// be executed in the renderer process for that window. +// All APIs exposed by the context bridge are available here. + +// Binds the buttons to the context bridge API. +document.getElementById('open-in-browser').addEventListener('click', () => { + shell.open(); +}); \ No newline at end of file diff --git a/docs/fiddles/system/system-app-user-information/.keep b/docs/fiddles/system/system-app-user-information/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/system/system-app-user-information/app-information/index.html b/docs/fiddles/system/system-app-user-information/app-information/index.html new file mode 100644 index 0000000000000..18b05ae23dae6 --- /dev/null +++ b/docs/fiddles/system/system-app-user-information/app-information/index.html @@ -0,0 +1,26 @@ + + + + + + +

+
+

App Information

+
+
+ + +
+

The main process app module can be used to get the path at which your app is located on the user's computer.

+

In this example, to get that information from the renderer process, we use the ipc module to send a message to the main process requesting the app's path.

+

See the app module documentation(opens in new window) for more.

+
+
+
+ + + \ No newline at end of file diff --git a/docs/fiddles/system/system-app-user-information/app-information/main.js b/docs/fiddles/system/system-app-user-information/app-information/main.js new file mode 100644 index 0000000000000..64141f98e88dd --- /dev/null +++ b/docs/fiddles/system/system-app-user-information/app-information/main.js @@ -0,0 +1,5 @@ +const {app, ipcMain} = require('electron') + +ipcMain.on('get-app-path', (event) => { + event.sender.send('got-app-path', app.getAppPath()) +}) \ No newline at end of file diff --git a/docs/fiddles/system/system-app-user-information/app-information/renderer.js b/docs/fiddles/system/system-app-user-information/app-information/renderer.js new file mode 100644 index 0000000000000..3f971abcab7c5 --- /dev/null +++ b/docs/fiddles/system/system-app-user-information/app-information/renderer.js @@ -0,0 +1,18 @@ +const {ipcRenderer} = require('electron') + +const appInfoBtn = document.getElementById('app-info') +const electron_doc_link = document.querySelectorAll('a[href]') + +appInfoBtn.addEventListener('click', () => { + ipcRenderer.send('get-app-path') +}) + +ipcRenderer.on('got-app-path', (event, path) => { + const message = `This app is located at: ${path}` + document.getElementById('got-app-info').innerHTML = message +}) + +electron_doc_link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) +}) \ No newline at end of file diff --git a/docs/fiddles/system/system-information/get-version-information/index.html b/docs/fiddles/system/system-information/get-version-information/index.html new file mode 100644 index 0000000000000..b19df1f2b534b --- /dev/null +++ b/docs/fiddles/system/system-information/get-version-information/index.html @@ -0,0 +1,26 @@ + + + + + + +
+
+

Get version information

+ Supports: Win, macOS, Linux | Process: Both +
+
+ + +
+

The process module is built into Node.js (therefore you can use this in both the main and renderer processes) and in Electron apps this object has a few more useful properties on it.

+

The example below gets the version of Electron in use by the app.

+

See the process documentation (opens in new window) for more.

+
+
+
+ + + diff --git a/docs/fiddles/system/system-information/get-version-information/main.js b/docs/fiddles/system/system-information/get-version-information/main.js new file mode 100644 index 0000000000000..daf87d0fda97a --- /dev/null +++ b/docs/fiddles/system/system-information/get-version-information/main.js @@ -0,0 +1,25 @@ +const { app, BrowserWindow } = require('electron') + +let mainWindow = null + +function createWindow () { + const windowOptions = { + width: 600, + height: 400, + title: 'Get version information', + webPreferences: { + nodeIntegration: true + } + } + + mainWindow = new BrowserWindow(windowOptions) + mainWindow.loadFile('index.html') + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/system/system-information/get-version-information/renderer.js b/docs/fiddles/system/system-information/get-version-information/renderer.js new file mode 100644 index 0000000000000..40f7f2cf2cf64 --- /dev/null +++ b/docs/fiddles/system/system-information/get-version-information/renderer.js @@ -0,0 +1,8 @@ +const versionInfoBtn = document.getElementById('version-info') + +const electronVersion = process.versions.electron + +versionInfoBtn.addEventListener('click', () => { + const message = `This app is using Electron version: ${electronVersion}` + document.getElementById('got-version-info').innerHTML = message +}) diff --git a/docs/fiddles/tutorial-first-app/index.html b/docs/fiddles/tutorial-first-app/index.html new file mode 100644 index 0000000000000..3d677b7c97b5b --- /dev/null +++ b/docs/fiddles/tutorial-first-app/index.html @@ -0,0 +1,21 @@ + + + + + + + Hello from Electron renderer! + + +

Hello from Electron renderer!

+

👋

+

+ + + diff --git a/docs/fiddles/tutorial-first-app/main.js b/docs/fiddles/tutorial-first-app/main.js new file mode 100644 index 0000000000000..10d57a0696f0f --- /dev/null +++ b/docs/fiddles/tutorial-first-app/main.js @@ -0,0 +1,26 @@ +const { app, BrowserWindow } = require('electron'); + +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + }); + + win.loadFile('index.html'); +}; + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/docs/fiddles/tutorial-preload/index.html b/docs/fiddles/tutorial-preload/index.html new file mode 100644 index 0000000000000..3d677b7c97b5b --- /dev/null +++ b/docs/fiddles/tutorial-preload/index.html @@ -0,0 +1,21 @@ + + + + + + + Hello from Electron renderer! + + +

Hello from Electron renderer!

+

👋

+

+ + + diff --git a/docs/fiddles/tutorial-preload/main.js b/docs/fiddles/tutorial-preload/main.js new file mode 100644 index 0000000000000..6b7184900e6dd --- /dev/null +++ b/docs/fiddles/tutorial-preload/main.js @@ -0,0 +1,30 @@ +const { app, BrowserWindow } = require('electron'); +const path = require('path'); + +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }); + + win.loadFile('index.html'); +}; + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/docs/fiddles/tutorial-preload/preload.js b/docs/fiddles/tutorial-preload/preload.js new file mode 100644 index 0000000000000..e0dbdce1b8b2f --- /dev/null +++ b/docs/fiddles/tutorial-preload/preload.js @@ -0,0 +1,7 @@ +const { contextBridge } = require('electron'); + +contextBridge.exposeInMainWorld('versions', { + node: () => process.versions.node, + chrome: () => process.versions.chrome, + electron: () => process.versions.electron, +}); diff --git a/docs/fiddles/tutorial-preload/renderer.js b/docs/fiddles/tutorial-preload/renderer.js new file mode 100644 index 0000000000000..7585229a91781 --- /dev/null +++ b/docs/fiddles/tutorial-preload/renderer.js @@ -0,0 +1,2 @@ +const information = document.getElementById('info'); +information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`; diff --git a/docs/fiddles/windows/crashes-and-hangs/.keep b/docs/fiddles/windows/crashes-and-hangs/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/windows/manage-windows/.keep b/docs/fiddles/windows/manage-windows/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/fiddles/windows/manage-windows/create-frameless-window/index.html b/docs/fiddles/windows/manage-windows/create-frameless-window/index.html new file mode 100644 index 0000000000000..c83c43d636ed9 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/create-frameless-window/index.html @@ -0,0 +1,26 @@ + + + + + + +
+
+

Create a frameless window

+ Supports: Win, macOS, Linux | Process: Main +
+

A frameless window is a window that has no "chrome", + such as toolbars, title bars, status bars, borders, etc. You can make + a browser window frameless by setting + frame to false when creating the window.

+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/fiddles/windows/manage-windows/create-frameless-window/main.js b/docs/fiddles/windows/manage-windows/create-frameless-window/main.js new file mode 100644 index 0000000000000..05d530736a3b8 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/create-frameless-window/main.js @@ -0,0 +1,22 @@ +const { app, BrowserWindow, ipcMain } = require('electron') + +ipcMain.on('create-frameless-window', (event, {url}) => { + const win = new BrowserWindow({ frame: false }) + win.loadURL(url) +}) + +function createWindow () { + const mainWindow = new BrowserWindow({ + width: 600, + height: 400, + title: 'Create a frameless window', + webPreferences: { + nodeIntegration: true + } + }) + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() +}) diff --git a/docs/fiddles/windows/manage-windows/create-frameless-window/renderer.js b/docs/fiddles/windows/manage-windows/create-frameless-window/renderer.js new file mode 100644 index 0000000000000..21f91ad561b37 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/create-frameless-window/renderer.js @@ -0,0 +1,8 @@ +const { ipcRenderer } = require('electron') + +const newWindowBtn = document.getElementById('frameless-window') + +newWindowBtn.addEventListener('click', () => { + const url = 'data:text/html,

Hello World!

Close this Window' + ipcRenderer.send('create-frameless-window', { url }) +}) diff --git a/docs/fiddles/windows/manage-windows/frameless-window/index.html b/docs/fiddles/windows/manage-windows/frameless-window/index.html new file mode 100644 index 0000000000000..58179e2e81ee5 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/frameless-window/index.html @@ -0,0 +1,77 @@ + + + + + Frameless window + + + +
+

Create and Manage Windows

+ +

+ The BrowserWindow module in Electron allows you to create a + new browser window or manage an existing one. +

+ +

+ Each browser window is a separate process, known as the renderer + process. This process, like the main process that controls the life + cycle of the app, has full access to the Node.js APIs. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Create a frameless window

+
+
+ +
+

+ A frameless window is a window that has no + + "chrome" + + , such as toolbars, title bars, status bars, borders, etc. You can + make a browser window frameless by setting frame to + false when creating the window. +

+ +

+ Windows can have a transparent background, too. By setting the + transparent option to true, you can also + make your frameless window transparent: +

+
+var win = new BrowserWindow({
+  transparent: true,
+  frame: false
+})
+ +

+ For more details, see the + + Window Customization + + + documentation. +

+
+
+
+ + + + diff --git a/docs/fiddles/windows/manage-windows/frameless-window/main.js b/docs/fiddles/windows/manage-windows/frameless-window/main.js new file mode 100644 index 0000000000000..d7925cd7ff4a6 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/frameless-window/main.js @@ -0,0 +1,46 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow } = require('electron') + +ipcMain.on('create-frameless-window', (event, {url}) => { + const win = new BrowserWindow({ frame: false }) + win.loadURL(url) +}) + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/windows/manage-windows/frameless-window/renderer.js b/docs/fiddles/windows/manage-windows/frameless-window/renderer.js new file mode 100644 index 0000000000000..21f91ad561b37 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/frameless-window/renderer.js @@ -0,0 +1,8 @@ +const { ipcRenderer } = require('electron') + +const newWindowBtn = document.getElementById('frameless-window') + +newWindowBtn.addEventListener('click', () => { + const url = 'data:text/html,

Hello World!

Close this Window' + ipcRenderer.send('create-frameless-window', { url }) +}) diff --git a/docs/fiddles/windows/manage-windows/manage-window-state/index.html b/docs/fiddles/windows/manage-windows/manage-window-state/index.html new file mode 100644 index 0000000000000..e9e39ceccc18e --- /dev/null +++ b/docs/fiddles/windows/manage-windows/manage-window-state/index.html @@ -0,0 +1,64 @@ + + + + + Manage window state + + + +
+

Create and Manage Windows

+ +

+ The BrowserWindow module in Electron allows you to create a + new browser window or manage an existing one. +

+ +

+ Each browser window is a separate process, known as the renderer + process. This process, like the main process that controls the life + cycle of the app, has full access to the Node.js APIs. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Manage window state

+
+
+ + +
+

+ In this demo we create a new window and listen for + move and resize events on it. Click the + demo button, change the new window and see the dimensions and + position update here, above. +

+

+ There are a lot of methods for controlling the state of the window + such as the size, location, and focus status as well as events to + listen to for window changes. Visit the + + documentation (opens in new window) + + for the full list. +

+
+
+
+ + + + diff --git a/docs/fiddles/windows/manage-windows/manage-window-state/main.js b/docs/fiddles/windows/manage-windows/manage-window-state/main.js new file mode 100644 index 0000000000000..166ff0fa62f5a --- /dev/null +++ b/docs/fiddles/windows/manage-windows/manage-window-state/main.js @@ -0,0 +1,56 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain } = require('electron') + +ipcMain.on('create-demo-window', (event) => { + const win = new BrowserWindow({ width: 400, height: 275 }) + + function updateReply() { + event.sender.send('bounds-changed', { + size: win.getSize(), + position: win.getPosition() + }) + } + + win.on('resize', updateReply) + win.on('move', updateReply) + win.loadURL('https://electronjs.org') +}) + +function createWindow () { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/windows/manage-windows/manage-window-state/renderer.js b/docs/fiddles/windows/manage-windows/manage-window-state/renderer.js new file mode 100644 index 0000000000000..7b1aa8ae23b11 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/manage-window-state/renderer.js @@ -0,0 +1,25 @@ +const { shell, ipcRenderer } = require('electron') + +const manageWindowBtn = document.getElementById('manage-window') + +const links = document.querySelectorAll('a[href]') + +ipcRenderer.on('bounds-changed', (event, bounds) => { + const manageWindowReply = document.getElementById('manage-window-reply') + const message = `Size: ${bounds.size} Position: ${bounds.position}` + manageWindowReply.textContent = message +}) + +manageWindowBtn.addEventListener('click', (event) => { + ipcRenderer.send('create-demo-window') +}) + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) diff --git a/docs/fiddles/windows/manage-windows/new-window/index.html b/docs/fiddles/windows/manage-windows/new-window/index.html new file mode 100644 index 0000000000000..08fbcab03c943 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/new-window/index.html @@ -0,0 +1,25 @@ + + + + + + +

Create a new window

+

Supports: Win, macOS, Linux | Process: Main

+ +

The BrowserWindow module gives you the ability to create new windows in your app.

+

There are a lot of options when creating a new window. A few are in this demo, but visit the documentation(opens in new window) +

+

ProTip

+ Use an invisible browser window to run background tasks. +

You can set a new browser window to not be shown (be invisible) in order to use that additional renderer process as a kind of new thread in which to run JavaScript in the background of your app. You do this by setting the show property to false when defining the new window.

+
var win = new BrowserWindow({
+  width: 400, height: 225, show: false
+})
+
+ + + diff --git a/docs/fiddles/windows/manage-windows/new-window/main.js b/docs/fiddles/windows/manage-windows/new-window/main.js new file mode 100644 index 0000000000000..2e51387e37aca --- /dev/null +++ b/docs/fiddles/windows/manage-windows/new-window/main.js @@ -0,0 +1,46 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain } = require('electron') + +ipcMain.on('new-window', (event, { url, width, height }) => { + const win = new BrowserWindow({ width, height }) + win.loadURL(url) +}) + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/windows/manage-windows/new-window/renderer.js b/docs/fiddles/windows/manage-windows/new-window/renderer.js new file mode 100644 index 0000000000000..ccb8b22643296 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/new-window/renderer.js @@ -0,0 +1,14 @@ +const { shell, ipcRenderer } = require('electron') + +const newWindowBtn = document.getElementById('new-window') +const link = document.getElementById('browser-window-link') + +newWindowBtn.addEventListener('click', (event) => { + const url = 'https://electronjs.org' + ipcRenderer.send('new-window', { url, width: 400, height: 320 }); +}) + +link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal("https://electronjs.org/docs/api/browser-window") +}) diff --git a/docs/fiddles/windows/manage-windows/window-events/index.html b/docs/fiddles/windows/manage-windows/window-events/index.html new file mode 100644 index 0000000000000..d244bf167506c --- /dev/null +++ b/docs/fiddles/windows/manage-windows/window-events/index.html @@ -0,0 +1,58 @@ + + + + + Window events + + + +
+

Create and Manage Windows

+ +

+ The BrowserWindow module in Electron allows you to create a + new browser window or manage an existing one. +

+ +

+ Each browser window is a separate process, known as the renderer + process. This process, like the main process that controls the life + cycle of the app, has full access to the Node.js APIs. +

+ +

+ Open the + + full API documentation (opens in new window) + + in your browser. +

+
+ +
+
+

Window events

+
+
+ + +
+

+ In this demo, we create a new window and listen for + blur event on it. Click the demo button to create a new + modal window, and switch focus back to the parent window by clicking + on it. You can click the Focus on Demo button to switch focus + to the modal window again. +

+
+
+
+ + + + diff --git a/docs/fiddles/windows/manage-windows/window-events/main.js b/docs/fiddles/windows/manage-windows/window-events/main.js new file mode 100644 index 0000000000000..3effec304fc4f --- /dev/null +++ b/docs/fiddles/windows/manage-windows/window-events/main.js @@ -0,0 +1,64 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain } = require('electron') + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + let demoWindow + ipcMain.on('show-demo-window', () => { + if (demoWindow) { + demoWindow.focus() + return + } + demoWindow = new BrowserWindow({ width: 600, height: 400 }) + demoWindow.loadURL('https://electronjs.org') + demoWindow.on('close', () => { + mainWindow.webContents.send('window-close') + }) + demoWindow.on('focus', () => { + mainWindow.webContents.send('window-focus') + }) + demoWindow.on('blur', () => { + mainWindow.webContents.send('window-blur') + }) + }) + + ipcMain.on('focus-demo-window', () => { + if (demoWindow) demoWindow.focus() + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On macOS it is common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/docs/fiddles/windows/manage-windows/window-events/renderer.js b/docs/fiddles/windows/manage-windows/window-events/renderer.js new file mode 100644 index 0000000000000..575d283a615a3 --- /dev/null +++ b/docs/fiddles/windows/manage-windows/window-events/renderer.js @@ -0,0 +1,39 @@ +const { shell, ipcRenderer } = require('electron') + +const listenToWindowBtn = document.getElementById('listen-to-window') +const focusModalBtn = document.getElementById('focus-on-modal-window') + +const hideFocusBtn = () => { + focusModalBtn.classList.add('disappear') + focusModalBtn.classList.remove('smooth-appear') + focusModalBtn.removeEventListener('click', focusWindow) +} + +const showFocusBtn = (btn) => { + focusModalBtn.classList.add('smooth-appear') + focusModalBtn.classList.remove('disappear') + focusModalBtn.addEventListener('click', focusWindow) +} +const focusWindow = () => { + ipcRenderer.send('focus-demo-window') +} + +ipcRenderer.on('window-focus', hideFocusBtn) +ipcRenderer.on('window-close', hideFocusBtn) +ipcRenderer.on('window-blur', showFocusBtn) + +listenToWindowBtn.addEventListener('click', () => { + ipcRenderer.send('show-demo-window') +}) + +const links = document.querySelectorAll('a[href]') + +Array.prototype.forEach.call(links, (link) => { + const url = link.getAttribute('href') + if (url.indexOf('http') === 0) { + link.addEventListener('click', (e) => { + e.preventDefault() + shell.openExternal(url) + }) + } +}) diff --git a/docs/glossary.md b/docs/glossary.md index dab5b97b3fc2f..a4bfa2a96071f 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -4,15 +4,39 @@ This page defines some terminology that is commonly used in Electron development ### ASAR -ASAR stands for Atom Shell Archive Format. An [asar][asar] archive is a simple +ASAR stands for Atom Shell Archive Format. An [asar] archive is a simple `tar`-like format that concatenates files into a single file. Electron can read arbitrary files from it without unpacking the whole file. -The ASAR format was created primarily to improve performance on Windows... TODO +The ASAR format was created primarily to improve performance on Windows when +reading large quantities of small files (e.g. when loading your app's JavaScript +dependency tree from `node_modules`). + +### code signing + +Code signing is a process where an app developer digitally signs their code to +ensure that it hasn't been tampered with after packaging. Both Windows and +macOS implement their own version of code signing. As a desktop app developer, +it's important that you sign your code if you plan on distributing it to the +general public. + +For more information, read the [Code Signing] tutorial. + +### context isolation + +Context isolation is a security measure in Electron that ensures that your +preload script cannot leak privileged Electron or Node.js APIs to the web +contents in your renderer process. With context isolation enabled, the +only way to expose APIs from your preload script is through the +`contextBridge` API. + +For more information, read the [Context Isolation] tutorial. + +See also: [preload script](#preload-script), [renderer process](#renderer-process) ### CRT -The C Run-time Library (CRT) is the part of the C++ Standard Library that +The C Runtime Library (CRT) is the part of the C++ Standard Library that incorporates the ISO C99 standard library. The Visual C++ libraries that implement the CRT support native code development, and both mixed native and managed code, and pure managed code for .NET development. @@ -20,8 +44,7 @@ managed code, and pure managed code for .NET development. ### DMG An Apple Disk Image is a packaging format used by macOS. DMG files are -commonly used for distributing application "installers". [electron-builder] -supports `dmg` as a build target. +commonly used for distributing application "installers". ### IME @@ -31,19 +54,15 @@ keyboards to input Chinese, Japanese, Korean and Indic characters. ### IDL -Interface description language. Write function signatures and data types in a format that can be used to generate interfaces in Java, C++, JavaScript, etc. +Interface description language. Write function signatures and data types in a +format that can be used to generate interfaces in Java, C++, JavaScript, etc. ### IPC -IPC stands for Inter-Process Communication. Electron uses IPC to send -serialized JSON messages between the [main] and [renderer] processes. - -### libchromiumcontent - -A shared library that includes the [Chromium Content module] and all its -dependencies (e.g., Blink, [V8], etc.). Also referred to as "libcc". +IPC stands for inter-process communication. Electron uses IPC to send +serialized JSON messages between the main and renderer processes. -- [github.com/electron/libchromiumcontent](https://github.com/electron/libchromiumcontent) +see also: [main process](#main-process), [renderer process](#renderer-process) ### main process @@ -68,10 +87,22 @@ MAS, see the [Mac App Store Submission Guide]. ### Mojo -An IPC system for communicating intra- or inter-process, and that's important because Chrome is keen on being able to split its work into separate processes or not, depending on memory pressures etc. +An IPC system for communicating intra- or inter-process, and that's important +because Chrome is keen on being able to split its work into separate processes +or not, depending on memory pressures etc. See https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md +See also: [IPC](#ipc) + +### MSI + +On Windows, MSI packages are used by the Windows Installer +(also known as Microsoft Installer) service to install and configure +applications. + +More information can be found in [Microsoft's documentation][msi]. + ### native modules Native modules (also called [addons] in @@ -85,22 +116,33 @@ likely to use a different V8 version from the Node binary installed in your system, you have to manually specify the location of Electron’s headers when building native modules. -See also [Using Native Node Modules]. +For more information, read the [Native Node Modules] tutorial. + +### notarization -### NSIS +Notarization is a macOS-specific process where a developer can send a +code-signed app to Apple servers to get verified for malicious +components through an automated service. -Nullsoft Scriptable Install System is a script-driven Installer -authoring tool for Microsoft Windows. It is released under a combination of -free software licenses, and is a widely-used alternative to commercial -proprietary products like InstallShield. [electron-builder] supports NSIS -as a build target. +See also: [code signing](#code-signing) ### OSR -OSR (Off-screen rendering) can be used for loading heavy page in +OSR (offscreen rendering) can be used for loading heavy page in background and then displaying it after (it will be much faster). It allows you to render page without showing it on screen. +For more information, read the [Offscreen Rendering][osr] tutorial. + +### preload script + +Preload scripts contain code that executes in a renderer process +before its web contents begin loading. These scripts run within +the renderer context, but are granted more privileges by having +access to Node.js APIs. + +See also: [renderer process](#renderer-process), [context isolation](#context-isolation) + ### process A process is an instance of a computer program that is being executed. Electron @@ -120,13 +162,17 @@ The renderer process is a browser window in your app. Unlike the main process, there can be multiple of these and each is run in a separate process. They can also be hidden. -In normal browsers, web pages usually run in a sandboxed environment and are not -allowed access to native resources. Electron users, however, have the power to -use Node.js APIs in web pages allowing lower level operating system -interactions. - See also: [process](#process), [main process](#main-process) +### sandbox + +The sandbox is a security feature inherited from Chromium that restricts +your renderer processes to a limited set of permissions. + +For more information, read the [Process Sandboxing] tutorial. + +See also: [process](#process) + ### Squirrel Squirrel is an open-source framework that enables Electron apps to update @@ -159,7 +205,7 @@ building it. V8's version numbers always correspond to those of Google Chrome. Chrome 59 includes V8 5.9, Chrome 58 includes V8 5.8, etc. -- [developers.google.com/v8](https://developers.google.com/v8) +- [v8.dev](https://v8.dev/) - [nodejs.org/api/v8.html](https://nodejs.org/api/v8.html) - [docs/development/v8-development.md](development/v8-development.md) @@ -174,13 +220,15 @@ embedded content. [addons]: https://nodejs.org/api/addons.html [asar]: https://github.com/electron/asar -[autoUpdater]: api/auto-updater.md -[Chromium Content module]: https://www.chromium.org/developers/content-module -[electron-builder]: https://github.com/electron-userland/electron-builder -[libchromiumcontent]: #libchromiumcontent -[Mac App Store Submission Guide]: tutorial/mac-app-store-submission-guide.md +[autoupdater]: api/auto-updater.md +[code signing]: tutorial/code-signing.md +[context isolation]: tutorial/context-isolation.md +[mac app store submission guide]: tutorial/mac-app-store-submission-guide.md [main]: #main-process +[msi]: https://docs.microsoft.com/en-us/windows/win32/msi/windows-installer-portal +[offscreen rendering]: tutorial/offscreen-rendering.md +[process sandboxing]: tutorial/sandbox.md [renderer]: #renderer-process [userland]: #userland -[Using Native Node Modules]: tutorial/using-native-node-modules.md -[V8]: #v8 +[using native node modules]: tutorial/using-native-node-modules.md +[v8]: #v8 diff --git a/docs/images/chrome-processes.png b/docs/images/chrome-processes.png new file mode 100644 index 0000000000000..1b0a1c0060f95 Binary files /dev/null and b/docs/images/chrome-processes.png differ diff --git a/docs/images/connection-status.png b/docs/images/connection-status.png new file mode 100644 index 0000000000000..6dcf2574e86a0 Binary files /dev/null and b/docs/images/connection-status.png differ diff --git a/docs/images/dark_mode.gif b/docs/images/dark_mode.gif new file mode 100644 index 0000000000000..d011564c90ccd Binary files /dev/null and b/docs/images/dark_mode.gif differ diff --git a/docs/images/dock-progress-bar.png b/docs/images/dock-progress-bar.png new file mode 100644 index 0000000000000..4dee6b4bde5e2 Binary files /dev/null and b/docs/images/dock-progress-bar.png differ diff --git a/docs/images/drag-and-drop.gif b/docs/images/drag-and-drop.gif new file mode 100644 index 0000000000000..b6427247e2412 Binary files /dev/null and b/docs/images/drag-and-drop.gif differ diff --git a/docs/images/gatekeeper.png b/docs/images/gatekeeper.png new file mode 100644 index 0000000000000..22567135b7da1 Binary files /dev/null and b/docs/images/gatekeeper.png differ diff --git a/docs/images/linux-progress-bar.png b/docs/images/linux-progress-bar.png new file mode 100644 index 0000000000000..fcc93f3d8c978 Binary files /dev/null and b/docs/images/linux-progress-bar.png differ diff --git a/docs/images/local-shortcut.png b/docs/images/local-shortcut.png new file mode 100644 index 0000000000000..600e78450eba8 Binary files /dev/null and b/docs/images/local-shortcut.png differ diff --git a/docs/images/macos-dock-menu.png b/docs/images/macos-dock-menu.png new file mode 100644 index 0000000000000..5d4b2356fc504 Binary files /dev/null and b/docs/images/macos-dock-menu.png differ diff --git a/docs/images/macos-progress-bar.png b/docs/images/macos-progress-bar.png new file mode 100644 index 0000000000000..d2b682fe417cb Binary files /dev/null and b/docs/images/macos-progress-bar.png differ diff --git a/docs/images/message-notification-renderer.png b/docs/images/message-notification-renderer.png new file mode 100644 index 0000000000000..87c8a876a2b4f Binary files /dev/null and b/docs/images/message-notification-renderer.png differ diff --git a/docs/images/mission-control-progress-bar.png b/docs/images/mission-control-progress-bar.png new file mode 100644 index 0000000000000..e7db40156ed4d Binary files /dev/null and b/docs/images/mission-control-progress-bar.png differ diff --git a/docs/images/notification-main.png b/docs/images/notification-main.png new file mode 100644 index 0000000000000..221c7230e3eb6 Binary files /dev/null and b/docs/images/notification-main.png differ diff --git a/docs/images/notification-renderer.png b/docs/images/notification-renderer.png new file mode 100644 index 0000000000000..e66bc96abd7fb Binary files /dev/null and b/docs/images/notification-renderer.png differ diff --git a/docs/images/online-event-detection.png b/docs/images/online-event-detection.png new file mode 100644 index 0000000000000..4f16489a7271c Binary files /dev/null and b/docs/images/online-event-detection.png differ diff --git a/docs/images/performance-cpu-prof.png b/docs/images/performance-cpu-prof.png new file mode 100644 index 0000000000000..d6e74d90ed5dc Binary files /dev/null and b/docs/images/performance-cpu-prof.png differ diff --git a/docs/images/performance-heap-prof.png b/docs/images/performance-heap-prof.png new file mode 100644 index 0000000000000..83772b4444244 Binary files /dev/null and b/docs/images/performance-heap-prof.png differ diff --git a/docs/images/preload-example.png b/docs/images/preload-example.png new file mode 100644 index 0000000000000..9f330b32de9ca Binary files /dev/null and b/docs/images/preload-example.png differ diff --git a/docs/images/recent-documents.png b/docs/images/recent-documents.png new file mode 100644 index 0000000000000..3542c1315e34a Binary files /dev/null and b/docs/images/recent-documents.png differ diff --git a/docs/images/represented-file.png b/docs/images/represented-file.png new file mode 100644 index 0000000000000..8ccb477ab7e5a Binary files /dev/null and b/docs/images/represented-file.png differ diff --git a/docs/images/simplest-electron-app.png b/docs/images/simplest-electron-app.png new file mode 100644 index 0000000000000..f79c363444622 Binary files /dev/null and b/docs/images/simplest-electron-app.png differ diff --git a/docs/images/versioning-sketch-2.png b/docs/images/versioning-sketch-2.png old mode 100755 new mode 100644 diff --git a/docs/images/vs-options-debugging-symbols.png b/docs/images/vs-options-debugging-symbols.png new file mode 100644 index 0000000000000..30ab3961379a9 Binary files /dev/null and b/docs/images/vs-options-debugging-symbols.png differ diff --git a/docs/images/vs-tools-options.png b/docs/images/vs-tools-options.png new file mode 100644 index 0000000000000..4acad752afbd0 Binary files /dev/null and b/docs/images/vs-tools-options.png differ diff --git a/docs/images/windows-progress-bar.png b/docs/images/windows-progress-bar.png new file mode 100644 index 0000000000000..92c261788654b Binary files /dev/null and b/docs/images/windows-progress-bar.png differ diff --git a/docs/images/windows-taskbar-icon-overlay.png b/docs/images/windows-taskbar-icon-overlay.png new file mode 100644 index 0000000000000..fb9a86d530bb1 Binary files /dev/null and b/docs/images/windows-taskbar-icon-overlay.png differ diff --git a/docs/images/windows-taskbar-jumplist.png b/docs/images/windows-taskbar-jumplist.png new file mode 100644 index 0000000000000..7660abcbb1051 Binary files /dev/null and b/docs/images/windows-taskbar-jumplist.png differ diff --git a/docs/images/windows-taskbar-thumbnail-toolbar.png b/docs/images/windows-taskbar-thumbnail-toolbar.png new file mode 100644 index 0000000000000..41a251e8d7b36 Binary files /dev/null and b/docs/images/windows-taskbar-thumbnail-toolbar.png differ diff --git a/docs/styleguide.md b/docs/styleguide.md index 0f3de4d496cc3..92a5c1ccf7333 100644 --- a/docs/styleguide.md +++ b/docs/styleguide.md @@ -2,15 +2,14 @@ These are the guidelines for writing Electron documentation. -## Titles +## Headings * Each page must have a single `#`-level title at the top. -* Chapters in the same page must have `##`-level titles. -* Sub-chapters need to increase the number of `#` in the title according to +* Chapters in the same page must have `##`-level headings. +* Sub-chapters need to increase the number of `#` in the heading according to their nesting depth. -* All words in the page's title must be capitalized, except for conjunctions - like "of" and "and" . -* Only the first word of a chapter title must be capitalized. +* The page's title must follow [APA title case][title-case]. +* All chapters must follow [APA sentence case][sentence-case]. Using `Quick Start` as example: @@ -44,11 +43,20 @@ For API references, there are exceptions to this rule. ## Markdown rules +This repository uses the [`markdownlint`][markdownlint] package to enforce consistent +Markdown styling. For the exact rules, see the `.markdownlint.json` file in the root +folder. + +There are a few style guidelines that aren't covered by the linter rules: + + * Use `sh` instead of `cmd` in code blocks (due to the syntax highlighter). -* Lines should be wrapped at 80 columns. +* Keep line lengths between 80 and 100 characters if possible for readability + purposes. * No nesting lists more than 2 levels (due to the markdown renderer). * All `js` and `javascript` code blocks are linted with -[standard-markdown](http://npm.im/standard-markdown). +[standard-markdown](https://www.npmjs.com/package/standard-markdown). +* For unordered lists, use asterisks instead of dashes. ## Picking words @@ -59,14 +67,15 @@ For API references, there are exceptions to this rule. The following rules only apply to the documentation of APIs. -### Page title +### Title and description -Each page must use the actual object name returned by `require('electron')` -as the title, such as `BrowserWindow`, `autoUpdater`, and `session`. +Each module's API doc must use the actual object name returned by `require('electron')` +as its title (such as `BrowserWindow`, `autoUpdater`, and `session`). -Under the page title must be a one-line description starting with `>`. +Directly under the page title, add a one-line description of the module +as a markdown quote (beginning with `>`). -Using `session` as example: +Using the `session` module as an example: ```markdown # session @@ -98,14 +107,19 @@ Using `autoUpdater` as an example: * API classes or classes that are part of modules must be listed under a `## Class: TheClassName` chapter. * One page can have multiple classes. -* Constructors must be listed with `###`-level titles. -* [Static Methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static) must be listed under a `### Static Methods` chapter. -* [Instance Methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Prototype_methods) must be listed under an `### Instance Methods` chapter. -* All methods that have a return value must start their description with "Returns `[TYPE]` - Return description" - * If the method returns an `Object`, its structure can be specified using a colon followed by a newline then an unordered list of properties in the same style as function parameters. +* Constructors must be listed with `###`-level headings. +* [Static Methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static) + must be listed under a `### Static Methods` chapter. +* [Instance Methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Prototype_methods) + must be listed under an `### Instance Methods` chapter. +* All methods that have a return value must start their description with + "Returns `[TYPE]` - [Return description]" + * If the method returns an `Object`, its structure can be specified using a colon + followed by a newline then an unordered list of properties in the same style as + function parameters. * Instance Events must be listed under an `### Instance Events` chapter. * Instance Properties must be listed under an `### Instance Properties` chapter. - * Instance properties must start with "A [Property Type] ..." + * Instance Properties must start with "A [Property Type] ..." Using the `Session` and `Cookies` classes as an example: @@ -141,21 +155,25 @@ Using the `Session` and `Cookies` classes as an example: #### `cookies.get(filter, callback)` ``` -### Methods +### Methods and their arguments The methods chapter must be in the following form: ```markdown ### `objectName.methodName(required[, optional]))` -* `required` String - A parameter description. +* `required` string - A parameter description. * `optional` Integer (optional) - Another parameter description. ... ``` -The title can be `###` or `####`-levels depending on whether it is a method of -a module or a class. +#### Heading level + +The heading can be `###` or `####`-levels depending on whether the method +belongs to a module or a class. + +#### Function signature For modules, the `objectName` is the module's name. For classes, it must be the name of the instance of the class, and must not be the same as the module's @@ -164,38 +182,42 @@ name. For example, the methods of the `Session` class under the `session` module must use `ses` as the `objectName`. -The optional arguments are notated by square brackets `[]` surrounding the optional argument -as well as the comma required if this optional argument follows another +Optional arguments are notated by square brackets `[]` surrounding the optional +argument as well as the comma required if this optional argument follows another argument: -```sh +```markdown required[, optional] ``` -Below the method is more detailed information on each of the arguments. The type -of argument is notated by either the common types: +#### Argument descriptions -* [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) -* [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) -* [`Object`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) -* [`Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) -* [`Boolean`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean) -* Or a custom type like Electron's [`WebContent`](api/web-contents.md) +More detailed information on each of the arguments is noted in an unordered list +below the method. The type of argument is notated by either JavaScript primitives +(e.g. `string`, `Promise`, or `Object`), a custom API structure like Electron's +[`Cookie`](api/structures/cookie.md), or the wildcard `any`. + +If the argument is of type `Array`, use `[]` shorthand with the type of value +inside the array (for example,`any[]` or `string[]`). + +If the argument is of type `Promise`, parametrize the type with what the promise +resolves to (for example, `Promise` or `Promise`). + +If an argument can be of multiple types, separate the types with `|`. + +The description for `Function` type arguments should make it clear how it may be +called and list the types of the parameters that will be passed to it. + +#### Platform-specific functionality If an argument or a method is unique to certain platforms, those platforms are denoted using a space-delimited italicized list following the datatype. Values can be `macOS`, `Windows` or `Linux`. ```markdown -* `animate` Boolean (optional) _macOS_ _Windows_ - Animate the thing. +* `animate` boolean (optional) _macOS_ _Windows_ - Animate the thing. ``` -`Array` type arguments must specify what elements the array may include in -the description below. - -The description for `Function` type arguments should make it clear how it may be -called and list the types of the parameters that will be passed to it. - ### Events The events chapter must be in following form: @@ -205,13 +227,13 @@ The events chapter must be in following form: Returns: -* `time` String +* `time` string ... ``` -The title can be `###` or `####`-levels depending on whether it is an event of -a module or a class. +The heading can be `###` or `####`-levels depending on whether the event +belongs to a module or a class. The arguments of an event follow the same rules as methods. @@ -225,9 +247,13 @@ The properties chapter must be in following form: ... ``` -The title can be `###` or `####`-levels depending on whether it is a property of -a module or a class. +The heading can be `###` or `####`-levels depending on whether the property +belongs to a module or a class. -## Documentation Translations +## Documentation translations See [electron/i18n](https://github.com/electron/i18n#readme) + +[title-case]: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case +[sentence-case]: https://apastyle.apa.org/style-grammar-guidelines/capitalization/sentence-case +[markdownlint]: https://github.com/DavidAnson/markdownlint diff --git a/docs/tutorial/about.md b/docs/tutorial/about.md deleted file mode 100644 index b39e026788e83..0000000000000 --- a/docs/tutorial/about.md +++ /dev/null @@ -1,60 +0,0 @@ -# About Electron - -[Electron](https://electronjs.org) is an open source library developed by GitHub for building cross-platform desktop applications with HTML, CSS, and JavaScript. Electron accomplishes this by combining [Chromium](https://www.chromium.org/Home) and [Node.js](https://nodejs.org) into a single runtime and apps can be packaged for Mac, Windows, and Linux. - -Electron began in 2013 as the framework on which [Atom](https://atom.io), GitHub's hackable text editor, would be built. The two were open sourced in the Spring of 2014. - -It has since become a popular tool used by open source developers, startups, and established companies. [See who is building on Electron](https://electronjs.org/apps). - -Read on to learn more about the contributors and releases of Electron or get started building with Electron in the [Quick Start Guide](quick-start.md). - -## Core Team and Contributors - -Electron is maintained by a team at GitHub as well as a group of [active contributors](https://github.com/electron/electron/graphs/contributors) from the community. Some of the contributors are individuals and some work at larger companies who are developing on Electron. We're happy to add frequent contributors to the project as maintainers. Read more about [contributing to Electron](https://github.com/electron/electron/blob/master/CONTRIBUTING.md). - -## Releases - -[Electron releases](https://github.com/electron/electron/releases) frequently. We release when there are significant bug fixes, new APIs or are updating versions of Chromium or Node.js. - -### Updating Dependencies - -Electron's version of Chromium is usually updated within one or two weeks after a new stable Chromium version is released, depending on the effort involved in the upgrade. - -When a new version of Node.js is released, Electron usually waits about a month before upgrading in order to bring in a more stable version. - -In Electron, Node.js and Chromium share a single V8 instance—usually the version that Chromium is using. Most of the time this _just works_ but sometimes it means patching Node.js. - -### Versioning - -As of version 2.0 Electron [follows `semver`](https://semver.org). -For most applications, and using any recent version of npm, -running `$ npm install electron` will do the right thing. - -The version update process is detailed explicitly in our [Versioning Doc](electron-versioning.md). - -### LTS - -Long term support of older versions of Electron does not currently exist. If your current version of Electron works for you, you can stay on it for as long as you'd like. If you want to make use of new features as they come in you should upgrade to a newer version. - -A major update came with version `v1.0.0`. If you're not yet using this version, you should [read more about the `v1.0.0` changes](https://electronjs.org/blog/electron-1-0). - -## Core Philosophy - -In order to keep Electron small (file size) and sustainable (the spread of dependencies and APIs) the project limits the scope of the core project. - -For instance, Electron uses Chromium's rendering library rather than all of Chromium. This makes it easier to upgrade Chromium but also means some browser features found in Google Chrome do not exist in Electron. - -New features added to Electron should primarily be native APIs. If a feature can be its own Node.js module, it probably should be. See the [Electron tools built by the community](https://electronjs.org/community). - -## History - -Below are milestones in Electron's history. - -| :calendar: | :tada: | -| --- | --- | -| **April 2013**| [Atom Shell is started](https://github.com/electron/electron/commit/6ef8875b1e93787fa9759f602e7880f28e8e6b45).| -| **May 2014** | [Atom Shell is open sourced](https://blog.atom.io/2014/05/06/atom-is-now-open-source.html). | -| **April 2015** | [Atom Shell is re-named Electron](https://github.com/electron/electron/pull/1389). | -| **May 2016** | [Electron releases `v1.0.0`](https://electronjs.org/blog/electron-1-0).| -| **May 2016** | [Electron apps compatible with Mac App Store](mac-app-store-submission-guide.md).| -| **August 2016** | [Windows Store support for Electron apps](windows-store-guide.md).| diff --git a/docs/tutorial/accessibility.md b/docs/tutorial/accessibility.md index 64510fdedb83d..31f0b83fef631 100644 --- a/docs/tutorial/accessibility.md +++ b/docs/tutorial/accessibility.md @@ -1,69 +1,31 @@ # Accessibility -Making accessible applications is important and we're happy to introduce new -functionality to [Devtron][devtron] and [Spectron][spectron] that gives -developers the opportunity to make their apps better for everyone. - ---- - Accessibility concerns in Electron applications are similar to those of -websites because they're both ultimately HTML. With Electron apps, however, -you can't use the online resources for accessibility audits because your app -doesn't have a URL to point the auditor to. - -These new features bring those auditing tools to your Electron app. You can -choose to add audits to your tests with Spectron or use them within DevTools -with Devtron. Read on for a summary of the tools. - -## Spectron - -In the testing framework Spectron, you can now audit each window and `` -tag in your application. For example: - -```javascript -app.client.auditAccessibility().then(function (audit) { - if (audit.failed) { - console.error(audit.message) - } -}) -``` - -You can read more about this feature in [Spectron's documentation][spectron-a11y]. - -## Devtron - -In Devtron, there is a new accessibility tab which will allow you to audit a -page in your app, sort and filter the results. - -![devtron screenshot][devtron-screenshot] - -Both of these tools are using the [Accessibility Developer Tools][a11y-devtools] -library built by Google for Chrome. You can learn more about the accessibility -audit rules this library uses on that [repository's wiki][a11y-devtools-wiki]. +websites because they're both ultimately HTML. -If you know of other great accessibility tools for Electron, add them to the -accessibility documentation with a pull request. +## Manually enabling accessibility features -## Enabling Accessibility +Electron applications will automatically enable accessibility features in the +presence of assistive technology (e.g. [JAWS](https://www.freedomscientific.com/products/software/jaws/) +on Windows or [VoiceOver](https://help.apple.com/voiceover/mac/10.15/) on macOS). +See Chrome's [accessibility documentation][a11y-docs] for more details. -Electron applications keep accessibility disabled by default for performance -reasons but there are multiple ways to enable it. +You can also manually toggle these features either within your Electron application +or by setting flags in third-party native software. -### Inside Application +### Using Electron's API -By using [`app.setAccessibilitySupportEnabled(enabled)`][setAccessibilitySupportEnabled], -you can expose accessibility switch to users in the application preferences. -User's system assistive utilities have priority over this setting and will -override it. +By using the [`app.setAccessibilitySupportEnabled(enabled)`][setAccessibilitySupportEnabled] +API, you can manually expose Chrome's accessibility tree to users in the application preferences. +Note that the user's system assistive utilities have priority over this setting and +will override it. -### Assistive Technology +### Within third-party software -Electron application will enable accessibility automatically when it detects -assistive technology (Windows) or VoiceOver (macOS). See Chrome's -[accessibility documentation][a11y-docs] for more details. +#### macOS -On macOS, third-party assistive technology can switch accessibility inside -Electron applications by setting the attribute `AXManualAccessibility` +On macOS, third-party assistive technology can toggle accessibility features inside +Electron applications by setting the `AXManualAccessibility` attribute programmatically: ```objc @@ -81,10 +43,6 @@ CFStringRef kAXManualAccessibility = CFSTR("AXManualAccessibility"); } ``` -[devtron]: https://electronjs.org/devtron -[devtron-screenshot]: https://cloud.githubusercontent.com/assets/1305617/17156618/9f9bcd72-533f-11e6-880d-389115f40a2a.png -[spectron]: https://electronjs.org/spectron -[spectron-a11y]: https://github.com/electron/spectron#accessibility-testing [a11y-docs]: https://www.chromium.org/developers/design-documents/accessibility#TOC-How-Chrome-detects-the-presence-of-Assistive-Technology [a11y-devtools]: https://github.com/GoogleChrome/accessibility-developer-tools [a11y-devtools-wiki]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules diff --git a/docs/tutorial/app-feedback-program.md b/docs/tutorial/app-feedback-program.md deleted file mode 100644 index 862dec5de9c3f..0000000000000 --- a/docs/tutorial/app-feedback-program.md +++ /dev/null @@ -1,3 +0,0 @@ -# Electron App Feedback Program - -Electron is working on building a streamlined release process and having faster releases. To help with that, we have the App Feedback Program for large-scale Electron apps to test our beta releases and report app-specific issues to the Electron team. We use this program to help us prioritize work and get applications upgraded to the next stable release as soon as possible. There are a few requirements we expect from participants, such as attending short, online weekly check-ins. Please visit the [blog post](https://electronjs.org/blog/app-feedback-program) for details and sign-up. diff --git a/docs/tutorial/application-architecture.md b/docs/tutorial/application-architecture.md deleted file mode 100644 index 162300c6e17ce..0000000000000 --- a/docs/tutorial/application-architecture.md +++ /dev/null @@ -1,141 +0,0 @@ -# Electron Application Architecture - -Before we can dive into Electron's APIs, we need to discuss the two process -types available in Electron. They are fundamentally different and important to -understand. - -## Main and Renderer Processes - -In Electron, the process that runs `package.json`'s `main` script is called -__the main process__. The script that runs in the main process can display a -GUI by creating web pages. An Electron app always has one main process, but -never more. - -Since Electron uses Chromium for displaying web pages, Chromium's -multi-process architecture is also used. Each web page in Electron runs in -its own process, which is called __the renderer process__. - -In normal browsers, web pages usually run in a sandboxed environment and are not -allowed access to native resources. Electron users, however, have the power to -use Node.js APIs in web pages allowing lower level operating system -interactions. - -### Differences Between Main Process and Renderer Process - -The main process creates web pages by creating `BrowserWindow` instances. Each -`BrowserWindow` instance runs the web page in its own renderer process. When a -`BrowserWindow` instance is destroyed, the corresponding renderer process -is also terminated. - -The main process manages all web pages and their corresponding renderer -processes. Each renderer process is isolated and only cares about the web page -running in it. - -In web pages, calling native GUI related APIs is not allowed because managing -native GUI resources in web pages is very dangerous and it is easy to leak -resources. If you want to perform GUI operations in a web page, the renderer -process of the web page must communicate with the main process to request that -the main process perform those operations. - -> #### Aside: Communication Between Processes -> In Electron, we have several ways to communicate between the main process -and renderer processes, such as [`ipcRenderer`](../api/ipc-renderer.md) and -[`ipcMain`](../api/ipc-main.md) modules for sending messages, and the -[remote](../api/remote.md) module for RPC style communication. There is also -an FAQ entry on [how to share data between web pages][share-data]. - -## Using Electron APIs - -Electron offers a number of APIs that support the development of a desktop -application in both the main process and the renderer process. In both -processes, you'd access Electron's APIs by requiring its included module: - -```javascript -const electron = require('electron') -``` - -All Electron APIs are assigned a process type. Many of them can only be -used from the main process, some of them only from a renderer process, -some from both. The documentation for each individual API will -state which process it can be used from. - -A window in Electron is for instance created using the `BrowserWindow` -class. It is only available in the main process. - -```javascript -// This will work in the main process, but be `undefined` in a -// renderer process: -const { BrowserWindow } = require('electron') - -const win = new BrowserWindow() -``` - -Since communication between the processes is possible, a renderer process -can call upon the main process to perform tasks. Electron comes with a -module called `remote` that exposes APIs usually only available on the -main process. In order to create a `BrowserWindow` from a renderer process, -we'd use the remote as a middle-man: - -```javascript -// This will work in a renderer process, but be `undefined` in the -// main process: -const { remote } = require('electron') -const { BrowserWindow } = remote - -const win = new BrowserWindow() -``` - -## Using Node.js APIs - -Electron exposes full access to Node.js both in the main and the renderer -process. This has two important implications: - -1) All APIs available in Node.js are available in Electron. Calling the -following code from an Electron app works: - -```javascript -const fs = require('fs') - -const root = fs.readdirSync('/') - -// This will print all files at the root-level of the disk, -// either '/' or 'C:\'. -console.log(root) -``` - -As you might already be able to guess, this has important security implications -if you ever attempt to load remote content. You can find more information and -guidance on loading remote content in our [security documentation][security]. - -2) You can use Node.js modules in your application. Pick your favorite npm -module. npm offers currently the world's biggest repository of open-source -code – the ability to use well-maintained and tested code that used to be -reserved for server applications is one of the key features of Electron. - -As an example, to use the official AWS SDK in your application, you'd first -install it as a dependency: - -```sh -npm install --save aws-sdk -``` - -Then, in your Electron app, require and use the module as if you were -building a Node.js application: - -```javascript -// A ready-to-use S3 Client -const S3 = require('aws-sdk/clients/s3') -``` - -There is one important caveat: Native Node.js modules (that is, modules that -require compilation of native code before they can be used) will need to be -compiled to be used with Electron. - -The vast majority of Node.js modules are _not_ native. Only 400 out of the -~650.000 modules are native. However, if you do need native modules, please -consult [this guide on how to recompile them for Electron][native-node]. - -[node-docs]: https://nodejs.org/en/docs/ -[security]: ./security.md -[native-node]: ./using-native-node-modules.md -[share-data]: ../faq.md#how-to-share-data-between-web-pages diff --git a/docs/tutorial/application-debugging.md b/docs/tutorial/application-debugging.md index d2f9b186c5e7a..a89b0ea4b07ae 100644 --- a/docs/tutorial/application-debugging.md +++ b/docs/tutorial/application-debugging.md @@ -15,7 +15,7 @@ can open them programmatically by calling the `openDevTools()` API on the ```javascript const { BrowserWindow } = require('electron') -let win = new BrowserWindow() +const win = new BrowserWindow() win.webContents.openDevTools() ``` @@ -36,3 +36,13 @@ For more information, see the [Debugging the Main Process documentation][main-de [node-inspect]: https://nodejs.org/en/docs/inspector/ [devtools]: https://developer.chrome.com/devtools [main-debug]: ./debugging-main-process.md + +## V8 Crashes + +If the V8 context crashes, the DevTools will display this message. + +`DevTools was disconnected from the page. Once page is reloaded, DevTools will automatically reconnect.` + +Chromium logs can be enabled via the `ELECTRON_ENABLE_LOGGING` environment variable. For more information, see the [environment variables documentation](../api/environment-variables.md#electron_enable_logging). + +Alternatively, the command line argument `--enable-logging` can be passed. More information is available in the [command line switches documentation](../api/command-line-switches.md#--enable-loggingfile). diff --git a/docs/tutorial/application-distribution.md b/docs/tutorial/application-distribution.md index 3eba901b2cba8..1b17541f2b3f5 100644 --- a/docs/tutorial/application-distribution.md +++ b/docs/tutorial/application-distribution.md @@ -1,162 +1,124 @@ -# Application Distribution +--- +title: 'Application Packaging' +description: 'To distribute your app with Electron, you need to package and rebrand it. To do this, you can either use specialized tooling or manual approaches.' +slug: application-distribution +hide_title: false +--- -To distribute your app with Electron, you need to package and rebrand it. The easiest way to do this is to use one of the following third party packaging tools: +To distribute your app with Electron, you need to package and rebrand it. To do this, you +can either use specialized tooling or manual approaches. -* [electron-forge](https://github.com/electron-userland/electron-forge) -* [electron-builder](https://github.com/electron-userland/electron-builder) -* [electron-packager](https://github.com/electron/electron-packager) +## With tooling -These tools will take care of all the steps you need to take to end up with a distributable Electron applications, such as packaging your application, rebranding the executable, setting the right icons and optionally creating installers. +There are a couple tools out there that exist to package and distribute your Electron app. +We recommend using [Electron Forge](https://www.electronforge.io). You can check out +its documentation directly, or refer to the [Packaging and Distribution](./tutorial-5-packaging.md) +part of the Electron tutorial. -## Manual distribution -You can also choose to manually get your app ready for distribution. The steps needed to do this are outlined below. +## Manual packaging -To distribute your app with Electron, you need to download Electron's [prebuilt +If you prefer the manual approach, there are 2 ways to distribute your application: + +- With prebuilt binaries +- With an app source code archive + +### With prebuilt binaries + +To distribute your app manually, you need to download Electron's [prebuilt binaries](https://github.com/electron/electron/releases). Next, the folder containing your app should be named `app` and placed in Electron's resources -directory as shown in the following examples. Note that the location of -Electron's prebuilt binaries is indicated with `electron/` in the examples -below. +directory as shown in the following examples. -On macOS: +:::note +The location of Electron's prebuilt binaries is indicated +with `electron/` in the examples below. +::: -```plaintext +```plain title='macOS' electron/Electron.app/Contents/Resources/app/ ├── package.json ├── main.js └── index.html ``` -On Windows and Linux: - -```plaintext +```plain title='Windows and Linux' electron/resources/app ├── package.json ├── main.js └── index.html ``` -Then execute `Electron.app` (or `electron` on Linux, `electron.exe` on Windows), -and Electron will start as your app. The `electron` directory will then be -your distribution to deliver to final users. +Then execute `Electron.app` on macOS, `electron` on Linux, or `electron.exe` +on Windows, and Electron will start as your app. The `electron` directory +will then be your distribution to deliver to users. -## Packaging Your App into a File +### With an app source code archive (asar) -Apart from shipping your app by copying all of its source files, you can also -package your app into an [asar](https://github.com/electron/asar) archive to avoid -exposing your app's source code to users. +Instead of shipping your app by copying all of its source files, you can +package your app into an [asar] archive to improve the performance of reading +files on platforms like Windows, if you are not already using a bundler such +as Parcel or Webpack. To use an `asar` archive to replace the `app` folder, you need to rename the archive to `app.asar`, and put it under Electron's resources directory like below, and Electron will then try to read the archive and start from it. -On macOS: - -```plaintext +```plain title='macOS' electron/Electron.app/Contents/Resources/ └── app.asar ``` -On Windows and Linux: - -```plaintext +```plain title='Windows' electron/resources/ └── app.asar ``` -More details can be found in [Application packaging](application-packaging.md). +You can find more details on how to use `asar` in the +[`electron/asar` repository][asar]. -## Rebranding with Downloaded Binaries +### Rebranding with downloaded binaries After bundling your app into Electron, you will want to rebrand Electron before distributing it to users. -### Windows - -You can rename `electron.exe` to any name you like, and edit its icon and other -information with tools like [rcedit](https://github.com/atom/rcedit). - -### macOS +- **Windows:** You can rename `electron.exe` to any name you like, and edit + its icon and other information with tools like [rcedit](https://github.com/electron/rcedit). +- **Linux:** You can rename the `electron` executable to any name you like. +- **macOS:** You can rename `Electron.app` to any name you want, and you also have to rename + the `CFBundleDisplayName`, `CFBundleIdentifier` and `CFBundleName` fields in the + following files: -You can rename `Electron.app` to any name you want, and you also have to rename -the `CFBundleDisplayName`, `CFBundleIdentifier` and `CFBundleName` fields in the -following files: + - `Electron.app/Contents/Info.plist` + - `Electron.app/Contents/Frameworks/Electron Helper.app/Contents/Info.plist` -* `Electron.app/Contents/Info.plist` -* `Electron.app/Contents/Frameworks/Electron Helper.app/Contents/Info.plist` + You can also rename the helper app to avoid showing `Electron Helper` in the + Activity Monitor, but make sure you have renamed the helper app's executable + file's name. -You can also rename the helper app to avoid showing `Electron Helper` in the -Activity Monitor, but make sure you have renamed the helper app's executable -file's name. + The structure of a renamed app would be like: -The structure of a renamed app would be like: - -```plaintext +```plain MyApp.app/Contents ├── Info.plist ├── MacOS/ -│   └── MyApp +│ └── MyApp └── Frameworks/ └── MyApp Helper.app ├── Info.plist └── MacOS/ -    └── MyApp Helper + └── MyApp Helper ``` -### Linux - -You can rename the `electron` executable to any name you like. +:::note -## Rebranding by Rebuilding Electron from Source - -It is also possible to rebrand Electron by changing the product name and +it is also possible to rebrand Electron by changing the product name and building it from source. To do this you need to set the build argument corresponding to the product name (`electron_product_name = "YourProductName"`) in the `args.gn` file and rebuild. -### Creating a Custom Electron Fork - -Creating a custom fork of Electron is almost certainly not something you will -need to do in order to build your app, even for "Production Level" applications. -Using a tool such as `electron-packager` or `electron-forge` will allow you to -"Rebrand" Electron without having to do these steps. - -You need to fork Electron when you have custom C++ code that you have patched -directly into Electron, that either cannot be upstreamed, or has been rejected -from the official version. As maintainers of Electron, we very much would like -to make your scenario work, so please try as hard as you can to get your changes -into the official version of Electron, it will be much much easier on you, and -we appreciate your help. - -#### Creating a Custom Release with surf-build - -1. Install [Surf](https://github.com/surf-build/surf), via npm: - `npm install -g surf-build@latest` - -2. Create a new S3 bucket and create the following empty directory structure: - - ```sh - - electron/ - - symbols/ - - dist/ - ``` - -3. Set the following Environment Variables: - - * `ELECTRON_GITHUB_TOKEN` - a token that can create releases on GitHub - * `ELECTRON_S3_ACCESS_KEY`, `ELECTRON_S3_BUCKET`, `ELECTRON_S3_SECRET_KEY` - - the place where you'll upload Node.js headers as well as symbols - * `ELECTRON_RELEASE` - Set to `true` and the upload part will run, leave unset - and `surf-build` will do CI-type checks, appropriate to run for every - pull request. - * `CI` - Set to `true` or else it will fail - * `GITHUB_TOKEN` - set it to the same as `ELECTRON_GITHUB_TOKEN` - * `SURF_TEMP` - set to `C:\Temp` on Windows to prevent path too long issues - * `TARGET_ARCH` - set to `ia32` or `x64` - -4. In `script/upload.py`, you _must_ set `ELECTRON_REPO` to your fork (`MYORG/electron`), - especially if you are a contributor to Electron proper. +Keep in mind this is not recommended as setting up the environment to compile +from source is not trivial and takes significant time. -5. `surf-build -r https://github.com/MYORG/electron -s YOUR_COMMIT -n 'surf-PLATFORM-ARCH'` +::: -6. Wait a very, very long time for the build to complete. +[asar]: https://github.com/electron/asar diff --git a/docs/tutorial/application-packaging.md b/docs/tutorial/application-packaging.md deleted file mode 100644 index 1dcdd08411c5c..0000000000000 --- a/docs/tutorial/application-packaging.md +++ /dev/null @@ -1,195 +0,0 @@ -# Application Packaging - -To mitigate [issues](https://github.com/joyent/node/issues/6960) around long -path names on Windows, slightly speed up `require` and conceal your source code -from cursory inspection, you can choose to package your app into an [asar][asar] -archive with little changes to your source code. - -Most users will get this feature for free, since it's supported out of the box -by [`electron-packager`][electron-packager], [`electron-forge`][electron-forge], -and [`electron-builder`][electron-builder]. If you are not using any of these -tools, read on. - -## Generating `asar` Archives - -An [asar][asar] archive is a simple tar-like format that concatenates files -into a single file. Electron can read arbitrary files from it without unpacking -the whole file. - -Steps to package your app into an `asar` archive: - -### 1. Install the asar Utility - -```sh -$ npm install -g asar -``` - -### 2. Package with `asar pack` - -```sh -$ asar pack your-app app.asar -``` - -## Using `asar` Archives - -In Electron there are two sets of APIs: Node APIs provided by Node.js and Web -APIs provided by Chromium. Both APIs support reading files from `asar` archives. - -### Node API - -With special patches in Electron, Node APIs like `fs.readFile` and `require` -treat `asar` archives as virtual directories, and the files in it as normal -files in the filesystem. - -For example, suppose we have an `example.asar` archive under `/path/to`: - -```sh -$ asar list /path/to/example.asar -/app.js -/file.txt -/dir/module.js -/static/index.html -/static/main.css -/static/jquery.min.js -``` - -Read a file in the `asar` archive: - -```javascript -const fs = require('fs') -fs.readFileSync('/path/to/example.asar/file.txt') -``` - -List all files under the root of the archive: - -```javascript -const fs = require('fs') -fs.readdirSync('/path/to/example.asar') -``` - -Use a module from the archive: - -```javascript -require('/path/to/example.asar/dir/module.js') -``` - -You can also display a web page in an `asar` archive with `BrowserWindow`: - -```javascript -const { BrowserWindow } = require('electron') -const win = new BrowserWindow() - -win.loadURL('file:///path/to/example.asar/static/index.html') -``` - -### Web API - -In a web page, files in an archive can be requested with the `file:` protocol. -Like the Node API, `asar` archives are treated as directories. - -For example, to get a file with `$.get`: - -```html - -``` - -### Treating an `asar` Archive as a Normal File - -For some cases like verifying the `asar` archive's checksum, we need to read the -content of an `asar` archive as a file. For this purpose you can use the built-in -`original-fs` module which provides original `fs` APIs without `asar` support: - -```javascript -const originalFs = require('original-fs') -originalFs.readFileSync('/path/to/example.asar') -``` - -You can also set `process.noAsar` to `true` to disable the support for `asar` in -the `fs` module: - -```javascript -const fs = require('fs') -process.noAsar = true -fs.readFileSync('/path/to/example.asar') -``` - -## Limitations of the Node API - -Even though we tried hard to make `asar` archives in the Node API work like -directories as much as possible, there are still limitations due to the -low-level nature of the Node API. - -### Archives Are Read-only - -The archives can not be modified so all Node APIs that can modify files will not -work with `asar` archives. - -### Working Directory Can Not Be Set to Directories in Archive - -Though `asar` archives are treated as directories, there are no actual -directories in the filesystem, so you can never set the working directory to -directories in `asar` archives. Passing them as the `cwd` option of some APIs -will also cause errors. - -### Extra Unpacking on Some APIs - -Most `fs` APIs can read a file or get a file's information from `asar` archives -without unpacking, but for some APIs that rely on passing the real file path to -underlying system calls, Electron will extract the needed file into a -temporary file and pass the path of the temporary file to the APIs to make them -work. This adds a little overhead for those APIs. - -APIs that requires extra unpacking are: - -* `child_process.execFile` -* `child_process.execFileSync` -* `fs.open` -* `fs.openSync` -* `process.dlopen` - Used by `require` on native modules - -### Fake Stat Information of `fs.stat` - -The `Stats` object returned by `fs.stat` and its friends on files in `asar` -archives is generated by guessing, because those files do not exist on the -filesystem. So you should not trust the `Stats` object except for getting file -size and checking file type. - -### Executing Binaries Inside `asar` Archive - -There are Node APIs that can execute binaries like `child_process.exec`, -`child_process.spawn` and `child_process.execFile`, but only `execFile` is -supported to execute binaries inside `asar` archive. - -This is because `exec` and `spawn` accept `command` instead of `file` as input, -and `command`s are executed under shell. There is no reliable way to determine -whether a command uses a file in asar archive, and even if we do, we can not be -sure whether we can replace the path in command without side effects. - -## Adding Unpacked Files to `asar` Archives - -As stated above, some Node APIs will unpack the file to the filesystem when -called. Apart from the performance issues, various anti-virus scanners might -be triggered by this behavior. - -As a workaround, you can leave various files unpacked using the `--unpack` option. -In the following example, shared libraries of native Node.js modules will not be -packed: - -```sh -$ asar pack app app.asar --unpack *.node -``` - -After running the command, you will notice that a folder named `app.asar.unpacked` -was created together with the `app.asar` file. It contains the unpacked files -and should be shipped together with the `app.asar` archive. - -[asar]: https://github.com/electron/asar -[electron-packager]: https://github.com/electron/electron-packager -[electron-forge]: https://github.com/electron-userland/electron-forge -[electron-builder]: https://github.com/electron-userland/electron-builder - diff --git a/docs/tutorial/automated-testing-with-a-custom-driver.md b/docs/tutorial/automated-testing-with-a-custom-driver.md deleted file mode 100644 index 0284a217d0c60..0000000000000 --- a/docs/tutorial/automated-testing-with-a-custom-driver.md +++ /dev/null @@ -1,135 +0,0 @@ -# Automated Testing with a Custom Driver - -To write automated tests for your Electron app, you will need a way to "drive" your application. [Spectron](https://electronjs.org/spectron) is a commonly-used solution which lets you emulate user actions via [WebDriver](http://webdriver.io/). However, it's also possible to write your own custom driver using node's builtin IPC-over-STDIO. The benefit of a custom driver is that it tends to require less overhead than Spectron, and lets you expose custom methods to your test suite. - -To create a custom driver, we'll use Node.js' [child_process](https://nodejs.org/api/child_process.html) API. The test suite will spawn the Electron process, then establish a simple messaging protocol: - -```js -var childProcess = require('child_process') -var electronPath = require('electron') - -// spawn the process -var env = { /* ... */ } -var stdio = ['inherit', 'inherit', 'inherit', 'ipc'] -var appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env }) - -// listen for IPC messages from the app -appProcess.on('message', (msg) => { - // ... -}) - -// send an IPC message to the app -appProcess.send({ my: 'message' }) -``` - -From within the Electron app, you can listen for messages and send replies using the Node.js [process](https://nodejs.org/api/process.html) API: - -```js -// listen for IPC messages from the test suite -process.on('message', (msg) => { - // ... -}) - -// send an IPC message to the test suite -process.send({ my: 'message' }) -``` - -We can now communicate from the test suite to the Electron app using the `appProcess` object. - -For convenience, you may want to wrap `appProcess` in a driver object that provides more high-level functions. Here is an example of how you can do this: - -```js -class TestDriver { - constructor ({ path, args, env }) { - this.rpcCalls = [] - - // start child process - env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages - this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env }) - - // handle rpc responses - this.process.on('message', (message) => { - // pop the handler - var rpcCall = this.rpcCalls[message.msgId] - if (!rpcCall) return - this.rpcCalls[message.msgId] = null - // reject/resolve - if (message.reject) rpcCall.reject(message.reject) - else rpcCall.resolve(message.resolve) - }) - - // wait for ready - this.isReady = this.rpc('isReady').catch((err) => { - console.error('Application failed to start', err) - this.stop() - process.exit(1) - }) - } - - // simple RPC call - // to use: driver.rpc('method', 1, 2, 3).then(...) - async rpc (cmd, ...args) { - // send rpc request - var msgId = this.rpcCalls.length - this.process.send({ msgId, cmd, args }) - return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject })) - } - - stop () { - this.process.kill() - } -} -``` - -In the app, you'd need to write a simple handler for the RPC calls: - -```js -if (process.env.APP_TEST_DRIVER) { - process.on('message', onMessage) -} - -async function onMessage ({ msgId, cmd, args }) { - var method = METHODS[cmd] - if (!method) method = () => new Error('Invalid method: ' + cmd) - try { - var resolve = await method(...args) - process.send({ msgId, resolve }) - } catch (err) { - var reject = { - message: err.message, - stack: err.stack, - name: err.name - } - process.send({ msgId, reject }) - } -} - -const METHODS = { - isReady () { - // do any setup needed - return true - } - // define your RPC-able methods here -} -``` - -Then, in your test suite, you can use your test-driver as follows: - -```js -var test = require('ava') -var electronPath = require('electron') - -var app = new TestDriver({ - path: electronPath, - args: ['./app'], - env: { - NODE_ENV: 'test' - } -}) -test.before(async t => { - await app.isReady -}) -test.after.always('cleanup', async t => { - await app.stop() -}) -``` diff --git a/docs/tutorial/automated-testing.md b/docs/tutorial/automated-testing.md new file mode 100644 index 0000000000000..56ad8282eed4b --- /dev/null +++ b/docs/tutorial/automated-testing.md @@ -0,0 +1,409 @@ +# Automated Testing + +Test automation is an efficient way of validating that your application code works as intended. +While Electron doesn't actively maintain its own testing solution, this guide will go over a couple +ways you can run end-to-end automated tests on your Electron app. + +## Using the WebDriver interface + +From [ChromeDriver - WebDriver for Chrome][chrome-driver]: + +> WebDriver is an open source tool for automated testing of web apps across many +> browsers. It provides capabilities for navigating to web pages, user input, +> JavaScript execution, and more. ChromeDriver is a standalone server which +> implements WebDriver's wire protocol for Chromium. It is being developed by +> members of the Chromium and WebDriver teams. + +There are a few ways that you can set up testing using WebDriver. + +### With WebdriverIO + +[WebdriverIO](https://webdriver.io/) (WDIO) is a test automation framework that provides a +Node.js package for testing with WebDriver. Its ecosystem also includes various plugins +(e.g. reporter and services) that can help you put together your test setup. + +#### Install the testrunner + +First you need to run the WebdriverIO starter toolkit in your project root directory: + +```sh npm2yarn +npx wdio . --yes +``` + +This installs all necessary packages for you and generates a `wdio.conf.js` configuration file. + +#### Connect WDIO to your Electron app + +Update the capabilities in your configuration file to point to your Electron app binary: + +```javascript title='wdio.conf.js' +export.config = { + // ... + capabilities: [{ + browserName: 'chrome', + 'goog:chromeOptions': { + binary: '/path/to/your/electron/binary', // Path to your Electron binary. + args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/ + } + }] + // ... +} +``` + +#### Run your tests + +To run your tests: + +```sh +$ npx wdio run wdio.conf.js +``` + +### With Selenium + +[Selenium](https://www.selenium.dev/) is a web automation framework that +exposes bindings to WebDriver APIs in many languages. Their Node.js bindings +are available under the `selenium-webdriver` package on NPM. + +#### Run a ChromeDriver server + +In order to use Selenium with Electron, you need to download the `electron-chromedriver` +binary, and run it: + +```sh npm2yarn +npm install --save-dev electron-chromedriver +./node_modules/.bin/chromedriver +Starting ChromeDriver (v2.10.291558) on port 9515 +Only local connections are allowed. +``` + +Remember the port number `9515`, which will be used later. + +#### Connect Selenium to ChromeDriver + +Next, install Selenium into your project: + +```sh npm2yarn +npm install --save-dev selenium-webdriver +``` + +Usage of `selenium-webdriver` with Electron is the same as with +normal websites, except that you have to manually specify how to connect +ChromeDriver and where to find the binary of your Electron app: + +```js title='test.js' +const webdriver = require('selenium-webdriver') +const driver = new webdriver.Builder() + // The "9515" is the port opened by ChromeDriver. + .usingServer('http://localhost:9515') + .withCapabilities({ + 'goog:chromeOptions': { + // Here is the path to your Electron binary. + binary: '/Path-to-Your-App.app/Contents/MacOS/Electron' + } + }) + .forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0 + .build() +driver.get('http://www.google.com') +driver.findElement(webdriver.By.name('q')).sendKeys('webdriver') +driver.findElement(webdriver.By.name('btnG')).click() +driver.wait(() => { + return driver.getTitle().then((title) => { + return title === 'webdriver - Google Search' + }) +}, 1000) +driver.quit() +``` + +## Using Playwright + +[Microsoft Playwright](https://playwright.dev) is an end-to-end testing framework built +using browser-specific remote debugging protocols, similar to the [Puppeteer] headless +Node.js API but geared towards end-to-end testing. Playwright has experimental Electron +support via Electron's support for the [Chrome DevTools Protocol] (CDP). + +### Install dependencies + +You can install Playwright through your preferred Node.js package manager. The Playwright team +recommends using the `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` environment variable to avoid +unnecessary browser downloads when testing an Electron app. + +```sh npm2yarn +PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --save-dev playwright +``` + +Playwright also comes with its own test runner, Playwright Test, which is built for end-to-end +testing. You can also install it as a dev dependency in your project: + +```sh npm2yarn +npm install --save-dev @playwright/test +``` + +:::caution Dependencies +This tutorial was written `playwright@1.16.3` and `@playwright/test@1.16.3`. Check out +[Playwright's releases][playwright-releases] page to learn about +changes that might affect the code below. +::: + +:::info Using third-party test runners +If you're interested in using an alternative test runner (e.g. Jest or Mocha), check out +Playwright's [Third-Party Test Runner][playwright-test-runners] guide. +::: + +### Write your tests + +Playwright launches your app in development mode through the `_electron.launch` API. +To point this API to your Electron app, you can pass the path to your main process +entry point (here, it is `main.js`). + +```js {5} +const { _electron: electron } = require('playwright') +const { test } = require('@playwright/test') + +test('launch app', async () => { + const electronApp = await electron.launch({ args: ['main.js'] }) + // close app + await electronApp.close() +}) +``` + +After that, you will access to an instance of Playwright's `ElectronApp` class. This +is a powerful class that has access to main process modules for example: + +```js {6-11} +const { _electron: electron } = require('playwright') +const { test } = require('@playwright/test') + +test('get isPackaged', async () => { + const electronApp = await electron.launch({ args: ['main.js'] }) + const isPackaged = await electronApp.evaluate(async ({ app }) => { + // This runs in Electron's main process, parameter here is always + // the result of the require('electron') in the main app script. + return app.isPackaged + }) + console.log(isPackaged) // false (because we're in development mode) + // close app + await electronApp.close() +}) +``` + +It can also create individual [Page][playwright-page] objects from Electron BrowserWindow instances. +For example, to grab the first BrowserWindow and save a screenshot: + +```js {6-7} +const { _electron: electron } = require('playwright') +const { test } = require('@playwright/test') + +test('save screenshot', async () => { + const electronApp = await electron.launch({ args: ['main.js'] }) + const window = await electronApp.firstWindow() + await window.screenshot({ path: 'intro.png' }) + // close app + await electronApp.close() +}) +``` + +Putting all this together using the PlayWright Test runner, let's create a `example.spec.js` +test file with a single test and assertion: + +```js title='example.spec.js' +const { _electron: electron } = require('playwright') +const { test, expect } = require('@playwright/test') + +test('example test', async () => { + const electronApp = await electron.launch({ args: ['.'] }) + const isPackaged = await electronApp.evaluate(async ({ app }) => { + // This runs in Electron's main process, parameter here is always + // the result of the require('electron') in the main app script. + return app.isPackaged; + }); + + expect(isPackaged).toBe(false); + + // Wait for the first BrowserWindow to open + // and return its Page object + const window = await electronApp.firstWindow() + await window.screenshot({ path: 'intro.png' }) + + // close app + await electronApp.close() +}); +``` + +Then, run Playwright Test using `npx playwright test`. You should see the test pass in your +console, and have an `intro.png` screenshot on your filesystem. + +```console +☁ $ npx playwright test + +Running 1 test using 1 worker + + ✓ example.spec.js:4:1 › example test (1s) +``` + +:::info +Playwright Test will automatically run any files matching the `.*(test|spec)\.(js|ts|mjs)` regex. +You can customize this match in the [Playwright Test configuration options][playwright-test-config]. +::: + +:::tip Further reading +Check out Playwright's documentation for the full [Electron][playwright-electron] +and [ElectronApplication][playwright-electronapplication] class APIs. +::: + +## Using a custom test driver + +It's also possible to write your own custom driver using Node.js' built-in IPC-over-STDIO. +Custom test drivers require you to write additional app code, but have lower overhead and let you +expose custom methods to your test suite. + +To create a custom driver, we'll use Node.js' [`child_process`](https://nodejs.org/api/child_process.html) API. +The test suite will spawn the Electron process, then establish a simple messaging protocol: + +```js title='testDriver.js' +const childProcess = require('child_process') +const electronPath = require('electron') + +// spawn the process +const env = { /* ... */ } +const stdio = ['inherit', 'inherit', 'inherit', 'ipc'] +const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env }) + +// listen for IPC messages from the app +appProcess.on('message', (msg) => { + // ... +}) + +// send an IPC message to the app +appProcess.send({ my: 'message' }) +``` + +From within the Electron app, you can listen for messages and send replies using the Node.js +[`process`](https://nodejs.org/api/process.html) API: + +```js title='main.js' +// listen for messages from the test suite +process.on('message', (msg) => { + // ... +}) + +// send a message to the test suite +process.send({ my: 'message' }) +``` + +We can now communicate from the test suite to the Electron app using the `appProcess` object. + +For convenience, you may want to wrap `appProcess` in a driver object that provides more +high-level functions. Here is an example of how you can do this. Let's start by creating +a `TestDriver` class: + +```js title='testDriver.js' +class TestDriver { + constructor ({ path, args, env }) { + this.rpcCalls = [] + + // start child process + env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages + this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env }) + + // handle rpc responses + this.process.on('message', (message) => { + // pop the handler + const rpcCall = this.rpcCalls[message.msgId] + if (!rpcCall) return + this.rpcCalls[message.msgId] = null + // reject/resolve + if (message.reject) rpcCall.reject(message.reject) + else rpcCall.resolve(message.resolve) + }) + + // wait for ready + this.isReady = this.rpc('isReady').catch((err) => { + console.error('Application failed to start', err) + this.stop() + process.exit(1) + }) + } + + // simple RPC call + // to use: driver.rpc('method', 1, 2, 3).then(...) + async rpc (cmd, ...args) { + // send rpc request + const msgId = this.rpcCalls.length + this.process.send({ msgId, cmd, args }) + return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject })) + } + + stop () { + this.process.kill() + } +} + +module.exports = { TestDriver }; +``` + +In your app code, can then write a simple handler to receive RPC calls: + +```js title='main.js' +const METHODS = { + isReady () { + // do any setup needed + return true + } + // define your RPC-able methods here +} + +const onMessage = async ({ msgId, cmd, args }) => { + let method = METHODS[cmd] + if (!method) method = () => new Error('Invalid method: ' + cmd) + try { + const resolve = await method(...args) + process.send({ msgId, resolve }) + } catch (err) { + const reject = { + message: err.message, + stack: err.stack, + name: err.name + } + process.send({ msgId, reject }) + } +} + +if (process.env.APP_TEST_DRIVER) { + process.on('message', onMessage) +} +``` + +Then, in your test suite, you can use your `TestDriver` class with the test automation +framework of your choosing. The following example uses +[`ava`](https://www.npmjs.com/package/ava), but other popular choices like Jest +or Mocha would work as well: + +```js title='test.js' +const test = require('ava') +const electronPath = require('electron') +const { TestDriver } = require('./testDriver') + +const app = new TestDriver({ + path: electronPath, + args: ['./app'], + env: { + NODE_ENV: 'test' + } +}) +test.before(async t => { + await app.isReady +}) +test.after.always('cleanup', async t => { + await app.stop() +}) +``` + +[chrome-driver]: https://sites.google.com/chromium.org/driver/ +[Puppeteer]: https://github.com/puppeteer/puppeteer +[playwright-electron]: https://playwright.dev/docs/api/class-electron/ +[playwright-electronapplication]: https://playwright.dev/docs/api/class-electronapplication +[playwright-page]: https://playwright.dev/docs/api/class-page +[playwright-releases]: https://github.com/microsoft/playwright/releases +[playwright-test-config]: https://playwright.dev/docs/api/class-testconfig#test-config-test-match +[playwright-test-runners]: https://playwright.dev/docs/test-runners/ +[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/ diff --git a/docs/tutorial/boilerplates-and-clis.md b/docs/tutorial/boilerplates-and-clis.md index d0ac313ceb44a..304254285180e 100644 --- a/docs/tutorial/boilerplates-and-clis.md +++ b/docs/tutorial/boilerplates-and-clis.md @@ -31,9 +31,8 @@ unifies the existing (and well maintained) build tools for Electron development into a cohesive package so that anyone can jump right in to Electron development. -Forge comes with [ready-to-use templates](https://electronforge.io/templates) for popular -frameworks like React, Vue, or Angular. It uses the same core modules used by the -greater Electron community (like [`electron-packager`](https://github.com/electron/electron-packager)) –  +Forge comes with [a ready-to-use template](https://electronforge.io/templates) using Webpack as a bundler. It includes an example typescript configuration and provides two configuration files to enable easy customization. It uses the same core modules used by the +greater Electron community (like [`electron-packager`](https://github.com/electron/electron-packager)) – changes made by Electron maintainers (like Slack) benefit Forge's users, too. You can find more information and documentation on [electronforge.io](https://electronforge.io/). diff --git a/docs/tutorial/code-signing.md b/docs/tutorial/code-signing.md index 1c2a73bfdd670..a035b480cd57e 100644 --- a/docs/tutorial/code-signing.md +++ b/docs/tutorial/code-signing.md @@ -1,77 +1,332 @@ -# Code Signing +--- +title: 'Code Signing' +description: 'Code signing is a security technology that you use to certify that an app was created by you.' +slug: code-signing +hide_title: false +--- Code signing is a security technology that you use to certify that an app was -created by you. +created by you. You should sign your application so it does not trigger any +operating system security checks. -On macOS the system can detect any change to the app, whether the change is +On macOS, the system can detect any change to the app, whether the change is introduced accidentally or by malicious code. -On Windows the system assigns a trust level to your code signing certificate which -if you don't have, or if your trust level is low will cause security dialogs to -appear when users start using your application. Trust level builds over time -so it's better to start code signing as early as possible. +On Windows, the system assigns a trust level to your code signing certificate +which if you don't have, or if your trust level is low, will cause security +dialogs to appear when users start using your application. Trust level builds +over time so it's better to start code signing as early as possible. -While it is possible to distribute unsigned apps, it is not recommended. -For example, here's what macOS users see when attempting to start an unsigned app: +While it is possible to distribute unsigned apps, it is not recommended. Both +Windows and macOS will, by default, prevent either the download or the execution +of unsigned applications. Starting with macOS Catalina (version 10.15), users +have to go through multiple manual steps to open unsigned applications. -![unsigned app warning on macOS](https://user-images.githubusercontent.com/2289/39488937-bdc854ba-4d38-11e8-88f8-7b3c125baefc.png) +![macOS Catalina Gatekeeper warning: The app cannot be opened because the developer cannot be verified](../images/gatekeeper.png) -> App can't be opened because it is from an unidentified developer +As you can see, users get two options: Move the app straight to the trash or +cancel running it. You don't want your users to see that dialog. If you are building an Electron app that you intend to package and distribute, -it should be code signed. The Mac and Windows app stores do not allow unsigned -apps. +it should be code signed. -# Signing macOS builds +## Signing & notarizing macOS builds -Before signing macOS builds, you must do the following: +Properly preparing macOS applications for release requires two steps. First, the +app needs to be code signed. Then, the app needs to be uploaded to Apple for a +process called **notarization**, where automated systems will further verify that +your app isn't doing anything to endanger its users. + +To start the process, ensure that you fulfill the requirements for signing and +notarizing your app: 1. Enroll in the [Apple Developer Program] (requires an annual fee) -2. Download and install [Xcode] +2. Download and install [Xcode] - this requires a computer running macOS 3. Generate, download, and install [signing certificates] -There are a number of tools for signing your packaged app: +Electron's ecosystem favors configuration and freedom, so there are multiple +ways to get your application signed and notarized. + +### Using Electron Forge + +If you're using Electron's favorite build tool, getting your application signed +and notarized requires a few additions to your configuration. [Forge](https://electronforge.io) is a +collection of the official Electron tools, using [`electron-packager`], +[`electron-osx-sign`], and [`electron-notarize`] under the hood. + +Let's take a look at an example `package.json` configuration with all required fields. Not all of them are +required: the tools will be clever enough to automatically find a suitable `identity`, for instance, +but we recommend that you are explicit. + +```json title="package.json" {7} +{ + "name": "my-app", + "version": "0.0.1", + "config": { + "forge": { + "packagerConfig": { + "osxSign": { + "identity": "Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)", + "hardened-runtime": true, + "entitlements": "entitlements.plist", + "entitlements-inherit": "entitlements.plist", + "signature-flags": "library" + }, + "osxNotarize": { + "appleId": "felix@felix.fun", + "appleIdPassword": "my-apple-id-password" + } + } + } + } +} +``` + +The `entitlements.plist` file referenced here needs the following macOS-specific entitlements +to assure the Apple security mechanisms that your app is doing these things +without meaning any harm: + +```xml title="entitlements.plist" + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.debugger + + + +``` + +Note that up until Electron 12, the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement was required +as well. However, it should not be used anymore if it can be avoided. + +To see all of this in action, check out Electron Fiddle's source code, +[especially its `electron-forge` configuration +file](https://github.com/electron/fiddle/blob/master/forge.config.js). + +If you plan to access the microphone or camera within your app using Electron's APIs, you'll also +need to add the following entitlements: + +```xml title="entitlements.plist" +com.apple.security.device.audio-input + +com.apple.security.device.camera + +``` + +If these are not present in your app's entitlements when you invoke, for example: + +```js title="main.js" +const { systemPreferences } = require('electron') +const microphone = systemPreferences.askForMediaAccess('microphone') +``` + +Your app may crash. See the Resource Access section in [Hardened Runtime](https://developer.apple.com/documentation/security/hardened_runtime) for more information and entitlements you may need. + +### Using Electron Builder + +Electron Builder comes with a custom solution for signing your application. You +can find [its documentation here](https://www.electron.build/code-signing). + +### Using Electron Packager + +If you're not using an integrated build pipeline like Forge or Builder, you +are likely using [`electron-packager`], which includes [`electron-osx-sign`] and +[`electron-notarize`]. + +If you're using Packager's API, you can pass [in configuration that both signs +and notarizes your application](https://electron.github.io/electron-packager/main/interfaces/electronpackager.options.html). + +```js +const packager = require('electron-packager') + +packager({ + dir: '/path/to/my/app', + osxSign: { + identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)', + 'hardened-runtime': true, + entitlements: 'entitlements.plist', + 'entitlements-inherit': 'entitlements.plist', + 'signature-flags': 'library' + }, + osxNotarize: { + appleId: 'felix@felix.fun', + appleIdPassword: 'my-apple-id-password' + } +}) +``` -- [`electron-osx-sign`] is a standalone tool for signing macOS packages. -- [`electron-packager`] bundles `electron-osx-sign`. If you're using `electron-packager`, -pass the `--osx-sign=true` flag to sign your build. - - [`electron-forge`] uses `electron-packager` internally, you can set the `osxSign` option - in your forge config. -- [`electron-builder`] has built-in code-signing capabilities. See [electron.build/code-signing](https://www.electron.build/code-signing) +The `entitlements.plist` file referenced here needs the following macOS-specific entitlements +to assure the Apple security mechanisms that your app is doing these things +without meaning any harm: -For more info, see the [Mac App Store Submission Guide]. +```xml title="entitlements.plist" + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.debugger + + + +``` -# Signing Windows builds +Up until Electron 12, the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement was required +as well. However, it should not be used anymore if it can be avoided. + +### Signing Mac App Store applications + +See the [Mac App Store Guide]. + +## Signing Windows builds Before signing Windows builds, you must do the following: 1. Get a Windows Authenticode code signing certificate (requires an annual fee) -2. Install Visual Studio 2015/2017 (to get the signing utility) +2. Install Visual Studio to get the signing utility (the free [Community + Edition](https://visualstudio.microsoft.com/vs/community/) is enough) + +You can get a code signing certificate from a lot of resellers. Prices vary, so +it may be worth your time to shop around. Popular resellers include: + +- [digicert](https://www.digicert.com/code-signing/microsoft-authenticode.htm) +- [Sectigo](https://sectigo.com/ssl-certificates-tls/code-signing) +- Amongst others, please shop around to find one that suits your needs! 😄 + +:::caution Keep your certificate password private +Your certificate password should be a **secret**. Do not share it publicly or +commit it to your source code. +::: + +### Using Electron Forge + +Once you have a code signing certificate file (`.pfx`), you can sign +[Squirrel.Windows][maker-squirrel] and [MSI][maker-msi] installers in Electron Forge +with the `certificateFile` and `certificatePassword` fields in their respective +configuration objects. + +For example, if you keep your Forge config in your `package.json` file and are +creating a Squirrel.Windows installer: + +```json {9-15} title='package.json' +{ + "name": "my-app", + "version": "0.0.1", + //... + "config": { + "forge": { + "packagerConfig": {}, + "makers": [ + { + "name": "@electron-forge/maker-squirrel", + "config": { + "certificateFile": "./cert.pfx", + "certificatePassword": "this-is-a-secret" + } + } + ] + } + } + //... +} +``` + +### Using electron-winstaller (Squirrel.Windows) + +[`electron-winstaller`] is a package that can generate Squirrel.Windows installers for your +Electron app. This is the tool used under the hood by Electron Forge's +[Squirrel.Windows Maker][maker-squirrel]. If you're not using Electron Forge and want to use +`electron-winstaller` directly, use the `certificateFile` and `certificatePassword` configuration +options when creating your installer. + +```js {10-11} +const electronInstaller = require('electron-winstaller') +// NB: Use this syntax within an async function, Node does not have support for +// top-level await as of Node 12. +try { + await electronInstaller.createWindowsInstaller({ + appDirectory: '/tmp/build/my-app-64', + outputDirectory: '/tmp/build/installer64', + authors: 'My App Inc.', + exe: 'myapp.exe', + certificateFile: './cert.pfx', + certificatePassword: 'this-is-a-secret', + }) + console.log('It worked!') +} catch (e) { + console.log(`No dice: ${e.message}`) +} +``` + +For full configuration options, check out the [`electron-winstaller`] repository! + +### Using electron-wix-msi (WiX MSI) + +[`electron-wix-msi`] is a package that can generate MSI installers for your +Electron app. This is the tool used under the hood by Electron Forge's [MSI Maker][maker-msi]. + +If you're not using Electron Forge and want to use `electron-wix-msi` directly, use the +`certificateFile` and `certificatePassword` configuration options +or pass in parameters directly to [SignTool.exe] with the `signWithParams` option. + +```js {12-13} +import { MSICreator } from 'electron-wix-msi' + +// Step 1: Instantiate the MSICreator +const msiCreator = new MSICreator({ + appDirectory: '/path/to/built/app', + description: 'My amazing Kitten simulator', + exe: 'kittens', + name: 'Kittens', + manufacturer: 'Kitten Technologies', + version: '1.1.2', + outputDirectory: '/path/to/output/folder', + certificateFile: './cert.pfx', + certificatePassword: 'this-is-a-secret', +}) + +// Step 2: Create a .wxs template file +const supportBinaries = await msiCreator.create() + +// 🆕 Step 2a: optionally sign support binaries if you +// sign you binaries as part of of your packaging script +supportBinaries.forEach(async (binary) => { + // Binaries are the new stub executable and optionally + // the Squirrel auto updater. + await signFile(binary) +}) -You can get a code signing certificate from a lot of resellers. Prices vary, so it may be worth your time to shop around. Popular resellers include: +// Step 3: Compile the template to a .msi file +await msiCreator.compile() +``` -* [digicert](https://www.digicert.com/code-signing/microsoft-authenticode.htm) -* [Comodo](https://www.comodo.com/landing/ssl-certificate/authenticode-signature/) -* [GoDaddy](https://au.godaddy.com/web-security/code-signing-certificate) -* Amongst others, please shop around to find one that suits your needs, Google is your friend :) +For full configuration options, check out the [`electron-wix-msi`] repository! -There are a number of tools for signing your packaged app: +### Using Electron Builder -- [`electron-winstaller`] will generate an installer for windows and sign it for you -- [`electron-forge`] can sign installers it generates through the Squirrel.Windows or MSI targets. -- [`electron-builder`] can sign some of its windows targets +Electron Builder comes with a custom solution for signing your application. You +can find [its documentation here](https://www.electron.build/code-signing). -## Windows Store +### Signing Windows Store applications See the [Windows Store Guide]. -[Apple Developer Program]: https://developer.apple.com/programs/ +[apple developer program]: https://developer.apple.com/programs/ [`electron-builder`]: https://github.com/electron-userland/electron-builder [`electron-forge`]: https://github.com/electron-userland/electron-forge [`electron-osx-sign`]: https://github.com/electron-userland/electron-osx-sign [`electron-packager`]: https://github.com/electron/electron-packager +[`electron-notarize`]: https://github.com/electron/electron-notarize [`electron-winstaller`]: https://github.com/electron/windows-installer -[Xcode]: https://developer.apple.com/xcode -[signing certificates]: https://github.com/electron-userland/electron-osx-sign/wiki/1.-Getting-Started#certificates -[Mac App Store Submission Guide]: mac-app-store-submission-guide.md -[Windows Store Guide]: windows-store-guide.md +[`electron-wix-msi`]: https://github.com/felixrieseberg/electron-wix-msi +[xcode]: https://developer.apple.com/xcode +[signing certificates]: https://github.com/electron/electron-osx-sign/wiki/1.-Getting-Started#certificates +[mac app store guide]: ./mac-app-store-submission-guide.md +[windows store guide]: ./windows-store-guide.md +[maker-squirrel]: https://www.electronforge.io/config/makers/squirrel.windows +[maker-msi]: https://www.electronforge.io/config/makers/wix-msi +[signtool.exe]: https://docs.microsoft.com/en-us/dotnet/framework/tools/signtool-exe diff --git a/docs/tutorial/context-isolation.md b/docs/tutorial/context-isolation.md new file mode 100644 index 0000000000000..140d9d75726bc --- /dev/null +++ b/docs/tutorial/context-isolation.md @@ -0,0 +1,105 @@ +# Context Isolation + +## What is it? + +Context Isolation is a feature that ensures that both your `preload` scripts and Electron's internal logic run in a separate context to the website you load in a [`webContents`](../api/web-contents.md). This is important for security purposes as it helps prevent the website from accessing Electron internals or the powerful APIs your preload script has access to. + +This means that the `window` object that your preload script has access to is actually a **different** object than the website would have access to. For example, if you set `window.hello = 'wave'` in your preload script and context isolation is enabled, `window.hello` will be undefined if the website tries to access it. + +Context isolation has been enabled by default since Electron 12, and it is a recommended security setting for _all applications_. + +## Migration + +> Without context isolation, I used to provide APIs from my preload script using `window.X = apiObject`. Now what? + +### Before: context isolation disabled + +Exposing APIs from your preload script to a loaded website in the renderer process is a common use-case. With context isolation disabled, your preload script would share a common global `window` object with the renderer. You could then attach arbitrary properties to a preload script: + +```javascript title='preload.js' +// preload with contextIsolation disabled +window.myAPI = { + doAThing: () => {} +} +``` + +The `doAThing()` function could then be used directly in the renderer process: + +```javascript title='renderer.js' +// use the exposed API in the renderer +window.myAPI.doAThing() +``` + +### After: context isolation enabled + +There is a dedicated module in Electron to help you do this in a painless way. The [`contextBridge`](../api/context-bridge.md) module can be used to **safely** expose APIs from your preload script's isolated context to the context the website is running in. The API will also be accessible from the website on `window.myAPI` just like it was before. + +```javascript title='preload.js' +// preload with contextIsolation enabled +const { contextBridge } = require('electron') + +contextBridge.exposeInMainWorld('myAPI', { + doAThing: () => {} +}) +``` + +```javascript title='renderer.js' +// use the exposed API in the renderer +window.myAPI.doAThing() +``` + +Please read the `contextBridge` documentation linked above to fully understand its limitations. For instance, you can't send custom prototypes or symbols over the bridge. + +## Security considerations + +Just enabling `contextIsolation` and using `contextBridge` does not automatically mean that everything you do is safe. For instance, this code is **unsafe**. + +```javascript title='preload.js' +// ❌ Bad code +contextBridge.exposeInMainWorld('myAPI', { + send: ipcRenderer.send +}) +``` + +It directly exposes a powerful API without any kind of argument filtering. This would allow any website to send arbitrary IPC messages, which you do not want to be possible. The correct way to expose IPC-based APIs would instead be to provide one method per IPC message. + +```javascript title='preload.js' +// ✅ Good code +contextBridge.exposeInMainWorld('myAPI', { + loadPreferences: () => ipcRenderer.invoke('load-prefs') +}) +``` + +## Usage with TypeScript + +If you're building your Electron app with TypeScript, you'll want to add types to your APIs exposed over the context bridge. The renderer's `window` object won't have the correct typings unless you extend the types with a [declaration file]. + +For example, given this `preload.ts` script: + +```typescript title='preload.ts' +contextBridge.exposeInMainWorld('electronAPI', { + loadPreferences: () => ipcRenderer.invoke('load-prefs') +}) +``` + +You can create a `renderer.d.ts` declaration file and globally augment the `Window` interface: + +```typescript title='renderer.d.ts' +export interface IElectronAPI { + loadPreferences: () => Promise, +} + +declare global { + interface Window { + electronAPI: IElectronAPI + } +} +``` + +Doing so will ensure that the TypeScript compiler will know about the `electronAPI` property on your global `window` object when writing scripts in your renderer process: + +```typescript title='renderer.ts' +window.electronAPI.loadPreferences() +``` + +[declaration file]: https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html diff --git a/docs/tutorial/dark-mode.md b/docs/tutorial/dark-mode.md new file mode 100644 index 0000000000000..bf16303e4ecb1 --- /dev/null +++ b/docs/tutorial/dark-mode.md @@ -0,0 +1,205 @@ +# Dark Mode + +## Overview + +### Automatically update the native interfaces + +"Native interfaces" include the file picker, window border, dialogs, context +menus, and more - anything where the UI comes from your operating system and +not from your app. The default behavior is to opt into this automatic theming +from the OS. + +### Automatically update your own interfaces + +If your app has its own dark mode, you should toggle it on and off in sync with +the system's dark mode setting. You can do this by using the +[prefer-color-scheme] CSS media query. + +### Manually update your own interfaces + +If you want to manually switch between light/dark modes, you can do this by +setting the desired mode in the +[themeSource](../api/native-theme.md#nativethemethemesource) +property of the `nativeTheme` module. This property's value will be propagated +to your Renderer process. Any CSS rules related to `prefers-color-scheme` will +be updated accordingly. + +## macOS settings + +In macOS 10.14 Mojave, Apple introduced a new [system-wide dark mode][system-wide-dark-mode] +for all macOS computers. If your Electron app has a dark mode, you can make it +follow the system-wide dark mode setting using +[the `nativeTheme` api](../api/native-theme.md). + +In macOS 10.15 Catalina, Apple introduced a new "automatic" dark mode option +for all macOS computers. In order for the `nativeTheme.shouldUseDarkColors` and +`Tray` APIs to work correctly in this mode on Catalina, you need to use Electron +`>=7.0.0`, or set `NSRequiresAquaSystemAppearance` to `false` in your +`Info.plist` file for older versions. Both [Electron Packager][electron-packager] +and [Electron Forge][electron-forge] have a +[`darwinDarkModeSupport` option][packager-darwindarkmode-api] +to automate the `Info.plist` changes during app build time. + +If you wish to opt-out while using Electron > 8.0.0, you must +set the `NSRequiresAquaSystemAppearance` key in the `Info.plist` file to +`true`. Please note that Electron 8.0.0 and above will not let you opt-out +of this theming, due to the use of the macOS 10.14 SDK. + +## Example + +This example demonstrates an Electron application that derives its theme colors from the +`nativeTheme`. Additionally, it provides theme toggle and reset controls using IPC channels. + +```javascript fiddle='docs/fiddles/features/macos-dark-mode' + +``` + +### How does this work? + +Starting with the `index.html` file: + +```html title='index.html' + + + + + Hello World! + + + + +

Hello World!

+

Current theme source: System

+ + + + + + + + +``` + +And the `styles.css` file: + +```css title='styles.css' +@media (prefers-color-scheme: dark) { + body { background: #333; color: white; } +} + +@media (prefers-color-scheme: light) { + body { background: #ddd; color: black; } +} +``` + +The example renders an HTML page with a couple elements. The `` + element shows which theme is currently selected, and the two ` + + + +``` + +To make these elements interactive, we'll be adding a few lines of code in the imported +`renderer.js` file that leverages the `window.electronAPI` functionality exposed from the preload +script: + +```javascript title='renderer.js (Renderer Process)' +const setButton = document.getElementById('btn') +const titleInput = document.getElementById('title') +setButton.addEventListener('click', () => { + const title = titleInput.value + window.electronAPI.setTitle(title) +}); +``` + +At this point, your demo should be fully functional. Try using the input field and see what happens +to your BrowserWindow title! + +## Pattern 2: Renderer to main (two-way) + +A common application for two-way IPC is calling a main process module from your renderer process +code and waiting for a result. This can be done by using [`ipcRenderer.invoke`] paired with +[`ipcMain.handle`]. + +In the following example, we'll be opening a native file dialog from the renderer process and +returning the selected file's path. + +For this demo, you'll need to add code to your main process, your renderer process, and a preload +script. The full code is below, but we'll be explaining each file individually in the following +sections. + +```fiddle docs/fiddles/ipc/pattern-2 +``` + +### 1. Listen for events with `ipcMain.handle` + +In the main process, we'll be creating a `handleFileOpen()` function that calls +`dialog.showOpenDialog` and returns the value of the file path selected by the user. This function +is used as a callback whenever an `ipcRender.invoke` message is sent through the `dialog:openFile` +channel from the renderer process. The return value is then returned as a Promise to the original +`invoke` call. + +:::caution A word on error handling +Errors thrown through `handle` in the main process are not transparent as they +are serialized and only the `message` property from the original error is +provided to the renderer process. Please refer to +[#24427](https://github.com/electron/electron/issues/24427) for details. +::: + +```javascript {6-13,25} title='main.js (Main Process)' +const { BrowserWindow, dialog, ipcMain } = require('electron') +const path = require('path') + +//... + +async function handleFileOpen() { + const { canceled, filePaths } = await dialog.showOpenDialog() + if (canceled) { + return + } else { + return filePaths[0] + } +} + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + mainWindow.loadFile('index.html') +} + +app.whenReady(() => { + ipcMain.handle('dialog:openFile', handleFileOpen) + createWindow() +}) +//... +``` + +:::tip on channel names +The `dialog:` prefix on the IPC channel name has no effect on the code. It only serves +as a namespace that helps with code readability. +::: + +:::info +Make sure you're loading the `index.html` and `preload.js` entry points for the following steps! +::: + +### 2. Expose `ipcRenderer.invoke` via preload + +In the preload script, we expose a one-line `openFile` function that calls and returns the value of +`ipcRenderer.invoke('dialog:openFile')`. We'll be using this API in the next step to call the +native dialog from our renderer's user interface. + +```javascript title='preload.js (Preload Script)' +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + openFile: () => ipcRenderer.invoke('dialog:openFile') +}) +``` + +:::caution Security warning +We don't directly expose the whole `ipcRenderer.invoke` API for [security reasons]. Make sure to +limit the renderer's access to Electron APIs as much as possible. +::: + +### 3. Build the renderer process UI + +Finally, let's build the HTML file that we load into our BrowserWindow. + +```html {10-11} title='index.html' + + + + + + + Dialog + + + + File path: + + + +``` + +The UI consists of a single `#btn` button element that will be used to trigger our preload API, and +a `#filePath` element that will be used to display the path of the selected file. Making these +pieces work will take a few lines of code in the renderer process script: + +```javascript title='renderer.js (Renderer Process)' +const btn = document.getElementById('btn') +const filePathElement = document.getElementById('filePath') + +btn.addEventListener('click', async () => { + const filePath = await window.electronAPI.openFile() + filePathElement.innerText = filePath +}) +``` + +In the above snippet, we listen for clicks on the `#btn` button, and call our +`window.electronAPI.openFile()` API to activate the native Open File dialog. We then display the +selected file path in the `#filePath` element. + +### Note: legacy approaches + +The `ipcRenderer.invoke` API was added in Electron 7 as a developer-friendly way to tackle two-way +IPC from the renderer process. However, there exist a couple alternative approaches to this IPC +pattern. + +:::warning Avoid legacy approaches if possible +We recommend using `ipcRenderer.invoke` whenever possible. The following two-way renderer-to-main +patterns are documented for historical purposes. +::: + +:::info +For the following examples, we're calling `ipcRenderer` directly from the preload script to keep +the code samples small. +::: + +#### Using `ipcRenderer.send` + +The `ipcRenderer.send` API that we used for single-way communication can also be leveraged to +perform two-way communication. This was the recommended way for asynchronous two-way communication +via IPC prior to Electron 7. + +```javascript title='preload.js (Preload Script)' +// You can also put expose this code to the renderer +// process with the `contextBridge` API +const { ipcRenderer } = require('electron') + +ipcRenderer.on('asynchronous-reply', (_event, arg) => { + console.log(arg) // prints "pong" in the DevTools console +}) +ipcRenderer.send('asynchronous-message', 'ping') +``` + +```javascript title='main.js (Main Process)' +ipcMain.on('asynchronous-message', (event, arg) => { + console.log(arg) // prints "ping" in the Node console + // works like `send`, but returning a message back + // to the renderer that sent the original message + event.reply('asynchronous-reply', 'pong') +}) +``` + +There are a couple downsides to this approach: + +* You need to set up a second `ipcRenderer.on` listener to handle the response in the renderer +process. With `invoke`, you get the response value returned as a Promise to the original API call. +* There's no obvious way to pair the `asynchronous-reply` message to the original +`asynchronous-message` one. If you have very frequent messages going back and forth through these +channels, you would need to add additional app code to track each call and response individually. + +#### Using `ipcRenderer.sendSync` + +The `ipcRenderer.sendSync` API sends a message to the main process and waits _synchronously_ for a +response. + +```javascript title='main.js (Main Process)' +const { ipcMain } = require('electron') +ipcMain.on('synchronous-message', (event, arg) => { + console.log(arg) // prints "ping" in the Node console + event.returnValue = 'pong' +}) +``` + +```javascript title='preload.js (Preload Script)' +// You can also put expose this code to the renderer +// process with the `contextBridge` API +const { ipcRenderer } = require('electron') + +const result = ipcRenderer.sendSync('synchronous-message', 'ping') +console.log(result) // prints "pong" in the DevTools console +``` + +The structure of this code is very similar to the `invoke` model, but we recommend +**avoiding this API** for performance reasons. Its synchronous nature means that it'll block the +renderer process until a reply is received. + +## Pattern 3: Main to renderer + +When sending a message from the main process to a renderer process, you need to specify which +renderer is receiving the message. Messages need to be sent to a renderer process +via its [`WebContents`] instance. This WebContents instance contains a [`send`][webcontents-send] method +that can be used in the same way as `ipcRenderer.send`. + +To demonstrate this pattern, we'll be building a number counter controlled by the native operating +system menu. + +For this demo, you'll need to add code to your main process, your renderer process, and a preload +script. The full code is below, but we'll be explaining each file individually in the following +sections. + +```fiddle docs/fiddles/ipc/pattern-3 +``` + +### 1. Send messages with the `webContents` module + +For this demo, we'll need to first build a custom menu in the main process using Electron's `Menu` +module that uses the `webContents.send` API to send an IPC message from the main process to the +target renderer. + +```javascript {11-26} title='main.js (Main Process)' +const {app, BrowserWindow, Menu, ipcMain} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + const menu = Menu.buildFromTemplate([ + { + label: app.name, + submenu: [ + { + click: () => mainWindow.webContents.send('update-counter', 1), + label: 'Increment', + }, + { + click: () => mainWindow.webContents.send('update-counter', -1), + label: 'Decrement', + } + ] + } + ]) + Menu.setApplicationMenu(menu) + + mainWindow.loadFile('index.html') +} +//... + +``` + +For the purposes of the tutorial, it's important to note that the `click` handler +sends a message (either `1` or `-1`) to the renderer process through the `update-counter` channel. + +```javascript +click: () => mainWindow.webContents.send('update-counter', -1) +``` + +:::info +Make sure you're loading the `index.html` and `preload.js` entry points for the following steps! +::: + +### 2. Expose `ipcRenderer.on` via preload + +Like in the previous renderer-to-main example, we use the `contextBridge` and `ipcRenderer` +modules in the preload script to expose IPC functionality to the renderer process: + +```javascript title='preload.js (Preload Script)' +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback) +}) +``` + +After loading the preload script, your renderer process should have access to the +`window.electronAPI.onUpdateCounter()` listener function. + +:::caution Security warning +We don't directly expose the whole `ipcRenderer.on` API for [security reasons]. Make sure to +limit the renderer's access to Electron APIs as much as possible. +::: + +:::info +In the case of this minimal example, you can call `ipcRenderer.on` directly in the preload script +rather than exposing it over the context bridge. + +```javascript title='preload.js (Preload Script)' +const { ipcRenderer } = require('electron') + +window.addEventListener('DOMContentLoaded', () => { + const counter = document.getElementById('counter') + ipcRenderer.on('update-counter', (_event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue + }) +}) +``` + +However, this approach has limited flexibility compared to exposing your preload APIs +over the context bridge, since your listener can't directly interact with your renderer code. +::: + +### 3. Build the renderer process UI + +To tie it all together, we'll create an interface in the loaded HTML file that contains a +`#counter` element that we'll use to display the values: + +```html {10} title='index.html' + + + + + + + Menu Counter + + + Current value: 0 + + + +``` + +Finally, to make the values update in the HTML document, we'll add a few lines of DOM manipulation +so that the value of the `#counter` element is updated whenever we fire an `update-counter` event. + +```javascript title='renderer.js (Renderer Process)' +const counter = document.getElementById('counter') + +window.electronAPI.onUpdateCounter((_event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue +}) +``` + +In the above code, we're passing in a callback to the `window.electronAPI.onUpdateCounter` function +exposed from our preload script. The second `value` parameter corresponds to the `1` or `-1` we +were passing in from the `webContents.send` call from the native menu. + +### Optional: returning a reply + +There's no equivalent for `ipcRenderer.invoke` for main-to-renderer IPC. Instead, you can +send a reply back to the main process from within the `ipcRenderer.on` callback. + +We can demonstrate this with slight modifications to the code from the previous example. In the +renderer process, use the `event` parameter to send a reply back to the main process through the +`counter-value` channel. + +```javascript title='renderer.js (Renderer Process)' +const counter = document.getElementById('counter') + +window.electronAPI.onUpdateCounter((event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue + event.sender.send('counter-value', newValue) +}) +``` + +In the main process, listen for `counter-value` events and handle them appropriately. + +```javascript title='main.js (Main Process)' +//... +ipcMain.on('counter-value', (_event, value) => { + console.log(value) // will print value to Node console +}) +//... +``` + +## Pattern 4: Renderer to renderer + +There's no direct way to send messages between renderer processes in Electron using the `ipcMain` +and `ipcRenderer` modules. To achieve this, you have two options: + +* Use the main process as a message broker between renderers. This would involve sending a message +from one renderer to the main process, which would forward the message to the other renderer. +* Pass a [MessagePort] from the main process to both renderers. This will allow direct communication +between renderers after the initial setup. + +## Object serialization + +Electron's IPC implementation uses the HTML standard +[Structured Clone Algorithm][sca] to serialize objects passed between processes, meaning that +only certain types of objects can be passed through IPC channels. + +In particular, DOM objects (e.g. `Element`, `Location` and `DOMMatrix`), Node.js objects +backed by C++ classes (e.g. `process.env`, some members of `Stream`), and Electron objects +backed by C++ classes (e.g. `WebContents`, `BrowserWindow` and `WebFrame`) are not serializable +with Structured Clone. + +[context isolation tutorial]: context-isolation.md +[security reasons]: ./context-isolation.md#security-considerations +[`ipcMain`]: ../api/ipc-main.md +[`ipcMain.handle`]: ../api/ipc-main.md#ipcmainhandlechannel-listener +[`ipcMain.on`]: ../api/ipc-main.md +[IpcMainEvent]: ../api/structures/ipc-main-event.md +[`ipcRenderer`]: ../api/ipc-renderer.md +[`ipcRenderer.invoke`]: ../api/ipc-renderer.md#ipcrendererinvokechannel-args +[`ipcRenderer.send`]: ../api/ipc-renderer.md +[MessagePort]: ./message-ports.md +[preload script]: process-model.md#preload-scripts +[process model docs]: process-model.md +[sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[`WebContents`]: ../api/web-contents.md +[webcontents-send]: ../api/web-contents.md#contentssendchannel-args diff --git a/docs/tutorial/keyboard-shortcuts.md b/docs/tutorial/keyboard-shortcuts.md index cc3736a7df097..30b9429e83cdb 100644 --- a/docs/tutorial/keyboard-shortcuts.md +++ b/docs/tutorial/keyboard-shortcuts.md @@ -1,62 +1,135 @@ # Keyboard Shortcuts -> Configure local and global keyboard shortcuts +## Overview -## Local Shortcuts +This feature allows you to configure local and global keyboard shortcuts +for your Electron application. -You can use the [Menu] module to configure keyboard shortcuts that will -be triggered only when the app is focused. To do so, specify an -[`accelerator`] property when creating a [MenuItem]. +## Example -```js +### Local Shortcuts + +Local keyboard shortcuts are triggered only when the application is focused. +To configure a local keyboard shortcut, you need to specify an [`accelerator`] +property when creating a [MenuItem] within the [Menu] module. + +Starting with a working application from the +[Quick Start Guide](quick-start.md), update the `main.js` file with the +following lines: + +```javascript fiddle='docs/fiddles/features/keyboard-shortcuts/local' const { Menu, MenuItem } = require('electron') -const menu = new Menu() +const menu = new Menu() menu.append(new MenuItem({ - label: 'Print', - accelerator: 'CmdOrCtrl+P', - click: () => { console.log('time to print stuff') } + label: 'Electron', + submenu: [{ + role: 'help', + accelerator: process.platform === 'darwin' ? 'Alt+Cmd+I' : 'Alt+Shift+I', + click: () => { console.log('Electron rocks!') } + }] })) + +Menu.setApplicationMenu(menu) ``` -You can configure different key combinations based on the user's operating system. +> NOTE: In the code above, you can see that the accelerator differs based on the +user's operating system. For MacOS, it is `Alt+Cmd+I`, whereas for Linux and +Windows, it is `Alt+Shift+I`. -```js -{ - accelerator: process.platform === 'darwin' ? 'Alt+Cmd+I' : 'Ctrl+Shift+I' -} -``` +After launching the Electron application, you should see the application menu +along with the local shortcut you just defined: -## Global Shortcuts +![Menu with a local shortcut](../images/local-shortcut.png) -You can use the [globalShortcut] module to detect keyboard events even when -the application does not have keyboard focus. +If you click `Help` or press the defined accelerator and then open the terminal +that you ran your Electron application from, you will see the message that was +generated after triggering the `click` event: "Electron rocks!". -```js +### Global Shortcuts + +To configure a global keyboard shortcut, you need to use the [globalShortcut] +module to detect keyboard events even when the application does not have +keyboard focus. + +Starting with a working application from the +[Quick Start Guide](quick-start.md), update the `main.js` file with the +following lines: + +```javascript fiddle='docs/fiddles/features/keyboard-shortcuts/global' const { app, globalShortcut } = require('electron') -app.on('ready', () => { - globalShortcut.register('CommandOrControl+X', () => { - console.log('CommandOrControl+X is pressed') +app.whenReady().then(() => { + globalShortcut.register('Alt+CommandOrControl+I', () => { + console.log('Electron loves global shortcuts!') }) -}) +}).then(createWindow) ``` -## Shortcuts within a BrowserWindow +> NOTE: In the code above, the `CommandOrControl` combination uses `Command` +on macOS and `Control` on Windows/Linux. -If you want to handle keyboard shortcuts for a [BrowserWindow], you can use the `keyup` and `keydown` event listeners on the window object inside the renderer process. +After launching the Electron application, if you press the defined key +combination then open the terminal that you ran your Electron application from, +you will see that Electron loves global shortcuts! -```js -window.addEventListener('keyup', doSomething, true) +### Shortcuts within a BrowserWindow + +#### Using web APIs + +If you want to handle keyboard shortcuts within a [BrowserWindow], you can +listen for the `keyup` and `keydown` [DOM events][dom-events] inside the +renderer process using the [addEventListener() API][addEventListener-api]. + +```javascript fiddle='docs/fiddles/features/keyboard-shortcuts/web-apis|focus=renderer.js' +const handleKeyPress = (event) => { + // You can put code here to handle the keypress. + document.getElementById("last-keypress").innerText = event.key; + console.log(`You pressed ${event.key}`); +} + +window.addEventListener('keyup', handleKeyPress, true); ``` -Note the third parameter `true` which means the listener will always receive key presses before other listeners so they can't have `stopPropagation()` called on them. +> Note: the third parameter `true` indicates that the listener will always receive +key presses before other listeners so they can't have `stopPropagation()` +called on them. + +#### Intercepting events in the main process The [`before-input-event`](../api/web-contents.md#event-before-input-event) event is emitted before dispatching `keydown` and `keyup` events in the page. It can be used to catch and handle custom shortcuts that are not visible in the menu. -If you don't want to do manual shortcut parsing there are libraries that do advanced key detection such as [mousetrap]. +Starting with a working application from the +[Quick Start Guide](quick-start.md), update the `main.js` file with the +following lines: + +```javascript fiddle='docs/fiddles/features/keyboard-shortcuts/interception-from-main' +const { app, BrowserWindow } = require('electron') + +app.whenReady().then(() => { + const win = new BrowserWindow({ width: 800, height: 600 }) + + win.loadFile('index.html') + win.webContents.on('before-input-event', (event, input) => { + if (input.control && input.key.toLowerCase() === 'i') { + console.log('Pressed Control+I') + event.preventDefault() + } + }) +}) +``` + +After launching the Electron application, if you open the terminal that you ran +your Electron application from and press `Ctrl+I` key combination, you will +see that this key combination was successfully intercepted. + +#### Using third-party libraries + +If you don't want to do manual shortcut parsing, there are libraries that do +advanced key detection, such as [mousetrap]. Below are examples of usage of the +`mousetrap` running in the Renderer process: ```js Mousetrap.bind('4', () => { console.log('4') }) @@ -90,3 +163,5 @@ Mousetrap.bind('up up down down left right left right b a enter', () => { [`accelerator`]: ../api/accelerator.md [BrowserWindow]: ../api/browser-window.md [mousetrap]: https://github.com/ccampbell/mousetrap +[dom-events]: https://developer.mozilla.org/en-US/docs/Web/Events +[addEventListener-api]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener diff --git a/docs/tutorial/launch-app-from-url-in-another-app.md b/docs/tutorial/launch-app-from-url-in-another-app.md new file mode 100644 index 0000000000000..d99c1f9503342 --- /dev/null +++ b/docs/tutorial/launch-app-from-url-in-another-app.md @@ -0,0 +1,214 @@ +--- +title: Deep Links +description: Set your Electron app as the default handler for a specific protocol. +slug: launch-app-from-url-in-another-app +hide_title: true +--- + +# Deep Links + +## Overview + + + +This guide will take you through the process of setting your Electron app as the default +handler for a specific [protocol](https://www.electronjs.org/docs/api/protocol). + +By the end of this tutorial, we will have set our app to intercept and handle +any clicked URLs that start with a specific protocol. In this guide, the protocol +we will use will be "`electron-fiddle://`". + +## Examples + +### Main Process (main.js) + +First, we will import the required modules from `electron`. These modules help +control our application lifecycle and create a native browser window. + +```javascript +const { app, BrowserWindow, shell } = require('electron') +const path = require('path') +``` + +Next, we will proceed to register our application to handle all "`electron-fiddle://`" protocols. + +```javascript +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient('electron-fiddle', process.execPath, [path.resolve(process.argv[1])]) + } +} else { + app.setAsDefaultProtocolClient('electron-fiddle') +} +``` + +We will now define the function in charge of creating our browser window and load our application's `index.html` file. + +```javascript +const createWindow = () => { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + mainWindow.loadFile('index.html') +} +``` + +In this next step, we will create our `BrowserWindow` and tell our application how to handle an event in which an external protocol is clicked. + +This code will be different in Windows compared to MacOS and Linux. This is due to Windows requiring additional code in order to open the contents of the protocol link within the same Electron instance. Read more about this [here](https://www.electronjs.org/docs/api/app#apprequestsingleinstancelock). + +#### Windows code: + +```javascript +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + app.quit() +} else { + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + }) + + // Create mainWindow, load the rest of the app, etc... + app.whenReady().then(() => { + createWindow() + }) + + // Handle the protocol. In this case, we choose to show an Error Box. + app.on('open-url', (event, url) => { + dialog.showErrorBox('Welcome Back', `You arrived from: ${url}`) + }) +} +``` + +#### MacOS and Linux code: + +```javascript +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + createWindow() +}) + +// Handle the protocol. In this case, we choose to show an Error Box. +app.on('open-url', (event, url) => { + dialog.showErrorBox('Welcome Back', `You arrived from: ${url}`) +}) +``` + +Finally, we will add some additional code to handle when someone closes our application. + +```javascript +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) +``` + +## Important notes + +### Packaging + +On macOS and Linux, this feature will only work when your app is packaged. It will not work when +you're launching it in development from the command-line. When you package your app you'll need to +make sure the macOS `Info.plist` and the Linux `.desktop` files for the app are updated to include +the new protocol handler. Some of the Electron tools for bundling and distributing apps handle +this for you. + +#### [Electron Forge](https://electronforge.io) + +If you're using Electron Forge, adjust `packagerConfig` for macOS support, and the configuration for +the appropriate Linux makers for Linux support, in your [Forge +configuration](https://www.electronforge.io/configuration) _(please note the following example only +shows the bare minimum needed to add the configuration changes)_: + +```json +{ + "config": { + "forge": { + "packagerConfig": { + "protocols": [ + { + "name": "Electron Fiddle", + "schemes": ["electron-fiddle"] + } + ] + }, + "makers": [ + { + "name": "@electron-forge/maker-deb", + "config": { + "mimeType": ["x-scheme-handler/electron-fiddle"] + } + } + ] + } + } +} +``` + +#### [Electron Packager](https://github.com/electron/electron-packager) + +For macOS support: + +If you're using Electron Packager's API, adding support for protocol handlers is similar to how +Electron Forge is handled, except +`protocols` is part of the Packager options passed to the `packager` function. + +```javascript +const packager = require('electron-packager') + +packager({ + // ...other options... + protocols: [ + { + name: 'Electron Fiddle', + schemes: ['electron-fiddle'] + } + ] + +}).then(paths => console.log(`SUCCESS: Created ${paths.join(', ')}`)) + .catch(err => console.error(`ERROR: ${err.message}`)) +``` + +If you're using Electron Packager's CLI, use the `--protocol` and `--protocol-name` flags. For +example: + +```shell +npx electron-packager . --protocol=electron-fiddle --protocol-name="Electron Fiddle" +``` + +## Conclusion + +After you start your Electron app, you can enter in a URL in your browser that contains the custom +protocol, for example `"electron-fiddle://open"` and observe that the application will respond and +show an error dialog box. + + + +```fiddle docs/fiddles/system/protocol-handler/launch-app-from-URL-in-another-app + +``` + + diff --git a/docs/tutorial/linux-desktop-actions.md b/docs/tutorial/linux-desktop-actions.md index eebc208554123..7f09d0a4e5255 100644 --- a/docs/tutorial/linux-desktop-actions.md +++ b/docs/tutorial/linux-desktop-actions.md @@ -1,17 +1,28 @@ -# Custom Linux Desktop Launcher Actions +--- +title: Desktop Launcher Actions +description: Add actions to the system launcher on Linux environments. +slug: linux-desktop-actions +hide_title: true +--- -On many Linux environments, you can add custom entries to its launcher -by modifying the `.desktop` file. For Canonical's Unity documentation, -see [Adding Shortcuts to a Launcher][unity-launcher]. For details on a -more generic implementation, see the [freedesktop.org Specification][spec]. +# Desktop Launcher Actions -__Launcher shortcuts of Audacious:__ +## Overview + +On many Linux environments, you can add custom entries to the system launcher +by modifying the `.desktop` file. For Canonical's Unity documentation, see +[Adding Shortcuts to a Launcher][unity-launcher]. For details on a more generic +implementation, see the [freedesktop.org Specification][spec]. ![audacious][audacious-launcher] -Generally speaking, shortcuts are added by providing a `Name` and `Exec` -property for each entry in the shortcuts menu. Unity will execute the -`Exec` field once clicked by the user. The format is as follows: +> NOTE: The screenshot above is an example of launcher shortcuts in Audacious +audio player + +To create a shortcut, you need to provide `Name` and `Exec` properties for the +entry you want to add to the shortcut menu. Unity will execute the command +defined in the `Exec` field after the user clicked the shortcut menu item. +An example of the `.desktop` file may look as follows: ```plaintext Actions=PlayPause;Next;Previous @@ -32,10 +43,10 @@ Exec=audacious -r OnlyShowIn=Unity; ``` -Unity's preferred way of telling your application what to do is to use -parameters. You can find these in your app in the global variable +The preferred way for Unity to instruct your application on what to do is using +parameters. You can find them in your application in the global variable `process.argv`. [unity-launcher]: https://help.ubuntu.com/community/UnityLaunchersAndDesktopFiles#Adding_shortcuts_to_a_launcher [audacious-launcher]: https://help.ubuntu.com/community/UnityLaunchersAndDesktopFiles?action=AttachFile&do=get&target=shortcuts.png -[spec]: https://specifications.freedesktop.org/desktop-entry-spec/1.1/ar01s11.html +[spec]: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html diff --git a/docs/tutorial/mac-app-store-submission-guide.md b/docs/tutorial/mac-app-store-submission-guide.md index 22c1462534d22..18e0fbfbb8cd4 100644 --- a/docs/tutorial/mac-app-store-submission-guide.md +++ b/docs/tutorial/mac-app-store-submission-guide.md @@ -1,53 +1,111 @@ # Mac App Store Submission Guide -Since v0.34.0, Electron allows submitting packaged apps to the Mac App Store -(MAS). This guide provides information on: how to submit your app and the -limitations of the MAS build. +This guide provides information on: -**Note:** Submitting an app to Mac App Store requires enrolling in the [Apple Developer -Program][developer-program], which costs money. +* How to sign Electron apps on macOS; +* How to submit Electron apps to Mac App Store (MAS); +* The limitations of the MAS build. -## How to Submit Your App +## Requirements -The following steps introduce a simple way to submit your app to Mac App Store. -However, these steps do not ensure your app will be approved by Apple; you -still need to read Apple's [Submitting Your App][submitting-your-app] guide on -how to meet the Mac App Store requirements. +To sign Electron apps, the following tools must be installed first: -### Get Certificate +* Xcode 11 or above. +* The [electron-osx-sign][electron-osx-sign] npm module. -To submit your app to the Mac App Store, you first must get a certificate from -Apple. You can follow these [existing guides][nwjs-guide] on web. +You also have to register an Apple Developer account and join the +[Apple Developer Program][developer-program]. -### Get Team ID +## Sign Electron apps -Before signing your app, you need to know the Team ID of your account. To locate -your Team ID, Sign in to [Apple Developer Center](https://developer.apple.com/account/), -and click Membership in the sidebar. Your Team ID appears in the Membership -Information section under the team name. +Electron apps can be distributed through Mac App Store or outside it. Each way +requires different ways of signing and testing. This guide focuses on +distribution via Mac App Store, but will also mention other methods. -### Sign Your App +The following steps describe how to get the certificates from Apple, how to sign +Electron apps, and how to test them. -After finishing the preparation work, you can package your app by following -[Application Distribution](application-distribution.md), and then proceed to -signing your app. +### Get certificates -First, you have to add a `ElectronTeamID` key to your app's `Info.plist`, which -has your Team ID as its value: +The simplest way to get signing certificates is to use Xcode: -```xml - - - ... - ElectronTeamID - TEAM_ID - - -``` +1. Open Xcode and open "Accounts" preferences; +2. Sign in with your Apple account; +3. Select a team and click "Manage Certificates"; +4. In the lower-left corner of the signing certificates sheet, click the Add + button (+), and add following certificates: + * "Apple Development" + * "Apple Distribution" + +The "Apple Development" certificate is used to sign apps for development and +testing, on machines that have been registered on Apple Developer website. The +method of registration will be described in +[Prepare provisioning profile](#prepare-provisioning-profile). + +Apps signed with the "Apple Development" certificate cannot be submitted to Mac +App Store. For that purpose, apps must be signed with the "Apple Distribution" +certificate instead. But note that apps signed with the "Apple Distribution" +certificate cannot run directly, they must be re-signed by Apple to be able to +run, which will only be possible after being downloaded from the Mac App Store. + +#### Other certificates + +You may notice that there are also other kinds of certificates. -Then, you need to prepare three entitlements files. +The "Developer ID Application" certificate is used to sign apps before +distributing them outside the Mac App Store. -`child.plist`: +The "Developer ID Installer" and "Mac Installer Distribution" certificates are +used to sign the Mac Installer Package instead of the app itself. Most Electron +apps do not use Mac Installer Package so they are generally not needed. + +The full list of certificate types can be found +[here](https://help.apple.com/xcode/mac/current/#/dev80c6204ec). + +Apps signed with "Apple Development" and "Apple Distribution" certificates can +only run under [App Sandbox][app-sandboxing], so they must use the MAS build of +Electron. However, the "Developer ID Application" certificate does not have this +restrictions, so apps signed with it can use either the normal build or the MAS +build of Electron. + +#### Legacy certificate names + +Apple has been changing the names of certificates during past years, you might +encounter them when reading old documentations, and some utilities are still +using one of the old names. + +* The "Apple Distribution" certificate was also named as "3rd Party Mac + Developer Application" and "Mac App Distribution". +* The "Apple Development" certificate was also named as "Mac Developer" and + "Development". + +### Prepare provisioning profile + +If you want to test your app on your local machine before submitting your app to +the Mac App Store, you have to sign the app with the "Apple Development" +certificate with the provisioning profile embedded in the app bundle. + +To [create a provisioning profile](https://help.apple.com/developer-account/#/devf2eb157f8), +you can follow the below steps: + +1. Open the "Certificates, Identifiers & Profiles" page on the + [Apple Developer](https://developer.apple.com/account) website. +2. Add a new App ID for your app in the "Identifiers" page. +3. Register your local machine in the "Devices" page. You can find your + machine's "Device ID" in the "Hardware" page of the "System Information" app. +4. Register a new Provisioning Profile in the "Profiles" page, and download it + to `/path/to/yourapp.provisionprofile`. + +### Enable Apple's App Sandbox + +Apps submitted to the Mac App Store must run under Apple's +[App Sandbox][app-sandboxing], and only the MAS build of Electron can run with +the App Sandbox. The standard darwin build of Electron will fail to launch +when run under App Sandbox. + +When signing the app with `electron-osx-sign`, it will automatically add the +necessary entitlements to your app's entitlements, but if you are using custom +entitlements, you must ensure App Sandbox capacity is added: ```xml @@ -56,13 +114,14 @@ Then, you need to prepare three entitlements files. com.apple.security.app-sandbox - com.apple.security.inherit - ``` -`parent.plist`: +#### Extra steps without `electron-osx-sign` + +If you are signing your app without using `electron-osx-sign`, you must ensure +the app bundle's entitlements have at least following keys: ```xml @@ -79,7 +138,11 @@ Then, you need to prepare three entitlements files. ``` -`loginhelper.plist`: +The `TEAM_ID` should be replaced with your Apple Developer account's Team ID, +and the `your.bundle.id` should be replaced with the App ID of the app. + +And the following entitlements must be added to the binaries and helpers in +the app's bundle: ```xml @@ -88,80 +151,97 @@ Then, you need to prepare three entitlements files. com.apple.security.app-sandbox + com.apple.security.inherit + ``` -You have to replace `TEAM_ID` with your Team ID, and replace `your.bundle.id` -with the Bundle ID of your app. - -And then sign your app with the following script: - -```sh -#!/bin/bash - -# Name of your app. -APP="YourApp" -# The path of your app to sign. -APP_PATH="/path/to/YourApp.app" -# The path to the location you want to put the signed package. -RESULT_PATH="~/Desktop/$APP.pkg" -# The name of certificates you requested. -APP_KEY="3rd Party Mac Developer Application: Company Name (APPIDENTITY)" -INSTALLER_KEY="3rd Party Mac Developer Installer: Company Name (APPIDENTITY)" -# The path of your plist files. -CHILD_PLIST="/path/to/child.plist" -PARENT_PLIST="/path/to/parent.plist" -LOGINHELPER_PLIST="/path/to/loginhelper.plist" - -FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks" - -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Electron Framework" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libnode.dylib" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/Contents/MacOS/$APP Helper" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/" -codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/Contents/MacOS/$APP Login Helper" -codesign -s "$APP_KEY" -f --entitlements "$LOGINHELPER_PLIST" "$APP_PATH/Contents/Library/LoginItems/$APP Login Helper.app/" -codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP" -codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH" - -productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_KEY" "$RESULT_PATH" +And the app bundle's `Info.plist` must include `ElectronTeamID` key, which has +your Apple Developer account's Team ID as its value: + +```xml + + + ... + ElectronTeamID + TEAM_ID + + +``` + +When using `electron-osx-sign` the `ElectronTeamID` key will be added +automatically by extracting the Team ID from the certificate's name. You may +need to manually add this key if `electron-osx-sign` could not find the correct +Team ID. + +### Sign apps for development + +To sign an app that can run on your development machine, you must sign it with +the "Apple Development" certificate and pass the provisioning profile to +`electron-osx-sign`. + +```bash +electron-osx-sign YourApp.app --identity='Apple Development' --provisioning-profile=/path/to/yourapp.provisionprofile ``` -If you are new to app sandboxing under macOS, you should also read through -Apple's [Enabling App Sandbox][enable-app-sandbox] to have a basic idea, then -add keys for the permissions needed by your app to the entitlements files. +If you are signing without `electron-osx-sign`, you must place the provisioning +profile to `YourApp.app/Contents/embedded.provisionprofile`. + +The signed app can only run on the machines that registered by the provisioning +profile, and this is the only way to test the signed app before submitting to +Mac App Store. + +### Sign apps for submitting to the Mac App Store -Apart from manually signing your app, you can also choose to use the -[electron-osx-sign][electron-osx-sign] module to do the job. +To sign an app that will be submitted to Mac App Store, you must sign it with +the "Apple Distribution" certificate. Note that apps signed with this +certificate will not run anywhere, unless it is downloaded from Mac App Store. -#### Sign Native Modules +```bash +electron-osx-sign YourApp.app --identity='Apple Distribution' +``` + +### Sign apps for distribution outside the Mac App Store -Native modules used in your app also need to be signed. If using -electron-osx-sign, be sure to include the path to the built binaries in the -argument list: +If you don't plan to submit the app to Mac App Store, you can sign it the +"Developer ID Application" certificate. In this way there is no requirement on +App Sandbox, and you should use the normal darwin build of Electron if you don't +use App Sandbox. -```sh -electron-osx-sign YourApp.app YourApp.app/Contents/Resources/app/node_modules/nativemodule/build/release/nativemodule +```bash +electron-osx-sign YourApp.app --identity='Developer ID Application' --no-gatekeeper-assess ``` -Also note that native modules may have intermediate files produced which should -not be included (as they would also need to be signed). If you use -[electron-packager][electron-packager] before version 8.1.0, add -`--ignore=.+\.o$` to your build step to ignore these files. Versions 8.1.0 and -later ignore those files by default. +By passing `--no-gatekeeper-assess`, the `electron-osx-sign` will skip the macOS +GateKeeper check as your app usually has not been notarized yet by this step. + + +This guide does not cover [App Notarization][app-notarization], but you might +want to do it otherwise Apple may prevent users from using your app outside Mac +App Store. + +## Submit Apps to the Mac App Store -### Upload Your App +After signing the app with the "Apple Distribution" certificate, you can +continue to submit it to Mac App Store. -After signing your app, you can use Application Loader to upload it to iTunes +However, this guide do not ensure your app will be approved by Apple; you +still need to read Apple's [Submitting Your App][submitting-your-app] guide on +how to meet the Mac App Store requirements. + +### Upload + +The Application Loader should be used to upload the signed app to iTunes Connect for processing, making sure you have [created a record][create-record] before uploading. -### Submit Your App for Review +If you are seeing errors like private APIs uses, you should check if the app is +using the MAS build of Electron. -After these steps, you can [submit your app for review][submit-for-review]. +### Submit for review + +After uploading, you should [submit your app for review][submit-for-review]. ## Limitations of MAS Build @@ -181,13 +261,13 @@ Also, due to the usage of app sandboxing, the resources which can be accessed by the app are strictly limited; you can read [App Sandboxing][app-sandboxing] for more information. -### Additional Entitlements +### Additional entitlements Depending on which Electron APIs your app uses, you may need to add additional -entitlements to your `parent.plist` file to be able to use these APIs from your -app's Mac App Store build. +entitlements to your app's entitlements file. Otherwise, the App Sandbox may +prevent you from using them. -#### Network Access +#### Network access Enable outgoing network connections to allow your app to connect to a server: @@ -242,14 +322,14 @@ Electron uses following cryptographic algorithms: * ECDH - ANS X9.63–2001 * HKDF - [NIST SP 800-56C](https://csrc.nist.gov/publications/nistpubs/800-56C/SP-800-56C.pdf) * PBKDF2 - [RFC 2898](https://tools.ietf.org/html/rfc2898) -* RSA - [RFC 3447](http://www.ietf.org/rfc/rfc3447) +* RSA - [RFC 3447](https://www.ietf.org/rfc/rfc3447) * SHA - [FIPS 180-4](https://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf) * Blowfish - https://www.schneier.com/cryptography/blowfish/ * CAST - [RFC 2144](https://tools.ietf.org/html/rfc2144), [RFC 2612](https://tools.ietf.org/html/rfc2612) * DES - [FIPS 46-3](https://csrc.nist.gov/publications/fips/fips46-3/fips46-3.pdf) * DH - [RFC 2631](https://tools.ietf.org/html/rfc2631) * DSA - [ANSI X9.30](https://webstore.ansi.org/RecordDetail.aspx?sku=ANSI+X9.30-1%3A1997) -* EC - [SEC 1](http://www.secg.org/sec1-v2.pdf) +* EC - [SEC 1](https://www.secg.org/sec1-v2.pdf) * IDEA - "On the Design and Security of Block Ciphers" book by X. Lai * MD2 - [RFC 1319](https://tools.ietf.org/html/rfc1319) * MD4 - [RFC 6150](https://tools.ietf.org/html/rfc6150) @@ -257,19 +337,16 @@ Electron uses following cryptographic algorithms: * MDC2 - [ISO/IEC 10118-2](https://wiki.openssl.org/index.php/Manual:Mdc2(3)) * RC2 - [RFC 2268](https://tools.ietf.org/html/rfc2268) * RC4 - [RFC 4345](https://tools.ietf.org/html/rfc4345) -* RC5 - http://people.csail.mit.edu/rivest/Rivest-rc5rev.pdf +* RC5 - https://people.csail.mit.edu/rivest/Rivest-rc5rev.pdf * RIPEMD - [ISO/IEC 10118-3](https://webstore.ansi.org/RecordDetail.aspx?sku=ISO%2FIEC%2010118-3:2004) [developer-program]: https://developer.apple.com/support/compare-memberships/ +[electron-osx-sign]: https://github.com/electron/electron-osx-sign +[app-sandboxing]: https://developer.apple.com/app-sandboxing/ +[app-notarization]: https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution [submitting-your-app]: https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/SubmittingYourApp/SubmittingYourApp.html -[nwjs-guide]: https://github.com/nwjs/nw.js/wiki/Mac-App-Store-%28MAS%29-Submission-Guideline#first-steps -[enable-app-sandbox]: https://developer.apple.com/library/ios/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html [create-record]: https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Chapters/CreatingiTunesConnectRecord.html -[electron-osx-sign]: https://github.com/electron-userland/electron-osx-sign -[electron-packager]: https://github.com/electron/electron-packager [submit-for-review]: https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Chapters/SubmittingTheApp.html -[app-sandboxing]: https://developer.apple.com/app-sandboxing/ [export-compliance]: https://help.apple.com/app-store-connect/#/devc3f64248f -[temporary-exception]: https://developer.apple.com/library/mac/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AppSandboxTemporaryExceptionEntitlements.html [user-selected]: https://developer.apple.com/library/mac/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html#//apple_ref/doc/uid/TP40011195-CH4-SW6 [network-access]: https://developer.apple.com/library/ios/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html#//apple_ref/doc/uid/TP40011195-CH4-SW9 diff --git a/docs/tutorial/macos-dock.md b/docs/tutorial/macos-dock.md index 3f4ab58333542..38cfcb48e6240 100644 --- a/docs/tutorial/macos-dock.md +++ b/docs/tutorial/macos-dock.md @@ -1,10 +1,16 @@ -# MacOS Dock +--- +title: Dock +description: Configure your application's Dock presence on macOS. +slug: macos-dock +hide_title: true +--- + +# Dock Electron has APIs to configure the app's icon in the macOS Dock. A macOS-only -API exists to create a custom dock menu, but -Electron also uses the app's dock icon to implement cross-platform features -like [recent documents][recent-documents] and -[application progress][progress-bar]. +API exists to create a custom dock menu, but Electron also uses the app dock +icon as the entry point for cross-platform features like +[recent documents][recent-documents] and [application progress][progress-bar]. The custom dock is commonly used to add shortcuts to tasks the user wouldn't want to open the whole app window for. @@ -13,11 +19,21 @@ __Dock menu of Terminal.app:__ ![Dock Menu][dock-menu-image] -To set your custom dock menu, you can use the `app.dock.setMenu` API, which is -only available on macOS: +To set your custom dock menu, you need to use the +[`app.dock.setMenu`](../api/dock.md#docksetmenumenu-macos) API, +which is only available on macOS. + +```javascript fiddle='docs/fiddles/features/macos-dock-menu' +const { app, BrowserWindow, Menu } = require('electron') -```javascript -const { app, Menu } = require('electron') +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + }) + + win.loadFile('index.html') +} const dockMenu = Menu.buildFromTemplate([ { @@ -33,9 +49,31 @@ const dockMenu = Menu.buildFromTemplate([ { label: 'New Command...' } ]) -app.dock.setMenu(dockMenu) +app.whenReady().then(() => { + if (process.platform === 'darwin') { + app.dock.setMenu(dockMenu) + } +}).then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) + ``` +After launching the Electron application, right click the application icon. +You should see the custom menu you just defined: + +![macOS dock menu](../images/macos-dock-menu.png) + [dock-menu-image]: https://cloud.githubusercontent.com/assets/639601/5069962/6032658a-6e9c-11e4-9953-aa84006bdfff.png [recent-documents]: ./recent-documents.md [progress-bar]: ./progress-bar.md diff --git a/docs/tutorial/message-ports.md b/docs/tutorial/message-ports.md new file mode 100644 index 0000000000000..b236f4133e450 --- /dev/null +++ b/docs/tutorial/message-ports.md @@ -0,0 +1,384 @@ +# MessagePorts in Electron + +[`MessagePort`][]s are a web feature that allow passing messages between +different contexts. It's like `window.postMessage`, but on different channels. +The goal of this document is to describe how Electron extends the Channel +Messaging model, and to give some examples of how you might use MessagePorts in +your app. + +Here is a very brief example of what a MessagePort is and how it works: + +```js title='renderer.js (Renderer Process)' +// MessagePorts are created in pairs. A connected pair of message ports is +// called a channel. +const channel = new MessageChannel() + +// The only difference between port1 and port2 is in how you use them. Messages +// sent to port1 will be received by port2 and vice-versa. +const port1 = channel.port1 +const port2 = channel.port2 + +// It's OK to send a message on the channel before the other end has registered +// a listener. Messages will be queued until a listener is registered. +port2.postMessage({ answer: 42 }) + +// Here we send the other end of the channel, port1, to the main process. It's +// also possible to send MessagePorts to other frames, or to Web Workers, etc. +ipcRenderer.postMessage('port', null, [port1]) +``` + +```js title='main.js (Main Process)' +// In the main process, we receive the port. +ipcMain.on('port', (event) => { + // When we receive a MessagePort in the main process, it becomes a + // MessagePortMain. + const port = event.ports[0] + + // MessagePortMain uses the Node.js-style events API, rather than the + // web-style events API. So .on('message', ...) instead of .onmessage = ... + port.on('message', (event) => { + // data is { answer: 42 } + const data = event.data + }) + + // MessagePortMain queues messages until the .start() method has been called. + port.start() +}) +``` + +The [Channel Messaging API][] documentation is a great way to learn more about +how MessagePorts work. + +## MessagePorts in the main process + +In the renderer, the `MessagePort` class behaves exactly as it does on the web. +The main process is not a web page, though—it has no Blink integration—and so +it does not have the `MessagePort` or `MessageChannel` classes. In order to +handle and interact with MessagePorts in the main process, Electron adds two +new classes: [`MessagePortMain`][] and [`MessageChannelMain`][]. These behave +similarly to the analogous classes in the renderer. + +`MessagePort` objects can be created in either the renderer or the main +process, and passed back and forth using the [`ipcRenderer.postMessage`][] and +[`WebContents.postMessage`][] methods. Note that the usual IPC methods like +`send` and `invoke` cannot be used to transfer `MessagePort`s, only the +`postMessage` methods can transfer `MessagePort`s. + +By passing `MessagePort`s via the main process, you can connect two pages that +might not otherwise be able to communicate (e.g. due to same-origin +restrictions). + +## Extension: `close` event + +Electron adds one feature to `MessagePort` that isn't present on the web, in +order to make MessagePorts more useful. That is the `close` event, which is +emitted when the other end of the channel is closed. Ports can also be +implicitly closed by being garbage-collected. + +In the renderer, you can listen for the `close` event either by assigning to +`port.onclose` or by calling `port.addEventListener('close', ...)`. In the main +process, you can listen for the `close` event by calling `port.on('close', +...)`. + +## Example use cases + +### Setting up a MessageChannel between two renderers + +In this example, the main process sets up a MessageChannel, then sends each port +to a different renderer. This allows renderers to send messages to each other +without needing to use the main process as an in-between. + +```js title='main.js (Main Process)' +const { BrowserWindow, app, MessageChannelMain } = require('electron') + +app.whenReady().then(async () => { + // create the windows. + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: false, + preload: 'preloadMain.js' + } + }) + + const secondaryWindow = BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: false, + preload: 'preloadSecondary.js' + } + }) + + // set up the channel. + const { port1, port2 } = new MessageChannelMain() + + // once the webContents are ready, send a port to each webContents with postMessage. + mainWindow.once('ready-to-show', () => { + mainWindow.webContents.postMessage('port', null, [port1]) + }) + + secondaryWindow.once('ready-to-show', () => { + secondaryWindow.webContents.postMessage('port', null, [port2]) + }) +}) +``` + +Then, in your preload scripts you receive the port through IPC and set up the +listeners. + +```js title='preloadMain.js and preloadSecondary.js (Preload scripts)' +const { ipcRenderer } = require('electron') + +ipcRenderer.on('port', e => { + // port received, make it globally available. + window.electronMessagePort = e.ports[0] + + window.electronMessagePort.onmessage = messageEvent => { + // handle message + } +}) +``` + +In this example messagePort is bound to the `window` object directly. It is better +to use `contextIsolation` and set up specific contextBridge calls for each of your +expected messages, but for the simplicity of this example we don't. You can find an +example of context isolation further down this page at [Communicating directly between the main process and the main world of a context-isolated page](#communicating-directly-between-the-main-process-and-the-main-world-of-a-context-isolated-page) + +That means window.messagePort is globally available and you can call +`postMessage` on it from anywhere in your app to send a message to the other +renderer. + +```js title='renderer.js (Renderer Process)' +// elsewhere in your code to send a message to the other renderers message handler +window.electronMessagePort.postmessage('ping') +``` + +### Worker process + +In this example, your app has a worker process implemented as a hidden window. +You want the app page to be able to communicate directly with the worker +process, without the performance overhead of relaying via the main process. + +```js title='main.js (Main Process)' +const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron') + +app.whenReady().then(async () => { + // The worker process is a hidden BrowserWindow, so that it will have access + // to a full Blink context (including e.g. , audio, fetch(), etc.) + const worker = new BrowserWindow({ + show: false, + webPreferences: { nodeIntegration: true } + }) + await worker.loadFile('worker.html') + + // The main window will send work to the worker process and receive results + // over a MessagePort. + const mainWindow = new BrowserWindow({ + webPreferences: { nodeIntegration: true } + }) + mainWindow.loadFile('app.html') + + // We can't use ipcMain.handle() here, because the reply needs to transfer a + // MessagePort. + ipcMain.on('request-worker-channel', (event) => { + // For security reasons, let's make sure only the frames we expect can + // access the worker. + if (event.senderFrame === mainWindow.webContents.mainFrame) { + // Create a new channel ... + const { port1, port2 } = new MessageChannelMain() + // ... send one end to the worker ... + worker.webContents.postMessage('new-client', null, [port1]) + // ... and the other end to the main window. + event.senderFrame.postMessage('provide-worker-channel', null, [port2]) + // Now the main window and the worker can communicate with each other + // without going through the main process! + } + }) +}) +``` + +```html title='worker.html' + +``` + +```html title='app.html' + +``` + +### Reply streams + +Electron's built-in IPC methods only support two modes: fire-and-forget +(e.g. `send`), or request-response (e.g. `invoke`). Using MessageChannels, you +can implement a "response stream", where a single request responds with a +stream of data. + +```js title='renderer.js (Renderer Process)' +const makeStreamingRequest = (element, callback) => { + // MessageChannels are lightweight--it's cheap to create a new one for each + // request. + const { port1, port2 } = new MessageChannel() + + // We send one end of the port to the main process ... + ipcRenderer.postMessage( + 'give-me-a-stream', + { element, count: 10 }, + [port2] + ) + + // ... and we hang on to the other end. The main process will send messages + // to its end of the port, and close it when it's finished. + port1.onmessage = (event) => { + callback(event.data) + } + port1.onclose = () => { + console.log('stream ended') + } +} + +makeStreamingRequest(42, (data) => { + console.log('got response data:', event.data) +}) +// We will see "got response data: 42" 10 times. +``` + +```js title='main.js (Main Process)' +ipcMain.on('give-me-a-stream', (event, msg) => { + // The renderer has sent us a MessagePort that it wants us to send our + // response over. + const [replyPort] = event.ports + + // Here we send the messages synchronously, but we could just as easily store + // the port somewhere and send messages asynchronously. + for (let i = 0; i < msg.count; i++) { + replyPort.postMessage(msg.element) + } + + // We close the port when we're done to indicate to the other end that we + // won't be sending any more messages. This isn't strictly necessary--if we + // didn't explicitly close the port, it would eventually be garbage + // collected, which would also trigger the 'close' event in the renderer. + replyPort.close() +}) +``` + +### Communicating directly between the main process and the main world of a context-isolated page + +When [context isolation][] is enabled, IPC messages from the main process to +the renderer are delivered to the isolated world, rather than to the main +world. Sometimes you want to deliver messages to the main world directly, +without having to step through the isolated world. + +```js title='main.js (Main Process)' +const { BrowserWindow, app, MessageChannelMain } = require('electron') +const path = require('path') + +app.whenReady().then(async () => { + // Create a BrowserWindow with contextIsolation enabled. + const bw = new BrowserWindow({ + webPreferences: { + contextIsolation: true, + preload: path.join(__dirname, 'preload.js') + } + }) + bw.loadURL('index.html') + + // We'll be sending one end of this channel to the main world of the + // context-isolated page. + const { port1, port2 } = new MessageChannelMain() + + // It's OK to send a message on the channel before the other end has + // registered a listener. Messages will be queued until a listener is + // registered. + port2.postMessage({ test: 21 }) + + // We can also receive messages from the main world of the renderer. + port2.on('message', (event) => { + console.log('from renderer main world:', event.data) + }) + port2.start() + + // The preload script will receive this IPC message and transfer the port + // over to the main world. + bw.webContents.postMessage('main-world-port', null, [port1]) +}) +``` + +```js title='preload.js (Preload Script)' +const { ipcRenderer } = require('electron') + +// We need to wait until the main world is ready to receive the message before +// sending the port. We create this promise in the preload so it's guaranteed +// to register the onload listener before the load event is fired. +const windowLoaded = new Promise(resolve => { + window.onload = resolve +}) + +ipcRenderer.on('main-world-port', async (event) => { + await windowLoaded + // We use regular window.postMessage to transfer the port from the isolated + // world to the main world. + window.postMessage('main-world-port', '*', event.ports) +}) +``` + +```html title='index.html' + +``` + +[context isolation]: context-isolation.md +[`ipcRenderer.postMessage`]: ../api/ipc-renderer.md#ipcrendererpostmessagechannel-message-transfer +[`WebContents.postMessage`]: ../api/web-contents.md#contentspostmessagechannel-message-transfer +[`MessagePortMain`]: ../api/message-port-main.md +[`MessageChannelMain`]: ../api/message-channel-main.md +[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API diff --git a/docs/tutorial/mojave-dark-mode-guide.md b/docs/tutorial/mojave-dark-mode-guide.md deleted file mode 100644 index c1ca37c0cf5d2..0000000000000 --- a/docs/tutorial/mojave-dark-mode-guide.md +++ /dev/null @@ -1,32 +0,0 @@ -# Mojave Dark Mode - -In macOS 10.14 Mojave, Apple introduced a new [system-wide dark mode](https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/) -for all macOS computers. If your app does have a dark mode, you can make your Electron app -follow the system-wide dark mode setting. - -In macOS 10.15 Catalina, Apple introduced a new "automatic" dark mode option for all macOS computers. In order -for the `isDarkMode` and `Tray` APIs to work correctly in this mode on Catalina you need to either have `NSRequiresAquaSystemAppearance` set to `false` in your `Info.plist` file or be on Electron `>=7.0.0`. - -## Automatically updating the native interfaces - -"Native Interfaces" include the file picker, window border, dialogs, context menus and more; basically anything where -the UI comes from macOS and not your app. The default behavior as of Electron 7.0.0 is to opt in to this automatic -theming from the OS. If you wish to opt out you must set the `NSRequiresAquaSystemAppearance` key in the `Info.plist` file -to `true`. Please note that once Electron starts building against the 10.14 SDK it will not be possible for you to opt -out of this theming. - -## Automatically updating your own interfaces - -If your app has its own dark mode you should toggle it on and off in sync with the system's dark mode setting. You can do -this by listening for the theme changed event on Electron's `systemPreferences` module. E.g. - -```js -const { systemPreferences } = require('electron') - -systemPreferences.subscribeNotification( - 'AppleInterfaceThemeChangedNotification', - function theThemeHasChanged () { - updateMyAppTheme(systemPreferences.isDarkMode()) - } -) -``` diff --git a/docs/tutorial/multithreading.md b/docs/tutorial/multithreading.md index b0d298e57e2a0..222db8337005e 100644 --- a/docs/tutorial/multithreading.md +++ b/docs/tutorial/multithreading.md @@ -10,7 +10,7 @@ so the `nodeIntegrationInWorker` option should be set to `true` in `webPreferences`. ```javascript -let win = new BrowserWindow({ +const win = new BrowserWindow({ webPreferences: { nodeIntegrationInWorker: true } @@ -44,7 +44,7 @@ loads no native modules after the Web Workers get started. process.dlopen = () => { throw new Error('Load native module is not safe') } -let worker = new Worker('script.js') +const worker = new Worker('script.js') ``` [web-workers]: https://developer.mozilla.org/en/docs/Web/API/Web_Workers_API/Using_web_workers diff --git a/docs/tutorial/native-file-drag-drop.md b/docs/tutorial/native-file-drag-drop.md index 99d254182609e..75ef4eb212cdc 100644 --- a/docs/tutorial/native-file-drag-drop.md +++ b/docs/tutorial/native-file-drag-drop.md @@ -1,31 +1,62 @@ # Native File Drag & Drop +## Overview + Certain kinds of applications that manipulate files might want to support the operating system's native file drag & drop feature. Dragging files into web content is common and supported by many websites. Electron additionally supports dragging files and content out from web content into the operating system's world. -To implement this feature in your app, you need to call `webContents.startDrag(item)` +To implement this feature in your app, you need to call the +[`webContents.startDrag(item)`](../api/web-contents.md#contentsstartdragitem) API in response to the `ondragstart` event. -In your renderer process, handle the `ondragstart` event and forward the -information to your main process. +## Example -```html -item - +}) +``` + +### Index.html + +Add a draggable element to `index.html`, and reference your renderer script: + +```html +
Drag me
+ ``` -Then, in the main process, augment the event with a path to the file that is -being dragged and an icon. +### Renderer.js + +In `renderer.js` set up the renderer process to handle drag events by calling the method you added via the [`contextBridge`] above. ```javascript +document.getElementById('drag').ondragstart = (event) => { + event.preventDefault() + window.electron.startDrag('drag-and-drop.md') +} +``` + +### Main.js + +In the Main process (`main.js` file), expand the received event with a path to the file that is +being dragged and an icon: + +```javascript fiddle='docs/fiddles/features/drag-and-drop' const { ipcMain } = require('electron') ipcMain.on('ondragstart', (event, filePath) => { @@ -35,3 +66,11 @@ ipcMain.on('ondragstart', (event, filePath) => { }) }) ``` + +After launching the Electron application, try dragging and dropping +the item from the BrowserWindow onto your desktop. In this guide, +the item is a Markdown file located in the root of the project: + +![Drag and drop](../images/drag-and-drop.gif) + +[`contextBridge`]: ../api/context-bridge.md diff --git a/docs/tutorial/notifications.md b/docs/tutorial/notifications.md index 90f11e1ab4858..59b4a8499276b 100644 --- a/docs/tutorial/notifications.md +++ b/docs/tutorial/notifications.md @@ -1,30 +1,84 @@ -# Notifications (Windows, Linux, macOS) +# Notifications -All three operating systems provide means for applications to send notifications -to the user. Electron conveniently allows developers to send notifications with -the [HTML5 Notification API](https://notifications.spec.whatwg.org/), using -the currently running operating system's native notification APIs to display it. +## Overview -**Note:** Since this is an HTML5 API it is only available in the renderer process. If -you want to show Notifications in the main process please check out the +All three operating systems provide means for applications to send +notifications to the user. The technique of showing notifications is different +for the Main and Renderer processes. + +For the Renderer process, Electron conveniently allows developers to send +notifications with the [HTML5 Notification API](https://notifications.spec.whatwg.org/), +using the currently running operating system's native notification APIs +to display it. + +To show notifications in the Main process, you need to use the [Notification](../api/notification.md) module. -```javascript -let myNotification = new Notification('Title', { - body: 'Lorem Ipsum Dolor Sit Amet' -}) +## Example + +### Show notifications in the Renderer process + +Starting with a working application from the +[Quick Start Guide](quick-start.md), add the following line to the +`index.html` file before the closing `` tag: + +```html + +``` + +...and add the `renderer.js` file: + +```javascript fiddle='docs/fiddles/features/notifications/renderer' +const NOTIFICATION_TITLE = 'Title' +const NOTIFICATION_BODY = 'Notification from the Renderer process. Click to log to console.' +const CLICK_MESSAGE = 'Notification clicked' + +new Notification(NOTIFICATION_TITLE, { body: NOTIFICATION_BODY }) + .onclick = () => console.log(CLICK_MESSAGE) +``` + +After launching the Electron application, you should see the notification: + +![Notification in the Renderer process](../images/notification-renderer.png) + +Additionally, if you click on the notification, the DOM will update to show "Notification clicked!". -myNotification.onclick = () => { - console.log('Notification clicked') +### Show notifications in the Main process + +Starting with a working application from the +[Quick Start Guide](quick-start.md), update the `main.js` file with the following lines: + +```javascript fiddle='docs/fiddles/features/notifications/main' +const { Notification } = require('electron') + +const NOTIFICATION_TITLE = 'Basic Notification' +const NOTIFICATION_BODY = 'Notification from the Main process' + +const showNotification = () => { + new Notification({ title: NOTIFICATION_TITLE, body: NOTIFICATION_BODY }).show() } + +app.whenReady().then(createWindow).then(showNotification) ``` +After launching the Electron application, you should see the system notification: + +![Notification in the Main process](../images/notification-main.png) + +## Additional information + While code and user experience across operating systems are similar, there are subtle differences. -## Windows -* On Windows 10, a shortcut to your app with an [Application User -Model ID][app-user-model-id] must be installed to the Start Menu. This can be overkill during development, so adding `node_modules\electron\dist\electron.exe` to your Start Menu also does the trick. Navigate to the file in Explorer, right-click and 'Pin to Start Menu'. You will then need to add the line `app.setAppUserModelId(process.execPath)` to your main process to see notifications. +### Windows + +* On Windows 10, a shortcut to your app with an +[Application User Model ID][app-user-model-id] must be installed to the +Start Menu. This can be overkill during development, so adding +`node_modules\electron\dist\electron.exe` to your Start Menu also does the +trick. Navigate to the file in Explorer, right-click and 'Pin to Start Menu'. +You will then need to add the line `app.setAppUserModelId(process.execPath)` to +your main process to see notifications. * On Windows 8.1 and Windows 8, a shortcut to your app with an [Application User Model ID][app-user-model-id] must be installed to the Start screen. Note, however, that it does not need to be pinned to the Start screen. @@ -44,7 +98,7 @@ to 200 characters. That said, that limitation has been removed in Windows 10, wi the Windows team asking developers to be reasonable. Attempting to send gigantic amounts of text to the API (thousands of characters) might result in instability. -### Advanced Notifications +#### Advanced Notifications Later versions of Windows allow for advanced notifications, with custom templates, images, and other flexible elements. To send those notifications (from either the @@ -53,47 +107,46 @@ main process or the renderer process), use the userland module which uses native Node addons to send `ToastNotification` and `TileNotification` objects. While notifications including buttons work with `electron-windows-notifications`, -handling replies requires the use of [`electron-windows-interactive-notifications`](https://github.com/felixrieseberg/electron-windows-interactive-notifications), which -helps with registering the required COM components and calling your Electron app with -the entered user data. +handling replies requires the use of +[`electron-windows-interactive-notifications`](https://github.com/felixrieseberg/electron-windows-interactive-notifications), +which helps with registering the required COM components and calling your +Electron app with the entered user data. -### Quiet Hours / Presentation Mode +#### Quiet Hours / Presentation Mode -To detect whether or not you're allowed to send a notification, use the userland module -[electron-notification-state](https://github.com/felixrieseberg/electron-notification-state). +To detect whether or not you're allowed to send a notification, use the +userland module [electron-notification-state](https://github.com/felixrieseberg/electron-notification-state). -This allows you to determine ahead of time whether or not Windows will silently throw -the notification away. +This allows you to determine ahead of time whether or not Windows will +silently throw the notification away. -## macOS +### macOS Notifications are straight-forward on macOS, but you should be aware of -[Apple's Human Interface guidelines regarding notifications](https://developer.apple.com/macos/human-interface-guidelines/system-capabilities/notifications/). +[Apple's Human Interface guidelines regarding notifications][apple-notification-guidelines]. Note that notifications are limited to 256 bytes in size and will be truncated if you exceed that limit. -### Advanced Notifications - -Later versions of macOS allow for notifications with an input field, allowing the user -to quickly reply to a notification. In order to send notifications with an input field, -use the userland module [node-mac-notifier](https://github.com/CharlieHess/node-mac-notifier). +[apple-notification-guidelines]: https://developer.apple.com/macos/human-interface-guidelines/system-capabilities/notifications/ -### Do not disturb / Session State +#### Do not disturb / Session State To detect whether or not you're allowed to send a notification, use the userland module -[electron-notification-state](https://github.com/felixrieseberg/electron-notification-state). +[electron-notification-state][electron-notification-state]. This will allow you to detect ahead of time whether or not the notification will be displayed. -## Linux +[electron-notification-state]: https://github.com/felixrieseberg/electron-notification-state + +### Linux Notifications are sent using `libnotify` which can show notifications on any desktop environment that follows [Desktop Notifications Specification][notification-spec], including Cinnamon, Enlightenment, Unity, GNOME, KDE. -[notification-spec]: https://developer.gnome.org/notification-spec/ +[notification-spec]: https://developer-old.gnome.org/notification-spec/ [app-user-model-id]: https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx [set-app-user-model-id]: ../api/app.md#appsetappusermodelidid-windows [squirrel-events]: https://github.com/electron/windows-installer/blob/master/README.md#handling-squirrel-events diff --git a/docs/tutorial/offscreen-rendering.md b/docs/tutorial/offscreen-rendering.md index acc18cdf0673a..3129f322c8e22 100644 --- a/docs/tutorial/offscreen-rendering.md +++ b/docs/tutorial/offscreen-rendering.md @@ -1,59 +1,63 @@ # Offscreen Rendering -Offscreen rendering lets you obtain the content of a browser window in a bitmap, -so it can be rendered anywhere, for example on a texture in a 3D scene. The -offscreen rendering in Electron uses a similar approach than the [Chromium -Embedded Framework](https://bitbucket.org/chromiumembedded/cef) project. +## Overview -Two modes of rendering can be used and only the dirty area is passed in the -`'paint'` event to be more efficient. The rendering can be stopped, continued -and the frame rate can be set. The specified frame rate is a top limit value, -when there is nothing happening on a webpage, no frames are generated. The -maximum frame rate is 60, because above that there is no benefit, only -performance loss. +Offscreen rendering lets you obtain the content of a `BrowserWindow` in a +bitmap, so it can be rendered anywhere, for example, on texture in a 3D scene. +The offscreen rendering in Electron uses a similar approach to that of the +[Chromium Embedded Framework](https://bitbucket.org/chromiumembedded/cef) +project. -**Note:** An offscreen window is always created as a [Frameless Window](../api/frameless-window.md). +*Notes*: -## Rendering Modes +* There are two rendering modes that can be used (see the section below) and only +the dirty area is passed to the `paint` event to be more efficient. +* You can stop/continue the rendering as well as set the frame rate. +* The maximum frame rate is 240 because greater values bring only performance +losses with no benefits. +* When nothing is happening on a webpage, no frames are generated. +* An offscreen window is always created as a +[Frameless Window](../tutorial/window-customization.md).. -### GPU accelerated +### Rendering Modes + +#### GPU accelerated GPU accelerated rendering means that the GPU is used for composition. Because of -that the frame has to be copied from the GPU which requires more performance, -thus this mode is quite a bit slower than the other one. The benefit of this -mode that WebGL and 3D CSS animations are supported. +that, the frame has to be copied from the GPU which requires more resources, +thus this mode is slower than the Software output device. The benefit of this +mode is that WebGL and 3D CSS animations are supported. -### Software output device +#### Software output device This mode uses a software output device for rendering in the CPU, so the frame -generation is much faster, thus this mode is preferred over the GPU accelerated -one. +generation is much faster. As a result, this mode is preferred over the GPU +accelerated one. -To enable this mode GPU acceleration has to be disabled by calling the +To enable this mode, GPU acceleration has to be disabled by calling the [`app.disableHardwareAcceleration()`][disablehardwareacceleration] API. -## Usage +## Example -``` javascript +```javascript fiddle='docs/fiddles/features/offscreen-rendering' const { app, BrowserWindow } = require('electron') +const fs = require('fs') app.disableHardwareAcceleration() let win -app.once('ready', () => { - win = new BrowserWindow({ - webPreferences: { - offscreen: true - } - }) +app.whenReady().then(() => { + win = new BrowserWindow({ webPreferences: { offscreen: true } }) - win.loadURL('http://github.com') + win.loadURL('https://github.com') win.webContents.on('paint', (event, dirty, image) => { - // updateBitmap(dirty, image.getBitmap()) + fs.writeFileSync('ex.png', image.toPNG()) }) - win.webContents.setFrameRate(30) + win.webContents.setFrameRate(60) }) ``` +After launching the Electron application, navigate to your application's +working folder, where you'll find the rendered image. [disablehardwareacceleration]: ../api/app.md#appdisablehardwareacceleration diff --git a/docs/tutorial/online-offline-events.md b/docs/tutorial/online-offline-events.md index 82d679183bfef..79b249f30b311 100644 --- a/docs/tutorial/online-offline-events.md +++ b/docs/tutorial/online-offline-events.md @@ -1,85 +1,89 @@ # Online/Offline Event Detection -[Online and offline event](https://developer.mozilla.org/en-US/docs/Online_and_offline_events) detection can be implemented in the renderer process using the [`navigator.onLine`](http://html5index.org/Offline%20-%20NavigatorOnLine.html) attribute, part of standard HTML5 API. -The `navigator.onLine` attribute returns `false` if any network requests are guaranteed to fail i.e. definitely offline (disconnected from the network). It returns `true` in all other cases. -Since all other conditions return `true`, one has to be mindful of getting false positives, as we cannot assume `true` value necessarily means that Electron can access the internet. Such as in cases where the computer is running a virtualization software that has virtual ethernet adapters that are always “connected.” -Therefore, if you really want to determine the internet access status of Electron, -you should develop additional means for checking. +## Overview -Example: +[Online and offline event](https://developer.mozilla.org/en-US/docs/Online_and_offline_events) +detection can be implemented in the Renderer process using the +[`navigator.onLine`](http://html5index.org/Offline%20-%20NavigatorOnLine.html) +attribute, part of standard HTML5 API. -_main.js_ +The `navigator.onLine` attribute returns: -```javascript -const { app, BrowserWindow } = require('electron') +* `false` if all network requests are guaranteed to fail (e.g. when disconnected from the network). +* `true` in all other cases. -let onlineStatusWindow +Since many cases return `true`, you should treat with care situations of +getting false positives, as we cannot always assume that `true` value means +that Electron can access the Internet. For example, in cases when the computer +is running a virtualization software that has virtual Ethernet adapters in "always +connected" state. Therefore, if you want to determine the Internet access +status of Electron, you should develop additional means for this check. -app.on('ready', () => { - onlineStatusWindow = new BrowserWindow({ width: 0, height: 0, show: false }) - onlineStatusWindow.loadURL(`file://${__dirname}/online-status.html`) -}) -``` +## Example -_online-status.html_ +Starting with an HTML file `index.html`, this example will demonstrate how the `navigator.onLine` API can be used to build a connection status indicator. -```html +```html title="index.html" + + + Hello World! + + - +

Connection status:

+ ``` -There may be instances where you want to respond to these events in the -main process as well. The main process however does not have a -`navigator` object and thus cannot detect these events directly. Using -Electron's inter-process communication utilities, the events can be forwarded -to the main process and handled as needed, as shown in the following example. +In order to mutate the DOM, create a `renderer.js` file that adds event listeners to the `'online'` and `'offline'` `window` events. The event handler sets the content of the `` element depending on the result of `navigator.onLine`. -_main.js_ +```js title='renderer.js' +const updateOnlineStatus = () => { + document.getElementById('status').innerHTML = navigator.onLine ? 'online' : 'offline' +} + +window.addEventListener('online', updateOnlineStatus) +window.addEventListener('offline', updateOnlineStatus) + +updateOnlineStatus() +``` + +Finally, create a `main.js` file for main process that creates the window. + +```js title='main.js' +const { app, BrowserWindow } = require('electron') -```javascript -const { app, BrowserWindow, ipcMain } = require('electron') -let onlineStatusWindow +const createWindow = () => { + const onlineStatusWindow = new BrowserWindow({ + width: 400, + height: 100 + }) -app.on('ready', () => { - onlineStatusWindow = new BrowserWindow({ width: 0, height: 0, show: false }) - onlineStatusWindow.loadURL(`file://${__dirname}/online-status.html`) + onlineStatusWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) }) -ipcMain.on('online-status-changed', (event, status) => { - console.log(status) +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } }) ``` -_online-status.html_ +After launching the Electron application, you should see the notification: -```html - - - - - - -``` +> Note: If you need to communicate the connection status to the main process, use the [IPC renderer](../api/ipc-renderer.md) API. diff --git a/docs/tutorial/performance.md b/docs/tutorial/performance.md new file mode 100644 index 0000000000000..f1648769ef4f1 --- /dev/null +++ b/docs/tutorial/performance.md @@ -0,0 +1,434 @@ +--- +title: Performance +description: A set of guidelines for building performant Electron apps +slug: performance +hide_title: true +toc_max_heading_level: 3 +--- + +# Performance + +Developers frequently ask about strategies to optimize the performance of +Electron applications. Software engineers, consumers, and framework developers +do not always agree on one single definition of what "performance" means. This +document outlines some of the Electron maintainers' favorite ways to reduce the +amount of memory, CPU, and disk resources being used while ensuring that your +app is responsive to user input and completes operations as quickly as +possible. Furthermore, we want all performance strategies to maintain a high +standard for your app's security. + +Wisdom and information about how to build performant websites with JavaScript +generally applies to Electron apps, too. To a certain extent, resources +discussing how to build performant Node.js applications also apply, but be +careful to understand that the term "performance" means different things for +a Node.js backend than it does for an application running on a client. + +This list is provided for your convenience – and is, much like our +[security checklist][security] – not meant to exhaustive. It is probably possible +to build a slow Electron app that follows all the steps outlined below. Electron +is a powerful development platform that enables you, the developer, to do more +or less whatever you want. All that freedom means that performance is largely +your responsibility. + +## Measure, Measure, Measure + +The list below contains a number of steps that are fairly straightforward and +easy to implement. However, building the most performant version of your app +will require you to go beyond a number of steps. Instead, you will have to +closely examine all the code running in your app by carefully profiling and +measuring. Where are the bottlenecks? When the user clicks a button, what +operations take up the brunt of the time? While the app is simply idling, which +objects take up the most memory? + +Time and time again, we have seen that the most successful strategy for building +a performant Electron app is to profile the running code, find the most +resource-hungry piece of it, and to optimize it. Repeating this seemingly +laborious process over and over again will dramatically increase your app's +performance. Experience from working with major apps like Visual Studio Code or +Slack has shown that this practice is by far the most reliable strategy to +improve performance. + +To learn more about how to profile your app's code, familiarize yourself with +the Chrome Developer Tools. For advanced analysis looking at multiple processes +at once, consider the [Chrome Tracing](https://www.chromium.org/developers/how-tos/trace-event-profiling-tool) tool. + +### Recommended Reading + +* [Get Started With Analyzing Runtime Performance][chrome-devtools-tutorial] +* [Talk: "Visual Studio Code - The First Second"][vscode-first-second] + +## Checklist: Performance recommendations + +Chances are that your app could be a little leaner, faster, and generally less +resource-hungry if you attempt these steps. + +1. [Carelessly including modules](#1-carelessly-including-modules) +2. [Loading and running code too soon](#2-loading-and-running-code-too-soon) +3. [Blocking the main process](#3-blocking-the-main-process) +4. [Blocking the renderer process](#4-blocking-the-renderer-process) +5. [Unnecessary polyfills](#5-unnecessary-polyfills) +6. [Unnecessary or blocking network requests](#6-unnecessary-or-blocking-network-requests) +7. [Bundle your code](#7-bundle-your-code) + +### 1. Carelessly including modules + +Before adding a Node.js module to your application, examine said module. How +many dependencies does that module include? What kind of resources does +it need to simply be called in a `require()` statement? You might find +that the module with the most downloads on the NPM package registry or the most stars on GitHub +is not in fact the leanest or smallest one available. + +#### Why? + +The reasoning behind this recommendation is best illustrated with a real-world +example. During the early days of Electron, reliable detection of network +connectivity was a problem, resulting many apps to use a module that exposed a +simple `isOnline()` method. + +That module detected your network connectivity by attempting to reach out to a +number of well-known endpoints. For the list of those endpoints, it depended on +a different module, which also contained a list of well-known ports. This +dependency itself relied on a module containing information about ports, which +came in the form of a JSON file with more than 100,000 lines of content. +Whenever the module was loaded (usually in a `require('module')` statement), +it would load all its dependencies and eventually read and parse this JSON +file. Parsing many thousands lines of JSON is a very expensive operation. On +a slow machine it can take up whole seconds of time. + +In many server contexts, startup time is virtually irrelevant. A Node.js server +that requires information about all ports is likely actually "more performant" +if it loads all required information into memory whenever the server boots at +the benefit of serving requests faster. The module discussed in this example is +not a "bad" module. Electron apps, however, should not be loading, parsing, and +storing in memory information that it does not actually need. + +In short, a seemingly excellent module written primarily for Node.js servers +running Linux might be bad news for your app's performance. In this particular +example, the correct solution was to use no module at all, and to instead use +connectivity checks included in later versions of Chromium. + +#### How? + +When considering a module, we recommend that you check: + +1. the size of dependencies included +2. the resources required to load (`require()`) it +3. the resources required to perform the action you're interested in + +Generating a CPU profile and a heap memory profile for loading a module can be done +with a single command on the command line. In the example below, we're looking at +the popular module `request`. + +```sh +node --cpu-prof --heap-prof -e "require('request')" +``` + +Executing this command results in a `.cpuprofile` file and a `.heapprofile` +file in the directory you executed it in. Both files can be analyzed using +the Chrome Developer Tools, using the `Performance` and `Memory` tabs +respectively. + +![Performance CPU Profile](../images/performance-cpu-prof.png) + +![Performance Heap Memory Profile](../images/performance-heap-prof.png) + +In this example, on the author's machine, we saw that loading `request` took +almost half a second, whereas `node-fetch` took dramatically less memory +and less than 50ms. + +### 2. Loading and running code too soon + +If you have expensive setup operations, consider deferring those. Inspect all +the work being executed right after the application starts. Instead of firing +off all operations right away, consider staggering them in a sequence more +closely aligned with the user's journey. + +In traditional Node.js development, we're used to putting all our `require()` +statements at the top. If you're currently writing your Electron application +using the same strategy _and_ are using sizable modules that you do not +immediately need, apply the same strategy and defer loading to a more +opportune time. + +#### Why? + +Loading modules is a surprisingly expensive operation, especially on Windows. +When your app starts, it should not make users wait for operations that are +currently not necessary. + +This might seem obvious, but many applications tend to do a large amount of +work immediately after the app has launched - like checking for updates, +downloading content used in a later flow, or performing heavy disk I/O +operations. + +Let's consider Visual Studio Code as an example. When you open a file, it will +immediately display the file to you without any code highlighting, prioritizing +your ability to interact with the text. Once it has done that work, it will +move on to code highlighting. + +#### How? + +Let's consider an example and assume that your application is parsing files +in the fictitious `.foo` format. In order to do that, it relies on the +equally fictitious `foo-parser` module. In traditional Node.js development, +you might write code that eagerly loads dependencies: + +```js title='parser.js' +const fs = require('fs') +const fooParser = require('foo-parser') + +class Parser { + constructor () { + this.files = fs.readdirSync('.') + } + + getParsedFiles () { + return fooParser.parse(this.files) + } +} + +const parser = new Parser() + +module.exports = { parser } +``` + +In the above example, we're doing a lot of work that's being executed as soon +as the file is loaded. Do we need to get parsed files right away? Could we +do this work a little later, when `getParsedFiles()` is actually called? + +```js title='parser.js' +// "fs" is likely already being loaded, so the `require()` call is cheap +const fs = require('fs') + +class Parser { + async getFiles () { + // Touch the disk as soon as `getFiles` is called, not sooner. + // Also, ensure that we're not blocking other operations by using + // the asynchronous version. + this.files = this.files || await fs.readdir('.') + + return this.files + } + + async getParsedFiles () { + // Our fictitious foo-parser is a big and expensive module to load, so + // defer that work until we actually need to parse files. + // Since `require()` comes with a module cache, the `require()` call + // will only be expensive once - subsequent calls of `getParsedFiles()` + // will be faster. + const fooParser = require('foo-parser') + const files = await this.getFiles() + + return fooParser.parse(files) + } +} + +// This operation is now a lot cheaper than in our previous example +const parser = new Parser() + +module.exports = { parser } +``` + +In short, allocate resources "just in time" rather than allocating them all +when your app starts. + +### 3. Blocking the main process + +Electron's main process (sometimes called "browser process") is special: It is +the parent process to all your app's other processes and the primary process +the operating system interacts with. It handles windows, interactions, and the +communication between various components inside your app. It also houses the +UI thread. + +Under no circumstances should you block this process and the UI thread with +long-running operations. Blocking the UI thread means that your entire app +will freeze until the main process is ready to continue processing. + +#### Why? + +The main process and its UI thread are essentially the control tower for major +operations inside your app. When the operating system tells your app about a +mouse click, it'll go through the main process before it reaches your window. +If your window is rendering a buttery-smooth animation, it'll need to talk to +the GPU process about that – once again going through the main process. + +Electron and Chromium are careful to put heavy disk I/O and CPU-bound operations +onto new threads to avoid blocking the UI thread. You should do the same. + +#### How? + +Electron's powerful multi-process architecture stands ready to assist you with +your long-running tasks, but also includes a small number of performance traps. + +1. For long running CPU-heavy tasks, make use of +[worker threads][worker-threads], consider moving them to the BrowserWindow, or +(as a last resort) spawn a dedicated process. + +2. Avoid using the synchronous IPC and the `@electron/remote` module as much +as possible. While there are legitimate use cases, it is far too easy to +unknowingly block the UI thread. + +3. Avoid using blocking I/O operations in the main process. In short, whenever +core Node.js modules (like `fs` or `child_process`) offer a synchronous or an +asynchronous version, you should prefer the asynchronous and non-blocking +variant. + +### 4. Blocking the renderer process + +Since Electron ships with a current version of Chrome, you can make use of the +latest and greatest features the Web Platform offers to defer or offload heavy +operations in a way that keeps your app smooth and responsive. + +#### Why? + +Your app probably has a lot of JavaScript to run in the renderer process. The +trick is to execute operations as quickly as possible without taking away +resources needed to keep scrolling smooth, respond to user input, or animations +at 60fps. + +Orchestrating the flow of operations in your renderer's code is +particularly useful if users complain about your app sometimes "stuttering". + +#### How? + +Generally speaking, all advice for building performant web apps for modern +browsers apply to Electron's renderers, too. The two primary tools at your +disposal are currently `requestIdleCallback()` for small operations and +`Web Workers` for long-running operations. + +*`requestIdleCallback()`* allows developers to queue up a function to be +executed as soon as the process is entering an idle period. It enables you to +perform low-priority or background work without impacting the user experience. +For more information about how to use it, +[check out its documentation on MDN][request-idle-callback]. + +*Web Workers* are a powerful tool to run code on a separate thread. There are +some caveats to consider – consult Electron's +[multithreading documentation][multithreading] and the +[MDN documentation for Web Workers][web-workers]. They're an ideal solution +for any operation that requires a lot of CPU power for an extended period of +time. + +### 5. Unnecessary polyfills + +One of Electron's great benefits is that you know exactly which engine will +parse your JavaScript, HTML, and CSS. If you're re-purposing code that was +written for the web at large, make sure to not polyfill features included in +Electron. + +#### Why? + +When building a web application for today's Internet, the oldest environments +dictate what features you can and cannot use. Even though Electron supports +well-performing CSS filters and animations, an older browser might not. Where +you could use WebGL, your developers may have chosen a more resource-hungry +solution to support older phones. + +When it comes to JavaScript, you may have included toolkit libraries like +jQuery for DOM selectors or polyfills like the `regenerator-runtime` to support +`async/await`. + +It is rare for a JavaScript-based polyfill to be faster than the equivalent +native feature in Electron. Do not slow down your Electron app by shipping your +own version of standard web platform features. + +#### How? + +Operate under the assumption that polyfills in current versions of Electron +are unnecessary. If you have doubts, check [caniuse.com](https://caniuse.com/) +and check if the [version of Chromium used in your Electron version](../api/process.md#processversionschrome-readonly) +supports the feature you desire. + +In addition, carefully examine the libraries you use. Are they really necessary? +`jQuery`, for example, was such a success that many of its features are now part +of the [standard JavaScript feature set available][jquery-need]. + +If you're using a transpiler/compiler like TypeScript, examine its configuration +and ensure that you're targeting the latest ECMAScript version supported by +Electron. + +### 6. Unnecessary or blocking network requests + +Avoid fetching rarely changing resources from the internet if they could easily +be bundled with your application. + +#### Why? + +Many users of Electron start with an entirely web-based app that they're +turning into a desktop application. As web developers, we are used to loading +resources from a variety of content delivery networks. Now that you are +shipping a proper desktop application, attempt to "cut the cord" where possible +and avoid letting your users wait for resources that never change and could +easily be included in your app. + +A typical example is Google Fonts. Many developers make use of Google's +impressive collection of free fonts, which comes with a content delivery +network. The pitch is straightforward: Include a few lines of CSS and Google +will take care of the rest. + +When building an Electron app, your users are better served if you download +the fonts and include them in your app's bundle. + +#### How? + +In an ideal world, your application wouldn't need the network to operate at +all. To get there, you must understand what resources your app is downloading +\- and how large those resources are. + +To do so, open up the developer tools. Navigate to the `Network` tab and check +the `Disable cache` option. Then, reload your renderer. Unless your app +prohibits such reloads, you can usually trigger a reload by hitting `Cmd + R` +or `Ctrl + R` with the developer tools in focus. + +The tools will now meticulously record all network requests. In a first pass, +take stock of all the resources being downloaded, focusing on the larger files +first. Are any of them images, fonts, or media files that don't change and +could be included with your bundle? If so, include them. + +As a next step, enable `Network Throttling`. Find the drop-down that currently +reads `Online` and select a slower speed such as `Fast 3G`. Reload your +renderer and see if there are any resources that your app is unnecessarily +waiting for. In many cases, an app will wait for a network request to complete +despite not actually needing the involved resource. + +As a tip, loading resources from the Internet that you might want to change +without shipping an application update is a powerful strategy. For advanced +control over how resources are being loaded, consider investing in +[Service Workers][service-workers]. + +### 7. Bundle your code + +As already pointed out in +"[Loading and running code too soon](#2-loading-and-running-code-too-soon)", +calling `require()` is an expensive operation. If you are able to do so, +bundle your application's code into a single file. + +#### Why? + +Modern JavaScript development usually involves many files and modules. While +that's perfectly fine for developing with Electron, we heavily recommend that +you bundle all your code into one single file to ensure that the overhead +included in calling `require()` is only paid once when your application loads. + +#### How? + +There are numerous JavaScript bundlers out there and we know better than to +anger the community by recommending one tool over another. We do however +recommend that you use a bundler that is able to handle Electron's unique +environment that needs to handle both Node.js and browser environments. + +As of writing this article, the popular choices include [Webpack][webpack], +[Parcel][parcel], and [rollup.js][rollup]. + +[security]: ./security.md +[chrome-devtools-tutorial]: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/ +[worker-threads]: https://nodejs.org/api/worker_threads.html +[web-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers +[request-idle-callback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback +[multithreading]: ./multithreading.md +[caniuse]: https://caniuse.com/ +[jquery-need]: http://youmightnotneedjquery.com/ +[service-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API +[webpack]: https://webpack.js.org/ +[parcel]: https://parceljs.org/ +[rollup]: https://rollupjs.org/ +[vscode-first-second]: https://www.youtube.com/watch?v=r0OeHRUCCb4 diff --git a/docs/tutorial/process-model.md b/docs/tutorial/process-model.md new file mode 100644 index 0000000000000..2cb37099b513b --- /dev/null +++ b/docs/tutorial/process-model.md @@ -0,0 +1,221 @@ +--- +title: 'Process Model' +description: 'Electron inherits its multi-process architecture from Chromium, which makes the framework architecturally very similar to a modern web browser. This guide will expand on the concepts applied in the tutorial.' +slug: process-model +hide_title: false +--- + +# Process Model + +Electron inherits its multi-process architecture from Chromium, which makes the framework +architecturally very similar to a modern web browser. This guide will expand on the +concepts applied in the [Tutorial][tutorial]. + +[tutorial]: ./tutorial-1-prerequisites.md + +## Why not a single process? + +Web browsers are incredibly complicated applications. Aside from their primary ability +to display web content, they have many secondary responsibilities, +such as managing multiple windows (or tabs) and loading third-party extensions. + +In the earlier days, browsers usually used a single process for all of this +functionality. Although this pattern meant less overhead for each tab you had open, +it also meant that one website crashing or hanging would affect the entire browser. + +## The multi-process model + +To solve this problem, the Chrome team decided that each tab would render in its own +process, limiting the harm that buggy or malicious code on a web page could cause to +the app as a whole. A single browser process then controls these processes, as well +as the application lifecycle as a whole. This diagram below from the [Chrome Comic] +visualizes this model: + +![Chrome's multi-process architecture](../images/chrome-processes.png) + +Electron applications are structured very similarly. As an app developer, you control +two types of processes: [main](#the-main-process) and [renderer](#the-renderer-process). +These are analogous to Chrome's own browser and renderer processes outlined above. + +[chrome comic]: https://www.google.com/googlebooks/chrome/ + +## The main process + +Each Electron app has a single main process, which acts as the application's entry +point. The main process runs in a Node.js environment, meaning it has the ability +to `require` modules and use all of Node.js APIs. + +### Window management + +The main process' primary purpose is to create and manage application windows with the +[`BrowserWindow`][browser-window] module. + +Each instance of the `BrowserWindow` class creates an application window that loads +a web page in a separate renderer process. You can interact with this web content +from the main process using the window's [`webContents`][web-contents] object. + +```js title='main.js' +const { BrowserWindow } = require('electron') + +const win = new BrowserWindow({ width: 800, height: 1500 }) +win.loadURL('https://github.com') + +const contents = win.webContents +console.log(contents) +``` + +> Note: A renderer process is also created for [web embeds][web-embed] such as the +> `BrowserView` module. The `webContents` object is also accessible for embedded +> web content. + +Because the `BrowserWindow` module is an [`EventEmitter`][event-emitter], you can also +add handlers for various user events (for example, minimizing or maximizing your window). + +When a `BrowserWindow` instance is destroyed, its corresponding renderer process gets +terminated as well. + +[browser-window]: ../api/browser-window.md +[web-embed]: ../tutorial/web-embeds.md +[web-contents]: ../api/web-contents.md +[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter + +### Application lifecycle + +The main process also controls your application's lifecycle through Electron's +[`app`][app] module. This module provides a large set of events and methods +that you can use to add custom application behaviour (for instance, programatically +quitting your application, modifying the application dock, or showing an About panel). + +As a practical example, the app shown in the [quick start guide][quick-start-lifecycle] +uses `app` APIs to create a more native application window experience. + +```js title='main.js' +// quitting the app when no windows are open on non-macOS platforms +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) +``` + +[app]: ../api/app.md +[quick-start-lifecycle]: ../tutorial/quick-start.md#manage-your-windows-lifecycle + +### Native APIs + +To extend Electron's features beyond being a Chromium wrapper for web contents, the +main process also adds custom APIs to interact with the user's operating system. +Electron exposes various modules that control native desktop functionality, such +as menus, dialogs, and tray icons. + +For a full list of Electron's main process modules, check out our API documentation. + +## The renderer process + +Each Electron app spawns a separate renderer process for each open `BrowserWindow` +(and each web embed). As its name implies, a renderer is responsible for +_rendering_ web content. For all intents and purposes, code ran in renderer processes +should behave according to web standards (insofar as Chromium does, at least). + +Therefore, all user interfaces and app functionality within a single browser +window should be written with the same tools and paradigms that you use on the +web. + +Although explaining every web spec is out of scope for this guide, the bare minimum +to understand is: + +- An HTML file is your entry point for the renderer process. +- UI styling is added through Cascading Style Sheets (CSS). +- Executable JavaScript code can be added through ` +``` + +The code contained in `renderer.js` can then use the same JavaScript APIs and tooling +you use for typical front-end development, such as using [`webpack`][webpack] to bundle +and minify your code or [React][react] to manage your user interfaces. + +[webpack]: https://webpack.js.org +[react]: https://reactjs.org + +### Recap + +After following the above steps, you should have a fully functional +Electron application that looks like this: + +![Simplest Electron app](../images/simplest-electron-app.png) + + +The full code is available below: + +```js +// main.js + +// Modules to control application life and create native browser window +const { app, BrowserWindow } = require('electron') +const path = require('path') + +const createWindow = () => { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + // and load the index.html of the app. + mainWindow.loadFile('index.html') + + // Open the DevTools. + // mainWindow.webContents.openDevTools() +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. +``` + +```js +// preload.js + +// All of the Node.js APIs are available in the preload process. +// It has the same sandbox as a Chrome extension. +window.addEventListener('DOMContentLoaded', () => { + const replaceText = (selector, text) => { + const element = document.getElementById(selector) + if (element) element.innerText = text + } + + for (const dependency of ['chrome', 'node', 'electron']) { + replaceText(`${dependency}-version`, process.versions[dependency]) + } +}) +``` + +```html + + + + + + + + + Hello World! + + +

Hello World!

+ We are using Node.js , + Chromium , + and Electron . + + + + + +``` + +```fiddle docs/fiddles/quick-start +``` + +To summarize all the steps we've done: + +* We bootstrapped a Node.js application and added Electron as a dependency. +* We created a `main.js` script that runs our main process, which controls our app + and runs in a Node.js environment. In this script, we used Electron's `app` and + `BrowserWindow` modules to create a browser window that displays web content + in a separate process (the renderer). +* In order to access certain Node.js functionality in the renderer, we attached + a preload script to our `BrowserWindow` constructor. + +## Package and distribute your application + +The fastest way to distribute your newly created app is using +[Electron Forge](https://www.electronforge.io). + +1. Add Electron Forge as a development dependency of your app, and use its `import` command to set up +Forge's scaffolding: + + ```sh npm2yarn + npm install --save-dev @electron-forge/cli + npx electron-forge import + + ✔ Checking your system + ✔ Initializing Git Repository + ✔ Writing modified package.json file + ✔ Installing dependencies + ✔ Writing modified package.json file + ✔ Fixing .gitignore + + We have ATTEMPTED to convert your app to be in a format that electron-forge understands. + + Thanks for using "electron-forge"!!! + ``` + +2. Create a distributable using Forge's `make` command: + + ```sh npm2yarn + npm run make + + > my-electron-app@1.0.0 make /my-electron-app + > electron-forge make + + ✔ Checking your system + ✔ Resolving Forge Config + We need to package your application before we can make it + ✔ Preparing to Package Application for arch: x64 + ✔ Preparing native dependencies + ✔ Packaging Application + Making for the following targets: zip + ✔ Making for target: zip - On platform: darwin - For arch: x64 + ``` + + Electron Forge creates the `out` folder where your package will be located: + + ```plain + // Example for macOS + out/ + ├── out/make/zip/darwin/x64/my-electron-app-darwin-x64-1.0.0.zip + ├── ... + └── out/my-electron-app-darwin-x64/my-electron-app.app/Contents/MacOS/my-electron-app + ``` diff --git a/docs/tutorial/recent-documents.md b/docs/tutorial/recent-documents.md index 2bb35d9410253..f1ca0c3251cb8 100644 --- a/docs/tutorial/recent-documents.md +++ b/docs/tutorial/recent-documents.md @@ -1,4 +1,13 @@ -# Recent Documents (Windows & macOS) +--- +title: Recent Documents +description: Provide a list of recent documents via Windows JumpList or macOS Dock +slug: recent-documents +hide_title: true +--- + +# Recent Documents + +## Overview Windows and macOS provide access to a list of recent documents opened by the application via JumpList or dock menu, respectively. @@ -11,33 +20,116 @@ __Application dock menu:__ ![macOS Dock Menu][dock-menu-image] -To add a file to recent documents, you can use the -[app.addRecentDocument][addrecentdocument] API: +## Example -```javascript -const { app } = require('electron') -app.addRecentDocument('/Users/USERNAME/Desktop/work.type') -``` +### Managing recent documents -And you can use [app.clearRecentDocuments][clearrecentdocuments] API to empty -the recent documents list: +```javascript fiddle='docs/fiddles/features/recent-documents' +const { app, BrowserWindow } = require('electron') +const fs = require('fs') +const path = require('path') -```javascript -const { app } = require('electron') -app.clearRecentDocuments() +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadFile('index.html') +} + +const fileName = 'recently-used.md' +fs.writeFile(fileName, 'Lorem Ipsum', () => { + app.addRecentDocument(path.join(__dirname, fileName)) +}) + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + app.clearRecentDocuments() + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) ``` -## Windows Notes +#### Adding a recent document + +To add a file to recent documents, use the +[app.addRecentDocument][addrecentdocument] API. + +After launching the Electron application, right click the application icon. +In this guide, the item is a Markdown file located in the root of the project. +You should see `recently-used.md` added to the list of recent files: + +![Recent document](../images/recent-documents.png) + +#### Clearing the list of recent documents -In order to be able to use this feature on Windows, your application has to be -registered as a handler of the file type of the document, otherwise the file -won't appear in JumpList even after you have added it. You can find everything +To clear the list of recent documents, use the +[app.clearRecentDocuments][clearrecentdocuments] API. +In this guide, the list of documents is cleared once all windows have been +closed. + +## Additional information + +### Windows Notes + +To use this feature on Windows, your application has to be registered as +a handler of the file type of the document, otherwise the file won't appear +in JumpList even after you have added it. You can find everything on registering your application in [Application Registration][app-registration]. When a user clicks a file from the JumpList, a new instance of your application will be started with the path of the file added as a command line argument. -## macOS Notes +### macOS Notes + +#### Add the Recent Documents list to the application menu + +You can add menu items to access and clear recent documents by adding the +following code snippet to your menu template: + +```json +{ + "submenu":[ + { + "label":"Open Recent", + "role":"recentdocuments", + "submenu":[ + { + "label":"Clear Recent", + "role":"clearrecentdocuments" + } + ] + } + ] +} +``` + +Make sure the application menu is added after the [`'ready'`](../api/app.md#event-ready) +event and not before, or the menu item will be disabled: + +```javascript +const { app, Menu } = require('electron') + +const template = [ + // Menu template here +] +const menu = Menu.buildFromTemplate(template) + +app.whenReady().then(() => { + Menu.setApplicationMenu(menu) +}) +``` + +![macOS Recent Documents menu item][menu-item-image] When a file is requested from the recent documents menu, the `open-file` event of `app` module will be emitted for it. @@ -47,3 +139,4 @@ of `app` module will be emitted for it. [addrecentdocument]: ../api/app.md#appaddrecentdocumentpath-macos-windows [clearrecentdocuments]: ../api/app.md#appclearrecentdocuments-macos-windows [app-registration]: https://msdn.microsoft.com/en-us/library/cc144104(VS.85).aspx +[menu-item-image]: https://user-images.githubusercontent.com/3168941/33003655-ea601c3a-cd70-11e7-97fa-7c062149cfb1.png diff --git a/docs/tutorial/repl.md b/docs/tutorial/repl.md index 4f37c3a625046..82d9c035e1b3d 100644 --- a/docs/tutorial/repl.md +++ b/docs/tutorial/repl.md @@ -1,26 +1,23 @@ # REPL -Read-Eval-Print-Loop (REPL) is a simple, interactive computer programming -environment that takes single user inputs (i.e. single expressions), evaluates -them, and returns the result to the user. +[Read-Eval-Print-Loop](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) (REPL) +is a simple, interactive computer programming environment that takes single user +inputs (i.e. single expressions), evaluates them, and returns the result to the user. -The `repl` module provides a REPL implementation that can be accessed using: +## Main process -* Assuming you have `electron` or `electron-prebuilt` installed as a local - project dependency: +Electron exposes the [Node.js `repl` module](https://nodejs.org/dist/latest/docs/api/repl.html) +through the `--interactive` CLI flag. Assuming you have `electron` installed as a local project +dependency, you should be able to access the REPL with the following command: ```sh ./node_modules/.bin/electron --interactive ``` -* Assuming you have `electron` or `electron-prebuilt` installed globally: - ```sh - electron --interactive - ``` - -This only creates a REPL for the main process. You can use the Console -tab of the Dev Tools to get a REPL for the renderer processes. +**Note:** `electron --interactive` is not available on Windows +(see [electron/electron#5776](https://github.com/electron/electron/pull/5776) for more details). -**Note:** `electron --interactive` is not available on Windows. +## Renderer process -More information can be found in the [Node.js REPL docs](https://nodejs.org/dist/latest/docs/api/repl.html). +You can use the DevTools Console tab to get a REPL for any renderer process. +To learn more, read [the Chrome documentation](https://developer.chrome.com/docs/devtools/console/). diff --git a/docs/tutorial/represented-file.md b/docs/tutorial/represented-file.md index b8867d628587d..a4d94d20154b1 100644 --- a/docs/tutorial/represented-file.md +++ b/docs/tutorial/represented-file.md @@ -1,28 +1,69 @@ -# Represented File for macOS BrowserWindows +--- +title: Representing Files in a BrowserWindow +description: Set a represented file in the macOS title bar. +slug: represented-file +hide_title: true +--- -On macOS a window can set its represented file, so the file's icon can show in -the title bar and when users Command-Click or Control-Click on the title a path -popup will show. +# Representing Files in a BrowserWindow -You can also set the edited state of a window so that the file icon can indicate -whether the document in this window has been modified. +## Overview -__Represented file popup menu:__ +On macOS, you can set a represented file for any window in your application. +The represented file's icon will be shown in the title bar, and when users +`Command-Click` or `Control-Click`, a popup with a path to the file will be +shown. ![Represented File][represented-image] +> NOTE: The screenshot above is an example where this feature is used to indicate the currently opened file in the Atom text editor. + +You can also set the edited state for a window so that the file icon can +indicate whether the document in this window has been modified. + To set the represented file of window, you can use the [BrowserWindow.setRepresentedFilename][setrepresentedfilename] and -[BrowserWindow.setDocumentEdited][setdocumentedited] APIs: +[BrowserWindow.setDocumentEdited][setdocumentedited] APIs. + +## Example + +```javascript fiddle='docs/fiddles/features/represented-file' +const { app, BrowserWindow } = require('electron') +const os = require('os'); + +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) +} -```javascript -const { BrowserWindow } = require('electron') +app.whenReady().then(() => { + const win = new BrowserWindow() -const win = new BrowserWindow() -win.setRepresentedFilename('/etc/passwd') -win.setDocumentEdited(true) + win.setRepresentedFilename(os.homedir()) + win.setDocumentEdited(true) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) ``` +After launching the Electron application, click on the title with `Command` or +`Control` key pressed. You should see a popup with the represented file at the top. +In this guide, this is the current user's home directory: + +![Represented file](../images/represented-file.png) + [represented-image]: https://cloud.githubusercontent.com/assets/639601/5082061/670a949a-6f14-11e4-987a-9aaa04b23c1d.png [setrepresentedfilename]: ../api/browser-window.md#winsetrepresentedfilenamefilename-macos [setdocumentedited]: ../api/browser-window.md#winsetdocumenteditededited-macos diff --git a/docs/tutorial/sandbox.md b/docs/tutorial/sandbox.md new file mode 100644 index 0000000000000..0eb955c5da75f --- /dev/null +++ b/docs/tutorial/sandbox.md @@ -0,0 +1,169 @@ +# Process Sandboxing + +One key security feature in Chromium is that processes can be executed within a sandbox. +The sandbox limits the harm that malicious code can cause by limiting access to most +system resources — sandboxed processes can only freely use CPU cycles and memory. +In order to perform operations requiring additional privilege, sandboxed processes +use dedicated communication channels to delegate tasks to more privileged processes. + +In Chromium, sandboxing is applied to most processes other than the main process. +This includes renderer processes, as well as utility processes such as the audio service, +the GPU service and the network service. + +See Chromium's [Sandbox design document][sandbox] for more information. + +## Electron's sandboxing policies + +Electron comes with a mixed sandbox environment, meaning sandboxed processes can run +alongside privileged ones. By default, renderer processes are not sandboxed, but +utility processes are. Note that as in Chromium, the main (browser) process is +privileged and cannot be sandboxed. + +Historically, this mixed sandbox approach was established because having Node.js available +in the renderer is an extremely powerful tool for app developers. Unfortunately, this +feature is also an equally massive security vulnerability. + +Theoretically, unsandboxed renderers are not a problem for desktop applications that +only display trusted code, but they make Electron less secure than Chromium for +displaying untrusted web content. However, even purportedly trusted code may be +dangerous — there are countless attack vectors that malicious actors can use, from +cross-site scripting to content injection to man-in-the-middle attacks on remotely loaded +websites, just to name a few. For this reason, we recommend enabling renderer sandboxing +for the vast majority of cases under an abundance of caution. + + +Note that there is an active discussion in the issue tracker to enable renderer sandboxing +by default. See [#28466][issue-28466]) for details. + +## Sandbox behaviour in Electron + +Sandboxed processes in Electron behave _mostly_ in the same way as Chromium's do, but +Electron has a few additional concepts to consider because it interfaces with Node.js. + +### Renderer processes + +When renderer processes in Electron are sandboxed, they behave in the same way as a +regular Chrome renderer would. A sandboxed renderer won't have a Node.js +environment initialized. + + +Therefore, when the sandbox is enabled, renderer processes can only perform privileged +tasks (such as interacting with the filesystem, making changes to the system, or spawning +subprocesses) by delegating these tasks to the main process via inter-process +communication (IPC). + +### Preload scripts + +In order to allow renderer processes to communicate with the main process, preload +scripts attached to sandboxed renderers will still have a polyfilled subset of Node.js +APIs available. A `require` function similar to Node's `require` module is exposed, +but can only import a subset of Electron and Node's built-in modules: + +* `electron` (only renderer process modules) +* [`events`](https://nodejs.org/api/events.html) +* [`timers`](https://nodejs.org/api/timers.html) +* [`url`](https://nodejs.org/api/url.html) + +In addition, the preload script also polyfills certain Node.js primitives as globals: + +* [`Buffer`](https://nodejs.org/api/Buffer.html) +* [`process`](../api/process.md) +* [`clearImmediate`](https://nodejs.org/api/timers.html#timers_clearimmediate_immediate) +* [`setImmediate`](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args) + +Because the `require` function is a polyfill with limited functionality, you will not be +able to use [CommonJS modules][commonjs] to separate your preload script into multiple +files. If you need to split your preload code, use a bundler such as [webpack][webpack] +or [Parcel][parcel]. + +Note that because the environment presented to the `preload` script is substantially +more privileged than that of a sandboxed renderer, it is still possible to leak +privileged APIs to untrusted code running in the renderer process unless +[`contextIsolation`][context-isolation] is enabled. + +## Configuring the sandbox + +### Enabling the sandbox for a single process + +In Electron, renderer sandboxing can be enabled on a per-process basis with +the `sandbox: true` preference in the [`BrowserWindow`][browser-window] constructor. + +```js +// main.js +app.whenReady().then(() => { + const win = new BrowserWindow({ + webPreferences: { + sandbox: true + } + }) + win.loadURL('https://google.com') +}) +``` + +### Enabling the sandbox globally + +If you want to force sandboxing for all renderers, you can also use the +[`app.enableSandbox`][enable-sandbox] API. Note that this API has to be called before the +app's `ready` event. + +```js +// main.js +app.enableSandbox() +app.whenReady().then(() => { + // no need to pass `sandbox: true` since `app.enableSandbox()` was called. + const win = new BrowserWindow() + win.loadURL('https://google.com') +}) +``` + +### Disabling Chromium's sandbox (testing only) + +You can also disable Chromium's sandbox entirely with the [`--no-sandbox`][no-sandbox] +CLI flag, which will disable the sandbox for all processes (including utility processes). +We highly recommend that you only use this flag for testing purposes, and **never** +in production. + +Note that the `sandbox: true` option will still disable the renderer's Node.js +environment. + +## A note on rendering untrusted content + +Rendering untrusted content in Electron is still somewhat uncharted territory, +though some apps are finding success (e.g. [Beaker Browser][beaker]). +Our goal is to get as close to Chrome as we can in terms of the security of +sandboxed content, but ultimately we will always be behind due to a few fundamental +issues: + +1. We do not have the dedicated resources or expertise that Chromium has to + apply to the security of its product. We do our best to make use of what we + have, to inherit everything we can from Chromium, and to respond quickly to + security issues, but Electron cannot be as secure as Chromium without the + resources that Chromium is able to dedicate. +2. Some security features in Chrome (such as Safe Browsing and Certificate + Transparency) require a centralized authority and dedicated servers, both of + which run counter to the goals of the Electron project. As such, we disable + those features in Electron, at the cost of the associated security they + would otherwise bring. +3. There is only one Chromium, whereas there are many thousands of apps built + on Electron, all of which behave slightly differently. Accounting for those + differences can yield a huge possibility space, and make it challenging to + ensure the security of the platform in unusual use cases. +4. We can't push security updates to users directly, so we rely on app vendors + to upgrade the version of Electron underlying their app in order for + security updates to reach users. + +While we make our best effort to backport Chromium security fixes to older +versions of Electron, we do not make a guarantee that every fix will be +backported. Your best chance at staying secure is to be on the latest stable +version of Electron. + +[sandbox]: https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md +[issue-28466]: https://github.com/electron/electron/issues/28466 +[browser-window]: ../api/browser-window.md +[enable-sandbox]: ../api/app.md#appenablesandbox +[no-sandbox]: ../api/command-line-switches.md#--no-sandbox +[commonjs]: https://nodejs.org/api/modules.html#modules_modules_commonjs_modules +[webpack]: https://webpack.js.org/ +[parcel]: https://parceljs.org/ +[context-isolation]: ./context-isolation.md +[beaker]: https://github.com/beakerbrowser/beaker diff --git a/docs/tutorial/security.md b/docs/tutorial/security.md index 1e475b8a20592..688dda5a68961 100644 --- a/docs/tutorial/security.md +++ b/docs/tutorial/security.md @@ -1,6 +1,24 @@ -# Security, Native Capabilities, and Your Responsibility +--- +title: Security +description: A set of guidelines for building secure Electron apps +slug: security +hide_title: true +toc_max_heading_level: 3 +--- +# Security + +:::info Reporting security issues +For information on how to properly disclose an Electron vulnerability, +see [SECURITY.md](https://github.com/electron/electron/tree/main/SECURITY.md). + +For upstream Chromium vulnerabilities: Electron keeps up to date with alternating +Chromium releases. For more information, see the +[Electron Release Timelines](../tutorial/electron-timelines.md) document. +::: + +## Preface -As web developers, we usually enjoy the strong security net of the browser - +As web developers, we usually enjoy the strong security net of the browser — the risks associated with the code we write are relatively small. Our websites are granted limited powers in a sandbox, and we trust that our users enjoy a browser built by a large team of engineers that is able to quickly respond to @@ -17,30 +35,12 @@ With that in mind, be aware that displaying arbitrary content from untrusted sources poses a severe security risk that Electron is not intended to handle. In fact, the most popular Electron apps (Atom, Slack, Visual Studio Code, etc) display primarily local content (or trusted, secure remote content without Node -integration) – if your application executes code from an online source, it is +integration) — if your application executes code from an online source, it is your responsibility to ensure that the code is not malicious. -## Reporting Security Issues - -For information on how to properly disclose an Electron vulnerability, -see [SECURITY.md](https://github.com/electron/electron/tree/master/SECURITY.md) - -## Chromium Security Issues and Upgrades +## General guidelines -While Electron strives to support new versions of Chromium as soon as possible, -developers should be aware that upgrading is a serious undertaking - involving -hand-editing dozens or even hundreds of files. Given the resources and -contributions available today, Electron will often not be on the very latest -version of Chromium, lagging behind by several weeks or a few months. - -We feel that our current system of updating the Chromium component strikes an -appropriate balance between the resources we have available and the needs of -the majority of applications built on top of the framework. We definitely are -interested in hearing more about specific use cases from the people that build -things on top of Electron. Pull requests and contributions supporting this -effort are always very welcome. - -## Security Is Everyone's Responsibility +### Security is everyone's responsibility It is important to remember that the security of your Electron application is the result of the overall security of the framework foundation @@ -53,7 +53,8 @@ When releasing your product, you’re also shipping a bundle composed of Electro Chromium shared library and Node.js. Vulnerabilities affecting these components may impact the security of your application. By updating Electron to the latest version, you ensure that critical vulnerabilities (such as *nodeIntegration bypasses*) -are already patched and cannot be exploited in your application. +are already patched and cannot be exploited in your application. For more information, +see "[Use a current version of Electron](#16-use-a-current-version-of-electron)". * **Evaluate your dependencies.** While NPM provides half a million reusable packages, it is your responsibility to choose trusted 3rd-party libraries. If you use outdated @@ -65,8 +66,7 @@ is your own code. Common web vulnerabilities, such as Cross-Site Scripting (XSS) have a higher security impact on Electron applications hence it is highly recommended to adopt secure software development best practices and perform security testing. - -## Isolation For Untrusted Content +### Isolation for untrusted content A security issue exists whenever you receive code from an untrusted source (e.g. a remote server) and execute it locally. As an example, consider a remote @@ -75,72 +75,74 @@ an attacker somehow manages to change said content (either by attacking the source directly, or by sitting between your app and the actual destination), they will be able to execute native code on the user's machine. -> :warning: Under no circumstances should you load and execute remote code with +:::warning +Under no circumstances should you load and execute remote code with Node.js integration enabled. Instead, use only local files (packaged together with your application) to execute Node.js code. To display remote content, use the [``][webview-tag] tag or [`BrowserView`][browser-view], make sure to disable the `nodeIntegration` and enable `contextIsolation`. +::: -## Electron Security Warnings - -From Electron 2.0 on, developers will see warnings and recommendations printed -to the developer console. They only show up when the binary's name is Electron, -indicating that a developer is currently looking at the console. +:::info Electron security warnings +Security warnings and recommendations are printed to the developer console. +They only show up when the binary's name is Electron, indicating that a developer +is currently looking at the console. You can force-enable or force-disable these warnings by setting `ELECTRON_ENABLE_SECURITY_WARNINGS` or `ELECTRON_DISABLE_SECURITY_WARNINGS` on either `process.env` or the `window` object. +::: -## Checklist: Security Recommendations +## Checklist: Security recommendations You should at least follow these steps to improve the security of your application: 1. [Only load secure content](#1-only-load-secure-content) 2. [Disable the Node.js integration in all renderers that display remote content](#2-do-not-enable-nodejs-integration-for-remote-content) 3. [Enable context isolation in all renderers that display remote content](#3-enable-context-isolation-for-remote-content) -4. [Use `ses.setPermissionRequestHandler()` in all sessions that load remote content](#4-handle-session-permission-requests-from-remote-content) -5. [Do not disable `webSecurity`](#5-do-not-disable-websecurity) -6. [Define a `Content-Security-Policy`](#6-define-a-content-security-policy) and use restrictive rules (i.e. `script-src 'self'`) -7. [Do not set `allowRunningInsecureContent` to `true`](#7-do-not-set-allowrunninginsecurecontent-to-true) -8. [Do not enable experimental features](#8-do-not-enable-experimental-features) -9. [Do not use `enableBlinkFeatures`](#9-do-not-use-enableblinkfeatures) -10. [``: Do not use `allowpopups`](#10-do-not-use-allowpopups) -11. [``: Verify options and params](#11-verify-webview-options-before-creation) -12. [Disable or limit navigation](#12-disable-or-limit-navigation) -13. [Disable or limit creation of new windows](#13-disable-or-limit-creation-of-new-windows) -14. [Do not use `openExternal` with untrusted content](#14-do-not-use-openexternal-with-untrusted-content) -15. [Disable the `remote` module](#15-disable-the-remote-module) -16. [Filter the `remote` module](#16-filter-the-remote-module) +4. [Enable process sandboxing](#4-enable-process-sandboxing) +5. [Use `ses.setPermissionRequestHandler()` in all sessions that load remote content](#5-handle-session-permission-requests-from-remote-content) +6. [Do not disable `webSecurity`](#6-do-not-disable-websecurity) +7. [Define a `Content-Security-Policy`](#7-define-a-content-security-policy) and use restrictive rules (i.e. `script-src 'self'`) +8. [Do not enable `allowRunningInsecureContent`](#8-do-not-enable-allowrunninginsecurecontent) +9. [Do not enable experimental features](#9-do-not-enable-experimental-features) +10. [Do not use `enableBlinkFeatures`](#10-do-not-use-enableblinkfeatures) +11. [``: Do not use `allowpopups`](#11-do-not-use-allowpopups-for-webviews) +12. [``: Verify options and params](#12-verify-webview-options-before-creation) +13. [Disable or limit navigation](#13-disable-or-limit-navigation) +14. [Disable or limit creation of new windows](#14-disable-or-limit-creation-of-new-windows) +15. [Do not use `shell.openExternal` with untrusted content](#15-do-not-use-shellopenexternal-with-untrusted-content) +16. [Use a current version of Electron](#16-use-a-current-version-of-electron) To automate the detection of misconfigurations and insecure patterns, it is possible to use -[electronegativity](https://github.com/doyensec/electronegativity). For +[Electronegativity](https://github.com/doyensec/electronegativity). For additional details on potential weaknesses and implementation bugs when developing applications using Electron, please refer to this [guide for -developers and auditors](https://doyensec.com/resources/us-17-Carettoni-Electronegativity-A-Study-Of-Electron-Security-wp.pdf) +developers and auditors](https://doyensec.com/resources/us-17-Carettoni-Electronegativity-A-Study-Of-Electron-Security-wp.pdf). -## 1) Only Load Secure Content +### 1. Only load secure content Any resources not included with your application should be loaded using a secure protocol like `HTTPS`. In other words, do not use insecure protocols like `HTTP`. Similarly, we recommend the use of `WSS` over `WS`, `FTPS` over `FTP`, and so on. -### Why? +#### Why? `HTTPS` has three main benefits: -1) It authenticates the remote server, ensuring your app connects to the correct +1. It authenticates the remote server, ensuring your app connects to the correct host instead of an impersonator. -2) It ensures data integrity, asserting that the data was not modified while in +1. It ensures data integrity, asserting that the data was not modified while in transit between your application and the host. -3) It encrypts the traffic between your user and the destination host, making it +1. It encrypts the traffic between your user and the destination host, making it more difficult to eavesdrop on the information sent between your app and the host. -### How? +#### How? -```js +```js title='main.js (Main Process)' // Bad browserWindow.loadURL('http://example.com') @@ -148,7 +150,7 @@ browserWindow.loadURL('http://example.com') browserWindow.loadURL('https://example.com') ``` -```html +```html title='index.html (Renderer Process)' @@ -158,10 +160,11 @@ browserWindow.loadURL('https://example.com') ``` +### 2. Do not enable Node.js integration for remote content -## 2) Do not enable Node.js Integration for Remote Content - -_This recommendation is the default behavior in Electron since 5.0.0._ +:::info +This recommendation is the default behavior in Electron since 5.0.0. +::: It is paramount that you do not enable Node.js integration in any renderer ([`BrowserWindow`][browser-window], [`BrowserView`][browser-view], or @@ -171,10 +174,10 @@ for an attacker to harm your users should they gain the ability to execute JavaScript on your website. After this, you can grant additional permissions for specific hosts. For example, -if you are opening a BrowserWindow pointed at `https://example.com/", you can +if you are opening a BrowserWindow pointed at `https://example.com/`, you can give that website exactly the abilities it needs, but no more. -### Why? +#### Why? A cross-site-scripting (XSS) attack is more dangerous if an attacker can jump out of the renderer process and execute code on the user's computer. @@ -183,12 +186,13 @@ power is usually limited to messing with the website that they are executed on. Disabling Node.js integration helps prevent an XSS from being escalated into a so-called "Remote Code Execution" (RCE) attack. -### How? +#### How? -```js +```js title='main.js (Main Process)' // Bad const mainWindow = new BrowserWindow({ webPreferences: { + contextIsolation: false, nodeIntegration: true, nodeIntegrationInWorker: true } @@ -197,7 +201,7 @@ const mainWindow = new BrowserWindow({ mainWindow.loadURL('https://example.com') ``` -```js +```js title='main.js (Main Process)' // Good const mainWindow = new BrowserWindow({ webPreferences: { @@ -208,7 +212,7 @@ const mainWindow = new BrowserWindow({ mainWindow.loadURL('https://example.com') ``` -```html +```html title='index.html (Renderer Process)' @@ -219,22 +223,13 @@ mainWindow.loadURL('https://example.com') When disabling Node.js integration, you can still expose APIs to your website that do consume Node.js modules or features. Preload scripts continue to have access to `require` and other Node.js features, allowing developers to expose a custom -API to remotely loaded content. +API to remotely loaded content via the [contextBridge API](../api/context-bridge.md). -In the following example preload script, the later loaded website will have -access to a `window.readConfig()` method, but no Node.js features. +### 3. Enable Context Isolation for remote content -```js -const { readFileSync } = require('fs') - -window.readConfig = function () { - const data = readFileSync('./config.json') - return data -} -``` - - -## 3) Enable Context Isolation for Remote Content +:::info +This recommendation is the default behavior in Electron since 12.0.0. +::: Context isolation is an Electron feature that allows developers to run code in preload scripts and in Electron APIs in a dedicated JavaScript context. In @@ -244,74 +239,45 @@ practice, that means that global objects like `Array.prototype.push` or Electron uses the same technology as Chromium's [Content Scripts](https://developer.chrome.com/extensions/content_scripts#execution-environment) to enable this behavior. -Even when you use `nodeIntegration: false` to enforce strong isolation and -prevent the use of Node primitives, `contextIsolation` must also be used. - -### Why? +Even when `nodeIntegration: false` is used, to truly enforce strong isolation +and prevent the use of Node primitives `contextIsolation` **must** also be used. -Context isolation allows each the scripts on running in the renderer to make -changes to its JavaScript environment without worrying about conflicting with -the scripts in the Electron API or the preload script. +:::info +For more information on what `contextIsolation` is and how to enable it please +see our dedicated [Context Isolation](context-isolation.md) document. +:::info -While still an experimental Electron feature, context isolation adds an -additional layer of security. It creates a new JavaScript world for Electron -APIs and preload scripts, which mitigates so-called "Prototype Pollution" attacks. +### 4. Enable process sandboxing -At the same time, preload scripts still have access to the `document` and -`window` objects. In other words, you're getting a decent return on a likely -very small investment. +[Sandboxing](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/design/sandbox.md) +is a Chromium feature that uses the operating system to +significantly limit what renderer processes have access to. You should enable +the sandbox in all renderers. Loading, reading or processing any untrusted +content in an unsandboxed process, including the main process, is not advised. -### How? +:::info +For more information on what `contextIsolation` is and how to enable it please +see our dedicated [Process Sandboxing](sandbox.md) document. +:::info -```js -// Main process -const mainWindow = new BrowserWindow({ - webPreferences: { - contextIsolation: true, - preload: path.join(app.getAppPath(), 'preload.js') - } -}) -``` +### 5. Handle session permission requests from remote content -```js -// Preload script - -// Set a variable in the page before it loads -webFrame.executeJavaScript('window.foo = "foo";') - -// The loaded page will not be able to access this, it is only available -// in this context -window.bar = 'bar' - -document.addEventListener('DOMContentLoaded', () => { - // Will log out 'undefined' since window.foo is only available in the main - // context - console.log(window.foo) - - // Will log out 'bar' since window.bar is available in this context - console.log(window.bar) -}) -``` - - -## 4) Handle Session Permission Requests From Remote Content - -You may have seen permission requests while using Chrome: They pop up whenever +You may have seen permission requests while using Chrome: they pop up whenever the website attempts to use a feature that the user has to manually approve ( like notifications). The API is based on the [Chromium permissions API](https://developer.chrome.com/extensions/permissions) and implements the same types of permissions. -### Why? +#### Why? By default, Electron will automatically approve all permission requests unless the developer has manually configured a custom handler. While a solid default, security-conscious developers might want to assume the very opposite. -### How? +#### How? -```js +```js title='main.js (Main Process)' const { session } = require('electron') session @@ -332,10 +298,11 @@ session }) ``` +### 6. Do not disable `webSecurity` -## 5) Do Not Disable WebSecurity - -_Recommendation is Electron's default_ +:::info +This recommendation is Electron's default. +::: You may have already guessed that disabling the `webSecurity` property on a renderer process ([`BrowserWindow`][browser-window], @@ -344,14 +311,15 @@ security features. Do not disable `webSecurity` in production applications. -### Why? +#### Why? Disabling `webSecurity` will disable the same-origin policy and set `allowRunningInsecureContent` property to `true`. In other words, it allows the execution of insecure code from different domains. -### How? -```js +#### How? + +```js title='main.js (Main Process)' // Bad const mainWindow = new BrowserWindow({ webPreferences: { @@ -360,12 +328,12 @@ const mainWindow = new BrowserWindow({ }) ``` -```js +```js title='main.js (Main Process)' // Good const mainWindow = new BrowserWindow() ``` -```html +```html title='index.html (Renderer Process)' @@ -373,14 +341,13 @@ const mainWindow = new BrowserWindow() ``` - -## 6) Define a Content Security Policy +### 7. Define a Content Security Policy A Content Security Policy (CSP) is an additional layer of protection against cross-site-scripting attacks and data injection attacks. We recommend that they be enabled by any website you load inside Electron. -### Why? +#### Why? CSP allows the server serving content to restrict and control the resources Electron can load for that given web page. `https://example.com` should @@ -388,6 +355,8 @@ be allowed to load scripts from the origins you defined while scripts from `https://evil.attacker.com` should not be allowed to run. Defining a CSP is an easy way to improve your application's security. +#### How? + The following CSP will allow Electron to execute scripts from the current website and from `apis.example.com`. @@ -399,14 +368,14 @@ Content-Security-Policy: '*' Content-Security-Policy: script-src 'self' https://apis.example.com ``` -### CSP HTTP Header +#### CSP HTTP headers Electron respects the [`Content-Security-Policy` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) which can be set using Electron's [`webRequest.onHeadersReceived`](../api/web-request.md#webrequestonheadersreceivedfilter-listener) handler: -```javascript +```javascript title='main.js (Main Process)' const { session } = require('electron') session.defaultSession.webRequest.onHeadersReceived((details, callback) => { @@ -419,23 +388,22 @@ session.defaultSession.webRequest.onHeadersReceived((details, callback) => { }) ``` -### CSP Meta Tag +#### CSP meta tag -CSP's preferred delivery mechanism is an HTTP header, however it is not possible +CSP's preferred delivery mechanism is an HTTP header. However, it is not possible to use this method when loading a resource using the `file://` protocol. It can -be useful in some cases, such as using the `file://` protocol, to set a policy -on a page directly in the markup using a `` tag: +be useful in some cases to set a policy on a page directly in the markup using a +`` tag: -```html +```html title='index.html (Renderer Process)' ``` -#### `webRequest.onHeadersReceived([filter, ]listener)` - +### 8. Do not enable `allowRunningInsecureContent` -## 7) Do Not Set `allowRunningInsecureContent` to `true` - -_Recommendation is Electron's default_ +:::info +This recommendation is Electron's default. +::: By default, Electron will not allow websites loaded over `HTTPS` to load and execute scripts, CSS, or plugins from insecure sources (`HTTP`). Setting the @@ -444,15 +412,15 @@ property `allowRunningInsecureContent` to `true` disables that protection. Loading the initial HTML of a website over `HTTPS` and attempting to load subsequent resources via `HTTP` is also known as "mixed content". -### Why? +#### Why? Loading content over `HTTPS` assures the authenticity and integrity of the loaded resources while encrypting the traffic itself. See the section on [only displaying secure content](#1-only-load-secure-content) for more details. -### How? +#### How? -```js +```js title='main.js (Main Process)' // Bad const mainWindow = new BrowserWindow({ webPreferences: { @@ -461,20 +429,21 @@ const mainWindow = new BrowserWindow({ }) ``` -```js +```js title='main.js (Main Process)' // Good const mainWindow = new BrowserWindow({}) ``` +### 9. Do not enable experimental features -## 8) Do Not Enable Experimental Features - -_Recommendation is Electron's default_ +:::info +This recommendation is Electron's default. +::: Advanced users of Electron can enable experimental Chromium features using the `experimentalFeatures` property. -### Why? +#### Why? Experimental features are, as the name suggests, experimental and have not been enabled for all Chromium users. Furthermore, their impact on Electron as a whole @@ -483,9 +452,9 @@ has likely not been tested. Legitimate use cases exist, but unless you know what you are doing, you should not enable this property. -### How? +#### How? -```js +```js title='main.js (Main Process)' // Bad const mainWindow = new BrowserWindow({ webPreferences: { @@ -494,21 +463,22 @@ const mainWindow = new BrowserWindow({ }) ``` -```js +```js title='main.js (Main Process)' // Good const mainWindow = new BrowserWindow({}) ``` +### 10. Do not use `enableBlinkFeatures` -## 9) Do Not Use `enableBlinkFeatures` - -_Recommendation is Electron's default_ +:::info +This recommendation is Electron's default. +::: Blink is the name of the rendering engine behind Chromium. As with `experimentalFeatures`, the `enableBlinkFeatures` property allows developers to enable features that have been disabled by default. -### Why? +#### Why? Generally speaking, there are likely good reasons if a feature was not enabled by default. Legitimate use cases for enabling specific features exist. As a @@ -516,25 +486,27 @@ developer, you should know exactly why you need to enable a feature, what the ramifications are, and how it impacts the security of your application. Under no circumstances should you enable features speculatively. -### How? -```js +#### How? + +```js title='main.js (Main Process)' // Bad const mainWindow = new BrowserWindow({ webPreferences: { - enableBlinkFeatures: ['ExecCommandInJavaScript'] + enableBlinkFeatures: 'ExecCommandInJavaScript' } }) ``` -```js +```js title='main.js (Main Process)' // Good const mainWindow = new BrowserWindow() ``` +### 11. Do not use `allowpopups` for WebViews -## 10) Do Not Use `allowpopups` - -_Recommendation is Electron's default_ +:::info +This recommendation is Electron's default. +::: If you are using [``][webview-tag], you might need the pages and scripts loaded in your `` tag to open new windows. The `allowpopups` attribute @@ -542,16 +514,16 @@ enables them to create new [`BrowserWindows`][browser-window] using the `window.open()` method. `` tags are otherwise not allowed to create new windows. -### Why? +#### Why? If you do not need popups, you are better off not allowing the creation of new [`BrowserWindows`][browser-window] by default. This follows the principle of minimally required access: Don't let a website create new popups unless you know it needs that feature. -### How? +#### How? -```html +```html title='index.html (Renderer Process)' @@ -559,8 +531,7 @@ you know it needs that feature. ``` - -## 11) Verify WebView Options Before Creation +### 12. Verify WebView options before creation A WebView created in a renderer process that does not have Node.js integration enabled will not be able to enable integration itself. However, a WebView will @@ -570,7 +541,7 @@ It is a good idea to control the creation of new [``][webview-tag] tags from the main process and to verify that their webPreferences do not disable security features. -### Why? +#### Why? Since `` live in the DOM, they can be created by a script running on your website even if Node.js integration is otherwise disabled. @@ -580,13 +551,13 @@ a renderer process. In most cases, developers do not need to disable any of those features - and you should therefore not allow different configurations for newly created [``][webview-tag] tags. -### How? +#### How? Before a [``][webview-tag] tag is attached, Electron will fire the `will-attach-webview` event on the hosting `webContents`. Use the event to prevent the creation of `webViews` with possibly insecure options. -```js +```js title='main.js (Main Process)' app.on('web-contents-created', (event, contents) => { contents.on('will-attach-webview', (event, webPreferences, params) => { // Strip away preload scripts if unused or verify their location is legitimate @@ -604,16 +575,16 @@ app.on('web-contents-created', (event, contents) => { }) ``` -Again, this list merely minimizes the risk, it does not remove it. If your goal +Again, this list merely minimizes the risk, but does not remove it. If your goal is to display a website, a browser will be a more secure option. -## 12) Disable or limit navigation +### 13. Disable or limit navigation If your app has no need to navigate or only needs to navigate to known pages, it is a good idea to limit navigation outright to that known scope, disallowing any other kinds of navigation. -### Why? +#### Why? Navigation is a common attack vector. If an attacker can convince your app to navigate away from its current page, they can possibly force your app to open @@ -626,7 +597,7 @@ A common attack pattern is that the attacker convinces your app's users to interact with the app in such a way that it navigates to one of the attacker's pages. This is usually done via links, plugins, or other user-generated content. -### How? +#### How? If your app has no need for navigation, you can call `event.preventDefault()` in a [`will-navigate`][will-navigate] handler. If you know which pages your app @@ -637,7 +608,7 @@ We recommend that you use Node's parser for URLs. Simple string comparisons can sometimes be fooled - a `startsWith('https://example.com')` test would let `https://example.com.attacker.com` through. -```js +```js title='main.js (Main Process)' const URL = require('url').URL app.on('web-contents-created', (event, contents) => { @@ -651,12 +622,12 @@ app.on('web-contents-created', (event, contents) => { }) ``` -## 13) Disable or limit creation of new windows +### 14. Disable or limit creation of new windows If you have a known set of windows, it's a good idea to limit the creation of additional windows in your app. -### Why? +#### Why? Much like navigation, the creation of new `webContents` is a common attack vector. Attackers attempt to convince your app to create new windows, frames, @@ -669,182 +640,96 @@ security at no cost. This is commonly the case for apps that open one `BrowserWindow` and do not need to open an arbitrary number of additional windows at runtime. -### How? +#### How? -[`webContents`][web-contents] will emit the [`new-window`][new-window] event -before creating new windows. That event will be passed, amongst other -parameters, the `url` the window was requested to open and the options used to -create it. We recommend that you use the event to scrutinize the creation of -windows, limiting it to only what you need. +[`webContents`][web-contents] will delegate to its [window open +handler][window-open-handler] before creating new windows. The handler will +receive, amongst other parameters, the `url` the window was requested to open +and the options used to create it. We recommend that you register a handler to +monitor the creation of windows, and deny any unexpected window creation. -```js +```js title='main.js (Main Process)' const { shell } = require('electron') app.on('web-contents-created', (event, contents) => { - contents.on('new-window', async (event, navigationUrl) => { + contents.setWindowOpenHandler(({ url }) => { // In this example, we'll ask the operating system // to open this event's url in the default browser. - event.preventDefault() + // + // See the following item for considerations regarding what + // URLs should be allowed through to shell.openExternal. + if (isSafeForExternalOpen(url)) { + setImmediate(() => { + shell.openExternal(url) + }) + } - await shell.openExternal(navigationUrl) + return { action: 'deny' } }) }) ``` -## 14) Do not use `openExternal` with untrusted content +### 15. Do not use `shell.openExternal` with untrusted content -Shell's [`openExternal`][open-external] allows opening a given protocol URI with -the desktop's native utilities. On macOS, for instance, this function is similar -to the `open` terminal command utility and will open the specific application -based on the URI and filetype association. +The shell module's [`openExternal`][open-external] API allows opening a given +protocol URI with the desktop's native utilities. On macOS, for instance, this +function is similar to the `open` terminal command utility and will open the +specific application based on the URI and filetype association. -### Why? +#### Why? Improper use of [`openExternal`][open-external] can be leveraged to compromise the user's host. When openExternal is used with untrusted content, it can be leveraged to execute arbitrary commands. -### How? +#### How? -```js +```js title='main.js (Main Process)' // Bad const { shell } = require('electron') shell.openExternal(USER_CONTROLLED_DATA_HERE) ``` -```js + +```js title='main.js (Main Process)' // Good const { shell } = require('electron') shell.openExternal('https://example.com/index.html') ``` -## 15) Disable the `remote` module - -The `remote` module provides a way for the renderer processes to -access APIs normally only available in the main process. Using it, a -renderer can invoke methods of a main process object without explicitly sending -inter-process messages. If your desktop application does not run untrusted -content, this can be a useful way to have your renderer processes access and -work with modules that are only available to the main process, such as -GUI-related modules (dialogs, menus, etc.). - -However, if your app can run untrusted content and even if you -[sandbox][sandbox] your renderer processes accordingly, the `remote` module -makes it easy for malicious code to escape the sandbox and have access to -system resources via the higher privileges of the main process. Therefore, -it should be disabled in such circumstances. - -### Why? - -`remote` uses an internal IPC channel to communicate with the main process. -"Prototype pollution" attacks can grant malicious code access to the internal -IPC channel, which can then be used to escape the sandbox by mimicking `remote` -IPC messages and getting access to main process modules running with higher -privileges. - -Additionally, it's possible for preload scripts to accidentally leak modules to a -sandboxed renderer. Leaking `remote` arms malicious code with a multitude -of main process modules with which to perform an attack. - -Disabling the `remote` module eliminates these attack vectors. Enabling -context isolation also prevents the "prototype pollution" attacks from -succeeding. - -### How? - -```js -// Bad if the renderer can run untrusted content -const mainWindow = new BrowserWindow({}) -``` - -```js -// Good -const mainWindow = new BrowserWindow({ - webPreferences: { - enableRemoteModule: false - } -}) -``` - -```html - - - - - -``` - -## 16) Filter the `remote` module +### 16. Use a current version of Electron -If you cannot disable the `remote` module, you should filter the globals, -Node, and Electron modules (so-called built-ins) accessible via `remote` -that your application does not require. This can be done by blocking -certain modules entirely and by replacing others with proxies that -expose only the functionality that your app needs. +You should strive for always using the latest available version of Electron. +Whenever a new major version is released, you should attempt to update your +app as quickly as possible. -### Why? +#### Why? -Due to the system access privileges of the main process, functionality -provided by the main process modules may be dangerous in the hands of -malicious code running in a compromised renderer process. By limiting -the set of accessible modules to the minimum that your app needs and -filtering out the others, you reduce the toolset that malicious code -can use to attack the system. +An application built with an older version of Electron, Chromium, and Node.js +is an easier target than an application that is using more recent versions of +those components. Generally speaking, security issues and exploits for older +versions of Chromium and Node.js are more widely available. -Note that the safest option is to -[fully disable the remote module](#15-disable-the-remote-module). If -you choose to filter access rather than completely disable the module, -you must be very careful to ensure that no escalation of privilege is -possible through the modules you allow past the filter. +Both Chromium and Node.js are impressive feats of engineering built by +thousands of talented developers. Given their popularity, their security is +carefully tested and analyzed by equally skilled security researchers. Many of +those researchers [disclose vulnerabilities responsibly][responsible-disclosure], +which generally means that researchers will give Chromium and Node.js some time +to fix issues before publishing them. Your application will be more secure if +it is running a recent version of Electron (and thus, Chromium and Node.js) for +which potential security issues are not as widely known. -### How? +#### How? -```js -const readOnlyFsProxy = require(/* ... */) // exposes only file read functionality - -const allowedModules = new Set(['crypto']) -const proxiedModules = new Map(['fs', readOnlyFsProxy]) -const allowedElectronModules = new Set(['shell']) -const allowedGlobals = new Set() - -app.on('remote-require', (event, webContents, moduleName) => { - if (proxiedModules.has(moduleName)) { - event.returnValue = proxiedModules.get(moduleName) - } - if (!allowedModules.has(moduleName)) { - event.preventDefault() - } -}) - -app.on('remote-get-builtin', (event, webContents, moduleName) => { - if (!allowedElectronModules.has(moduleName)) { - event.preventDefault() - } -}) - -app.on('remote-get-global', (event, webContents, globalName) => { - if (!allowedGlobals.has(globalName)) { - event.preventDefault() - } -}) - -app.on('remote-get-current-window', (event, webContents) => { - event.preventDefault() -}) - -app.on('remote-get-current-web-contents', (event, webContents) => { - event.preventDefault() -}) - -app.on('remote-get-guest-web-contents', (event, webContents, guestWebContents) => { - event.preventDefault() -}) -``` +Migrate your app one major version at a time, while referring to Electron's +[Breaking Changes][breaking-changes] document to see if any code needs to +be updated. +[breaking-changes]: ../breaking-changes.md [browser-window]: ../api/browser-window.md [browser-view]: ../api/browser-view.md [webview-tag]: ../api/webview-tag.md [web-contents]: ../api/web-contents.md -[new-window]: ../api/web-contents.md#event-new-window +[window-open-handler]: ../api/web-contents.md#contentssetwindowopenhandlerhandler [will-navigate]: ../api/web-contents.md#event-will-navigate -[open-external]: ../api/shell.md#shellopenexternalurl-options-callback -[sandbox]: ../api/sandbox-option.md +[open-external]: ../api/shell.md#shellopenexternalurl-options +[responsible-disclosure]: https://en.wikipedia.org/wiki/Responsible_disclosure diff --git a/docs/tutorial/snapcraft.md b/docs/tutorial/snapcraft.md index a6516b6ae2353..0c479a5830013 100644 --- a/docs/tutorial/snapcraft.md +++ b/docs/tutorial/snapcraft.md @@ -1,4 +1,4 @@ -# Snapcraft Guide (Ubuntu Software Center & More) +# Snapcraft Guide (Linux) This guide provides information on how to package your Electron application for any Snapcraft environment, including the Ubuntu Software Center. @@ -19,16 +19,9 @@ There are three ways to create a `.snap` file: 2) Using `electron-installer-snap`, which takes `electron-packager`'s output. 3) Using an already created `.deb` package. -In all cases, you will need to have the `snapcraft` tool installed. We -recommend building on Ubuntu 16.04 (or the current LTS). - -```sh -snap install snapcraft --classic -``` - -While it _is possible_ to install `snapcraft` on macOS using Homebrew, it -is not able to build `snap` packages and is focused on managing packages -in the store. +In some cases, you will need to have the `snapcraft` tool installed. +Instructions to install `snapcraft` for your particular distribution are +available [here](https://snapcraft.io/docs/installing-snapcraft). ## Using `electron-installer-snap` @@ -61,7 +54,6 @@ The output should look roughly like this: ├── libgcrypt.so.11 ├── libnode.so ├── locales - ├── natives_blob.bin ├── resources ├── v8_context_snapshot.bin └── version @@ -87,6 +79,78 @@ snap(options) .then(snapPath => console.log(`Created snap at ${snapPath}!`)) ``` +## Using `snapcraft` with `electron-packager` + +### Step 1: Create Sample Snapcraft Project + +Create your project directory and add the following to `snap/snapcraft.yaml`: + +```yaml +name: electron-packager-hello-world +version: '0.1' +summary: Hello World Electron app +description: | + Simple Hello World Electron app as an example +base: core18 +confinement: strict +grade: stable + +apps: + electron-packager-hello-world: + command: electron-quick-start/electron-quick-start --no-sandbox + extensions: [gnome-3-34] + plugs: + - browser-support + - network + - network-bind + environment: + # Correct the TMPDIR path for Chromium Framework/Electron to ensure + # libappindicator has readable resources. + TMPDIR: $XDG_RUNTIME_DIR + +parts: + electron-quick-start: + plugin: nil + source: https://github.com/electron/electron-quick-start.git + override-build: | + npm install electron electron-packager + npx electron-packager . --overwrite --platform=linux --output=release-build --prune=true + cp -rv ./electron-quick-start-linux-* $SNAPCRAFT_PART_INSTALL/electron-quick-start + build-snaps: + - node/14/stable + build-packages: + - unzip + stage-packages: + - libnss3 + - libnspr4 +``` + +If you want to apply this example to an existing project: + +- Replace `source: https://github.com/electron/electron-quick-start.git` with `source: .`. +- Replace all instances of `electron-quick-start` with your project's name. + +### Step 2: Build the snap + +```sh +$ snapcraft + + +Snapped electron-packager-hello-world_0.1_amd64.snap +``` + +### Step 3: Install the snap + +```sh +sudo snap install electron-packager-hello-world_0.1_amd64.snap --dangerous +``` + +### Step 4: Run the snap + +```sh +electron-packager-hello-world +``` + ## Using an Existing Debian Package Snapcraft is capable of taking an existing `.deb` file and turning it into diff --git a/docs/tutorial/spellchecker.md b/docs/tutorial/spellchecker.md new file mode 100644 index 0000000000000..4854d6515a533 --- /dev/null +++ b/docs/tutorial/spellchecker.md @@ -0,0 +1,74 @@ +# SpellChecker + +Electron has built-in support for Chromium's spellchecker since Electron 8. On Windows and Linux this is powered by Hunspell dictionaries, and on macOS it makes use of the native spellchecker APIs. + +## How to enable the spellchecker? + +For Electron 9 and higher the spellchecker is enabled by default. For Electron 8 you need to enable it in `webPreferences`. + +```js +const myWindow = new BrowserWindow({ + webPreferences: { + spellcheck: true + } +}) +``` + +## How to set the languages the spellchecker uses? + +On macOS as we use the native APIs there is no way to set the language that the spellchecker uses. By default on macOS the native spellchecker will automatically detect the language being used for you. + +For Windows and Linux there are a few Electron APIs you should use to set the languages for the spellchecker. + +```js +// Sets the spellchecker to check English US and French +myWindow.session.setSpellCheckerLanguages(['en-US', 'fr']) + +// An array of all available language codes +const possibleLanguages = myWindow.session.availableSpellCheckerLanguages +``` + +By default the spellchecker will enable the language matching the current OS locale. + +## How do I put the results of the spellchecker in my context menu? + +All the required information to generate a context menu is provided in the [`context-menu`](../api/web-contents.md#event-context-menu) event on each `webContents` instance. A small example +of how to make a context menu with this information is provided below. + +```js +const { Menu, MenuItem } = require('electron') + +myWindow.webContents.on('context-menu', (event, params) => { + const menu = new Menu() + + // Add each spelling suggestion + for (const suggestion of params.dictionarySuggestions) { + menu.append(new MenuItem({ + label: suggestion, + click: () => mainWindow.webContents.replaceMisspelling(suggestion) + })) + } + + // Allow users to add the misspelled word to the dictionary + if (params.misspelledWord) { + menu.append( + new MenuItem({ + label: 'Add to dictionary', + click: () => mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) + }) + ) + } + + menu.popup() +}) +``` + +## Does the spellchecker use any Google services? + +Although the spellchecker itself does not send any typings, words or user input to Google services the hunspell dictionary files are downloaded from a Google CDN by default. If you want to avoid this you can provide an alternative URL to download the dictionaries from. + +```js +myWindow.session.setSpellCheckerDictionaryDownloadURL('https://example.com/dictionaries/') +``` + +Check out the docs for [`session.setSpellCheckerDictionaryDownloadURL`](../api/session.md#sessetspellcheckerdictionarydownloadurlurl) for more information on where to get the dictionary files from and how you need to host them. diff --git a/docs/tutorial/support.md b/docs/tutorial/support.md index 86ade0f5ec38c..e757ec22e21f1 100644 --- a/docs/tutorial/support.md +++ b/docs/tutorial/support.md @@ -3,26 +3,30 @@ ## Finding Support If you have a security concern, -please see the [security document](../../SECURITY.md). +please see the [security document](https://github.com/electron/electron/tree/main/SECURITY.md). If you're looking for programming help, for answers to questions, or to join in discussion with other developers who use Electron, you can interact with the community in these locations: -- [`electron`](https://discuss.atom.io/c/electron) category on the Atom -forums -- `#atom-shell` channel on Freenode -- `#electron` channel on [Atom's Slack](https://discuss.atom.io/t/join-us-on-slack/16638?source_topic_id=25406) -- [`electron-ru`](https://telegram.me/electron_ru) *(Russian)* -- [`electron-br`](https://electron-br.slack.com) *(Brazilian Portuguese)* -- [`electron-kr`](https://electron-kr.github.io/electron-kr) *(Korean)* -- [`electron-jp`](https://electron-jp.slack.com) *(Japanese)* -- [`electron-tr`](https://electron-tr.herokuapp.com) *(Turkish)* -- [`electron-id`](https://electron-id.slack.com) *(Indonesia)* -- [`electron-pl`](https://electronpl.github.io) *(Poland)* + +* [Electron's Discord server](https://discord.com/invite/APGC3k5yaH) has channels for: + * Getting help + * Ecosystem apps like [Electron Forge](https://github.com/electron-userland/electron-forge) and [Electron Fiddle](https://github.com/electron/fiddle) + * Sharing ideas with other Electron app developers + * And more! +* [`electron`](https://discuss.atom.io/c/electron) category on the Atom forums +* `#electron` channel on [Atom's Slack](https://discuss.atom.io/t/join-us-on-slack/16638?source_topic_id=25406) +* [`electron-ru`](https://telegram.me/electron_ru) *(Russian)* +* [`electron-br`](https://electron-br.slack.com) *(Brazilian Portuguese)* +* [`electron-kr`](https://electron-kr.github.io/electron-kr) *(Korean)* +* [`electron-jp`](https://electron-jp.slack.com) *(Japanese)* +* [`electron-tr`](https://electron-tr.herokuapp.com) *(Turkish)* +* [`electron-id`](https://electron-id.slack.com) *(Indonesia)* +* [`electron-pl`](https://electronpl.github.io) *(Poland)* If you'd like to contribute to Electron, -see the [contributing document](../../CONTRIBUTING.md). +see the [contributing document](https://github.com/electron/electron/blob/main/CONTRIBUTING.md). If you've found a bug in a [supported version](#supported-versions) of Electron, please report it with the [issue tracker](../development/issues.md). @@ -33,24 +37,43 @@ tools and resources. ## Supported Versions +_**Note:** Beginning in September 2021 with Electron 15, the Electron team +will temporarily support the latest **four** stable major versions. This +extended support is intended to help Electron developers transition to +the [new eight week release cadence](https://electronjs.org/blog/8-week-cadence), and will continue until May 2022, with +the release of Electron 19. At that time, the Electron team will drop support +back to the latest three stable major versions._ + The latest three *stable* major versions are supported by the Electron team. -For example, if the latest release is 6.x.y, then the 5.x.y as well -as the 4.x.y series are supported. +For example, if the latest release is 6.1.x, then the 5.0.x as well +as the 4.2.x series are supported. We only support the latest minor release +for each stable release series. This means that in the case of a security fix +6.1.x will receive the fix, but we will not release a new version of 6.0.x. -The latest stable release unilaterally receives all fixes from `master`, +The latest stable release unilaterally receives all fixes from `main`, and the version prior to that receives the vast majority of those fixes as time and bandwidth warrants. The oldest supported release line will receive only security fixes directly. All supported release lines will accept external pull requests to backport -fixes previously merged to `master`, though this may be on a case-by-case +fixes previously merged to `main`, though this may be on a case-by-case basis for some older supported lines. All contested decisions around release -line backports will be resolved by the [Releases Working Group](https://github.com/electron/governance/tree/master/wg-releases) as an agenda item at their weekly meeting the week the backport PR is raised. +line backports will be resolved by the [Releases Working Group](https://github.com/electron/governance/tree/main/wg-releases) as an agenda item at their weekly meeting the week the backport PR is raised. + +When an API is changed or removed in a way that breaks existing functionality, the +previous functionality will be supported for a minimum of two major versions when +possible before being removed. For example, if a function takes three arguments, +and that number is reduced to two in major version 10, the three-argument version would +continue to work until, at minimum, major version 12. Past the minimum two-version +threshold, we will attempt to support backwards compatibility beyond two versions +until the maintainers feel the maintenance burden is too high to continue doing so. ### Currently supported versions -- 6.x.y -- 5.x.y -- 4.x.y + +* 18.x.y +* 17.x.y +* 16.x.y +* 15.x.y ### End-of-life @@ -78,7 +101,9 @@ Following platforms are supported by Electron: ### macOS Only 64bit binaries are provided for macOS, and the minimum macOS version -supported is macOS 10.10 (Yosemite). +supported is macOS 10.11 (El Capitan). + +Native support for Apple Silicon (`arm64`) devices was added in Electron 11.0.0. ### Windows @@ -86,26 +111,18 @@ Windows 7 and later are supported, older operating systems are not supported (and do not work). Both `ia32` (`x86`) and `x64` (`amd64`) binaries are provided for Windows. -Running Electron apps on Windows for ARM devices is possible by using the -ia32 binary. +[Native support for Windows on Arm (`arm64`) devices was added in Electron 6.0.8.](windows-arm.md). +Running apps packaged with previous versions is possible using the ia32 binary. ### Linux -The prebuilt `ia32` (`i686`) and `x64` (`amd64`) binaries of Electron are built on -Ubuntu 12.04, the `armv7l` binary is built against ARM v7 with hard-float ABI and -NEON for Debian Wheezy. - -[Until the release of Electron 2.0][arm-breaking-change], Electron will also -continue to release the `armv7l` binary with a simple `arm` suffix. Both binaries -are identical. +The prebuilt binaries of Electron are built on Ubuntu 18.04. Whether the prebuilt binary can run on a distribution depends on whether the distribution includes the libraries that Electron is linked to on the building -platform, so only Ubuntu 12.04 is guaranteed to work, but following platforms +platform, so only Ubuntu 18.04 is guaranteed to work, but following platforms are also verified to be able to run the prebuilt binaries of Electron: -* Ubuntu 12.04 and newer -* Fedora 21 -* Debian 8 - -[arm-breaking-change]: https://github.com/electron/electron/blob/master/docs/api/breaking-changes.md#duplicate-arm-assets +* Ubuntu 14.04 and newer +* Fedora 24 and newer +* Debian 8 and newer diff --git a/docs/tutorial/testing-on-headless-ci.md b/docs/tutorial/testing-on-headless-ci.md index 153d621ce36ba..3594d03fbd4b6 100644 --- a/docs/tutorial/testing-on-headless-ci.md +++ b/docs/tutorial/testing-on-headless-ci.md @@ -3,7 +3,7 @@ Being based on Chromium, Electron requires a display driver to function. If Chromium can't find a display driver, Electron will fail to launch - and therefore not executing any of your tests, regardless of how you are running -them. Testing Electron-based apps on Travis, Circle, Jenkins or similar Systems +them. Testing Electron-based apps on Travis, CircleCI, Jenkins or similar Systems requires therefore a little bit of configuration. In essence, we need to use a virtual display driver. @@ -49,10 +49,9 @@ install: For Jenkins, a [Xvfb plugin is available](https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin). -### Circle CI +### CircleCI -Circle CI is awesome and has Xvfb and `$DISPLAY` -[already set up, so no further configuration is required](https://circleci.com/docs/environment#browsers). +CircleCI is awesome and has Xvfb and `$DISPLAY` already set up, so no further configuration is required. ### AppVeyor diff --git a/docs/tutorial/testing-widevine-cdm.md b/docs/tutorial/testing-widevine-cdm.md index cbae6beb421ce..b2bcd7cde4a6f 100644 --- a/docs/tutorial/testing-widevine-cdm.md +++ b/docs/tutorial/testing-widevine-cdm.md @@ -49,7 +49,7 @@ The library file `widevinecdm.dll` will be under `Program Files(x86)/Google/Chrome/Application/CHROME_VERSION/WidevineCdm/_platform_specific/win_(x86|x64)/` directory. -### On MacOS +### On macOS The library file `libwidevinecdm.dylib` will be under `/Applications/Google Chrome.app/Contents/Versions/CHROME_VERSION/Google Chrome Framework.framework/Versions/A/Libraries/WidevineCdm/_platform_specific/mac_(x86|x64)/` @@ -79,7 +79,7 @@ app.commandLine.appendSwitch('widevine-cdm-path', '/path/to/widevine_library') app.commandLine.appendSwitch('widevine-cdm-version', '1.4.8.866') let win = null -app.on('ready', () => { +app.whenReady().then(() => { win = new BrowserWindow() win.show() }) diff --git a/docs/tutorial/tray.md b/docs/tutorial/tray.md new file mode 100644 index 0000000000000..1f0c8e6b6fc88 --- /dev/null +++ b/docs/tutorial/tray.md @@ -0,0 +1,83 @@ +--- +title: Tray +description: This guide will take you through the process of creating + a Tray icon with its own context menu to the system's notification area. +slug: tray +hide_title: true +--- + +# Tray + +## Overview + + + +This guide will take you through the process of creating a +[Tray](https://www.electronjs.org/docs/api/tray) icon with +its own context menu to the system's notification area. + +On MacOS and Ubuntu, the Tray will be located on the top +right corner of your screen, adjacent to your battery and wifi icons. +On Windows, the Tray will usually be located in the bottom right corner. + +## Example + +### main.js + +First we must import `app`, `Tray`, `Menu`, `nativeImage` from `electron`. + +```js +const { app, Tray, Menu, nativeImage } = require('electron') +``` + +Next we will create our Tray. To do this, we will use a +[`NativeImage`](https://www.electronjs.org/docs/api/native-image) icon, +which can be created through any one of these +[methods](https://www.electronjs.org/docs/api/native-image#methods). +Note that we wrap our Tray creation code within an +[`app.whenReady`](https://www.electronjs.org/docs/api/app#appwhenready) +as we will need to wait for our electron app to finish initializing. + +```js title='main.js' +let tray + +app.whenReady().then(() => { + const icon = nativeImage.createFromPath('path/to/asset.png') + tray = new Tray(icon) + + // note: your contextMenu, Tooltip and Title code will go here! +}) +``` + +Great! Now we can start attaching a context menu to our Tray, like so: + +```js +const contextMenu = Menu.buildFromTemplate([ + { label: 'Item1', type: 'radio' }, + { label: 'Item2', type: 'radio' }, + { label: 'Item3', type: 'radio', checked: true }, + { label: 'Item4', type: 'radio' } +]) + +tray.setContextMenu(contextMenu) +``` + +The code above will create 4 separate radio-type items in the context menu. +To read more about constructing native menus, click +[here](https://www.electronjs.org/docs/api/menu#menubuildfromtemplatetemplate). + +Finally, let's give our tray a tooltip and a title. + +```js +tray.setToolTip('This is my application') +tray.setTitle('This is my title') +``` + +## Conclusion + +After you start your electron app, you should see the Tray residing +in either the top or bottom right of your screen, depending on your +operating system. + +```fiddle docs/fiddles/native-ui/tray +``` diff --git a/docs/tutorial/tutorial-1-prerequisites.md b/docs/tutorial/tutorial-1-prerequisites.md new file mode 100644 index 0000000000000..4c73758ab4023 --- /dev/null +++ b/docs/tutorial/tutorial-1-prerequisites.md @@ -0,0 +1,143 @@ +--- +title: 'Prerequisites' +description: 'This guide will step you through the process of creating a barebones Hello World app in Electron, similar to electron/electron-quick-start.' +slug: tutorial-prerequisites +hide_title: false +--- + +:::info Follow along the tutorial + +This is **part 1** of the Electron tutorial. + +1. **[Prerequisites][prerequisites]** +1. [Building your First App][building your first app] +1. [Using Preload Scripts][preload] +1. [Adding Features][features] +1. [Packaging Your Application][packaging] +1. [Publishing and Updating][updates] + +::: + +Electron is a framework for building desktop applications using JavaScript, +HTML, and CSS. By embedding [Chromium][chromium] and [Node.js][node] into a +single binary file, Electron allows you to create cross-platform apps that +work on Windows, macOS, and Linux with a single JavaScript codebase. + +This tutorial will guide you through the process of developing a desktop +application with Electron and distributing it to end users. + +## Assumptions + +Electron is a native wrapper layer for web apps and is run in a Node.js environment. +Therefore, this tutorial assumes you are generally familiar with Node and +front-end web development basics. If you need to do some background reading before +continuing, we recommend the following resources: + +- [Getting started with the Web (MDN Web Docs)][mdn-guide] +- [Introduction to Node.js][node-guide] + +## Required tools + +### Code editor + +You will need a text editor to write your code. We recommend using [Visual Studio Code], +although you can choose whichever one you prefer. + +### Command line + +Throughout the tutorial, we will ask you to use various command-line interfaces (CLIs). You can +type these commands into your system's default terminal: + +- Windows: Command Prompt or PowerShell +- macOS: Terminal +- Linux: varies depending on distribution (e.g. GNOME Terminal, Konsole) + +Most code editors also come with an integrated terminal, which you can also use. + +### Git and GitHub + +Git is a commonly-used version control system for source code, and GitHub is a collaborative +development platform built on top of it. Although neither is strictly necessary to building +an Electron application, we will use GitHub releases to set up automatic updates later +on in the tutorial. Therefore, we'll require you to: + +- [Create a GitHub account](https://github.com/join) +- [Install Git](https://github.com/git-guides/install-git) + +If you're unfamiliar with how Git works, we recommend reading GitHub's [Git guides]. You can also +use the [GitHub Desktop] app if you prefer using a visual interface over the command line. + +We recommend that you create a local Git repository and publish it to GitHub before starting +the tutorial, and commit your code after every step. + +:::info Installing Git via GitHub Desktop + +GitHub Desktop will install the latest version of Git on your system if you don't already have +it installed. + +::: + +### Node.js and npm + +To begin developing an Electron app, you need to install the [Node.js][node-download] +runtime and its bundled npm package manager onto your system. We recommend that you +use the latest long-term support (LTS) version. + +:::tip + +Please install Node.js using pre-built installers for your platform. +You may encounter incompatibility issues with different development tools otherwise. +If you are using macOS, we recommend using a package manager like [Homebrew] or +[nvm] to avoid any directory permission issues. + +::: + +To check that Node.js was installed correctly, you can use the `-v` flag when +running the `node` and `npm` commands. These should print out the installed +versions. + +```sh +$ node -v +v16.14.2 +$ npm -v +8.7.0 +``` + +:::caution + +Although you need Node.js installed locally to scaffold an Electron project, +Electron **does not use your system's Node.js installation to run its code**. Instead, it +comes bundled with its own Node.js runtime. This means that your end users do not +need to install Node.js themselves as a prerequisite to running your app. + +To check which version of Node.js is running in your app, you can access the global +[`process.versions`] variable in the main process or preload script. You can also reference +the list of versions in the [electron/releases] repository. + +::: + + + +[chromium]: https://www.chromium.org/ +[electron/releases]: https://github.com/electron/releases/blob/master/readme.md#releases +[homebrew]: https://brew.sh/ +[mdn-guide]: https://developer.mozilla.org/en-US/docs/Learn/ +[node]: https://nodejs.org/ +[node-guide]: https://nodejs.dev/learn +[node-download]: https://nodejs.org/en/download/ +[nvm]: https://github.com/nvm-sh/nvm +[process-model]: ./process-model.md +[`process.versions`]: https://nodejs.org/api/process.html#processversions +[github]: https://github.com/ +[git guides]: https://github.com/git-guides/ +[github desktop]: https://desktop.github.com/ +[visual studio code]: https://code.visualstudio.com/ + + + +[prerequisites]: tutorial-1-prerequisites.md +[building your first app]: tutorial-2-first-app.md +[preload]: tutorial-3-preload.md +[features]: tutorial-4-adding-features.md +[packaging]: tutorial-5-packaging.md +[updates]: tutorial-6-publishing-updating.md diff --git a/docs/tutorial/tutorial-2-first-app.md b/docs/tutorial/tutorial-2-first-app.md new file mode 100644 index 0000000000000..714d069827493 --- /dev/null +++ b/docs/tutorial/tutorial-2-first-app.md @@ -0,0 +1,480 @@ +--- +title: 'Building your First App' +description: 'This guide will step you through the process of creating a barebones Hello World app in Electron, similar to electron/electron-quick-start.' +slug: tutorial-first-app +hide_title: false +--- + +:::info Follow along the tutorial + +This is **part 2** of the Electron tutorial. + +1. [Prerequisites][prerequisites] +1. **[Building your First App][building your first app]** +1. [Using Preload Scripts][preload] +1. [Adding Features][features] +1. [Packaging Your Application][packaging] +1. [Publishing and Updating][updates] + +::: + +## Learning goals + +In this part of the tutorial, you will learn how to set up your Electron project +and write a minimal starter application. By the end of this section, +you should be able to run a working Electron app in development mode from +your terminal. + +## Setting up your project + +:::caution Avoid WSL + +If you are on a Windows machine, please do not use [Windows Subsystem for Linux][wsl] (WSL) +when following this tutorial as you will run into issues when trying to execute the +application. + + + +::: + +### Initializing your npm project + +Electron apps are scaffolded using npm, with the package.json file +as an entry point. Start by creating a folder and initializing an npm package +within it with `npm init`. + +```sh npm2yarn +mkdir my-electron-app && cd my-electron-app +npm init +``` + +This command will prompt you to configure some fields in your package.json. +There are a few rules to follow for the purposes of this tutorial: + +- _entry point_ should be `main.js` (you will be creating that file soon). +- _author_, _license_, and _description_ can be any value, but will be necessary for + [packaging][packaging] later on. + +Then, install Electron into your app's **devDependencies**, which is the list of external +development-only package dependencies not required in production. + +:::info Why is Electron a devDependency? + +This may seem counter-intuitive since your production code is running Electron APIs. +However, packaged apps will come bundled with the Electron binary, eliminating the need to specify +it as a production dependency. + +::: + +```sh npm2yarn +npm install electron --save-dev +``` + +Your package.json file should look something like this after initializing your package +and installing Electron. You should also now have a `node_modules` folder containing +the Electron executable, as well as a `package-lock.json` lockfile that specifies +the exact dependency versions to install. + +```json title='package.json' +{ + "name": "my-electron-app", + "version": "1.0.0", + "description": "Hello World!", + "main": "main.js", + "author": "Jane Doe", + "license": "MIT", + "devDependencies": { + "electron": "19.0.0" + } +} +``` + +:::info Advanced Electron installation steps + +If installing Electron directly fails, please refer to our [Advanced Installation][installation] +documentation for instructions on download mirrors, proxies, and troubleshooting steps. + +::: + +### Adding a .gitignore + +The [`.gitignore`][gitignore] file specifies which files and directories to avoid tracking +with Git. You should place a copy of [GitHub's Node.js gitignore template][gitignore-template] +into your project's root folder to avoid committing your project's `node_modules` folder. + +## Running an Electron app + +:::tip Further reading + +Read [Electron's process model][process-model] documentation to better +understand how Electron's multiple processes work together. + +::: + +The [`main`][package-json-main] script you defined in package.json is the entry point of any +Electron application. This script controls the **main process**, which runs in a Node.js +environment and is responsible for controlling your app's lifecycle, displaying native +interfaces, performing privileged operations, and managing renderer processes +(more on that later). + +Before creating your first Electron app, you will first use a trivial script to ensure your +main process entry point is configured correctly. Create a `main.js` file in the root folder +of your project with a single line of code: + +```js title='main.js' +console.log(`Hello from Electron 👋`) +``` + +Because Electron's main process is a Node.js runtime, you can execute arbitrary Node.js code +with the `electron` command (you can even use it as a [REPL]). To execute this script, +add `electron .` to the `start` command in the [`scripts`][package-scripts] +field of your package.json. This command will tell the Electron executable to look for the main +script in the current directory and run it in dev mode. + +```json {8-10} title='package.json' +{ + "name": "my-electron-app", + "version": "1.0.0", + "description": "Hello World!", + "main": "main.js", + "author": "Jane Doe", + "license": "MIT", + "scripts": { + "start": "electron ." + }, + "devDependencies": { + "electron": "^19.0.0" + } +} +``` + +```sh npm2yarn +npm run start +``` + +Your terminal should print out `Hello from Electron 👋`. Congratulations, +you have executed your first line of code in Electron! Next, you will learn +how to create user interfaces with HTML and load that into a native window. + +## Loading a web page into a BrowserWindow + +In Electron, each window displays a web page that can be loaded either from a local HTML +file or a remote web address. For this example, you will be loading in a local file. Start +by creating a barebones web page in an `index.html` file in the root folder of your project: + +```html title='index.html' + + + + + + + + Hello from Electron renderer! + + +

Hello from Electron renderer!

+

👋

+ + +``` + +Now that you have a web page, you can load it into an Electron [BrowserWindow][browser-window]. +Replace the contents your `main.js` file with the following code. We will explain each +highlighted block separately. + +```js {1,3-10,12-14} title='main.js' showLineNumbers +const { app, BrowserWindow } = require('electron') + +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + }) + + win.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() +}) +``` + +### Importing modules + +```js title='main.js (Line 1)' +const { app, BrowserWindow } = require('electron') +``` + +In the first line, we are importing two Electron modules +with CommonJS module syntax: + +- [app][app], which controls your application's event lifecycle. +- [BrowserWindow][browser-window], which creates and manages app windows. + +:::info Capitalization conventions + +You might have noticed the capitalization difference between the **a**pp +and **B**rowser**W**indow modules. Electron follows typical JavaScript conventions here, +where PascalCase modules are instantiable class constructors (e.g. BrowserWindow, Tray, +Notification) whereas camelCase modules are not instantiable (e.g. app, ipcRenderer, webContents). + +::: + +:::warning ES Modules in Electron + +[ECMAScript modules](https://nodejs.org/api/esm.html) (i.e. using `import` to load a module) +are currently not directly supported in Electron. You can find more information about the +state of ESM in Electron in [electron/electron#21457](https://github.com/electron/electron/issues/21457). + +::: + +### Writing a reusable function to instantiate windows + +The `createWindow()` function loads your web page into a new BrowserWindow instance: + +```js title='main.js (Lines 3-10)' +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + }) + + win.loadFile('index.html') +} +``` + +### Calling your function when the app is ready + +```js title='main.js (Lines 12-14)' +app.whenReady().then(() => { + createWindow() +}) +``` + +Many of Electron's core modules are Node.js [event emitters] that adhere to Node's asynchronous +event-driven architecture. The app module is one of these emitters. + +In Electron, BrowserWindows can only be created after the app module's [`ready`][app-ready] event +is fired. You can wait for this event by using the [`app.whenReady()`][app-when-ready] API and +calling `createWindow()` once its promise is fulfilled. + +:::info + +You typically listen to Node.js events by using an emitter's `.on` function. + +```diff ++ app.on('ready').then(() => { +- app.whenReady().then(() => { + createWindow() +}) +``` + +However, Electron exposes `app.whenReady()` as a helper specifically for the `ready` event to +avoid subtle pitfalls with directly listening to that event in particular. +See [electron/electron#21972](https://github.com/electron/electron/pull/21972) for details. + +::: + +At this point, running your Electron application's `start` command should successfully +open a window that displays your web page! + +Each web page your app displays in a window will run in a separate process called a +**renderer** process (or simply _renderer_ for short). Renderer processes have access +to the same JavaScript APIs and tooling you use for typical front-end web +development, such as using [webpack] to bundle and minify your code or [React][react] +to build your user interfaces. + +## Managing your app's window lifecycle + +Application windows behave differently on each operating system. Rather than +enforce these conventions by default, Electron gives you the choice to implement +them in your app code if you wish to follow them. You can implement basic window +conventions by listening for events emitted by the app and BrowserWindow modules. + +:::tip Process-specific control flow + +Checking against Node's [`process.platform`][node-platform] variable can help you +to run code conditionally on certain platforms. Note that there are only three +possible platforms that Electron can run in: `win32` (Windows), `linux` (Linux), +and `darwin` (macOS). + +::: + +### Quit the app when all windows are closed (Windows & Linux) + +On Windows and Linux, closing all windows will generally quit an application entirely. +To implement this pattern in your Electron app, listen for the app module's +[`window-all-closed`][window-all-closed] event, and call [`app.quit()`][app-quit] +to exit your app if the user is not on macOS. + +```js +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) +``` + +### Open a window if none are open (macOS) + +In contrast, macOS apps generally continue running even without any windows open. +Activating the app when no windows are available should open a new one. + +To implement this feature, listen for the app module's [`activate`][activate] +event, and call your existing `createWindow()` method if no BrowserWindows are open. + +Because windows cannot be created before the `ready` event, you should only listen for +`activate` events after your app is initialized. Do this by only listening for activate +events inside your existing `whenReady()` callback. + +```js +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) +``` + +## Final starter code + +```fiddle docs/fiddles/tutorial-first-app + +``` + +## Optional: Debugging from VS Code + +If you want to debug your application using VS Code, you have need attach VS Code to +both the main and renderer processes. Here is a sample configuration for you to +run. Create a launch.json configuration in a new `.vscode` folder in your project: + +```json title='.vscode/launch.json' +{ + "version": "0.2.0", + "compounds": [ + { + "name": "Main + renderer", + "configurations": ["Main", "Renderer"], + "stopAll": true + } + ], + "configurations": [ + { + "name": "Renderer", + "port": 9222, + "request": "attach", + "type": "pwa-chrome", + "webRoot": "${workspaceFolder}" + }, + { + "name": "Main", + "type": "pwa-node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "args": [".", "--remote-debugging-port=9222"], + "outputCapture": "std", + "console": "integratedTerminal" + } + ] +} +``` + +The "Main + renderer" option will appear when you select "Run and Debug" +from the sidebar, allowing you to set breakpoints and inspect all the variables among +other things in both the main and renderer processes. + +What we have done in the `launch.json` file is to create 3 configurations: + +- `Main` is used to start the main process and also expose port 9222 for remote debugging + (`--remote-debugging-port=9222`). This is the port that we will use to attach the debugger + for the `Renderer`. Because the main process is a Node.js process, the type is set to + `pwa-node` (`pwa-` is the prefix that tells VS Code to use the latest JavaScript debugger). +- `Renderer` is used to debug the renderer process. Because the main process is the one + that creates the process, we have to "attach" to it (`"request": "attach"`) instead of + creating a new one. + The renderer process is a web one, so the debugger we have to use is `pwa-chrome`. +- `Main + renderer` is a [compound task] that executes the previous ones simultaneously. + +:::caution + +Because we are attaching to a process in `Renderer`, it is possible that the first lines of +your code will be skipped as the debugger will not have had enough time to connect before they are +being executed. +You can work around this by refreshing the page or setting a timeout before executing the code +in development mode. + +::: + +:::info Further reading + +If you want to dig deeper in the debugging area, the following guides provide more information: + +- [Application Debugging] +- [DevTools Extensions][devtools extension] + +::: + +## Summary + +Electron applications are set up using npm packages. The Electron executable should be installed +in your project's `devDependencies` and can be run in development mode using a script in your +package.json file. + +The executable runs the JavaScript entry point found in the `main` property of your package.json. +This file controls Electron's **main process**, which runs an instance of Node.js and is +responsible for your app's lifecycle, displaying native interfaces, performing privileged operations, +and managing renderer processes. + +**Renderer processes** (or renderers for short) are responsible for display graphical content. You can +load a web page into a renderer by pointing it to either a web address or a local HTML file. +Renderers behave very similarly to regular web pages and have access to the same web APIs. + +In the next section of the tutorial, we will be learning how to augment the renderer process with +privileged APIs and how to communicate between processes. + + + +[activate]: ../api/app.md#event-activate-macos +[advanced-installation]: installation.md +[app]: ../api/app.md +[app-quit]: ../api/app.md#appquit +[app-ready]: ../api/app.md#event-ready +[app-when-ready]: ../api/app.md#appwhenready +[application debugging]: ./application-debugging.md +[browser-window]: ../api/browser-window.md +[commonjs]: https://nodejs.org/docs/../api/modules.html#modules_modules_commonjs_modules +[compound task]: https://code.visualstudio.com/Docs/editor/tasks#_compound-tasks +[devtools extension]: ./devtools-extension.md +[event emitters]: https://nodejs.org/api/events.html#events +[gitignore]: https://git-scm.com/docs/gitignore +[gitignore-template]: https://github.com/github/gitignore/blob/main/Node.gitignore +[installation]: ./installation.md +[node-platform]: https://nodejs.org/api/process.html#process_process_platform +[package-json-main]: https://docs.npmjs.com/cli/v7/configuring-npm/package-json#main +[package-scripts]: https://docs.npmjs.com/cli/v7/using-npm/scripts +[process-model]: process-model.md +[react]: https://reactjs.org +[repl]: ./repl.md +[sandbox]: ./sandbox.md +[webpack]: https://webpack.js.org +[window-all-closed]: ../api/app.md#event-window-all-closed +[wsl]: https://docs.microsoft.com/en-us/windows/wsl/about#what-is-wsl-2 + + + +[prerequisites]: tutorial-1-prerequisites.md +[building your first app]: tutorial-2-first-app.md +[preload]: tutorial-3-preload.md +[features]: tutorial-4-adding-features.md +[packaging]: tutorial-5-packaging.md +[updates]: tutorial-6-publishing-updating.md diff --git a/docs/tutorial/tutorial-3-preload.md b/docs/tutorial/tutorial-3-preload.md new file mode 100644 index 0000000000000..6f84cfe421909 --- /dev/null +++ b/docs/tutorial/tutorial-3-preload.md @@ -0,0 +1,271 @@ +--- +title: 'Using Preload Scripts' +description: 'This guide will step you through the process of creating a barebones Hello World app in Electron, similar to electron/electron-quick-start.' +slug: tutorial-preload +hide_title: false +--- + +:::info Follow along the tutorial + +This is **part 3** of the Electron tutorial. + +1. [Prerequisites][prerequisites] +1. [Building your First App][building your first app] +1. **[Using Preload Scripts][preload]** +1. [Adding Features][features] +1. [Packaging Your Application][packaging] +1. [Publishing and Updating][updates] + +::: + +## Learning goals + +In this part of the tutorial, you will learn what a preload script is and how to use one +to securely expose privileged APIs into the renderer process. You will also learn how to +communicate between main and renderer processes with Electron's inter-process +communication (IPC) modules. + +## What is a preload script? + +Electron's main process is a Node.js environment that has full operating system access. +On top of [Electron modules][modules], you can also access [Node.js built-ins][node-api], +as well as any packages installed via npm. On the other hand, renderer processes run web +pages and do not run Node.js by default for security reasons. + +To bridge Electron's different process types together, we will need to use a special script +called a **preload**. + +## Augmenting the renderer with a preload script + +A BrowserWindow's preload script runs in a context that has access to both the HTML DOM +and a Node.js environment. Preload scripts are injected before a web page loads in the renderer, +similar to a Chrome extension's [content scripts][content-script]. To add features to your renderer +that require privileged access, you can define [global] objects through the +[contextBridge][contextbridge] API. + +To demonstrate this concept, you will create a preload script that exposes your app's +versions of Chrome, Node, and Electron into the renderer. + +Add a new `preload.js` script that exposes selected properties of Electron's `process.versions` +object to the renderer process in a `versions` global variable. + +```js title="preload.js" +const { contextBridge } = require('electron') + +contextBridge.exposeInMainWorld('versions', { + node: () => process.versions.node, + chrome: () => process.versions.chrome, + electron: () => process.versions.electron, + // we can also expose variables, not just functions +}) +``` + +To attach this script to your renderer process, pass its path to the +`webPreferences.preload` option in the BrowserWindow constructor: + +```js {8-10} title="main.js" +const { app, BrowserWindow } = require('electron') +const path = require('path') + +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }) + + win.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() +}) +``` + +:::info + +There are two Node.js concepts that are used here: + +- The [`__dirname`][dirname] string points to the path of the currently executing script + (in this case, your project's root folder). +- The [`path.join`][path-join] API joins multiple path segments together, creating a + combined path string that works across all platforms. + +::: + +At this point, the renderer has access to the `versions` global, so let's display that +information in the window. This variable can be accessed via `window.versions` or simply +`versions`. Create a `renderer.js` script that uses the [`document.getElementById`] +DOM API to replace the displayed text for the HTML element with `info` as its `id` property. + +```js title="renderer.js" +const information = document.getElementById('info') +information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})` +``` + +Then, modify your `index.html` by adding a new element with `info` as its `id` property, +and attach your `renderer.js` script: + +```html {18,20} title="index.html" + + + + + + + Hello from Electron renderer! + + +

Hello from Electron renderer!

+

👋

+

+ + + +``` + +After following the above steps, your app should look something like this: + +![Electron app showing This app is using Chrome (v102.0.5005.63), Node.js (v16.14.2), and Electron (v19.0.3)](../images/preload-example.png) + +And the code should look like this: + +```fiddle docs/fiddles/tutorial-preload + +``` + +## Communicating between processes + +As we have mentioned above, Electron's main and renderer process have distinct responsibilities +and are not interchangeable. This means it is not possible to access the Node.js APIs directly +from the renderer process, nor the HTML Document Object Model (DOM) from the main process. + +The solution for this problem is to use Electron's `ipcMain` and `ipcRenderer` modules for +inter-process communication (IPC). To send a message from your web page to the main process, +you can set up a main process handler with `ipcMain.handle` and +then expose a function that calls `ipcRenderer.invoke` to trigger the handler in your preload script. + +To illustrate, we will add a global function to the renderer called `ping()` +that will return a string from the main process. + +First, set up the `invoke` call in your preload script: + +```js {1,7} title="preload.js" +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('versions', { + node: () => process.versions.node, + chrome: () => process.versions.chrome, + electron: () => process.versions.electron, + ping: () => ipcRenderer.invoke('ping'), + // we can also expose variables, not just functions +}) +``` + +:::caution IPC security + +Notice how we wrap the `ipcRenderer.invoke('ping')` call in a helper function rather +than expose the `ipcRenderer` module directly via context bridge. You **never** want to +directly expose the entire `ipcRenderer` module via preload. This would give your renderer +the ability to send arbitrary IPC messages to the main process, which becomes a powerful +attack vector for malicious code. + +::: + +Then, set up your `handle` listener in the main process. We do this _before_ +loading the HTML file so that the handler is guaranteed to be ready before +you send out the `invoke` call from the renderer. + +```js {1,11} title="main.js" +const { ipcMain } = require('electron') + +const createWindow = () => { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }) + ipcMain.handle('ping', () => 'pong') + win.loadFile('index.html') +} +``` + +Once you have the sender and receiver set up, you can now send messages from the renderer +to the main process through the `'ping'` channel you just defined. + +```js title='renderer.js' +const func = async () => { + const response = await window.versions.ping() + console.log(response) // prints out 'pong' +} + +func() +``` + +:::info + +For more in-depth explanations on using the `ipcRenderer` and `ipcMain` modules, +check out the full [Inter-Process Communication][ipc] guide. + +::: + +## Summary + +A preload script contains code that runs before your web page is loaded into the browser +window. It has access to both DOM APIs and Node.js environment, and is often used to +expose privileged APIs to the renderer via the `contextBridge` API. + +Because the main and renderer processes have very different responsibilities, Electron +apps often use the preload script to set up inter-process communication (IPC) interfaces +to pass arbitrary messages between the two kinds of processes. + +In the next part of the tutorial, we will be showing you resources on adding more +functionality to your app, then teaching you distributing your app to users. + + + +[advanced-installation]: ./installation.md +[application debugging]: ./application-debugging.md +[app]: ../api/app.md +[app-ready]: ../api/app.md#event-ready +[app-when-ready]: ../api/app.md#appwhenready +[browser-window]: ../api/browser-window.md +[commonjs]: https://nodejs.org/docs/latest/api/modules.html#modules_modules_commonjs_modules +[compound task]: https://code.visualstudio.com/Docs/editor/tasks#_compound-tasks +[content-script]: https://developer.chrome.com/docs/extensions/mv3/content_scripts/ +[contextbridge]: ../api/context-bridge.md +[context-isolation]: ./context-isolation.md +[`document.getelementbyid`]: https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById +[devtools-extension]: ./devtools-extension.md +[dirname]: https://nodejs.org/api/modules.html#modules_dirname +[global]: https://developer.mozilla.org/en-US/docs/Glossary/Global_object +[ipc]: ./ipc.md +[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +[modules]: ../api/app.md +[node-api]: https://nodejs.org/dist/latest/docs/api/ +[package-json-main]: https://docs.npmjs.com/cli/v7/configuring-npm/package-json#main +[package-scripts]: https://docs.npmjs.com/cli/v7/using-npm/scripts +[path-join]: https://nodejs.org/api/path.html#path_path_join_paths +[process-model]: ./process-model.md +[react]: https://reactjs.org +[sandbox]: ./sandbox.md +[webpack]: https://webpack.js.org + + + +[prerequisites]: tutorial-1-prerequisites.md +[building your first app]: tutorial-2-first-app.md +[preload]: tutorial-3-preload.md +[features]: tutorial-4-adding-features.md +[packaging]: tutorial-5-packaging.md +[updates]: tutorial-6-publishing-updating.md diff --git a/docs/tutorial/tutorial-4-adding-features.md b/docs/tutorial/tutorial-4-adding-features.md new file mode 100644 index 0000000000000..b7c776c1dbd67 --- /dev/null +++ b/docs/tutorial/tutorial-4-adding-features.md @@ -0,0 +1,77 @@ +--- +title: 'Adding Features' +description: 'In this step of the tutorial, we will share some resources you should read to add features to your application' +slug: tutorial-adding-features +hide_title: false +--- + +:::info Follow along the tutorial + +This is **part 4** of the Electron tutorial. + +1. [Prerequisites][prerequisites] +1. [Building your First App][building your first app] +1. [Using Preload Scripts][preload] +1. **[Adding Features][features]** +1. [Packaging Your Application][packaging] +1. [Publishing and Updating][updates] + +::: + +## Adding application complexity + +If you have been following along, you should have a functional Electron application +with a static user interface. From this starting point, you can generally progress +in developing your app in two broad directions: + +1. Adding complexity to your renderer process' web app code +1. Deeper integrations with the operating system and Node.js + +It is important to understand the distinction between these two broad concepts. For the +first point, Electron-specific resources are not necessary. Building a pretty to-do +list in Electron is just pointing your Electron BrowserWindow to a pretty +to-do list web app. Ultimately, you are building your renderer's UI using the same tools +(HTML, CSS, JavaScript) that you would on the web. Therefore, Electron's docs will +not go in-depth on how to use standard web tools. + +On the other hand, Electron also provides a rich set of tools that allow +you to integrate with the desktop environment, from creating tray icons to adding +global shortcuts to displaying native menus. It also gives you all the power of a +Node.js environment in the main process. This set of capabilities separates +Electron applications from running a website in a browser tab, and are the +focus of Electron's documentation. + +## How-to examples + +Electron's documentation has many tutorials to help you with more advanced topics +and deeper operating system integrations. To get started, check out the +[How-To Examples][how-to] doc. + +:::note Let us know if something is missing! + +If you can't find what you are looking for, please let us know on [GitHub] or in +our [Discord server][discord]! + +::: + +## What's next? + +For the rest of the tutorial, we will be shifting away from application code +and giving you a look at how you can get your app from your developer machine +into end users' hands. + + + +[discord]: https://discord.com/invite/APGC3k5yaH +[github]: https://github.com/electron/electronjs.org-new/issues/new +[how to]: ./examples.md +[node-platform]: https://nodejs.org/api/process.html#process_process_platform + + + +[prerequisites]: tutorial-1-prerequisites.md +[building your first app]: tutorial-2-first-app.md +[preload]: tutorial-3-preload.md +[features]: tutorial-4-adding-features.md +[packaging]: tutorial-5-packaging.md +[updates]: tutorial-6-publishing-updating.md diff --git a/docs/tutorial/tutorial-5-packaging.md b/docs/tutorial/tutorial-5-packaging.md new file mode 100644 index 0000000000000..3ab4f15b50d84 --- /dev/null +++ b/docs/tutorial/tutorial-5-packaging.md @@ -0,0 +1,225 @@ +--- +title: 'Packaging Your Application' +description: 'To distribute your app with Electron, you need to package it and create installers.' +slug: tutorial-packaging +hide_title: false +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Follow along the tutorial + +This is **part 5** of the Electron tutorial. + +1. [Prerequisites][prerequisites] +1. [Building your First App][building your first app] +1. [Using Preload Scripts][preload] +1. [Adding Features][features] +1. **[Packaging Your Application][packaging]** +1. [Publishing and Updating][updates] + +::: + +## Learning goals + +In this part of the tutorial, we'll be going over the basics of packaging and distributing +your app with [Electron Forge]. + +## Using Electron Forge + +Electron does not have any tooling for packaging and distribution bundled into its core +modules. Once you have a working Electron app in dev mode, you need to use +additional tooling to create a packaged app you can distribute to your users (also known +as a **distributable**). Distributables can be either installers (e.g. MSI on Windows) or +portable executable files (e.g. `.app` on macOS). + +Electron Forge is an all-in-one tool that handles the packaging and distribution of Electron +apps. Under the hood, it combines a lot of existing Electron tools (e.g. [`electron-packager`], +[`@electron/osx-sign`], [`electron-winstaller`], etc.) into a single interface so you do not +have to worry about wiring them all together. + +### Importing your project into Forge + +You can install Electron Forge's CLI in your project's `devDependencies` and import your +existing project with a handy conversion script. + +```sh npm2yarn +npm install --save-dev @electron-forge/cli +npx electron-forge import +``` + +Once the conversion script is done, Forge should have added a few scripts +to your `package.json` file. + +```json title='package.json' + //... + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make" + }, + //... +``` + +:::info CLI documentation + +For more information on `make` and other Forge APIs, check out +the [Electron Forge CLI documentation]. + +::: + +You should also notice that your package.json now has a few more packages installed +under your `devDependencies`, and contains an added `config.forge` field with an array +of makers configured. **Makers** are Forge plugins that create distributables from +your source code. You should see multiple makers in the pre-populated configuration, +one for each target platform. + +### Creating a distributable + +To create a distributable, use your project's new `make` script, which runs the +`electron-forge make` command. + +```sh npm2yarn +npm run make +``` + +This `make` command contains two steps: + +1. It will first run `electron-forge package` under the hood, which bundles your app + code together with the Electron binary. The packaged code is generated into a folder. +1. It will then use this packaged app folder to create a separate distributable for each + configured maker. + +After the script runs, you should see an `out` folder containing both the distributable +and a folder containing the packaged application code. + +```plain title='macOS output example' +out/ +├── out/make/zip/darwin/x64/my-electron-app-darwin-x64-1.0.0.zip +├── ... +└── out/my-electron-app-darwin-x64/my-electron-app.app/Contents/MacOS/my-electron-app +``` + +The distributable in the `out/make` folder should be ready to launch! You have now +created your first bundled Electron application. + +:::tip Distributable formats + +Electron Forge can be configured to create distributables in different OS-specific formats +(e.g. DMG, deb, MSI, etc.). See Forge's [Makers] documentation for all configuration options. + +::: + +:::note Packaging without Electron Forge + +If you want to manually package your code, or if you're just interested understanding the +mechanics behind packaging an Electron app, check out the full [Application Packaging] +documentation. + +::: + +## Important: signing your code + +In order to distribute desktop applications to end users, we _highly recommended_ for you +to **code sign** your Electron app. Code signing is an important part of shipping +desktop applications, and is mandatory for the auto-update step in the final part +of the tutorial. + +Code signing is a security technology that you use to certify that a desktop app was +created by a known source. Windows and macOS have their own OS-specific code signing +systems that will make it difficult for users to download or launch unsigned applications. + +If you already have code signing certificates for Windows and macOS, you can set your +credentials in your Forge configuration. Otherwise, please refer to the full +[Code Signing] documentation to learn how to purchase a certificate and for more information +on the desktop app code signing process. + +On macOS, code signing is done at the app packaging level. On Windows, distributable installers +are signed instead. + + + + +```json title='package.json' {6-18} +{ + //... + "config": { + "forge": { + //... + "packagerConfig": { + "osxSign": { + "identity": "Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)", + "hardened-runtime": true, + "entitlements": "entitlements.plist", + "entitlements-inherit": "entitlements.plist", + "signature-flags": "library" + }, + "osxNotarize": { + "appleId": "felix@felix.fun", + "appleIdPassword": "this-is-a-secret" + } + } + //... + } + } + //... +} +``` + + + + +```json title='package.json' {6-14} +{ + //... + "config": { + "forge": { + //... + "makers": [ + { + "name": "@electron-forge/maker-squirrel", + "config": { + "certificateFile": "./cert.pfx", + "certificatePassword": "this-is-a-secret" + } + } + ] + //... + } + } + //... +} +``` + + + + +## Summary + +Electron applications need to be packaged to be distributed to users. In this tutorial, +you imported your app into Electron Forge and configured it to package your app and +generate installers. + +In order for your application to be trusted by the user's system, you need to digitally +certify that the distributable is authentic and untampered by code signing it. Your app +can be signed through Forge once you configure it to use your code signing certificate +information. + +[`@electron/osx-sign`]: https://github.com/electron/osx-sign +[application packaging]: ./application-distribution.md +[code signing]: ./code-signing.md +[`electron-packager`]: https://github.com/electron/electron-packager +[`electron-winstaller`]: https://github.com/electron/windows-installer +[electron forge]: https://www.electronforge.io +[electron forge cli documentation]: https://www.electronforge.io/cli#commands +[makers]: https://www.electronforge.io/config/makers + + + +[prerequisites]: tutorial-1-prerequisites.md +[building your first app]: tutorial-2-first-app.md +[preload]: tutorial-3-preload.md +[features]: tutorial-4-adding-features.md +[packaging]: tutorial-5-packaging.md +[updates]: tutorial-6-publishing-updating.md diff --git a/docs/tutorial/tutorial-6-publishing-updating.md b/docs/tutorial/tutorial-6-publishing-updating.md new file mode 100644 index 0000000000000..65b89766d88f2 --- /dev/null +++ b/docs/tutorial/tutorial-6-publishing-updating.md @@ -0,0 +1,251 @@ +--- +title: 'Publishing and Updating' +description: "There are several ways to update an Electron application. The easiest and officially supported one is taking advantage of the built-in Squirrel framework and Electron's autoUpdater module." +slug: tutorial-publishing-updating +hide_title: false +--- + +:::info Follow along the tutorial + +This is **part 6** of the Electron tutorial. + +1. [Prerequisites][prerequisites] +1. [Building your First App][building your first app] +1. [Using Preload Scripts][preload] +1. [Adding Features][features] +1. [Packaging Your Application][packaging] +1. **[Publishing and Updating][updates]** + +::: + +## Learning goals + +If you've been following along, this is the last step of the tutorial! In this part, +you will publish your app to GitHub releases and integrate automatic updates +into your app code. + +## Using update.electronjs.org + +The Electron maintainers provide a free auto-updating service for open-source apps +at https://update.electronjs.org. Its requirements are: + +- Your app runs on macOS or Windows +- Your app has a public GitHub repository +- Builds are published to [GitHub releases] +- Builds are [code signed][code-signed] + +At this point, we'll assume that you have already pushed all your +code to a public GitHub repository. + +:::info Alternative update services + +If you're using an alternate repository host (e.g. GitLab or Bitbucket) or if +you need to keep your code repository private, please refer to our +[step-by-step guide][update-server] on hosting your own Electron update server. + +::: + +## Publishing a GitHub release + +Electron Forge has [Publisher] plugins that can automate the distribution +of your packaged application to various sources. In this tutorial, we will +be using the GitHub Publisher, which will allow us to publish +our code to GitHub releases. + +### Generating a personal access token + +Forge cannot publish to any repository on GitHub without permission. You +need to pass in an authenticated token that gives Forge access to +your GitHub releases. The easiest way to do this is to +[create a new personal access token (PAT)][new-pat] +with the `public_repo` scope, which gives write access to your public repositories. +**Make sure to keep this token a secret.** + +### Setting up the GitHub Publisher + +#### Installing the module + +Forge's [GitHub Publisher] is a plugin that +needs to be installed in your project's `devDependencies`: + +```sh npm2yarn +npm install --save-dev @electron-forge/publisher-github +``` + +#### Configuring the publisher in Forge + +Once you have it installed, you need to set it up in your Forge +configuration. A full list of options is documented in the Forge's +[`PublisherGitHubConfig`] API docs. + +```json title='package.json' {6-16} +{ + //... + "config": { + "forge": { + "publishers": [ + { + "name": "@electron-forge/publisher-github", + "config": { + "repository": { + "owner": "github-user-name", + "name": "github-repo-name" + }, + "prerelease": false, + "draft": true + } + } + ] + } + } + //... +} +``` + +:::tip Drafting releases before publishing + +Notice that you have configured Forge to publish your release as a draft. +This will allow you to see the release with its generated artifacts +without actually publishing it to your end users. You can manually +publish your releases via GitHub after writing release notes and +double-checking that your distributables work. + +::: + +#### Setting up your authentication token + +You also need to make the Publisher aware of your authentication token. +By default, it will use the value stored in the `GITHUB_TOKEN` environment +variable. + +### Running the publish command + +Add Forge's [publish command] to your npm scripts. + +```json {6} title='package.json' + //... + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make", + "publish": "electron-forge publish" + }, + //... +``` + +This command will run your configured makers and publish the output distributables to a new +GitHub release. + +```sh npm2yarn +npm run publish +``` + +By default, this will only publish a single distributable for your host operating system and +architecture. You can publish for different architectures by passing in the `--arch` flag to your +Forge commands. + +The name of this release will correspond to the `version` field in your project's package.json file. + +:::tip Tagging releases + +Optionally, you can also [tag your releases in Git][git-tag] so that your +release is associated with a labeled point in your code history. npm comes +with a handy [`npm version`](https://docs.npmjs.com/cli/v8/commands/npm-version) +command that can handle the version bumping and tagging for you. + +::: + +#### Bonus: Publishing in GitHub Actions + +Publishing locally can be painful, especially because you can only create distributables +for your host operating system (i.e. you can't publish a Window `.exe` file from macOS). + +A solution for this would be to publish your app via automation workflows +such as [GitHub Actions], which can run tasks in the +cloud on Ubuntu, macOS, and Windows. This is the exact approach taken by [Electron Fiddle]. +You can refer to Fiddle's [Build and Release pipeline][fiddle-build] +and [Forge configuration][fiddle-forge-config] +for more details. + +## Instrumenting your updater code + +Now that we have a functional release system via GitHub releases, we now need to tell our +Electron app to download an update whenever a new release is out. Electron apps do this +via the [autoUpdater] module, which reads from an update server feed to check if a new version +is available for download. + +The update.electronjs.org service provides an updater-compatible feed. For example, Electron +Fiddle v0.28.0 will check the endpoint at https://update.electronjs.org/electron/fiddle/darwin/v0.28.0 +to see if a newer GitHub release is available. + +After your release is published to GitHub, the update.electronjs.org service should work +for your application. The only step left is to configure the feed with the autoUpdater module. + +To make this process easier, the Electron team maintains the [`update-electron-app`] module, +which sets up the autoUpdater boilerplate for update.electronjs.org in one function +call — no configuration required. This module will search for the update.electronjs.org +feed that matches your project's package.json `"repository"` field. + +First, install the module as a runtime dependency. + +```sh npm2yarn +npm install update-electron-app +``` + +Then, import the module and call it immediately in the main process. + +```js title='main.js' +require('update-electron-app')() +``` + +And that is all it takes! Once your application is packaged, it will update itself for each new +GitHub release that you publish. + +## Summary + +In this tutorial, we configured Electron Forge's GitHub Publisher to upload your app's +distributables to GitHub releases. Since distributables cannot always be generated +between platforms, we recommend setting up your building and publishing flow +in a Continuous Integration pipeline if you do not have access to machines. + +Electron applications can self-update by pointing the autoUpdater module to an update server feed. +update.electronjs.org is a free update server provided by Electron for open-source applications +published on GitHub releases. Configuring your Electron app to use this service is as easy as +installing and importing the `update-electron-app` module. + +If your application is not eligible for update.electronjs.org, you should instead deploy your +own update server and configure the autoUpdater module yourself. + +:::info 🌟 You're done! + +From here, you have officially completed our tutorial to Electron. Feel free to explore the +rest of our docs and happy developing! If you have questions, please stop by our community +[Discord server]. + +::: + +[autoupdater]: ../api/auto-updater.md +[code-signed]: ./code-signing.md +[discord server]: https://discord.com/invite/APGC3k5yaH +[electron fiddle]: https://electronjs.org/fiddle +[fiddle-build]: https://github.com/electron/fiddle/blob/master/.github/workflows/build.yaml +[fiddle-forge-config]: https://github.com/electron/fiddle/blob/master/forge.config.js +[github actions]: https://github.com/features/actions +[github publisher]: https://www.electronforge.io/config/publishers/github +[github releases]: https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository +[git tag]: https://git-scm.com/book/en/v2/Git-Basics-Tagging +[new-pat]: https://github.com/settings/tokens/new +[publish command]: https://www.electronforge.io/cli#publish +[publisher]: https://www.electronforge.io/config/publishers +[`publishergithubconfig`]: https://js.electronforge.io/publisher/github/interfaces/publishergithubconfig +[`update-electron-app`]: https://github.com/electron/update-electron-app +[update-server]: ./updates.md + + + +[prerequisites]: tutorial-1-prerequisites.md +[building your first app]: tutorial-2-first-app.md +[preload]: tutorial-3-preload.md +[features]: tutorial-4-adding-features.md +[packaging]: tutorial-5-packaging.md +[updates]: tutorial-6-publishing-updating.md diff --git a/docs/tutorial/updates.md b/docs/tutorial/updates.md index 26256afe9feb4..530d9658d496d 100644 --- a/docs/tutorial/updates.md +++ b/docs/tutorial/updates.md @@ -1,130 +1,132 @@ -# Updating Applications - -There are several ways to update an Electron application. The easiest and -officially supported one is taking advantage of the built-in +--- +title: 'Updating Applications' +description: "There are several ways to update an Electron application. The easiest and officially supported one is taking advantage of the built-in Squirrel framework and Electron's autoUpdater module." +slug: updates +hide_title: false +--- + +There are several ways to provide automatic updates to your Electron application. +The easiest and officially supported one is taking advantage of the built-in [Squirrel](https://github.com/Squirrel) framework and Electron's [autoUpdater](../api/auto-updater.md) module. -## Using `update.electronjs.org` +## Using update.electronjs.org -GitHub's Electron team maintains [update.electronjs.org], a free and open-source +The Electron team maintains [update.electronjs.org], a free and open-source webservice that Electron apps can use to self-update. The service is designed for Electron apps that meet the following criteria: - App runs on macOS or Windows - App has a public GitHub repository -- Builds are published to GitHub Releases -- Builds are code-signed +- Builds are published to [GitHub Releases][gh-releases] +- Builds are [code-signed](./code-signing.md) The easiest way to use this service is by installing [update-electron-app], a Node.js module preconfigured for use with update.electronjs.org. -Install the module: +Install the module using your Node.js package manager of choice: -```sh +```sh npm2yarn npm install update-electron-app ``` -Invoke the updater from your app's main process file: +Then, invoke the updater from your app's main process file: -```js +```js title="main.js" require('update-electron-app')() ``` By default, this module will check for updates at app startup, then every ten -minutes. When an update is found, it will automatically be downloaded in the background. When the download completes, a dialog is displayed allowing the user -to restart the app. +minutes. When an update is found, it will automatically be downloaded in the background. +When the download completes, a dialog is displayed allowing the user to restart the app. If you need to customize your configuration, you can -[pass options to `update-electron-app`][update-electron-app] +[pass options to update-electron-app][update-electron-app] or [use the update service directly][update.electronjs.org]. -## Using `electron-builder` - -If your app is packaged with [`electron-builder`][electron-builder-lib] you can use the -[electron-updater] module, which does not require a server and allows for updates -from S3, GitHub or any other static file host. This sidesteps Electron's built-in -update mechanism, meaning that the rest of this documentation will not apply to -`electron-builder`'s updater. - -## Deploying an Update Server +## Using other update services If you're developing a private Electron application, or if you're not publishing releases to GitHub Releases, it may be necessary to run your own update server. +### Step 1: Deploying an update server + Depending on your needs, you can choose from one of these: - [Hazel][hazel] – Update server for private or open-source apps which can be -deployed for free on [Now][now]. It pulls from [GitHub Releases][gh-releases] -and leverages the power of GitHub's CDN. + deployed for free on [Vercel][vercel]. It pulls from [GitHub Releases][gh-releases] + and leverages the power of GitHub's CDN. - [Nuts][nuts] – Also uses [GitHub Releases][gh-releases], but caches app -updates on disk and supports private repositories. + updates on disk and supports private repositories. - [electron-release-server][electron-release-server] – Provides a dashboard for -handling releases and does not require releases to originate on GitHub. + handling releases and does not require releases to originate on GitHub. - [Nucleus][nucleus] – A complete update server for Electron apps maintained by -Atlassian. Supports multiple applications and channels; uses a static file store -to minify server cost. + Atlassian. Supports multiple applications and channels; uses a static file store + to minify server cost. + +Once you've deployed your update server, you can instrument your app code to receive and +apply the updates with Electron's [autoUpdater] module. -## Implementing Updates in Your App +### Step 2: Receiving updates in your app -Once you've deployed your update server, continue with importing the required -modules in your code. The following code might vary for different server -software, but it works like described when using -[Hazel](https://github.com/zeit/hazel). +First, import the required modules in your main process code. The following code might +vary for different server software, but it works like described when using [Hazel][hazel]. -**Important:** Please ensure that the code below will only be executed in -your packaged app, and not in development. You can use -[electron-is-dev](https://github.com/sindresorhus/electron-is-dev) to check for -the environment. +:::warning Check your execution environment! -```javascript +Please ensure that the code below will only be executed in your packaged app, and not in development. +You can use the [app.isPackaged](../api/app.md#appispackaged-readonly) API to check the environment. + +::: + +```javascript title='main.js' const { app, autoUpdater, dialog } = require('electron') ``` -Next, construct the URL of the update server and tell +Next, construct the URL of the update server feed and tell [autoUpdater](../api/auto-updater.md) about it: -```javascript +```javascript title='main.js' const server = 'https://your-deployment-url.com' -const feed = `${server}/update/${process.platform}/${app.getVersion()}` +const url = `${server}/update/${process.platform}/${app.getVersion()}` -autoUpdater.setFeedURL(feed) +autoUpdater.setFeedURL({ url }) ``` As the final step, check for updates. The example below will check every minute: -```javascript +```javascript title='main.js' setInterval(() => { autoUpdater.checkForUpdates() }, 60000) ``` -Once your application is [packaged](../tutorial/application-distribution.md), +Once your application is [packaged](./application-distribution.md), it will receive an update for each new [GitHub Release](https://help.github.com/articles/creating-releases/) that you publish. -## Applying Updates +### Step 3: Notifying users when updates are available Now that you've configured the basic update mechanism for your application, you need to ensure that the user will get notified when there's an update. This -can be achieved using the autoUpdater API -[events](../api/auto-updater.md#events): +can be achieved using the [autoUpdater API events](../api/auto-updater.md#events): -```javascript +```javascript title="main.js" autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => { const dialogOpts = { type: 'info', buttons: ['Restart', 'Later'], title: 'Application Update', message: process.platform === 'win32' ? releaseNotes : releaseName, - detail: 'A new version has been downloaded. Restart the application to apply the updates.' + detail: + 'A new version has been downloaded. Restart the application to apply the updates.', } - dialog.showMessageBox(dialogOpts, (response) => { - if (response === 0) autoUpdater.quitAndInstall() + dialog.showMessageBox(dialogOpts).then((returnValue) => { + if (returnValue.response === 0) autoUpdater.quitAndInstall() }) }) ``` @@ -133,17 +135,25 @@ Also make sure that errors are [being handled](../api/auto-updater.md#event-error). Here's an example for logging them to `stderr`: -```javascript -autoUpdater.on('error', message => { +```javascript title="main.js" +autoUpdater.on('error', (message) => { console.error('There was a problem updating the application') console.error(message) }) ``` -[electron-builder-lib]: https://github.com/electron-userland/electron-builder -[electron-updater]: https://www.electron.build/auto-update -[now]: https://zeit.co/now -[hazel]: https://github.com/zeit/hazel +:::info Handling updates manually + +Because the requests made by autoUpdate aren't under your direct control, you may find situations +that are difficult to handle (such as if the update server is behind authentication). The `url` +field supports the `file://` protocol, which means that with some effort, you can sidestep the +server-communication aspect of the process by loading your update from a local directory. +[Here's an example of how this could work](https://github.com/electron/electron/issues/5020#issuecomment-477636990). + +::: + +[vercel]: https://vercel.com +[hazel]: https://github.com/vercel/hazel [nuts]: https://github.com/GitbookIO/nuts [gh-releases]: https://help.github.com/articles/creating-releases/ [electron-release-server]: https://github.com/ArekSredzki/electron-release-server diff --git a/docs/tutorial/using-native-node-modules.md b/docs/tutorial/using-native-node-modules.md index 8d47cd9d53f22..ccd00c1b3c9b6 100644 --- a/docs/tutorial/using-native-node-modules.md +++ b/docs/tutorial/using-native-node-modules.md @@ -1,8 +1,9 @@ -# Using Native Node Modules +# Native Node Modules -Native Node modules are supported by Electron, but since Electron is very -likely to use a different V8 version from the Node binary installed on your -system, the modules you use will need to be recompiled for Electron. Otherwise, +Native Node.js modules are supported by Electron, but since Electron has a different +[application binary interface (ABI)][abi] from a given Node.js binary (due to +differences such as using Chromium's BoringSSL instead of OpenSSL), the native +modules you use will need to be recompiled for Electron. Otherwise, you will get the following class of error when you try to run your app: ```sh @@ -23,9 +24,11 @@ You can install modules like other Node projects, and then rebuild the modules for Electron with the [`electron-rebuild`][electron-rebuild] package. This module can automatically determine the version of Electron and handle the manual steps of downloading headers and rebuilding native modules for your app. +If you are using [Electron Forge][electron-forge], this tool is used automatically +in both development mode and when making distributables. -For example, to install `electron-rebuild` and then rebuild modules with it -via the command line: +For example, to install the standalone `electron-rebuild` tool and then rebuild +modules with it via the command line: ```sh npm install --save-dev electron-rebuild @@ -33,12 +36,12 @@ npm install --save-dev electron-rebuild # Every time you run "npm install", run this: ./node_modules/.bin/electron-rebuild -# On Windows if you have trouble, try: +# If you have trouble on Windows, try: .\node_modules\.bin\electron-rebuild.cmd ``` -For more information on usage and integration with other tools, consult the -project's README. +For more information on usage and integration with other tools such as [Electron +Packager][electron-packager], consult the project's README. ### Using `npm` @@ -87,7 +90,7 @@ match a public release, instruct `npm` to use the version of Node you have bundl with your custom build. ```sh -npm rebuild --nodedir=/path/to/electron/vendor/node +npm rebuild --nodedir=/path/to/src/out/Default/gen/node_headers ``` ## Troubleshooting @@ -129,13 +132,13 @@ should look like this: In particular, it's important that: -- you link against `node.lib` from _Electron_ and not Node. If you link against +* you link against `node.lib` from _Electron_ and not Node. If you link against the wrong `node.lib` you will get load-time errors when you require the module in Electron. -- you include the flag `/DELAYLOAD:node.exe`. If the `node.exe` link is not +* you include the flag `/DELAYLOAD:node.exe`. If the `node.exe` link is not delayed, then the delay-load hook won't get a chance to fire and the node symbols won't be correctly resolved. -- `win_delay_load_hook.obj` is linked directly into the final DLL. If the hook +* `win_delay_load_hook.obj` is linked directly into the final DLL. If the hook is set up in a dependent DLL, it won't fire at the right time. See [`node-gyp`](https://github.com/nodejs/node-gyp/blob/e2401e1395bef1d3c8acec268b42dc5fb71c4a38/src/win_delay_load_hook.cc) @@ -147,23 +150,25 @@ for an example delay-load hook if you're implementing your own. native Node modules with prebuilt binaries for multiple versions of Node and Electron. -If modules provide binaries for the usage in Electron, make sure to omit -`--build-from-source` and the `npm_config_build_from_source` environment -variable in order to take full advantage of the prebuilt binaries. +If the `prebuild`-powered module provide binaries for the usage in Electron, +make sure to omit `--build-from-source` and the `npm_config_build_from_source` +environment variable in order to take full advantage of the prebuilt binaries. ## Modules that rely on `node-pre-gyp` The [`node-pre-gyp` tool][node-pre-gyp] provides a way to deploy native Node modules with prebuilt binaries, and many popular modules are using it. -Usually those modules work fine under Electron, but sometimes when Electron uses -a newer version of V8 than Node and/or there are ABI changes, bad things may -happen. So in general, it is recommended to always build native modules from -source code. `electron-rebuild` handles this for you automatically. +Sometimes those modules work fine under Electron, but when there are no +Electron-specific binaries available, you'll need to build from source. +Because of this, it is recommended to use `electron-rebuild` for these modules. -If you are following the `npm` way of installing modules, then this is done -by default, if not, you have to pass `--build-from-source` to `npm`, or set the -`npm_config_build_from_source` environment variable. +If you are following the `npm` way of installing modules, you'll need to pass +`--build-from-source` to `npm`, or set the `npm_config_build_from_source` +environment variable. +[abi]: https://en.wikipedia.org/wiki/Application_binary_interface [electron-rebuild]: https://github.com/electron/electron-rebuild +[electron-forge]: https://electronforge.io/ +[electron-packager]: https://github.com/electron/electron-packager [node-pre-gyp]: https://github.com/mapbox/node-pre-gyp diff --git a/docs/tutorial/using-pepper-flash-plugin.md b/docs/tutorial/using-pepper-flash-plugin.md index d432785bdc5e1..0a060e72eb97a 100644 --- a/docs/tutorial/using-pepper-flash-plugin.md +++ b/docs/tutorial/using-pepper-flash-plugin.md @@ -1,82 +1,6 @@ -# Using Pepper Flash Plugin +# Pepper Flash Plugin -Electron supports the Pepper Flash plugin. To use the Pepper Flash plugin in -Electron, you should manually specify the location of the Pepper Flash plugin -and then enable it in your application. +Electron no longer supports the Pepper Flash plugin, as Chrome has removed support. -## Prepare a Copy of Flash Plugin - -On macOS and Linux, the details of the Pepper Flash plugin can be found by -navigating to `chrome://flash` in the Chrome browser. Its location and version -are useful for Electron's Pepper Flash support. You can also copy it to another -location. - -## Add Electron Switch - -You can directly add `--ppapi-flash-path` and `--ppapi-flash-version` to the -Electron command line or by using the `app.commandLine.appendSwitch` method -before the app ready event. Also, turn on `plugins` option of `BrowserWindow`. - -For example: - -```javascript -const { app, BrowserWindow } = require('electron') -const path = require('path') - -// Specify flash path, supposing it is placed in the same directory with main.js. -let pluginName -switch (process.platform) { - case 'win32': - pluginName = 'pepflashplayer.dll' - break - case 'darwin': - pluginName = 'PepperFlashPlayer.plugin' - break - case 'linux': - pluginName = 'libpepflashplayer.so' - break -} -app.commandLine.appendSwitch('ppapi-flash-path', path.join(__dirname, pluginName)) - -// Optional: Specify flash version, for example, v17.0.0.169 -app.commandLine.appendSwitch('ppapi-flash-version', '17.0.0.169') - -app.on('ready', () => { - let win = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - plugins: true - } - }) - win.loadURL(`file://${__dirname}/index.html`) - // Something else -}) -``` - -You can also try loading the system wide Pepper Flash plugin instead of shipping -the plugins yourself, its path can be received by calling -`app.getPath('pepperFlashSystemPlugin')`. - -## Enable Flash Plugin in a `` Tag - -Add `plugins` attribute to `` tag. - -```html - -``` - -## Troubleshooting - -You can check if Pepper Flash plugin was loaded by inspecting -`navigator.plugins` in the console of devtools (although you can't know if the -plugin's path is correct). - -The architecture of Pepper Flash plugin has to match Electron's one. On Windows, -a common error is to use 32bit version of Flash plugin against 64bit version of -Electron. - -On Windows the path passed to `--ppapi-flash-path` has to use `\` as path -delimiter, using POSIX-style paths will not work. - -For some operations, such as streaming media using RTMP, it is necessary to grant wider permissions to players’ `.swf` files. One way of accomplishing this, is to use [nw-flash-trust](https://github.com/szwacz/nw-flash-trust). +See [Chromium's Flash Roadmap](https://www.chromium.org/flash-roadmap) for more +details. diff --git a/docs/tutorial/using-selenium-and-webdriver.md b/docs/tutorial/using-selenium-and-webdriver.md deleted file mode 100644 index c2478629d5ed9..0000000000000 --- a/docs/tutorial/using-selenium-and-webdriver.md +++ /dev/null @@ -1,173 +0,0 @@ -# Using Selenium and WebDriver - -From [ChromeDriver - WebDriver for Chrome][chrome-driver]: - -> WebDriver is an open source tool for automated testing of web apps across many -> browsers. It provides capabilities for navigating to web pages, user input, -> JavaScript execution, and more. ChromeDriver is a standalone server which -> implements WebDriver's wire protocol for Chromium. It is being developed by -> members of the Chromium and WebDriver teams. - -## Setting up Spectron - -[Spectron][spectron] is the officially supported ChromeDriver testing framework -for Electron. It is built on top of [WebdriverIO](http://webdriver.io/) and -has helpers to access Electron APIs in your tests and bundles ChromeDriver. - -```sh -$ npm install --save-dev spectron -``` - -```javascript -// A simple test to verify a visible window is opened with a title -const Application = require('spectron').Application -const assert = require('assert') - -const myApp = new Application({ - path: '/Applications/MyApp.app/Contents/MacOS/MyApp' -}) - -const verifyWindowIsVisibleWithTitle = async (app) => { - await app.start() - try { - // Check if the window is visible - const isVisible = await app.browserWindow.isVisible() - // Verify the window is visible - assert.strictEqual(isVisible, true) - // Get the window's title - const title = await app.client.getTitle() - // Verify the window's title - assert.strictEqual(title, 'My App') - } catch (error) { - // Log any failures - console.error('Test failed', error.message) - } - // Stop the application - await app.stop() -} - -verifyWindowIsVisibleWithTitle(myApp) -``` - -## Setting up with WebDriverJs - -[WebDriverJs](https://code.google.com/p/selenium/wiki/WebDriverJs) provides -a Node package for testing with web driver, we will use it as an example. - -### 1. Start ChromeDriver - -First you need to download the `chromedriver` binary, and run it: - -```sh -$ npm install electron-chromedriver -$ ./node_modules/.bin/chromedriver -Starting ChromeDriver (v2.10.291558) on port 9515 -Only local connections are allowed. -``` - -Remember the port number `9515`, which will be used later - -### 2. Install WebDriverJS - -```sh -$ npm install selenium-webdriver -``` - -### 3. Connect to ChromeDriver - -The usage of `selenium-webdriver` with Electron is the same with -upstream, except that you have to manually specify how to connect -chrome driver and where to find Electron's binary: - -```javascript -const webdriver = require('selenium-webdriver') - -const driver = new webdriver.Builder() - // The "9515" is the port opened by chrome driver. - .usingServer('http://localhost:9515') - .withCapabilities({ - chromeOptions: { - // Here is the path to your Electron binary. - binary: '/Path-to-Your-App.app/Contents/MacOS/Electron' - } - }) - .forBrowser('electron') - .build() - -driver.get('http://www.google.com') -driver.findElement(webdriver.By.name('q')).sendKeys('webdriver') -driver.findElement(webdriver.By.name('btnG')).click() -driver.wait(() => { - return driver.getTitle().then((title) => { - return title === 'webdriver - Google Search' - }) -}, 1000) - -driver.quit() -``` - -## Setting up with WebdriverIO - -[WebdriverIO](http://webdriver.io/) provides a Node package for testing with web -driver. - -### 1. Start ChromeDriver - -First you need to download the `chromedriver` binary, and run it: - -```sh -$ npm install electron-chromedriver -$ ./node_modules/.bin/chromedriver --url-base=wd/hub --port=9515 -Starting ChromeDriver (v2.10.291558) on port 9515 -Only local connections are allowed. -``` - -Remember the port number `9515`, which will be used later - -### 2. Install WebdriverIO - -```sh -$ npm install webdriverio -``` - -### 3. Connect to chrome driver - -```javascript -const webdriverio = require('webdriverio') -const options = { - host: 'localhost', // Use localhost as chrome driver server - port: 9515, // "9515" is the port opened by chrome driver. - desiredCapabilities: { - browserName: 'chrome', - chromeOptions: { - binary: '/Path-to-Your-App/electron', // Path to your Electron binary. - args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/ - } - } -} - -let client = webdriverio.remote(options) - -client - .init() - .url('http://google.com') - .setValue('#q', 'webdriverio') - .click('#btnG') - .getTitle().then((title) => { - console.log('Title was: ' + title) - }) - .end() -``` - -## Workflow - -To test your application without rebuilding Electron, -[place](https://github.com/electron/electron/blob/master/docs/tutorial/application-distribution.md) -your app source into Electron's resource directory. - -Alternatively, pass an argument to run with your Electron binary that points to -your app's folder. This eliminates the need to copy-paste your app into -Electron's resource directory. - -[chrome-driver]: https://sites.google.com/a/chromium.org/chromedriver/ -[spectron]: https://electronjs.org/spectron diff --git a/docs/tutorial/web-embeds.md b/docs/tutorial/web-embeds.md new file mode 100644 index 0000000000000..ec6088ec01613 --- /dev/null +++ b/docs/tutorial/web-embeds.md @@ -0,0 +1,57 @@ +# Web Embeds + +## Overview + +If you want to embed (third-party) web content in an Electron `BrowserWindow`, +there are three options available to you: ``); + await delay(1000); + w.webContents.debugger.attach('1.1'); + const targets = await w.webContents.debugger.sendCommand('Target.getTargets'); + const iframeTarget = targets.targetInfos.find((t: any) => t.type === 'iframe'); + const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', { + targetId: iframeTarget.targetId, + flatten: true + }); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: 10, + y: 10, + clickCount: 1, + button: 'left' + }, sessionId); + let willNavigateEmitted = false; + w.webContents.on('will-navigate', () => { + willNavigateEmitted = true; + }); + await emittedOnce(w.webContents, 'did-navigate'); + expect(willNavigateEmitted).to.be.true(); + }); + }); + + describe('will-redirect event', () => { + let server = null as unknown as http.Server; + let url = null as unknown as string; + before((done) => { + server = http.createServer((req, res) => { + if (req.url === '/302') { + res.setHeader('Location', '/200'); + res.statusCode = 302; + res.end(); + } else if (req.url === '/navigate-302') { + res.end(``); + } else { + res.end(); + } + }); + server.listen(0, '127.0.0.1', () => { + url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + done(); + }); + }); + + after(() => { + server.close(); + }); + it('is emitted on redirects', async () => { + const willRedirect = emittedOnce(w.webContents, 'will-redirect'); + w.loadURL(`${url}/302`); + await willRedirect; + }); + + it('is emitted after will-navigate on redirects', async () => { + let navigateCalled = false; + w.webContents.on('will-navigate', () => { + navigateCalled = true; + }); + const willRedirect = emittedOnce(w.webContents, 'will-redirect'); + w.loadURL(`${url}/navigate-302`); + await willRedirect; + expect(navigateCalled).to.equal(true, 'should have called will-navigate first'); + }); + + it('is emitted before did-stop-loading on redirects', async () => { + let stopCalled = false; + w.webContents.on('did-stop-loading', () => { + stopCalled = true; + }); + const willRedirect = emittedOnce(w.webContents, 'will-redirect'); + w.loadURL(`${url}/302`); + await willRedirect; + expect(stopCalled).to.equal(false, 'should not have called did-stop-loading first'); + }); + + it('allows the window to be closed from the event listener', (done) => { + w.webContents.once('will-redirect', () => { + w.close(); + done(); + }); + w.loadURL(`${url}/302`); + }); + + it('can be prevented', (done) => { + w.webContents.once('will-redirect', (event) => { + event.preventDefault(); + }); + w.webContents.on('will-navigate', (e, u) => { + expect(u).to.equal(`${url}/302`); + }); + w.webContents.on('did-stop-loading', () => { + try { + expect(w.webContents.getURL()).to.equal( + `${url}/navigate-302`, + 'url should not have changed after navigation event' + ); + done(); + } catch (e) { + done(e); + } + }); + w.webContents.on('will-redirect', (e, u) => { + try { + expect(u).to.equal(`${url}/200`); + } catch (e) { + done(e); + } + }); + w.loadURL(`${url}/navigate-302`); + }); + }); + }); + } describe('focus and visibility', () => { - let w = null as unknown as BrowserWindow + let w = null as unknown as BrowserWindow; beforeEach(() => { - w = new BrowserWindow({show: false}) - }) + w = new BrowserWindow({ show: false }); + }); afterEach(async () => { - await closeWindow(w) - w = null as unknown as BrowserWindow - }) + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); describe('BrowserWindow.show()', () => { - it('should focus on window', () => { - w.show() - if (process.platform === 'darwin' && !isCI) { - // on CI, the Electron window will be the only one open, so it'll get - // focus. on not-CI, some other window will have focus, and we don't - // steal focus any more, so we expect isFocused to be false. - expect(w.isFocused()).to.equal(false) - } else { - expect(w.isFocused()).to.equal(true) - } - }) - it('should make the window visible', () => { - w.show() - expect(w.isVisible()).to.equal(true) - }) - it('emits when window is shown', (done) => { - w.once('show', () => { - expect(w.isVisible()).to.equal(true) - done() - }) - w.show() - }) - }) + it('should focus on window', async () => { + await emittedOnce(w, 'focus', () => w.show()); + expect(w.isFocused()).to.equal(true); + }); + it('should make the window visible', async () => { + await emittedOnce(w, 'focus', () => w.show()); + expect(w.isVisible()).to.equal(true); + }); + it('emits when window is shown', async () => { + const show = emittedOnce(w, 'show'); + w.show(); + await show; + expect(w.isVisible()).to.equal(true); + }); + }); describe('BrowserWindow.hide()', () => { it('should defocus on window', () => { - w.hide() - expect(w.isFocused()).to.equal(false) - }) + w.hide(); + expect(w.isFocused()).to.equal(false); + }); it('should make the window not visible', () => { - w.show() - w.hide() - expect(w.isVisible()).to.equal(false) - }) + w.show(); + w.hide(); + expect(w.isVisible()).to.equal(false); + }); it('emits when window is hidden', async () => { - const shown = emittedOnce(w, 'show') - w.show() - await shown - const hidden = emittedOnce(w, 'hide') - w.hide() - await hidden - expect(w.isVisible()).to.equal(false) - }) - }) + const shown = emittedOnce(w, 'show'); + w.show(); + await shown; + const hidden = emittedOnce(w, 'hide'); + w.hide(); + await hidden; + expect(w.isVisible()).to.equal(false); + }); + }); describe('BrowserWindow.showInactive()', () => { it('should not focus on window', () => { - w.showInactive() - expect(w.isFocused()).to.equal(false) - }) - }) + w.showInactive(); + expect(w.isFocused()).to.equal(false); + }); + + // TODO(dsanders11): Enable for Linux once CI plays nice with these kinds of tests + ifit(process.platform !== 'linux')('should not restore maximized windows', async () => { + const maximize = emittedOnce(w, 'maximize'); + const shown = emittedOnce(w, 'show'); + w.maximize(); + // TODO(dsanders11): The maximize event isn't firing on macOS for a window initially hidden + if (process.platform !== 'darwin') { + await maximize; + } else { + await delay(1000); + } + w.showInactive(); + await shown; + expect(w.isMaximized()).to.equal(true); + }); + }); describe('BrowserWindow.focus()', () => { it('does not make the window become visible', () => { - expect(w.isVisible()).to.equal(false) - w.focus() - expect(w.isVisible()).to.equal(false) - }) - }) - - describe('BrowserWindow.blur()', () => { - it('removes focus from window', () => { - w.blur() - expect(w.isFocused()).to.equal(false) - }) - }) + expect(w.isVisible()).to.equal(false); + w.focus(); + expect(w.isVisible()).to.equal(false); + }); + + ifit(process.platform !== 'win32')('focuses a blurred window', async () => { + { + const isBlurred = emittedOnce(w, 'blur'); + const isShown = emittedOnce(w, 'show'); + w.show(); + w.blur(); + await isShown; + await isBlurred; + } + expect(w.isFocused()).to.equal(false); + w.focus(); + expect(w.isFocused()).to.equal(true); + }); + + ifit(process.platform !== 'linux')('acquires focus status from the other windows', async () => { + const w1 = new BrowserWindow({ show: false }); + const w2 = new BrowserWindow({ show: false }); + const w3 = new BrowserWindow({ show: false }); + { + const isFocused3 = emittedOnce(w3, 'focus'); + const isShown1 = emittedOnce(w1, 'show'); + const isShown2 = emittedOnce(w2, 'show'); + const isShown3 = emittedOnce(w3, 'show'); + w1.show(); + w2.show(); + w3.show(); + await isShown1; + await isShown2; + await isShown3; + await isFocused3; + } + // TODO(RaisinTen): Investigate why this assertion fails only on Linux. + expect(w1.isFocused()).to.equal(false); + expect(w2.isFocused()).to.equal(false); + expect(w3.isFocused()).to.equal(true); + + w1.focus(); + expect(w1.isFocused()).to.equal(true); + expect(w2.isFocused()).to.equal(false); + expect(w3.isFocused()).to.equal(false); + + w2.focus(); + expect(w1.isFocused()).to.equal(false); + expect(w2.isFocused()).to.equal(true); + expect(w3.isFocused()).to.equal(false); + + w3.focus(); + expect(w1.isFocused()).to.equal(false); + expect(w2.isFocused()).to.equal(false); + expect(w3.isFocused()).to.equal(true); + + { + const isClosed1 = emittedOnce(w1, 'closed'); + const isClosed2 = emittedOnce(w2, 'closed'); + const isClosed3 = emittedOnce(w3, 'closed'); + w1.destroy(); + w2.destroy(); + w3.destroy(); + await isClosed1; + await isClosed2; + await isClosed3; + } + }); + }); + + // TODO(RaisinTen): Make this work on Windows too. + // Refs: https://github.com/electron/electron/issues/20464. + ifdescribe(process.platform !== 'win32')('BrowserWindow.blur()', () => { + it('removes focus from window', async () => { + { + const isFocused = emittedOnce(w, 'focus'); + const isShown = emittedOnce(w, 'show'); + w.show(); + await isShown; + await isFocused; + } + expect(w.isFocused()).to.equal(true); + w.blur(); + expect(w.isFocused()).to.equal(false); + }); + + ifit(process.platform !== 'linux')('transfers focus status to the next window', async () => { + const w1 = new BrowserWindow({ show: false }); + const w2 = new BrowserWindow({ show: false }); + const w3 = new BrowserWindow({ show: false }); + { + const isFocused3 = emittedOnce(w3, 'focus'); + const isShown1 = emittedOnce(w1, 'show'); + const isShown2 = emittedOnce(w2, 'show'); + const isShown3 = emittedOnce(w3, 'show'); + w1.show(); + w2.show(); + w3.show(); + await isShown1; + await isShown2; + await isShown3; + await isFocused3; + } + // TODO(RaisinTen): Investigate why this assertion fails only on Linux. + expect(w1.isFocused()).to.equal(false); + expect(w2.isFocused()).to.equal(false); + expect(w3.isFocused()).to.equal(true); + + w3.blur(); + expect(w1.isFocused()).to.equal(false); + expect(w2.isFocused()).to.equal(true); + expect(w3.isFocused()).to.equal(false); + + w2.blur(); + expect(w1.isFocused()).to.equal(true); + expect(w2.isFocused()).to.equal(false); + expect(w3.isFocused()).to.equal(false); + + w1.blur(); + expect(w1.isFocused()).to.equal(false); + expect(w2.isFocused()).to.equal(false); + expect(w3.isFocused()).to.equal(true); + + { + const isClosed1 = emittedOnce(w1, 'closed'); + const isClosed2 = emittedOnce(w2, 'closed'); + const isClosed3 = emittedOnce(w3, 'closed'); + w1.destroy(); + w2.destroy(); + w3.destroy(); + await isClosed1; + await isClosed2; + await isClosed3; + } + }); + }); describe('BrowserWindow.getFocusedWindow()', () => { - it('returns the opener window when dev tools window is focused', (done) => { - w.show() - w.webContents.once('devtools-focused', () => { - expect(BrowserWindow.getFocusedWindow()).to.equal(w) - done() - }) - w.webContents.openDevTools({ mode: 'undocked' }) - }) - }) + it('returns the opener window when dev tools window is focused', async () => { + await emittedOnce(w, 'focus', () => w.show()); + w.webContents.openDevTools({ mode: 'undocked' }); + await emittedOnce(w.webContents, 'devtools-focused'); + expect(BrowserWindow.getFocusedWindow()).to.equal(w); + }); + }); describe('BrowserWindow.moveTop()', () => { it('should not steal focus', async () => { - const posDelta = 50 - const wShownInactive = emittedOnce(w, 'show') - w.showInactive() - await wShownInactive - expect(w.isFocused()).to.equal(false) - - const otherWindow = new BrowserWindow({ show: false, title: 'otherWindow' }) - const otherWindowShown = emittedOnce(otherWindow, 'show') - const otherWindowFocused = emittedOnce(otherWindow, 'focus') - otherWindow.show() - await otherWindowShown - await otherWindowFocused - expect(otherWindow.isFocused()).to.equal(true) - - w.moveTop() - const wPos = w.getPosition() - const wMoving = emittedOnce(w, 'move') - w.setPosition(wPos[0] + posDelta, wPos[1] + posDelta) - await wMoving - expect(w.isFocused()).to.equal(false) - expect(otherWindow.isFocused()).to.equal(true) - - const wFocused = emittedOnce(w, 'focus') - w.focus() - await wFocused - expect(w.isFocused()).to.equal(true) - - otherWindow.moveTop() - const otherWindowPos = otherWindow.getPosition() - const otherWindowMoving = emittedOnce(otherWindow, 'move') - otherWindow.setPosition(otherWindowPos[0] + posDelta, otherWindowPos[1] + posDelta) - await otherWindowMoving - expect(otherWindow.isFocused()).to.equal(false) - expect(w.isFocused()).to.equal(true) - - await closeWindow(otherWindow, { assertNotWindows: false }) - expect(BrowserWindow.getAllWindows()).to.have.lengthOf(1) - }) - }) - - }) + const posDelta = 50; + const wShownInactive = emittedOnce(w, 'show'); + w.showInactive(); + await wShownInactive; + expect(w.isFocused()).to.equal(false); + + const otherWindow = new BrowserWindow({ show: false, title: 'otherWindow' }); + const otherWindowShown = emittedOnce(otherWindow, 'show'); + const otherWindowFocused = emittedOnce(otherWindow, 'focus'); + otherWindow.show(); + await otherWindowShown; + await otherWindowFocused; + expect(otherWindow.isFocused()).to.equal(true); + + w.moveTop(); + const wPos = w.getPosition(); + const wMoving = emittedOnce(w, 'move'); + w.setPosition(wPos[0] + posDelta, wPos[1] + posDelta); + await wMoving; + expect(w.isFocused()).to.equal(false); + expect(otherWindow.isFocused()).to.equal(true); + + const wFocused = emittedOnce(w, 'focus'); + const otherWindowBlurred = emittedOnce(otherWindow, 'blur'); + w.focus(); + await wFocused; + await otherWindowBlurred; + expect(w.isFocused()).to.equal(true); + + otherWindow.moveTop(); + const otherWindowPos = otherWindow.getPosition(); + const otherWindowMoving = emittedOnce(otherWindow, 'move'); + otherWindow.setPosition(otherWindowPos[0] + posDelta, otherWindowPos[1] + posDelta); + await otherWindowMoving; + expect(otherWindow.isFocused()).to.equal(false); + expect(w.isFocused()).to.equal(true); + + await closeWindow(otherWindow, { assertNotWindows: false }); + expect(BrowserWindow.getAllWindows()).to.have.lengthOf(1); + }); + }); + + ifdescribe(features.isDesktopCapturerEnabled())('BrowserWindow.moveAbove(mediaSourceId)', () => { + it('should throw an exception if wrong formatting', async () => { + const fakeSourceIds = [ + 'none', 'screen:0', 'window:fake', 'window:1234', 'foobar:1:2' + ]; + fakeSourceIds.forEach((sourceId) => { + expect(() => { + w.moveAbove(sourceId); + }).to.throw(/Invalid media source id/); + }); + }); + + it('should throw an exception if wrong type', async () => { + const fakeSourceIds = [null as any, 123 as any]; + fakeSourceIds.forEach((sourceId) => { + expect(() => { + w.moveAbove(sourceId); + }).to.throw(/Error processing argument at index 0 */); + }); + }); + + it('should throw an exception if invalid window', async () => { + // It is very unlikely that these window id exist. + const fakeSourceIds = ['window:99999999:0', 'window:123456:1', + 'window:123456:9']; + fakeSourceIds.forEach((sourceId) => { + expect(() => { + w.moveAbove(sourceId); + }).to.throw(/Invalid media source id/); + }); + }); + + it('should not throw an exception', async () => { + const w2 = new BrowserWindow({ show: false, title: 'window2' }); + const w2Shown = emittedOnce(w2, 'show'); + w2.show(); + await w2Shown; + + expect(() => { + w.moveAbove(w2.getMediaSourceId()); + }).to.not.throw(); + + await closeWindow(w2, { assertNotWindows: false }); + }); + }); + + describe('BrowserWindow.setFocusable()', () => { + it('can set unfocusable window to focusable', async () => { + const w2 = new BrowserWindow({ focusable: false }); + const w2Focused = emittedOnce(w2, 'focus'); + w2.setFocusable(true); + w2.focus(); + await w2Focused; + await closeWindow(w2, { assertNotWindows: false }); + }); + }); + + describe('BrowserWindow.isFocusable()', () => { + it('correctly returns whether a window is focusable', async () => { + const w2 = new BrowserWindow({ focusable: false }); + expect(w2.isFocusable()).to.be.false(); + + w2.setFocusable(true); + expect(w2.isFocusable()).to.be.true(); + await closeWindow(w2, { assertNotWindows: false }); + }); + }); + }); describe('sizing', () => { - let w = null as unknown as BrowserWindow + let w = null as unknown as BrowserWindow; + beforeEach(() => { - w = new BrowserWindow({show: false, width: 400, height: 400}) - }) + w = new BrowserWindow({ show: false, width: 400, height: 400 }); + }); + afterEach(async () => { - await closeWindow(w) - w = null as unknown as BrowserWindow - }) + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); describe('BrowserWindow.setBounds(bounds[, animate])', () => { it('sets the window bounds with full bounds', () => { - const fullBounds = { x: 440, y: 225, width: 500, height: 400 } - w.setBounds(fullBounds) + const fullBounds = { x: 440, y: 225, width: 500, height: 400 }; + w.setBounds(fullBounds); - expectBoundsEqual(w.getBounds(), fullBounds) - }) + expectBoundsEqual(w.getBounds(), fullBounds); + }); it('sets the window bounds with partial bounds', () => { - const fullBounds = { x: 440, y: 225, width: 500, height: 400 } - w.setBounds(fullBounds) + const fullBounds = { x: 440, y: 225, width: 500, height: 400 }; + w.setBounds(fullBounds); + + const boundsUpdate = { width: 200 }; + w.setBounds(boundsUpdate as any); - const boundsUpdate = { width: 200 } - w.setBounds(boundsUpdate as any) + const expectedBounds = Object.assign(fullBounds, boundsUpdate); + expectBoundsEqual(w.getBounds(), expectedBounds); + }); + + ifit(process.platform === 'darwin')('on macOS', () => { + it('emits \'resized\' event after animating', async () => { + const fullBounds = { x: 440, y: 225, width: 500, height: 400 }; + w.setBounds(fullBounds, true); - const expectedBounds = Object.assign(fullBounds, boundsUpdate) - expectBoundsEqual(w.getBounds(), expectedBounds) - }) - }) + await expect(emittedOnce(w, 'resized')).to.eventually.be.fulfilled(); + }); + }); + }); describe('BrowserWindow.setSize(width, height)', () => { it('sets the window size', async () => { - const size = [300, 400] + const size = [300, 400]; + + const resized = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resized; - const resized = emittedOnce(w, 'resize') - w.setSize(size[0], size[1]) - await resized + expectBoundsEqual(w.getSize(), size); + }); + + ifit(process.platform === 'darwin')('on macOS', () => { + it('emits \'resized\' event after animating', async () => { + const size = [300, 400]; + w.setSize(size[0], size[1], true); - expectBoundsEqual(w.getSize(), size) - }) - }) + await expect(emittedOnce(w, 'resized')).to.eventually.be.fulfilled(); + }); + }); + }); describe('BrowserWindow.setMinimum/MaximumSize(width, height)', () => { it('sets the maximum and minimum size of the window', () => { - expect(w.getMinimumSize()).to.deep.equal([0, 0]) - expect(w.getMaximumSize()).to.deep.equal([0, 0]) + expect(w.getMinimumSize()).to.deep.equal([0, 0]); + expect(w.getMaximumSize()).to.deep.equal([0, 0]); - w.setMinimumSize(100, 100) - expectBoundsEqual(w.getMinimumSize(), [100, 100]) - expectBoundsEqual(w.getMaximumSize(), [0, 0]) + w.setMinimumSize(100, 100); + expectBoundsEqual(w.getMinimumSize(), [100, 100]); + expectBoundsEqual(w.getMaximumSize(), [0, 0]); - w.setMaximumSize(900, 600) - expectBoundsEqual(w.getMinimumSize(), [100, 100]) - expectBoundsEqual(w.getMaximumSize(), [900, 600]) - }) - }) + w.setMaximumSize(900, 600); + expectBoundsEqual(w.getMinimumSize(), [100, 100]); + expectBoundsEqual(w.getMaximumSize(), [900, 600]); + }); + }); describe('BrowserWindow.setAspectRatio(ratio)', () => { - it('resets the behaviour when passing in 0', (done) => { - const size = [300, 400] - w.setAspectRatio(1 / 2) - w.setAspectRatio(0) - w.once('resize', () => { - expectBoundsEqual(w.getSize(), size) - done() - }) - w.setSize(size[0], size[1]) - }) - }) + it('resets the behaviour when passing in 0', async () => { + const size = [300, 400]; + w.setAspectRatio(1 / 2); + w.setAspectRatio(0); + const resize = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resize; + expectBoundsEqual(w.getSize(), size); + }); + + it('doesn\'t change bounds when maximum size is set', () => { + w.setMenu(null); + w.setMaximumSize(400, 400); + // Without https://github.com/electron/electron/pull/29101 + // following call would shrink the window to 384x361. + // There would be also DCHECK in resize_utils.cc on + // debug build. + w.setAspectRatio(1.0); + expectBoundsEqual(w.getSize(), [400, 400]); + }); + }); describe('BrowserWindow.setPosition(x, y)', () => { - it('sets the window position', (done) => { - const pos = [10, 10] - w.once('move', () => { - const newPos = w.getPosition() - expect(newPos).to.deep.equal(pos) - done() - }) - w.setPosition(pos[0], pos[1]) - }) - }) + it('sets the window position', async () => { + const pos = [10, 10]; + const move = emittedOnce(w, 'move'); + w.setPosition(pos[0], pos[1]); + await move; + expect(w.getPosition()).to.deep.equal(pos); + }); + }); describe('BrowserWindow.setContentSize(width, height)', () => { - it('sets the content size', (done) => { + it('sets the content size', async () => { // NB. The CI server has a very small screen. Attempting to size the window // larger than the screen will limit the window's size to the screen and // cause the test to fail. - const size = [456, 567] - w.setContentSize(size[0], size[1]) - setImmediate(() => { - const after = w.getContentSize() - expect(after).to.deep.equal(size) - done() - }) - }) - it('works for a frameless window', (done) => { - w.destroy() + const size = [456, 567]; + w.setContentSize(size[0], size[1]); + await new Promise(setImmediate); + const after = w.getContentSize(); + expect(after).to.deep.equal(size); + }); + + it('works for a frameless window', async () => { + w.destroy(); w = new BrowserWindow({ show: false, frame: false, width: 400, height: 400 - }) - const size = [456, 567] - w.setContentSize(size[0], size[1]) - setImmediate(() => { - const after = w.getContentSize() - expect(after).to.deep.equal(size) - done() - }) - }) - }) + }); + const size = [456, 567]; + w.setContentSize(size[0], size[1]); + await new Promise(setImmediate); + const after = w.getContentSize(); + expect(after).to.deep.equal(size); + }); + }); describe('BrowserWindow.setContentBounds(bounds)', () => { - it('sets the content size and position', (done) => { - const bounds = { x: 10, y: 10, width: 250, height: 250 } - w.once('resize', () => { - setTimeout(() => { - expectBoundsEqual(w.getContentBounds(), bounds) - done() - }) - }) - w.setContentBounds(bounds) - }) - it('works for a frameless window', (done) => { - w.destroy() + it('sets the content size and position', async () => { + const bounds = { x: 10, y: 10, width: 250, height: 250 }; + const resize = emittedOnce(w, 'resize'); + w.setContentBounds(bounds); + await resize; + await delay(); + expectBoundsEqual(w.getContentBounds(), bounds); + }); + it('works for a frameless window', async () => { + w.destroy(); w = new BrowserWindow({ show: false, frame: false, width: 300, height: 300 - }) - const bounds = { x: 10, y: 10, width: 250, height: 250 } - w.once('resize', () => { - setTimeout(() => { - expect(w.getContentBounds()).to.deep.equal(bounds) - done() - }) - }) - w.setContentBounds(bounds) - }) - }) - - describe(`BrowserWindow.getNormalBounds()`, () => { - describe(`Normal state`, () => { - it(`checks normal bounds after resize`, (done) => { - const size = [300, 400] - w.once('resize', () => { - expectBoundsEqual(w.getNormalBounds(), w.getBounds()) - done() - }) - w.setSize(size[0], size[1]) - }) - it(`checks normal bounds after move`, (done) => { - const pos = [10, 10] - w.once('move', () => { - expectBoundsEqual(w.getNormalBounds(), w.getBounds()) - done() - }) - w.setPosition(pos[0], pos[1]) - }) - }) - ifdescribe(process.platform !== 'linux')(`Maximized state`, () => { - it(`checks normal bounds when maximized`, (done) => { - const bounds = w.getBounds() + }); + const bounds = { x: 10, y: 10, width: 250, height: 250 }; + const resize = emittedOnce(w, 'resize'); + w.setContentBounds(bounds); + await resize; + await delay(); + expectBoundsEqual(w.getContentBounds(), bounds); + }); + }); + + describe('BrowserWindow.getBackgroundColor()', () => { + it('returns default value if no backgroundColor is set', () => { + w.destroy(); + w = new BrowserWindow({}); + expect(w.getBackgroundColor()).to.equal('#FFFFFF'); + }); + it('returns correct value if backgroundColor is set', () => { + const backgroundColor = '#BBAAFF'; + w.destroy(); + w = new BrowserWindow({ + backgroundColor: backgroundColor + }); + expect(w.getBackgroundColor()).to.equal(backgroundColor); + }); + it('returns correct value from setBackgroundColor()', () => { + const backgroundColor = '#AABBFF'; + w.destroy(); + w = new BrowserWindow({}); + w.setBackgroundColor(backgroundColor); + expect(w.getBackgroundColor()).to.equal(backgroundColor); + }); + it('returns correct color with multiple passed formats', () => { + w.destroy(); + w = new BrowserWindow({}); + + w.setBackgroundColor('#AABBFF'); + expect(w.getBackgroundColor()).to.equal('#AABBFF'); + + w.setBackgroundColor('blueviolet'); + expect(w.getBackgroundColor()).to.equal('#8A2BE2'); + + w.setBackgroundColor('rgb(255, 0, 185)'); + expect(w.getBackgroundColor()).to.equal('#FF00B9'); + + w.setBackgroundColor('rgba(245, 40, 145, 0.8)'); + expect(w.getBackgroundColor()).to.equal('#F52891'); + + w.setBackgroundColor('hsl(155, 100%, 50%)'); + expect(w.getBackgroundColor()).to.equal('#00FF95'); + }); + }); + + describe('BrowserWindow.getNormalBounds()', () => { + describe('Normal state', () => { + it('checks normal bounds after resize', async () => { + const size = [300, 400]; + const resize = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resize; + expectBoundsEqual(w.getNormalBounds(), w.getBounds()); + }); + + it('checks normal bounds after move', async () => { + const pos = [10, 10]; + const move = emittedOnce(w, 'move'); + w.setPosition(pos[0], pos[1]); + await move; + expectBoundsEqual(w.getNormalBounds(), w.getBounds()); + }); + }); + + ifdescribe(process.platform !== 'linux')('Maximized state', () => { + it('checks normal bounds when maximized', async () => { + const bounds = w.getBounds(); + const maximize = emittedOnce(w, 'maximize'); + w.show(); + w.maximize(); + await maximize; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('updates normal bounds after resize and maximize', async () => { + const size = [300, 400]; + const resize = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resize; + const original = w.getBounds(); + + const maximize = emittedOnce(w, 'maximize'); + w.maximize(); + await maximize; + + const normal = w.getNormalBounds(); + const bounds = w.getBounds(); + + expect(normal).to.deep.equal(original); + expect(normal).to.not.deep.equal(bounds); + + const close = emittedOnce(w, 'close'); + w.close(); + await close; + }); + + it('updates normal bounds after move and maximize', async () => { + const pos = [10, 10]; + const move = emittedOnce(w, 'move'); + w.setPosition(pos[0], pos[1]); + await move; + const original = w.getBounds(); + + const maximize = emittedOnce(w, 'maximize'); + w.maximize(); + await maximize; + + const normal = w.getNormalBounds(); + const bounds = w.getBounds(); + + expect(normal).to.deep.equal(original); + expect(normal).to.not.deep.equal(bounds); + + const close = emittedOnce(w, 'close'); + w.close(); + await close; + }); + + it('checks normal bounds when unmaximized', async () => { + const bounds = w.getBounds(); w.once('maximize', () => { - expectBoundsEqual(w.getNormalBounds(), bounds) - done() - }) - w.show() - w.maximize() - }) - it(`checks normal bounds when unmaximized`, (done) => { - const bounds = w.getBounds() + w.unmaximize(); + }); + const unmaximize = emittedOnce(w, 'unmaximize'); + w.show(); + w.maximize(); + await unmaximize; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('does not change size for a frameless window with min size', async () => { + w.destroy(); + w = new BrowserWindow({ + show: false, + frame: false, + width: 300, + height: 300, + minWidth: 300, + minHeight: 300 + }); + const bounds = w.getBounds(); w.once('maximize', () => { - w.unmaximize() - }) - w.once('unmaximize', () => { - expectBoundsEqual(w.getNormalBounds(), bounds) - done() - }) - w.show() - w.maximize() - }) - }) - ifdescribe(process.platform !== 'linux')(`Minimized state`, () => { - it(`checks normal bounds when minimized`, (done) => { - const bounds = w.getBounds() + w.unmaximize(); + }); + const unmaximize = emittedOnce(w, 'unmaximize'); + w.show(); + w.maximize(); + await unmaximize; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('correctly checks transparent window maximization state', async () => { + w.destroy(); + w = new BrowserWindow({ + show: false, + width: 300, + height: 300, + transparent: true + }); + + const maximize = emittedOnce(w, 'maximize'); + w.show(); + w.maximize(); + await maximize; + expect(w.isMaximized()).to.equal(true); + const unmaximize = emittedOnce(w, 'unmaximize'); + w.unmaximize(); + await unmaximize; + expect(w.isMaximized()).to.equal(false); + }); + + it('returns the correct value for windows with an aspect ratio', async () => { + w.destroy(); + w = new BrowserWindow({ + show: false, + fullscreenable: false + }); + + w.setAspectRatio(16 / 11); + + const maximize = emittedOnce(w, 'resize'); + w.show(); + w.maximize(); + await maximize; + + expect(w.isMaximized()).to.equal(true); + w.resizable = false; + expect(w.isMaximized()).to.equal(true); + }); + }); + + ifdescribe(process.platform !== 'linux')('Minimized state', () => { + it('checks normal bounds when minimized', async () => { + const bounds = w.getBounds(); + const minimize = emittedOnce(w, 'minimize'); + w.show(); + w.minimize(); + await minimize; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('updates normal bounds after move and minimize', async () => { + const pos = [10, 10]; + const move = emittedOnce(w, 'move'); + w.setPosition(pos[0], pos[1]); + await move; + const original = w.getBounds(); + + const minimize = emittedOnce(w, 'minimize'); + w.minimize(); + await minimize; + + const normal = w.getNormalBounds(); + + expect(original).to.deep.equal(normal); + expectBoundsEqual(normal, w.getBounds()); + }); + + it('updates normal bounds after resize and minimize', async () => { + const size = [300, 400]; + const resize = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resize; + const original = w.getBounds(); + + const minimize = emittedOnce(w, 'minimize'); + w.minimize(); + await minimize; + + const normal = w.getNormalBounds(); + + expect(original).to.deep.equal(normal); + expectBoundsEqual(normal, w.getBounds()); + }); + + it('checks normal bounds when restored', async () => { + const bounds = w.getBounds(); w.once('minimize', () => { - expectBoundsEqual(w.getNormalBounds(), bounds) - done() - }) - w.show() - w.minimize() - }) - it(`checks normal bounds when restored`, (done) => { - const bounds = w.getBounds() + w.restore(); + }); + const restore = emittedOnce(w, 'restore'); + w.show(); + w.minimize(); + await restore; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('does not change size for a frameless window with min size', async () => { + w.destroy(); + w = new BrowserWindow({ + show: false, + frame: false, + width: 300, + height: 300, + minWidth: 300, + minHeight: 300 + }); + const bounds = w.getBounds(); w.once('minimize', () => { - w.restore() - }) - w.once('restore', () => { - expectBoundsEqual(w.getNormalBounds(), bounds) - done() - }) - w.show() - w.minimize() - }) - }) - ifdescribe(process.platform === 'win32')(`Fullscreen state`, () => { - it(`checks normal bounds when fullscreen'ed`, (done) => { - const bounds = w.getBounds() - w.once('enter-full-screen', () => { - expectBoundsEqual(w.getNormalBounds(), bounds) - done() - }) - w.show() - w.setFullScreen(true) - }) - it(`checks normal bounds when unfullscreen'ed`, (done) => { - const bounds = w.getBounds() - w.once('enter-full-screen', () => { - w.setFullScreen(false) - }) - w.once('leave-full-screen', () => { - expectBoundsEqual(w.getNormalBounds(), bounds) - done() - }) - w.show() - w.setFullScreen(true) - }) - }) - }) - }) + w.restore(); + }); + const restore = emittedOnce(w, 'restore'); + w.show(); + w.minimize(); + await restore; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + }); + + ifdescribe(process.platform === 'win32')('Fullscreen state', () => { + it('with properties', () => { + it('can be set with the fullscreen constructor option', () => { + w = new BrowserWindow({ fullscreen: true }); + expect(w.fullScreen).to.be.true(); + }); + + it('can be changed', () => { + w.fullScreen = false; + expect(w.fullScreen).to.be.false(); + w.fullScreen = true; + expect(w.fullScreen).to.be.true(); + }); + + it('checks normal bounds when fullscreen\'ed', async () => { + const bounds = w.getBounds(); + const enterFullScreen = emittedOnce(w, 'enter-full-screen'); + w.show(); + w.fullScreen = true; + await enterFullScreen; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('updates normal bounds after resize and fullscreen', async () => { + const size = [300, 400]; + const resize = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resize; + const original = w.getBounds(); + + const fsc = emittedOnce(w, 'enter-full-screen'); + w.fullScreen = true; + await fsc; + + const normal = w.getNormalBounds(); + const bounds = w.getBounds(); + + expect(normal).to.deep.equal(original); + expect(normal).to.not.deep.equal(bounds); + + const close = emittedOnce(w, 'close'); + w.close(); + await close; + }); + + it('updates normal bounds after move and fullscreen', async () => { + const pos = [10, 10]; + const move = emittedOnce(w, 'move'); + w.setPosition(pos[0], pos[1]); + await move; + const original = w.getBounds(); + + const fsc = emittedOnce(w, 'enter-full-screen'); + w.fullScreen = true; + await fsc; + + const normal = w.getNormalBounds(); + const bounds = w.getBounds(); + + expect(normal).to.deep.equal(original); + expect(normal).to.not.deep.equal(bounds); + + const close = emittedOnce(w, 'close'); + w.close(); + await close; + }); + + it('checks normal bounds when unfullscreen\'ed', async () => { + const bounds = w.getBounds(); + w.once('enter-full-screen', () => { + w.fullScreen = false; + }); + const leaveFullScreen = emittedOnce(w, 'leave-full-screen'); + w.show(); + w.fullScreen = true; + await leaveFullScreen; + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + }); + + it('with functions', () => { + it('can be set with the fullscreen constructor option', () => { + w = new BrowserWindow({ fullscreen: true }); + expect(w.isFullScreen()).to.be.true(); + }); + + it('can be changed', () => { + w.setFullScreen(false); + expect(w.isFullScreen()).to.be.false(); + w.setFullScreen(true); + expect(w.isFullScreen()).to.be.true(); + }); + + it('checks normal bounds when fullscreen\'ed', async () => { + const bounds = w.getBounds(); + w.show(); + + const enterFullScreen = emittedOnce(w, 'enter-full-screen'); + w.setFullScreen(true); + await enterFullScreen; + + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + + it('updates normal bounds after resize and fullscreen', async () => { + const size = [300, 400]; + const resize = emittedOnce(w, 'resize'); + w.setSize(size[0], size[1]); + await resize; + const original = w.getBounds(); + + const fsc = emittedOnce(w, 'enter-full-screen'); + w.setFullScreen(true); + await fsc; + + const normal = w.getNormalBounds(); + const bounds = w.getBounds(); + + expect(normal).to.deep.equal(original); + expect(normal).to.not.deep.equal(bounds); + + const close = emittedOnce(w, 'close'); + w.close(); + await close; + }); + + it('updates normal bounds after move and fullscreen', async () => { + const pos = [10, 10]; + const move = emittedOnce(w, 'move'); + w.setPosition(pos[0], pos[1]); + await move; + const original = w.getBounds(); + + const fsc = emittedOnce(w, 'enter-full-screen'); + w.setFullScreen(true); + await fsc; + + const normal = w.getNormalBounds(); + const bounds = w.getBounds(); + + expect(normal).to.deep.equal(original); + expect(normal).to.not.deep.equal(bounds); + + const close = emittedOnce(w, 'close'); + w.close(); + await close; + }); + + it('checks normal bounds when unfullscreen\'ed', async () => { + const bounds = w.getBounds(); + w.show(); + + const enterFullScreen = emittedOnce(w, 'enter-full-screen'); + w.setFullScreen(true); + await enterFullScreen; + + const leaveFullScreen = emittedOnce(w, 'leave-full-screen'); + w.setFullScreen(false); + await leaveFullScreen; + + expectBoundsEqual(w.getNormalBounds(), bounds); + }); + }); + }); + }); + }); ifdescribe(process.platform === 'darwin')('tabbed windows', () => { - let w = null as unknown as BrowserWindow + let w = null as unknown as BrowserWindow; beforeEach(() => { - w = new BrowserWindow({show: false}) - }) + w = new BrowserWindow({ show: false }); + }); afterEach(async () => { - await closeWindow(w) - w = null as unknown as BrowserWindow - }) + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); describe('BrowserWindow.selectPreviousTab()', () => { it('does not throw', () => { expect(() => { - w.selectPreviousTab() - }).to.not.throw() - }) - }) + w.selectPreviousTab(); + }).to.not.throw(); + }); + }); describe('BrowserWindow.selectNextTab()', () => { it('does not throw', () => { expect(() => { - w.selectNextTab() - }).to.not.throw() - }) - }) + w.selectNextTab(); + }).to.not.throw(); + }); + }); describe('BrowserWindow.mergeAllWindows()', () => { it('does not throw', () => { expect(() => { - w.mergeAllWindows() - }).to.not.throw() - }) - }) + w.mergeAllWindows(); + }).to.not.throw(); + }); + }); describe('BrowserWindow.moveTabToNewWindow()', () => { it('does not throw', () => { expect(() => { - w.moveTabToNewWindow() - }).to.not.throw() - }) - }) + w.moveTabToNewWindow(); + }).to.not.throw(); + }); + }); describe('BrowserWindow.toggleTabBar()', () => { it('does not throw', () => { expect(() => { - w.toggleTabBar() - }).to.not.throw() - }) - }) + w.toggleTabBar(); + }).to.not.throw(); + }); + }); describe('BrowserWindow.addTabbedWindow()', () => { it('does not throw', async () => { - const tabbedWindow = new BrowserWindow({}) + const tabbedWindow = new BrowserWindow({}); expect(() => { - w.addTabbedWindow(tabbedWindow) - }).to.not.throw() + w.addTabbedWindow(tabbedWindow); + }).to.not.throw(); - expect(BrowserWindow.getAllWindows()).to.have.lengthOf(2) // w + tabbedWindow + expect(BrowserWindow.getAllWindows()).to.have.lengthOf(2); // w + tabbedWindow - await closeWindow(tabbedWindow, { assertNotWindows: false }) - expect(BrowserWindow.getAllWindows()).to.have.lengthOf(1) // w - }) + await closeWindow(tabbedWindow, { assertNotWindows: false }); + expect(BrowserWindow.getAllWindows()).to.have.lengthOf(1); // w + }); it('throws when called on itself', () => { expect(() => { - w.addTabbedWindow(w) - }).to.throw('AddTabbedWindow cannot be called by a window on itself.') - }) - }) - }) - - describe('autoHideMenuBar property', () => { - afterEach(closeAllWindows) - it('exists', () => { - const w = new BrowserWindow({show: false}) - expect(w).to.have.property('autoHideMenuBar') - - // TODO(codebytere): remove when propertyification is complete - expect(w.setAutoHideMenuBar).to.be.a('function') - expect(w.isMenuBarAutoHide).to.be.a('function') - }) - }) + w.addTabbedWindow(w); + }).to.throw('AddTabbedWindow cannot be called by a window on itself.'); + }); + }); + }); + + describe('autoHideMenuBar state', () => { + afterEach(closeAllWindows); + + it('for properties', () => { + it('can be set with autoHideMenuBar constructor option', () => { + const w = new BrowserWindow({ show: false, autoHideMenuBar: true }); + expect(w.autoHideMenuBar).to.be.true('autoHideMenuBar'); + }); + + it('can be changed', () => { + const w = new BrowserWindow({ show: false }); + expect(w.autoHideMenuBar).to.be.false('autoHideMenuBar'); + w.autoHideMenuBar = true; + expect(w.autoHideMenuBar).to.be.true('autoHideMenuBar'); + w.autoHideMenuBar = false; + expect(w.autoHideMenuBar).to.be.false('autoHideMenuBar'); + }); + }); + + it('for functions', () => { + it('can be set with autoHideMenuBar constructor option', () => { + const w = new BrowserWindow({ show: false, autoHideMenuBar: true }); + expect(w.isMenuBarAutoHide()).to.be.true('autoHideMenuBar'); + }); + + it('can be changed', () => { + const w = new BrowserWindow({ show: false }); + expect(w.isMenuBarAutoHide()).to.be.false('autoHideMenuBar'); + w.setAutoHideMenuBar(true); + expect(w.isMenuBarAutoHide()).to.be.true('autoHideMenuBar'); + w.setAutoHideMenuBar(false); + expect(w.isMenuBarAutoHide()).to.be.false('autoHideMenuBar'); + }); + }); + }); describe('BrowserWindow.capturePage(rect)', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('returns a Promise with a Buffer', async () => { - const w = new BrowserWindow({show: false}) + const w = new BrowserWindow({ show: false }); const image = await w.capturePage({ x: 0, y: 0, width: 100, height: 100 - }) + }); + + expect(image.isEmpty()).to.equal(true); + }); + + it('resolves after the window is hidden', async () => { + const w = new BrowserWindow({ show: false }); + w.loadFile(path.join(fixtures, 'pages', 'a.html')); + await emittedOnce(w, 'ready-to-show'); + w.show(); + + const visibleImage = await w.capturePage(); + expect(visibleImage.isEmpty()).to.equal(false); - expect(image.isEmpty()).to.equal(true) - }) + w.hide(); + + const hiddenImage = await w.capturePage(); + const isEmpty = process.platform !== 'darwin'; + expect(hiddenImage.isEmpty()).to.equal(isEmpty); + }); + + it('resolves after the window is hidden and capturer count is non-zero', async () => { + const w = new BrowserWindow({ show: false }); + w.webContents.setBackgroundThrottling(false); + w.loadFile(path.join(fixtures, 'pages', 'a.html')); + await emittedOnce(w, 'ready-to-show'); + + w.webContents.incrementCapturerCount(); + const image = await w.capturePage(); + expect(image.isEmpty()).to.equal(false); + }); it('preserves transparency', async () => { - const w = new BrowserWindow({show: false, transparent: true}) - w.loadURL('about:blank') - await emittedOnce(w, 'ready-to-show') - w.show() + const w = new BrowserWindow({ show: false, transparent: true }); + w.loadFile(path.join(fixtures, 'pages', 'theme-color.html')); + await emittedOnce(w, 'ready-to-show'); + w.show(); - const image = await w.capturePage() - const imgBuffer = image.toPNG() + const image = await w.capturePage(); + const imgBuffer = image.toPNG(); // Check the 25th byte in the PNG. // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha - expect(imgBuffer[25]).to.equal(6) - }) - }) + expect(imgBuffer[25]).to.equal(6); + }); + + it('should increase the capturer count', () => { + const w = new BrowserWindow({ show: false }); + w.webContents.incrementCapturerCount(); + expect(w.webContents.isBeingCaptured()).to.be.true(); + w.webContents.decrementCapturerCount(); + expect(w.webContents.isBeingCaptured()).to.be.false(); + }); + }); describe('BrowserWindow.setProgressBar(progress)', () => { - let w = null as unknown as BrowserWindow + let w = null as unknown as BrowserWindow; before(() => { - w = new BrowserWindow({show: false}) - }) + w = new BrowserWindow({ show: false }); + }); after(async () => { - await closeWindow(w) - w = null as unknown as BrowserWindow - }) + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); it('sets the progress', () => { expect(() => { if (process.platform === 'darwin') { - app.dock.setIcon(path.join(fixtures, 'assets', 'logo.png')) + app.dock.setIcon(path.join(fixtures, 'assets', 'logo.png')); } - w.setProgressBar(0.5) + w.setProgressBar(0.5); if (process.platform === 'darwin') { - app.dock.setIcon(null as any) + app.dock.setIcon(null as any); } - w.setProgressBar(-1) - }).to.not.throw() - }) + w.setProgressBar(-1); + }).to.not.throw(); + }); it('sets the progress using "paused" mode', () => { expect(() => { - w.setProgressBar(0.5, { mode: 'paused' }) - }).to.not.throw() - }) + w.setProgressBar(0.5, { mode: 'paused' }); + }).to.not.throw(); + }); it('sets the progress using "error" mode', () => { expect(() => { - w.setProgressBar(0.5, { mode: 'error' }) - }).to.not.throw() - }) + w.setProgressBar(0.5, { mode: 'error' }); + }).to.not.throw(); + }); it('sets the progress using "normal" mode', () => { expect(() => { - w.setProgressBar(0.5, { mode: 'normal' }) - }).to.not.throw() - }) - }) + w.setProgressBar(0.5, { mode: 'normal' }); + }).to.not.throw(); + }); + }); describe('BrowserWindow.setAlwaysOnTop(flag, level)', () => { - let w = null as unknown as BrowserWindow + let w = null as unknown as BrowserWindow; - beforeEach(() => { - w = new BrowserWindow({show: false}) - }) + afterEach(closeAllWindows); - afterEach(async () => { - await closeWindow(w) - w = null as unknown as BrowserWindow - }) + beforeEach(() => { + w = new BrowserWindow({ show: true }); + }); it('sets the window as always on top', () => { - expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop') - w.setAlwaysOnTop(true, 'screen-saver') - expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop') - w.setAlwaysOnTop(false) - expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop') - w.setAlwaysOnTop(true) - expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop') - }) - - ifit(process.platform === 'darwin')('raises an error when relativeLevel is out of bounds', () => { - expect(() => { - w.setAlwaysOnTop(true, 'normal', -2147483644) - }).to.throw() - - expect(() => { - w.setAlwaysOnTop(true, 'normal', 2147483632) - }).to.throw() - }) + expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop'); + w.setAlwaysOnTop(true, 'screen-saver'); + expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop'); + w.setAlwaysOnTop(false); + expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop'); + w.setAlwaysOnTop(true); + expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop'); + }); ifit(process.platform === 'darwin')('resets the windows level on minimize', () => { - expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop') - w.setAlwaysOnTop(true, 'screen-saver') - expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop') - w.minimize() - expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop') - w.restore() - expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop') - }) - - it('causes the right value to be emitted on `always-on-top-changed`', (done) => { - w.on('always-on-top-changed', (e, alwaysOnTop) => { - expect(alwaysOnTop).to.be.true('is not alwaysOnTop') - done() - }) - - expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop') - w.setAlwaysOnTop(true) - }) - }) + expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop'); + w.setAlwaysOnTop(true, 'screen-saver'); + expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop'); + w.minimize(); + expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop'); + w.restore(); + expect(w.isAlwaysOnTop()).to.be.true('is not alwaysOnTop'); + }); + + it('causes the right value to be emitted on `always-on-top-changed`', async () => { + const alwaysOnTopChanged = emittedOnce(w, 'always-on-top-changed'); + expect(w.isAlwaysOnTop()).to.be.false('is alwaysOnTop'); + w.setAlwaysOnTop(true); + const [, alwaysOnTop] = await alwaysOnTopChanged; + expect(alwaysOnTop).to.be.true('is not alwaysOnTop'); + }); + + ifit(process.platform === 'darwin')('honors the alwaysOnTop level of a child window', () => { + w = new BrowserWindow({ show: false }); + const c = new BrowserWindow({ parent: w }); + c.setAlwaysOnTop(true, 'screen-saver'); + + expect(w.isAlwaysOnTop()).to.be.false(); + expect(c.isAlwaysOnTop()).to.be.true('child is not always on top'); + expect((c as any)._getAlwaysOnTopLevel()).to.equal('screen-saver'); + }); + }); + + describe('preconnect feature', () => { + let w = null as unknown as BrowserWindow; + + let server = null as unknown as http.Server; + let url = null as unknown as string; + let connections = 0; + + beforeEach(async () => { + connections = 0; + server = http.createServer((req, res) => { + if (req.url === '/link') { + res.setHeader('Content-type', 'text/html'); + res.end('foo'); + return; + } + res.end(); + }); + server.on('connection', () => { connections++; }); + + await new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve())); + url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + afterEach(async () => { + server.close(); + await closeWindow(w); + w = null as unknown as BrowserWindow; + server = null as unknown as http.Server; + }); + + it('calling preconnect() connects to the server', async () => { + w = new BrowserWindow({ show: false }); + w.webContents.on('did-start-navigation', (event, url) => { + w.webContents.session.preconnect({ url, numSockets: 4 }); + }); + await w.loadURL(url); + expect(connections).to.equal(4); + }); + + it('does not preconnect unless requested', async () => { + w = new BrowserWindow({ show: false }); + await w.loadURL(url); + expect(connections).to.equal(1); + }); + + it('parses ', async () => { + w = new BrowserWindow({ show: true }); + const p = emittedOnce(w.webContents.session, 'preconnect'); + w.loadURL(url + '/link'); + const [, preconnectUrl, allowCredentials] = await p; + expect(preconnectUrl).to.equal('http://example.com/'); + expect(allowCredentials).to.be.true('allowCredentials'); + }); + }); describe('BrowserWindow.setAutoHideCursor(autoHide)', () => { - let w = null as unknown as BrowserWindow + let w = null as unknown as BrowserWindow; beforeEach(() => { - w = new BrowserWindow({show: false}) - }) + w = new BrowserWindow({ show: false }); + }); afterEach(async () => { - await closeWindow(w) - w = null as unknown as BrowserWindow - }) + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); ifit(process.platform === 'darwin')('on macOS', () => { it('allows changing cursor auto-hiding', () => { expect(() => { - w.setAutoHideCursor(false) - w.setAutoHideCursor(true) - }).to.not.throw() - }) - }) + w.setAutoHideCursor(false); + w.setAutoHideCursor(true); + }).to.not.throw(); + }); + }); ifit(process.platform !== 'darwin')('on non-macOS platforms', () => { it('is not available', () => { - expect(w.setAutoHideCursor).to.be.undefined('setAutoHideCursor function') - }) - }) - }) + expect(w.setAutoHideCursor).to.be.undefined('setAutoHideCursor function'); + }); + }); + }); ifdescribe(process.platform === 'darwin')('BrowserWindow.setWindowButtonVisibility()', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('does not throw', () => { - const w = new BrowserWindow({show: false}) - expect(() => { - w.setWindowButtonVisibility(true) - w.setWindowButtonVisibility(false) - }).to.not.throw() - }) - - it('throws with custom title bar buttons', () => { + const w = new BrowserWindow({ show: false }); expect(() => { - const w = new BrowserWindow({ - show: false, - titleBarStyle: 'customButtonsOnHover', - frame: false - }) - w.setWindowButtonVisibility(true) - }).to.throw('Not supported for this window') - }) - }) + w.setWindowButtonVisibility(true); + w.setWindowButtonVisibility(false); + }).to.not.throw(); + }); + + it('changes window button visibility for normal window', () => { + const w = new BrowserWindow({ show: false }); + expect(w._getWindowButtonVisibility()).to.equal(true); + w.setWindowButtonVisibility(false); + expect(w._getWindowButtonVisibility()).to.equal(false); + w.setWindowButtonVisibility(true); + expect(w._getWindowButtonVisibility()).to.equal(true); + }); + + it('changes window button visibility for frameless window', () => { + const w = new BrowserWindow({ show: false, frame: false }); + expect(w._getWindowButtonVisibility()).to.equal(false); + w.setWindowButtonVisibility(true); + expect(w._getWindowButtonVisibility()).to.equal(true); + w.setWindowButtonVisibility(false); + expect(w._getWindowButtonVisibility()).to.equal(false); + }); + + it('changes window button visibility for hiddenInset window', () => { + const w = new BrowserWindow({ show: false, frame: false, titleBarStyle: 'hiddenInset' }); + expect(w._getWindowButtonVisibility()).to.equal(true); + w.setWindowButtonVisibility(false); + expect(w._getWindowButtonVisibility()).to.equal(false); + w.setWindowButtonVisibility(true); + expect(w._getWindowButtonVisibility()).to.equal(true); + }); + + // Buttons of customButtonsOnHover are always hidden unless hovered. + it('does not change window button visibility for customButtonsOnHover window', () => { + const w = new BrowserWindow({ show: false, frame: false, titleBarStyle: 'customButtonsOnHover' }); + expect(w._getWindowButtonVisibility()).to.equal(false); + w.setWindowButtonVisibility(true); + expect(w._getWindowButtonVisibility()).to.equal(false); + w.setWindowButtonVisibility(false); + expect(w._getWindowButtonVisibility()).to.equal(false); + }); + + it('correctly updates when entering/exiting fullscreen for hidden style', async () => { + const w = new BrowserWindow({ show: false, frame: false, titleBarStyle: 'hidden' }); + expect(w._getWindowButtonVisibility()).to.equal(true); + w.setWindowButtonVisibility(false); + expect(w._getWindowButtonVisibility()).to.equal(false); + + const enterFS = emittedOnce(w, 'enter-full-screen'); + w.setFullScreen(true); + await enterFS; + + const leaveFS = emittedOnce(w, 'leave-full-screen'); + w.setFullScreen(false); + await leaveFS; + + w.setWindowButtonVisibility(true); + expect(w._getWindowButtonVisibility()).to.equal(true); + }); + + it('correctly updates when entering/exiting fullscreen for hiddenInset style', async () => { + const w = new BrowserWindow({ show: false, frame: false, titleBarStyle: 'hiddenInset' }); + expect(w._getWindowButtonVisibility()).to.equal(true); + w.setWindowButtonVisibility(false); + expect(w._getWindowButtonVisibility()).to.equal(false); + + const enterFS = emittedOnce(w, 'enter-full-screen'); + w.setFullScreen(true); + await enterFS; + + const leaveFS = emittedOnce(w, 'leave-full-screen'); + w.setFullScreen(false); + await leaveFS; + + w.setWindowButtonVisibility(true); + expect(w._getWindowButtonVisibility()).to.equal(true); + }); + }); ifdescribe(process.platform === 'darwin')('BrowserWindow.setVibrancy(type)', () => { - afterEach(closeAllWindows) + let appProcess: childProcess.ChildProcessWithoutNullStreams | undefined; + + afterEach(() => { + if (appProcess && !appProcess.killed) { + appProcess.kill(); + appProcess = undefined; + } + closeAllWindows(); + }); it('allows setting, changing, and removing the vibrancy', () => { - const w = new BrowserWindow({show: false}) + const w = new BrowserWindow({ show: false }); + expect(() => { + w.setVibrancy('light'); + w.setVibrancy('dark'); + w.setVibrancy(null); + w.setVibrancy('ultra-dark'); + w.setVibrancy('' as any); + }).to.not.throw(); + }); + + it('does not crash if vibrancy is set to an invalid value', () => { + const w = new BrowserWindow({ show: false }); expect(() => { - w.setVibrancy('light') - w.setVibrancy('dark') - w.setVibrancy(null) - w.setVibrancy('ultra-dark') - w.setVibrancy('' as any) - }).to.not.throw() - }) - }) + w.setVibrancy('i-am-not-a-valid-vibrancy-type' as any); + }).to.not.throw(); + }); + + it('Allows setting a transparent window via CSS', async () => { + const appPath = path.join(__dirname, 'fixtures', 'apps', 'background-color-transparent'); + + appProcess = childProcess.spawn(process.execPath, [appPath]); + + const [code] = await emittedOnce(appProcess, 'exit'); + expect(code).to.equal(0); + }); + }); + + ifdescribe(process.platform === 'darwin')('trafficLightPosition', () => { + const pos = { x: 10, y: 10 }; + afterEach(closeAllWindows); + + describe('BrowserWindow.getTrafficLightPosition(pos)', () => { + it('gets position property for "hidden" titleBarStyle', () => { + const w = new BrowserWindow({ show: false, titleBarStyle: 'hidden', trafficLightPosition: pos }); + expect(w.getTrafficLightPosition()).to.deep.equal(pos); + }); + + it('gets position property for "customButtonsOnHover" titleBarStyle', () => { + const w = new BrowserWindow({ show: false, titleBarStyle: 'customButtonsOnHover', trafficLightPosition: pos }); + expect(w.getTrafficLightPosition()).to.deep.equal(pos); + }); + }); + + describe('BrowserWindow.setTrafficLightPosition(pos)', () => { + it('sets position property for "hidden" titleBarStyle', () => { + const w = new BrowserWindow({ show: false, titleBarStyle: 'hidden', trafficLightPosition: pos }); + const newPos = { x: 20, y: 20 }; + w.setTrafficLightPosition(newPos); + expect(w.getTrafficLightPosition()).to.deep.equal(newPos); + }); + + it('sets position property for "customButtonsOnHover" titleBarStyle', () => { + const w = new BrowserWindow({ show: false, titleBarStyle: 'customButtonsOnHover', trafficLightPosition: pos }); + const newPos = { x: 20, y: 20 }; + w.setTrafficLightPosition(newPos); + expect(w.getTrafficLightPosition()).to.deep.equal(newPos); + }); + }); + }); ifdescribe(process.platform === 'win32')('BrowserWindow.setAppDetails(options)', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('supports setting the app details', () => { - const w = new BrowserWindow({show: false}) - const iconPath = path.join(fixtures, 'assets', 'icon.ico') + const w = new BrowserWindow({ show: false }); + const iconPath = path.join(fixtures, 'assets', 'icon.ico'); expect(() => { - w.setAppDetails({ appId: 'my.app.id' }) - w.setAppDetails({ appIconPath: iconPath, appIconIndex: 0 }) - w.setAppDetails({ appIconPath: iconPath }) - w.setAppDetails({ relaunchCommand: 'my-app.exe arg1 arg2', relaunchDisplayName: 'My app name' }) - w.setAppDetails({ relaunchCommand: 'my-app.exe arg1 arg2' }) - w.setAppDetails({ relaunchDisplayName: 'My app name' }) + w.setAppDetails({ appId: 'my.app.id' }); + w.setAppDetails({ appIconPath: iconPath, appIconIndex: 0 }); + w.setAppDetails({ appIconPath: iconPath }); + w.setAppDetails({ relaunchCommand: 'my-app.exe arg1 arg2', relaunchDisplayName: 'My app name' }); + w.setAppDetails({ relaunchCommand: 'my-app.exe arg1 arg2' }); + w.setAppDetails({ relaunchDisplayName: 'My app name' }); w.setAppDetails({ appId: 'my.app.id', appIconPath: iconPath, appIconIndex: 0, relaunchCommand: 'my-app.exe arg1 arg2', relaunchDisplayName: 'My app name' - }) - w.setAppDetails({}) - }).to.not.throw() + }); + w.setAppDetails({}); + }).to.not.throw(); expect(() => { - (w.setAppDetails as any)() - }).to.throw('Insufficient number of arguments.') - }) - }) + (w.setAppDetails as any)(); + }).to.throw('Insufficient number of arguments.'); + }); + }); describe('BrowserWindow.fromId(id)', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('returns the window with id', () => { - const w = new BrowserWindow({show: false}) - expect(BrowserWindow.fromId(w.id).id).to.equal(w.id) - }) - }) + const w = new BrowserWindow({ show: false }); + expect(BrowserWindow.fromId(w.id)!.id).to.equal(w.id); + }); + }); + + describe('Opening a BrowserWindow from a link', () => { + let appProcess: childProcess.ChildProcessWithoutNullStreams | undefined; + + afterEach(() => { + if (appProcess && !appProcess.killed) { + appProcess.kill(); + appProcess = undefined; + } + }); + + it('can properly open and load a new window from a link', async () => { + const appPath = path.join(__dirname, 'fixtures', 'apps', 'open-new-window-from-link'); + + appProcess = childProcess.spawn(process.execPath, [appPath]); + + const [code] = await emittedOnce(appProcess, 'exit'); + expect(code).to.equal(0); + }); + }); describe('BrowserWindow.fromWebContents(webContents)', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('returns the window with the webContents', () => { - const w = new BrowserWindow({show: false}) - const found = BrowserWindow.fromWebContents(w.webContents) - expect(found.id).to.equal(w.id) - }) + const w = new BrowserWindow({ show: false }); + const found = BrowserWindow.fromWebContents(w.webContents); + expect(found!.id).to.equal(w.id); + }); - it('returns undefined for webContents without a BrowserWindow', () => { - const contents = (webContents as any).create({}) + it('returns null for webContents without a BrowserWindow', () => { + const contents = (webContents as any).create({}); try { - expect(BrowserWindow.fromWebContents(contents)).to.be.undefined('BrowserWindow.fromWebContents(contents)') + expect(BrowserWindow.fromWebContents(contents)).to.be.null('BrowserWindow.fromWebContents(contents)'); } finally { - contents.destroy() + contents.destroy(); } - }) - }) + }); + + it('returns the correct window for a BrowserView webcontents', async () => { + const w = new BrowserWindow({ show: false }); + const bv = new BrowserView(); + w.setBrowserView(bv); + defer(() => { + w.removeBrowserView(bv); + (bv.webContents as any).destroy(); + }); + await bv.webContents.loadURL('about:blank'); + expect(BrowserWindow.fromWebContents(bv.webContents)!.id).to.equal(w.id); + }); + + it('returns the correct window for a WebView webcontents', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } }); + w.loadURL('data:text/html,'); + // NOTE(nornagon): Waiting for 'did-attach-webview' is a workaround for + // https://github.com/electron/electron/issues/25413, and is not integral + // to the test. + const p = emittedOnce(w.webContents, 'did-attach-webview'); + const [, webviewContents] = await emittedOnce(app, 'web-contents-created'); + expect(BrowserWindow.fromWebContents(webviewContents)!.id).to.equal(w.id); + await p; + }); + + it('is usable immediately on browser-window-created', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('window.open(""); null'); + const [win, winFromWebContents] = await new Promise((resolve) => { + app.once('browser-window-created', (e, win) => { + resolve([win, BrowserWindow.fromWebContents(win.webContents)]); + }); + }); + expect(winFromWebContents).to.equal(win); + }); + }); describe('BrowserWindow.openDevTools()', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('does not crash for frameless window', () => { - const w = new BrowserWindow({ show: false, frame: false }) - w.webContents.openDevTools() - }) - }) + const w = new BrowserWindow({ show: false, frame: false }); + w.webContents.openDevTools(); + }); + }); describe('BrowserWindow.fromBrowserView(browserView)', () => { - afterEach(closeAllWindows) - - it('returns the window with the browserView', () => { - const w = new BrowserWindow({ show: false }) - const bv = new BrowserView - w.setBrowserView(bv) - expect(BrowserWindow.fromBrowserView(bv)!.id).to.equal(w.id) - // if BrowserView isn't explicitly destroyed, it will crash in GC later - bv.destroy() - }) + afterEach(closeAllWindows); + + it('returns the window with the BrowserView', () => { + const w = new BrowserWindow({ show: false }); + const bv = new BrowserView(); + w.setBrowserView(bv); + defer(() => { + w.removeBrowserView(bv); + (bv.webContents as any).destroy(); + }); + expect(BrowserWindow.fromBrowserView(bv)!.id).to.equal(w.id); + }); + + it('returns the window when there are multiple BrowserViews', () => { + const w = new BrowserWindow({ show: false }); + const bv1 = new BrowserView(); + w.addBrowserView(bv1); + const bv2 = new BrowserView(); + w.addBrowserView(bv2); + defer(() => { + w.removeBrowserView(bv1); + w.removeBrowserView(bv2); + (bv1.webContents as any).destroy(); + (bv2.webContents as any).destroy(); + }); + expect(BrowserWindow.fromBrowserView(bv1)!.id).to.equal(w.id); + expect(BrowserWindow.fromBrowserView(bv2)!.id).to.equal(w.id); + }); it('returns undefined if not attached', () => { - const bv = new BrowserView - expect(BrowserWindow.fromBrowserView(bv)).to.be.null('BrowserWindow associated with bv') - // if BrowserView isn't explicitly destroyed, it will crash in GC later - bv.destroy() - }) - }) + const bv = new BrowserView(); + defer(() => { + (bv.webContents as any).destroy(); + }); + expect(BrowserWindow.fromBrowserView(bv)).to.be.null('BrowserWindow associated with bv'); + }); + }); describe('BrowserWindow.setOpacity(opacity)', () => { - afterEach(closeAllWindows) - it('make window with initial opacity', () => { - const w = new BrowserWindow({ show: false, opacity: 0.5 }) - expect(w.getOpacity()).to.equal(0.5) - }) - it('allows setting the opacity', () => { - const w = new BrowserWindow({ show: false }) - expect(() => { - w.setOpacity(0.0) - expect(w.getOpacity()).to.equal(0.0) - w.setOpacity(0.5) - expect(w.getOpacity()).to.equal(0.5) - w.setOpacity(1.0) - expect(w.getOpacity()).to.equal(1.0) - }).to.not.throw() - }) - }) + afterEach(closeAllWindows); + + ifdescribe(process.platform !== 'linux')(('Windows and Mac'), () => { + it('make window with initial opacity', () => { + const w = new BrowserWindow({ show: false, opacity: 0.5 }); + expect(w.getOpacity()).to.equal(0.5); + }); + it('allows setting the opacity', () => { + const w = new BrowserWindow({ show: false }); + expect(() => { + w.setOpacity(0.0); + expect(w.getOpacity()).to.equal(0.0); + w.setOpacity(0.5); + expect(w.getOpacity()).to.equal(0.5); + w.setOpacity(1.0); + expect(w.getOpacity()).to.equal(1.0); + }).to.not.throw(); + }); + + it('clamps opacity to [0.0...1.0]', () => { + const w = new BrowserWindow({ show: false, opacity: 0.5 }); + w.setOpacity(100); + expect(w.getOpacity()).to.equal(1.0); + w.setOpacity(-100); + expect(w.getOpacity()).to.equal(0.0); + }); + }); + + ifdescribe(process.platform === 'linux')(('Linux'), () => { + it('sets 1 regardless of parameter', () => { + const w = new BrowserWindow({ show: false }); + w.setOpacity(0); + expect(w.getOpacity()).to.equal(1.0); + w.setOpacity(0.5); + expect(w.getOpacity()).to.equal(1.0); + }); + }); + }); describe('BrowserWindow.setShape(rects)', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('allows setting shape', () => { - const w = new BrowserWindow({ show: false }) + const w = new BrowserWindow({ show: false }); expect(() => { - w.setShape([]) - w.setShape([{ x: 0, y: 0, width: 100, height: 100 }]) - w.setShape([{ x: 0, y: 0, width: 100, height: 100 }, { x: 0, y: 200, width: 1000, height: 100 }]) - w.setShape([]) - }).to.not.throw() - }) - }) + w.setShape([]); + w.setShape([{ x: 0, y: 0, width: 100, height: 100 }]); + w.setShape([{ x: 0, y: 0, width: 100, height: 100 }, { x: 0, y: 200, width: 1000, height: 100 }]); + w.setShape([]); + }).to.not.throw(); + }); + }); describe('"useContentSize" option', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('make window created with content size when used', () => { const w = new BrowserWindow({ show: false, width: 400, height: 400, useContentSize: true - }) - const contentSize = w.getContentSize() - expect(contentSize).to.deep.equal([400, 400]) - }) + }); + const contentSize = w.getContentSize(); + expect(contentSize).to.deep.equal([400, 400]); + }); it('make window created with window size when not used', () => { const w = new BrowserWindow({ show: false, width: 400, - height: 400, - }) - const size = w.getSize() - expect(size).to.deep.equal([400, 400]) - }) + height: 400 + }); + const size = w.getSize(); + expect(size).to.deep.equal([400, 400]); + }); it('works for a frameless window', () => { const w = new BrowserWindow({ show: false, @@ -1300,105 +2349,250 @@ describe('BrowserWindow module', () => { width: 400, height: 400, useContentSize: true - }) - const contentSize = w.getContentSize() - expect(contentSize).to.deep.equal([400, 400]) - const size = w.getSize() - expect(size).to.deep.equal([400, 400]) - }) - }) - - ifdescribe(process.platform === 'darwin' && parseInt(os.release().split('.')[0]) >= 14)('"titleBarStyle" option', () => { - afterEach(closeAllWindows) + }); + const contentSize = w.getContentSize(); + expect(contentSize).to.deep.equal([400, 400]); + const size = w.getSize(); + expect(size).to.deep.equal([400, 400]); + }); + }); + + ifdescribe(process.platform === 'win32' || (process.platform === 'darwin' && semver.gte(os.release(), '14.0.0')))('"titleBarStyle" option', () => { + const testWindowsOverlay = async (style: any) => { + const w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + titleBarStyle: style, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + }, + titleBarOverlay: true + }); + const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html'); + if (process.platform === 'darwin') { + await w.loadFile(overlayHTML); + } else { + const overlayReady = emittedOnce(ipcMain, 'geometrychange'); + await w.loadFile(overlayHTML); + await overlayReady; + } + const overlayEnabled = await w.webContents.executeJavaScript('navigator.windowControlsOverlay.visible'); + expect(overlayEnabled).to.be.true('overlayEnabled'); + const overlayRect = await w.webContents.executeJavaScript('getJSOverlayProperties()'); + expect(overlayRect.y).to.equal(0); + if (process.platform === 'darwin') { + expect(overlayRect.x).to.be.greaterThan(0); + } else { + expect(overlayRect.x).to.equal(0); + } + expect(overlayRect.width).to.be.greaterThan(0); + expect(overlayRect.height).to.be.greaterThan(0); + const cssOverlayRect = await w.webContents.executeJavaScript('getCssOverlayProperties();'); + expect(cssOverlayRect).to.deep.equal(overlayRect); + const geometryChange = emittedOnce(ipcMain, 'geometrychange'); + w.setBounds({ width: 800 }); + const [, newOverlayRect] = await geometryChange; + expect(newOverlayRect.width).to.equal(overlayRect.width + 400); + }; + afterEach(closeAllWindows); + afterEach(() => { ipcMain.removeAllListeners('geometrychange'); }); it('creates browser window with hidden title bar', () => { const w = new BrowserWindow({ show: false, width: 400, height: 400, titleBarStyle: 'hidden' - }) - const contentSize = w.getContentSize() - expect(contentSize).to.deep.equal([400, 400]) - }) - it('creates browser window with hidden inset title bar', () => { + }); + const contentSize = w.getContentSize(); + expect(contentSize).to.deep.equal([400, 400]); + }); + ifit(process.platform === 'darwin')('creates browser window with hidden inset title bar', () => { const w = new BrowserWindow({ show: false, width: 400, height: 400, titleBarStyle: 'hiddenInset' - }) - const contentSize = w.getContentSize() - expect(contentSize).to.deep.equal([400, 400]) - }) - }) + }); + const contentSize = w.getContentSize(); + expect(contentSize).to.deep.equal([400, 400]); + }); + it('sets Window Control Overlay with hidden title bar', async () => { + await testWindowsOverlay('hidden'); + }); + ifit(process.platform === 'darwin')('sets Window Control Overlay with hidden inset title bar', async () => { + await testWindowsOverlay('hiddenInset'); + }); + + ifdescribe(process.platform === 'win32')('when an invalid titleBarStyle is initially set', () => { + let w: BrowserWindow; - ifdescribe(process.platform === 'darwin')('"enableLargerThanScreen" option', () => { - afterEach(closeAllWindows) - it('can move the window out of screen', () => { - const w = new BrowserWindow({ show: true, enableLargerThanScreen: true }) - w.setPosition(-10, -10) - const after = w.getPosition() - expect(after).to.deep.equal([-10, -10]) - }) - it('without it, cannot move the window out of screen', () => { - const w = new BrowserWindow({ show: true, enableLargerThanScreen: false }) - w.setPosition(-10, -10) - const after = w.getPosition() - expect(after[1]).to.be.at.least(0) - }) - it('can set the window larger than screen', () => { - const w = new BrowserWindow({ show: true, enableLargerThanScreen: true }) - const size = screen.getPrimaryDisplay().size - size.width += 100 - size.height += 100 - w.setSize(size.width, size.height) - expectBoundsEqual(w.getSize(), [size.width, size.height]) - }) - it('without it, cannot set the window larger than screen', () => { - const w = new BrowserWindow({ show: true, enableLargerThanScreen: false }) - const size = screen.getPrimaryDisplay().size - size.width += 100 - size.height += 100 - w.setSize(size.width, size.height) - expect(w.getSize()[1]).to.at.most(screen.getPrimaryDisplay().size.height) - }) - }) + beforeEach(() => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + }, + titleBarOverlay: { + color: '#0000f0', + symbolColor: '#ffffff' + }, + titleBarStyle: 'hiddenInset' + }); + }); - ifdescribe(process.platform === 'darwin')('"zoomToPageWidth" option', () => { - afterEach(closeAllWindows) - it('sets the window width to the page width when used', () => { - const w = new BrowserWindow({ - show: false, - width: 500, + afterEach(async () => { + await closeAllWindows(); + }); + + it('does not crash changing minimizability ', () => { + expect(() => { + w.setMinimizable(false); + }).to.not.throw(); + }); + + it('does not crash changing maximizability', () => { + expect(() => { + w.setMaximizable(false); + }).to.not.throw(); + }); + }); + }); + + ifdescribe(process.platform === 'win32' || (process.platform === 'darwin' && semver.gte(os.release(), '14.0.0')))('"titleBarOverlay" option', () => { + const testWindowsOverlayHeight = async (size: any) => { + const w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + titleBarStyle: 'hidden', + webPreferences: { + nodeIntegration: true, + contextIsolation: false + }, + titleBarOverlay: { + height: size + } + }); + const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html'); + if (process.platform === 'darwin') { + await w.loadFile(overlayHTML); + } else { + const overlayReady = emittedOnce(ipcMain, 'geometrychange'); + await w.loadFile(overlayHTML); + await overlayReady; + } + const overlayEnabled = await w.webContents.executeJavaScript('navigator.windowControlsOverlay.visible'); + expect(overlayEnabled).to.be.true('overlayEnabled'); + const overlayRectPreMax = await w.webContents.executeJavaScript('getJSOverlayProperties()'); + await w.maximize(); + const max = await w.isMaximized(); + expect(max).to.equal(true); + const overlayRectPostMax = await w.webContents.executeJavaScript('getJSOverlayProperties()'); + + expect(overlayRectPreMax.y).to.equal(0); + if (process.platform === 'darwin') { + expect(overlayRectPreMax.x).to.be.greaterThan(0); + } else { + expect(overlayRectPreMax.x).to.equal(0); + } + expect(overlayRectPreMax.width).to.be.greaterThan(0); + + expect(overlayRectPreMax.height).to.equal(size); + // Confirm that maximization only affected the height of the buttons and not the title bar + expect(overlayRectPostMax.height).to.equal(size); + }; + afterEach(closeAllWindows); + afterEach(() => { ipcMain.removeAllListeners('geometrychange'); }); + it('sets Window Control Overlay with title bar height of 40', async () => { + await testWindowsOverlayHeight(40); + }); + }); + + ifdescribe(process.platform === 'darwin')('"enableLargerThanScreen" option', () => { + afterEach(closeAllWindows); + it('can move the window out of screen', () => { + const w = new BrowserWindow({ show: true, enableLargerThanScreen: true }); + w.setPosition(-10, 50); + const after = w.getPosition(); + expect(after).to.deep.equal([-10, 50]); + }); + it('cannot move the window behind menu bar', () => { + const w = new BrowserWindow({ show: true, enableLargerThanScreen: true }); + w.setPosition(-10, -10); + const after = w.getPosition(); + expect(after[1]).to.be.at.least(0); + }); + it('can move the window behind menu bar if it has no frame', () => { + const w = new BrowserWindow({ show: true, enableLargerThanScreen: true, frame: false }); + w.setPosition(-10, -10); + const after = w.getPosition(); + expect(after[0]).to.be.equal(-10); + expect(after[1]).to.be.equal(-10); + }); + it('without it, cannot move the window out of screen', () => { + const w = new BrowserWindow({ show: true, enableLargerThanScreen: false }); + w.setPosition(-10, -10); + const after = w.getPosition(); + expect(after[1]).to.be.at.least(0); + }); + it('can set the window larger than screen', () => { + const w = new BrowserWindow({ show: true, enableLargerThanScreen: true }); + const size = screen.getPrimaryDisplay().size; + size.width += 100; + size.height += 100; + w.setSize(size.width, size.height); + expectBoundsEqual(w.getSize(), [size.width, size.height]); + }); + it('without it, cannot set the window larger than screen', () => { + const w = new BrowserWindow({ show: true, enableLargerThanScreen: false }); + const size = screen.getPrimaryDisplay().size; + size.width += 100; + size.height += 100; + w.setSize(size.width, size.height); + expect(w.getSize()[1]).to.at.most(screen.getPrimaryDisplay().size.height); + }); + }); + + ifdescribe(process.platform === 'darwin')('"zoomToPageWidth" option', () => { + afterEach(closeAllWindows); + it('sets the window width to the page width when used', () => { + const w = new BrowserWindow({ + show: false, + width: 500, height: 400, zoomToPageWidth: true - }) - w.maximize() - expect(w.getSize()[0]).to.equal(500) - }) - }) + }); + w.maximize(); + expect(w.getSize()[0]).to.equal(500); + }); + }); describe('"tabbingIdentifier" option', () => { - afterEach(closeAllWindows) + afterEach(closeAllWindows); it('can be set on a window', () => { expect(() => { + /* eslint-disable no-new */ new BrowserWindow({ tabbingIdentifier: 'group1' - }) + }); new BrowserWindow({ tabbingIdentifier: 'group2', frame: false - }) - }).not.to.throw() - }) - }) + }); + /* eslint-enable no-new */ + }).not.to.throw(); + }); + }); describe('"webPreferences" option', () => { - afterEach(() => { ipcMain.removeAllListeners('answer') }) - afterEach(closeAllWindows) + afterEach(() => { ipcMain.removeAllListeners('answer'); }); + afterEach(closeAllWindows); describe('"preload" option', () => { - const doesNotLeakSpec = (name: string, webPrefs: {nodeIntegration: boolean, sandbox: boolean, contextIsolation: boolean}) => { + const doesNotLeakSpec = (name: string, webPrefs: { nodeIntegration: boolean, sandbox: boolean, contextIsolation: boolean }) => { it(name, async () => { const w = new BrowserWindow({ webPreferences: { @@ -1406,129 +2600,148 @@ describe('BrowserWindow module', () => { preload: path.resolve(fixtures, 'module', 'empty.js') }, show: false - }) - w.loadFile(path.join(fixtures, 'api', 'no-leak.html')) - const [, result] = await emittedOnce(ipcMain, 'leak-result') - expect(result).to.have.property('require', 'undefined') - expect(result).to.have.property('exports', 'undefined') - expect(result).to.have.property('windowExports', 'undefined') - expect(result).to.have.property('windowPreload', 'undefined') - expect(result).to.have.property('windowRequire', 'undefined') - }) - } + }); + w.loadFile(path.join(fixtures, 'api', 'no-leak.html')); + const [, result] = await emittedOnce(ipcMain, 'leak-result'); + expect(result).to.have.property('require', 'undefined'); + expect(result).to.have.property('exports', 'undefined'); + expect(result).to.have.property('windowExports', 'undefined'); + expect(result).to.have.property('windowPreload', 'undefined'); + expect(result).to.have.property('windowRequire', 'undefined'); + }); + }; doesNotLeakSpec('does not leak require', { nodeIntegration: false, sandbox: false, contextIsolation: false - }) + }); doesNotLeakSpec('does not leak require when sandbox is enabled', { nodeIntegration: false, sandbox: true, contextIsolation: false - }) + }); doesNotLeakSpec('does not leak require when context isolation is enabled', { nodeIntegration: false, sandbox: false, contextIsolation: true - }) + }); doesNotLeakSpec('does not leak require when context isolation and sandbox are enabled', { nodeIntegration: false, sandbox: true, contextIsolation: true - }) + }); + it('does not leak any node globals on the window object with nodeIntegration is disabled', async () => { + let w = new BrowserWindow({ + webPreferences: { + contextIsolation: false, + nodeIntegration: false, + preload: path.resolve(fixtures, 'module', 'empty.js') + }, + show: false + }); + w.loadFile(path.join(fixtures, 'api', 'globals.html')); + const [, notIsolated] = await emittedOnce(ipcMain, 'leak-result'); + expect(notIsolated).to.have.property('globals'); + + w.destroy(); + w = new BrowserWindow({ + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + preload: path.resolve(fixtures, 'module', 'empty.js') + }, + show: false + }); + w.loadFile(path.join(fixtures, 'api', 'globals.html')); + const [, isolated] = await emittedOnce(ipcMain, 'leak-result'); + expect(isolated).to.have.property('globals'); + const notIsolatedGlobals = new Set(notIsolated.globals); + for (const isolatedGlobal of isolated.globals) { + notIsolatedGlobals.delete(isolatedGlobal); + } + expect([...notIsolatedGlobals]).to.deep.equal([], 'non-isoalted renderer should have no additional globals'); + }); it('loads the script before other scripts in window', async () => { - const preload = path.join(fixtures, 'module', 'set-global.js') + const preload = path.join(fixtures, 'module', 'set-global.js'); const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, + contextIsolation: false, preload } - }) - w.loadFile(path.join(fixtures, 'api', 'preload.html')) - const [, test] = await emittedOnce(ipcMain, 'answer') - expect(test).to.eql('preload') - }) - it('can successfully delete the Buffer global', async () => { - const preload = path.join(fixtures, 'module', 'delete-buffer.js') - const w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - preload - } - }) - w.loadFile(path.join(fixtures, 'api', 'preload.html')) - const [, test] = await emittedOnce(ipcMain, 'answer') - expect(test.toString()).to.eql('buffer') - }) + }); + w.loadFile(path.join(fixtures, 'api', 'preload.html')); + const [, test] = await emittedOnce(ipcMain, 'answer'); + expect(test).to.eql('preload'); + }); it('has synchronous access to all eventual window APIs', async () => { - const preload = path.join(fixtures, 'module', 'access-blink-apis.js') + const preload = path.join(fixtures, 'module', 'access-blink-apis.js'); const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, + contextIsolation: false, preload } - }) - w.loadFile(path.join(fixtures, 'api', 'preload.html')) - const [, test] = await emittedOnce(ipcMain, 'answer') - expect(test).to.be.an('object') - expect(test.atPreload).to.be.an('array') - expect(test.atLoad).to.be.an('array') - expect(test.atPreload).to.deep.equal(test.atLoad, 'should have access to the same window APIs') - }) - }) + }); + w.loadFile(path.join(fixtures, 'api', 'preload.html')); + const [, test] = await emittedOnce(ipcMain, 'answer'); + expect(test).to.be.an('object'); + expect(test.atPreload).to.be.an('array'); + expect(test.atLoad).to.be.an('array'); + expect(test.atPreload).to.deep.equal(test.atLoad, 'should have access to the same window APIs'); + }); + }); describe('session preload scripts', function () { const preloads = [ path.join(fixtures, 'module', 'set-global-preload-1.js'), path.join(fixtures, 'module', 'set-global-preload-2.js'), path.relative(process.cwd(), path.join(fixtures, 'module', 'set-global-preload-3.js')) - ] - const defaultSession = session.defaultSession + ]; + const defaultSession = session.defaultSession; beforeEach(() => { - expect(defaultSession.getPreloads()).to.deep.equal([]) - defaultSession.setPreloads(preloads) - }) + expect(defaultSession.getPreloads()).to.deep.equal([]); + defaultSession.setPreloads(preloads); + }); afterEach(() => { - defaultSession.setPreloads([]) - }) + defaultSession.setPreloads([]); + }); it('can set multiple session preload script', () => { - expect(defaultSession.getPreloads()).to.deep.equal(preloads) - }) + expect(defaultSession.getPreloads()).to.deep.equal(preloads); + }); const generateSpecs = (description: string, sandbox: boolean) => { describe(description, () => { - it('loads the script before other scripts in window including normal preloads', function (done) { - ipcMain.once('vars', function (event, preload1, preload2, preload3) { - expect(preload1).to.equal('preload-1') - expect(preload2).to.equal('preload-1-2') - expect(preload3).to.be.null('preload 3') - done() - }) + it('loads the script before other scripts in window including normal preloads', async () => { const w = new BrowserWindow({ show: false, webPreferences: { sandbox, - preload: path.join(fixtures, 'module', 'get-global-preload.js') + preload: path.join(fixtures, 'module', 'get-global-preload.js'), + contextIsolation: false } - }) - w.loadURL('about:blank') - }) - }) - } - - generateSpecs('without sandbox', false) - generateSpecs('with sandbox', true) - }) + }); + w.loadURL('about:blank'); + const [, preload1, preload2, preload3] = await emittedOnce(ipcMain, 'vars'); + expect(preload1).to.equal('preload-1'); + expect(preload2).to.equal('preload-1-2'); + expect(preload3).to.be.undefined('preload 3'); + }); + }); + }; + + generateSpecs('without sandbox', false); + generateSpecs('with sandbox', true); + }); describe('"additionalArguments" option', () => { it('adds extra args to process.argv in the renderer process', async () => { - const preload = path.join(fixtures, 'module', 'check-arguments.js') + const preload = path.join(fixtures, 'module', 'check-arguments.js'); const w = new BrowserWindow({ show: false, webPreferences: { @@ -1536,14 +2749,14 @@ describe('BrowserWindow module', () => { preload, additionalArguments: ['--my-magic-arg'] } - }) - w.loadFile(path.join(fixtures, 'api', 'blank.html')) - const [, argv] = await emittedOnce(ipcMain, 'answer') - expect(argv).to.include('--my-magic-arg') - }) + }); + w.loadFile(path.join(fixtures, 'api', 'blank.html')); + const [, argv] = await emittedOnce(ipcMain, 'answer'); + expect(argv).to.include('--my-magic-arg'); + }); it('adds extra value args to process.argv in the renderer process', async () => { - const preload = path.join(fixtures, 'module', 'check-arguments.js') + const preload = path.join(fixtures, 'module', 'check-arguments.js'); const w = new BrowserWindow({ show: false, webPreferences: { @@ -1551,129 +2764,83 @@ describe('BrowserWindow module', () => { preload, additionalArguments: ['--my-magic-arg=foo'] } - }) - w.loadFile(path.join(fixtures, 'api', 'blank.html')) - const [, argv] = await emittedOnce(ipcMain, 'answer') - expect(argv).to.include('--my-magic-arg=foo') - }) - }) + }); + w.loadFile(path.join(fixtures, 'api', 'blank.html')); + const [, argv] = await emittedOnce(ipcMain, 'answer'); + expect(argv).to.include('--my-magic-arg=foo'); + }); + }); describe('"node-integration" option', () => { it('disables node integration by default', async () => { - const preload = path.join(fixtures, 'module', 'send-later.js') + const preload = path.join(fixtures, 'module', 'send-later.js'); const w = new BrowserWindow({ show: false, webPreferences: { - preload + preload, + contextIsolation: false } - }) - w.loadFile(path.join(fixtures, 'api', 'blank.html')) - const [, typeofProcess, typeofBuffer] = await emittedOnce(ipcMain, 'answer') - expect(typeofProcess).to.equal('undefined') - expect(typeofBuffer).to.equal('undefined') - }) - }) - - describe('"enableRemoteModule" option', () => { - const generateSpecs = (description: string, sandbox: boolean) => { - describe(description, () => { - const preload = path.join(fixtures, 'module', 'preload-remote.js') - - it('enables the remote module by default', async () => { - const w = new BrowserWindow({ - show: false, - webPreferences: { - preload, - sandbox - } - }) - const p = emittedOnce(ipcMain, 'remote') - w.loadFile(path.join(fixtures, 'api', 'blank.html')) - const [, remote] = await p - expect(remote).to.equal('object') - }) - - it('disables the remote module when false', async () => { - const w = new BrowserWindow({ - show: false, - webPreferences: { - preload, - sandbox, - enableRemoteModule: false - } - }) - const p = emittedOnce(ipcMain, 'remote') - w.loadFile(path.join(fixtures, 'api', 'blank.html')) - const [, remote] = await p - expect(remote).to.equal('undefined') - }) - }) - } - - generateSpecs('without sandbox', false) - generateSpecs('with sandbox', true) - }) + }); + w.loadFile(path.join(fixtures, 'api', 'blank.html')); + const [, typeofProcess, typeofBuffer] = await emittedOnce(ipcMain, 'answer'); + expect(typeofProcess).to.equal('undefined'); + expect(typeofBuffer).to.equal('undefined'); + }); + }); describe('"sandbox" option', () => { - function waitForEvents(emitter: {once: Function}, events: string[], callback: () => void) { - let count = events.length - for (const event of events) { - emitter.once(event, () => { - if (!--count) callback() - }) - } - } + const preload = path.join(path.resolve(__dirname, 'fixtures'), 'module', 'preload-sandbox.js'); - const preload = path.join(fixtures, 'module', 'preload-sandbox.js') - - let server: http.Server = null as unknown as http.Server - let serverUrl: string = null as unknown as string + let server: http.Server = null as unknown as http.Server; + let serverUrl: string = null as unknown as string; before((done) => { server = http.createServer((request, response) => { switch (request.url) { case '/cross-site': - response.end(`

${request.url}

`) - break + response.end(`

${request.url}

`); + break; default: - throw new Error(`unsupported endpoint: ${request.url}`) + throw new Error(`unsupported endpoint: ${request.url}`); } }).listen(0, '127.0.0.1', () => { - serverUrl = 'http://127.0.0.1:' + (server.address() as AddressInfo).port - done() - }) - }) + serverUrl = 'http://127.0.0.1:' + (server.address() as AddressInfo).port; + done(); + }); + }); after(() => { - server.close() - }) + server.close(); + }); it('exposes ipcRenderer to preload script', async () => { const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, - preload + preload, + contextIsolation: false } - }) - w.loadFile(path.join(fixtures, 'api', 'preload.html')) - const [, test] = await emittedOnce(ipcMain, 'answer') - expect(test).to.equal('preload') - }) + }); + w.loadFile(path.join(fixtures, 'api', 'preload.html')); + const [, test] = await emittedOnce(ipcMain, 'answer'); + expect(test).to.equal('preload'); + }); it('exposes ipcRenderer to preload script (path has special chars)', async () => { - const preloadSpecialChars = path.join(fixtures, 'module', 'preload-sandboxæø åü.js') + const preloadSpecialChars = path.join(fixtures, 'module', 'preload-sandboxæø åü.js'); const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, - preload: preloadSpecialChars + preload: preloadSpecialChars, + contextIsolation: false } - }) - w.loadFile(path.join(fixtures, 'api', 'preload.html')) - const [, test] = await emittedOnce(ipcMain, 'answer') - expect(test).to.equal('preload') - }) + }); + w.loadFile(path.join(fixtures, 'api', 'preload.html')); + const [, test] = await emittedOnce(ipcMain, 'answer'); + expect(test).to.equal('preload'); + }); it('exposes "loaded" event to preload script', async () => { const w = new BrowserWindow({ @@ -1682,113 +2849,129 @@ describe('BrowserWindow module', () => { sandbox: true, preload } - }) - w.loadURL('about:blank') - await emittedOnce(ipcMain, 'process-loaded') - }) + }); + w.loadURL('about:blank'); + await emittedOnce(ipcMain, 'process-loaded'); + }); it('exposes "exit" event to preload script', async () => { const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, - preload + preload, + contextIsolation: false } - }) - const htmlPath = path.join(fixtures, 'api', 'sandbox.html?exit-event') - const pageUrl = 'file://' + htmlPath - w.loadURL(pageUrl) - const [, url] = await emittedOnce(ipcMain, 'answer') + }); + const htmlPath = path.join(__dirname, 'fixtures', 'api', 'sandbox.html?exit-event'); + const pageUrl = 'file://' + htmlPath; + w.loadURL(pageUrl); + const [, url] = await emittedOnce(ipcMain, 'answer'); const expectedUrl = process.platform === 'win32' ? 'file:///' + htmlPath.replace(/\\/g, '/') - : pageUrl - expect(url).to.equal(expectedUrl) - }) + : pageUrl; + expect(url).to.equal(expectedUrl); + }); it('should open windows in same domain with cross-scripting enabled', async () => { const w = new BrowserWindow({ - show: false, + show: true, webPreferences: { sandbox: true, - preload + preload, + contextIsolation: false } - }) - w.webContents.once('new-window', (event, url, frameName, disposition, options) => { - options.webPreferences.preload = preload - }) - const htmlPath = path.join(fixtures, 'api', 'sandbox.html?window-open') - const pageUrl = 'file://' + htmlPath - const answer = emittedOnce(ipcMain, 'answer') - w.loadURL(pageUrl) - const [, url, frameName, , options] = await emittedOnce(w.webContents, 'new-window') + }); + + w.webContents.setWindowOpenHandler(() => ({ + action: 'allow', + overrideBrowserWindowOptions: { + webPreferences: { + preload + } + } + })); + + const htmlPath = path.join(__dirname, 'fixtures', 'api', 'sandbox.html?window-open'); + const pageUrl = 'file://' + htmlPath; + const answer = emittedOnce(ipcMain, 'answer'); + w.loadURL(pageUrl); + const [, { url, frameName, options }] = await emittedOnce(w.webContents, 'did-create-window'); const expectedUrl = process.platform === 'win32' ? 'file:///' + htmlPath.replace(/\\/g, '/') - : pageUrl - expect(url).to.equal(expectedUrl) - expect(frameName).to.equal('popup!') - expect(options.width).to.equal(500) - expect(options.height).to.equal(600) - const [, html] = await answer - expect(html).to.equal('

scripting from opener

') - }) + : pageUrl; + expect(url).to.equal(expectedUrl); + expect(frameName).to.equal('popup!'); + expect(options.width).to.equal(500); + expect(options.height).to.equal(600); + const [, html] = await answer; + expect(html).to.equal('

scripting from opener

'); + }); it('should open windows in another domain with cross-scripting disabled', async () => { const w = new BrowserWindow({ - show: false, + show: true, webPreferences: { sandbox: true, - preload + preload, + contextIsolation: false } - }) + }); + + w.webContents.setWindowOpenHandler(() => ({ + action: 'allow', + overrideBrowserWindowOptions: { + webPreferences: { + preload + } + } + })); - w.webContents.once('new-window', (event, url, frameName, disposition, options) => { - options.webPreferences.preload = preload - }) w.loadFile( - path.join(fixtures, 'api', 'sandbox.html'), + path.join(__dirname, 'fixtures', 'api', 'sandbox.html'), { search: 'window-open-external' } - ) + ); // Wait for a message from the main window saying that it's ready. - await emittedOnce(ipcMain, 'opener-loaded') + await emittedOnce(ipcMain, 'opener-loaded'); // Ask the opener to open a popup with window.opener. - const expectedPopupUrl = `${serverUrl}/cross-site` // Set in "sandbox.html". + const expectedPopupUrl = `${serverUrl}/cross-site`; // Set in "sandbox.html". - w.webContents.send('open-the-popup', expectedPopupUrl) + w.webContents.send('open-the-popup', expectedPopupUrl); // The page is going to open a popup that it won't be able to close. // We have to close it from here later. - const [, popupWindow] = await emittedOnce(app, 'browser-window-created') + const [, popupWindow] = await emittedOnce(app, 'browser-window-created'); // Ask the popup window for details. - const detailsAnswer = emittedOnce(ipcMain, 'child-loaded') - popupWindow.webContents.send('provide-details') - const [, openerIsNull, , locationHref] = await detailsAnswer - expect(openerIsNull).to.be.false('window.opener is null') - expect(locationHref).to.equal(expectedPopupUrl) + const detailsAnswer = emittedOnce(ipcMain, 'child-loaded'); + popupWindow.webContents.send('provide-details'); + const [, openerIsNull, , locationHref] = await detailsAnswer; + expect(openerIsNull).to.be.false('window.opener is null'); + expect(locationHref).to.equal(expectedPopupUrl); // Ask the page to access the popup. - const touchPopupResult = emittedOnce(ipcMain, 'answer') - w.webContents.send('touch-the-popup') - const [, popupAccessMessage] = await touchPopupResult + const touchPopupResult = emittedOnce(ipcMain, 'answer'); + w.webContents.send('touch-the-popup'); + const [, popupAccessMessage] = await touchPopupResult; // Ask the popup to access the opener. - const touchOpenerResult = emittedOnce(ipcMain, 'answer') - popupWindow.webContents.send('touch-the-opener') - const [, openerAccessMessage] = await touchOpenerResult + const touchOpenerResult = emittedOnce(ipcMain, 'answer'); + popupWindow.webContents.send('touch-the-opener'); + const [, openerAccessMessage] = await touchOpenerResult; // We don't need the popup anymore, and its parent page can't close it, // so let's close it from here before we run any checks. - await closeWindow(popupWindow, { assertNotWindows: false }) + await closeWindow(popupWindow, { assertNotWindows: false }); expect(popupAccessMessage).to.be.a('string', - `child's .document is accessible from its parent window`) - expect(popupAccessMessage).to.match(/^Blocked a frame with origin/) + 'child\'s .document is accessible from its parent window'); + expect(popupAccessMessage).to.match(/^Blocked a frame with origin/); expect(openerAccessMessage).to.be.a('string', - `opener .document is accessible from a popup window`) - expect(openerAccessMessage).to.match(/^Blocked a frame with origin/) - }) + 'opener .document is accessible from a popup window'); + expect(openerAccessMessage).to.match(/^Blocked a frame with origin/); + }); it('should inherit the sandbox setting in opened windows', async () => { const w = new BrowserWindow({ @@ -1796,16 +2979,14 @@ describe('BrowserWindow module', () => { webPreferences: { sandbox: true } - }) + }); - const preloadPath = path.join(fixtures, 'api', 'new-window-preload.js') - w.webContents.once('new-window', (event, url, frameName, disposition, options) => { - options.webPreferences.preload = preloadPath - }) - w.loadFile(path.join(fixtures, 'api', 'new-window.html')) - const [, args] = await emittedOnce(ipcMain, 'answer') - expect(args).to.include('--enable-sandbox') - }) + const preloadPath = path.join(mainFixtures, 'api', 'new-window-preload.js'); + w.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { preload: preloadPath } } })); + w.loadFile(path.join(fixtures, 'api', 'new-window.html')); + const [, { argv }] = await emittedOnce(ipcMain, 'answer'); + expect(argv).to.include('--enable-sandbox'); + }); it('should open windows with the options configured via new-window event listeners', async () => { const w = new BrowserWindow({ @@ -1813,84 +2994,90 @@ describe('BrowserWindow module', () => { webPreferences: { sandbox: true } - }) + }); + + const preloadPath = path.join(mainFixtures, 'api', 'new-window-preload.js'); + w.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { preload: preloadPath, contextIsolation: false } } })); + w.loadFile(path.join(fixtures, 'api', 'new-window.html')); + const [[, childWebContents]] = await Promise.all([ + emittedOnce(app, 'web-contents-created'), + emittedOnce(ipcMain, 'answer') + ]); + const webPreferences = childWebContents.getLastWebPreferences(); + expect(webPreferences.contextIsolation).to.equal(false); + }); - const preloadPath = path.join(fixtures, 'api', 'new-window-preload.js') - w.webContents.once('new-window', (event, url, frameName, disposition, options) => { - options.webPreferences.preload = preloadPath - options.webPreferences.foo = 'bar' - }) - w.loadFile(path.join(fixtures, 'api', 'new-window.html')) - const [, , webPreferences] = await emittedOnce(ipcMain, 'answer') - expect(webPreferences.foo).to.equal('bar') - }) - - it('should set ipc event sender correctly', (done) => { + it('should set ipc event sender correctly', async () => { const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, - preload + preload, + contextIsolation: false } - }) - let childWc: WebContents | null = null - w.webContents.on('new-window', (event, url, frameName, disposition, options) => { - options.webPreferences.preload = preload - childWc = options.webContents - expect(w.webContents).to.not.equal(childWc) - }) + }); + let childWc: WebContents | null = null; + w.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { preload, contextIsolation: false } } })); + + w.webContents.on('did-create-window', (win) => { + childWc = win.webContents; + expect(w.webContents).to.not.equal(childWc); + }); + ipcMain.once('parent-ready', function (event) { - expect(event.sender).to.equal(w.webContents, 'sender should be the parent') - event.sender.send('verified') - }) + expect(event.sender).to.equal(w.webContents, 'sender should be the parent'); + event.sender.send('verified'); + }); ipcMain.once('child-ready', function (event) { - expect(childWc).to.not.be.null('child webcontents should be available') - expect(event.sender).to.equal(childWc, 'sender should be the child') - event.sender.send('verified') - }) - waitForEvents(ipcMain, [ + expect(childWc).to.not.be.null('child webcontents should be available'); + expect(event.sender).to.equal(childWc, 'sender should be the child'); + event.sender.send('verified'); + }); + + const done = Promise.all([ 'parent-answer', 'child-answer' - ], done) - w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'verify-ipc-sender' }) - }) + ].map(name => emittedOnce(ipcMain, name))); + w.loadFile(path.join(__dirname, 'fixtures', 'api', 'sandbox.html'), { search: 'verify-ipc-sender' }); + await done; + }); describe('event handling', () => { - let w: BrowserWindow = null as unknown as BrowserWindow + let w: BrowserWindow = null as unknown as BrowserWindow; beforeEach(() => { - w = new BrowserWindow({show: false, webPreferences: {sandbox: true}}) - }) - it('works for window events', (done) => { - waitForEvents(w, [ - 'page-title-updated' - ], done) - w.loadURL(`data:text/html,`) - }) - - it('works for stop events', (done) => { - waitForEvents(w.webContents, [ + w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + }); + it('works for window events', async () => { + const pageTitleUpdated = emittedOnce(w, 'page-title-updated'); + w.loadURL('data:text/html,'); + await pageTitleUpdated; + }); + + it('works for stop events', async () => { + const done = Promise.all([ 'did-navigate', 'did-fail-load', 'did-stop-loading' - ], done) - w.loadURL(`data:text/html,`) - }) + ].map(name => emittedOnce(w.webContents, name))); + w.loadURL('data:text/html,'); + await done; + }); - it('works for web contents events', (done) => { - waitForEvents(w.webContents, [ + it('works for web contents events', async () => { + const done = Promise.all([ 'did-finish-load', 'did-frame-finish-load', 'did-navigate-in-page', - // TODO(nornagon): sandboxed pages should also emit will-navigate - // 'will-navigate', + 'will-navigate', 'did-start-loading', 'did-stop-loading', 'did-frame-finish-load', 'dom-ready' - ], done) - w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'webcontents-events' }) - }) - }) + ].map(name => emittedOnce(w.webContents, name))); + w.loadFile(path.join(__dirname, 'fixtures', 'api', 'sandbox.html'), { search: 'webcontents-events' }); + await done; + }); + }); it('supports calling preventDefault on new-window events', (done) => { const w = new BrowserWindow({ @@ -1898,128 +3085,67 @@ describe('BrowserWindow module', () => { webPreferences: { sandbox: true } - }) - const initialWebContents = webContents.getAllWebContents().map((i) => i.id) + }); + const initialWebContents = webContents.getAllWebContents().map((i) => i.id); w.webContents.once('new-window', (e) => { - e.preventDefault() + e.preventDefault(); // We need to give it some time so the windows get properly disposed (at least on OSX). setTimeout(() => { - const currentWebContents = webContents.getAllWebContents().map((i) => i.id) - expect(currentWebContents).to.deep.equal(initialWebContents) - done() - }, 100) - }) - w.loadFile(path.join(fixtures, 'pages', 'window-open.html')) - }) - - // see #9387 - it('properly manages remote object references after page reload', (done) => { - const w = new BrowserWindow({ - show: false, - webPreferences: { - preload, - sandbox: true - } - }) - w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'reload-remote' }) - - ipcMain.on('get-remote-module-path', (event) => { - event.returnValue = path.join(fixtures, 'module', 'hello.js') - }) - - let reload = false - ipcMain.on('reloaded', (event) => { - event.returnValue = reload - reload = !reload - }) - - ipcMain.once('reload', (event) => { - event.sender.reload() - }) - - ipcMain.once('answer', (event, arg) => { - ipcMain.removeAllListeners('reloaded') - ipcMain.removeAllListeners('get-remote-module-path') - expect(arg).to.equal('hi') - done() - }) - }) - - it('properly manages remote object references after page reload in child window', (done) => { - const w = new BrowserWindow({ - show: false, - webPreferences: { - preload, - sandbox: true - } - }) - w.webContents.once('new-window', (event, url, frameName, disposition, options) => { - options.webPreferences.preload = preload - }) - - w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'reload-remote-child' }) - - ipcMain.on('get-remote-module-path', (event) => { - event.returnValue = path.join(fixtures, 'module', 'hello-child.js') - }) - - let reload = false - ipcMain.on('reloaded', (event) => { - event.returnValue = reload - reload = !reload - }) - - ipcMain.once('reload', (event) => { - event.sender.reload() - }) - - ipcMain.once('answer', (event, arg) => { - ipcMain.removeAllListeners('reloaded') - ipcMain.removeAllListeners('get-remote-module-path') - expect(arg).to.equal('hi child window') - done() - }) - }) + const currentWebContents = webContents.getAllWebContents().map((i) => i.id); + try { + expect(currentWebContents).to.deep.equal(initialWebContents); + done(); + } catch (error) { + done(e); + } + }, 100); + }); + w.loadFile(path.join(fixtures, 'pages', 'window-open.html')); + }); it('validates process APIs access in sandboxed renderer', async () => { const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, - preload + preload, + contextIsolation: false } - }) + }); w.webContents.once('preload-error', (event, preloadPath, error) => { - throw error - }) - process.env.sandboxmain = 'foo' - w.loadFile(path.join(fixtures, 'api', 'preload.html')) - const [, test] = await emittedOnce(ipcMain, 'answer') - expect(test.hasCrash).to.be.true('has crash') - expect(test.hasHang).to.be.true('has hang') - expect(test.heapStatistics).to.be.an('object') - expect(test.blinkMemoryInfo).to.be.an('object') - expect(test.processMemoryInfo).to.be.an('object') - expect(test.systemVersion).to.be.a('string') - expect(test.cpuUsage).to.be.an('object') - expect(test.ioCounters).to.be.an('object') - expect(test.arch).to.equal(process.arch) - expect(test.platform).to.equal(process.platform) - expect(test.env).to.deep.equal(process.env) - expect(test.execPath).to.equal(process.helperExecPath) - expect(test.sandboxed).to.be.true('sandboxed') - expect(test.type).to.equal('renderer') - expect(test.version).to.equal(process.version) - expect(test.versions).to.deep.equal(process.versions) + throw error; + }); + process.env.sandboxmain = 'foo'; + w.loadFile(path.join(fixtures, 'api', 'preload.html')); + const [, test] = await emittedOnce(ipcMain, 'answer'); + expect(test.hasCrash).to.be.true('has crash'); + expect(test.hasHang).to.be.true('has hang'); + expect(test.heapStatistics).to.be.an('object'); + expect(test.blinkMemoryInfo).to.be.an('object'); + expect(test.processMemoryInfo).to.be.an('object'); + expect(test.systemVersion).to.be.a('string'); + expect(test.cpuUsage).to.be.an('object'); + expect(test.ioCounters).to.be.an('object'); + expect(test.uptime).to.be.a('number'); + expect(test.arch).to.equal(process.arch); + expect(test.platform).to.equal(process.platform); + expect(test.env).to.deep.equal(process.env); + expect(test.execPath).to.equal(process.helperExecPath); + expect(test.sandboxed).to.be.true('sandboxed'); + expect(test.contextIsolated).to.be.false('contextIsolated'); + expect(test.type).to.equal('renderer'); + expect(test.version).to.equal(process.version); + expect(test.versions).to.deep.equal(process.versions); + expect(test.contextId).to.be.a('string'); if (process.platform === 'linux' && test.osSandbox) { - expect(test.creationTime).to.be.null('creation time') - expect(test.systemMemoryInfo).to.be.null('system memory info') + expect(test.creationTime).to.be.null('creation time'); + expect(test.systemMemoryInfo).to.be.null('system memory info'); } else { - expect(test.creationTime).to.be.a('number') - expect(test.systemMemoryInfo).to.be.an('object') + expect(test.creationTime).to.be.a('number'); + expect(test.systemMemoryInfo).to.be.an('object'); } - }) + }); it('webview in sandbox renderer', async () => { const w = new BrowserWindow({ @@ -2027,303 +3153,315 @@ describe('BrowserWindow module', () => { webPreferences: { sandbox: true, preload, - webviewTag: true + webviewTag: true, + contextIsolation: false } - }) - const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview') - const webviewDomReady = emittedOnce(ipcMain, 'webview-dom-ready') - w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html')) - - const [, webContents] = await didAttachWebview - const [, id] = await webviewDomReady - expect(webContents.id).to.equal(id) - }) - }) + }); + const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview'); + const webviewDomReady = emittedOnce(ipcMain, 'webview-dom-ready'); + w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html')); + + const [, webContents] = await didAttachWebview; + const [, id] = await webviewDomReady; + expect(webContents.id).to.equal(id); + }); + }); - describe('nativeWindowOpen option', () => { - let w: BrowserWindow = null as unknown as BrowserWindow + describe('child windows', () => { + let w: BrowserWindow = null as unknown as BrowserWindow; beforeEach(() => { w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, - nativeWindowOpen: true, // tests relies on preloads in opened windows - nodeIntegrationInSubFrames: true + nodeIntegrationInSubFrames: true, + contextIsolation: false } - }) - }) - - it('opens window of about:blank with cross-scripting enabled', (done) => { - ipcMain.once('answer', (event, content) => { - expect(content).to.equal('Hello') - done() - }) - w.loadFile(path.join(fixtures, 'api', 'native-window-open-blank.html')) - }) - it('opens window of same domain with cross-scripting enabled', (done) => { - ipcMain.once('answer', (event, content) => { - expect(content).to.equal('Hello') - done() - }) - w.loadFile(path.join(fixtures, 'api', 'native-window-open-file.html')) - }) - it('blocks accessing cross-origin frames', (done) => { - ipcMain.once('answer', (event, content) => { - expect(content).to.equal('Blocked a frame with origin "file://" from accessing a cross-origin frame.') - done() - }) - w.loadFile(path.join(fixtures, 'api', 'native-window-open-cross-origin.html')) - }) - it('opens window from '); + resp = res; + // don't end the response yet + }); + await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)); + const { port } = s.address() as AddressInfo; + const p = new Promise(resolve => { + w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => { + if (!isMainFrame) { + resolve(); + } + }); + }); + const main = w.loadURL(`http://127.0.0.1:${port}`); + await p; + resp.end(); + await main; + s.close(); + }); + + it("doesn't resolve when a subframe loads", async () => { + let resp = null as unknown as http.ServerResponse; + const s = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.write(''); + resp = res; + // don't end the response yet + }); + await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)); + const { port } = s.address() as AddressInfo; + const p = new Promise(resolve => { + w.webContents.on('did-frame-finish-load', (event, isMainFrame) => { + if (!isMainFrame) { + resolve(); + } + }); + }); + const main = w.loadURL(`http://127.0.0.1:${port}`); + await p; + resp.destroy(); // cause the main request to fail + await expect(main).to.eventually.be.rejected() + .and.have.property('errno', -355); // ERR_INCOMPLETE_CHUNKED_ENCODING + s.close(); + }); + }); + + describe('getFocusedWebContents() API', () => { + afterEach(closeAllWindows); + + const testFn = (process.platform === 'win32' && process.arch === 'arm64' ? it.skip : it); + testFn('returns the focused web contents', async () => { + const w = new BrowserWindow({ show: true }); + await w.loadFile(path.join(__dirname, 'fixtures', 'blank.html')); + expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.id); + + const devToolsOpened = emittedOnce(w.webContents, 'devtools-opened'); + w.webContents.openDevTools(); + await devToolsOpened; + expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.devToolsWebContents!.id); + const devToolsClosed = emittedOnce(w.webContents, 'devtools-closed'); + w.webContents.closeDevTools(); + await devToolsClosed; + expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.id); + }); + + it('does not crash when called on a detached dev tools window', async () => { + const w = new BrowserWindow({ show: true }); + + w.webContents.openDevTools({ mode: 'detach' }); + w.webContents.inspectElement(100, 100); + + // For some reason we have to wait for two focused events...? + await emittedOnce(w.webContents, 'devtools-focused'); + + expect(() => { webContents.getFocusedWebContents(); }).to.not.throw(); + + // Work around https://github.com/electron/electron/issues/19985 + await delay(); + + const devToolsClosed = emittedOnce(w.webContents, 'devtools-closed'); + w.webContents.closeDevTools(); + await devToolsClosed; + expect(() => { webContents.getFocusedWebContents(); }).to.not.throw(); + }); + }); + + describe('setDevToolsWebContents() API', () => { + afterEach(closeAllWindows); + it('sets arbitrary webContents as devtools', async () => { + const w = new BrowserWindow({ show: false }); + const devtools = new BrowserWindow({ show: false }); + const promise = emittedOnce(devtools.webContents, 'dom-ready'); + w.webContents.setDevToolsWebContents(devtools.webContents); + w.webContents.openDevTools(); + await promise; + expect(devtools.webContents.getURL().startsWith('devtools://devtools')).to.be.true(); + const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name'); + expect(result).to.equal('InspectorFrontendHostImpl'); + devtools.destroy(); + }); + }); + + describe('isFocused() API', () => { + it('returns false when the window is hidden', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(w.isVisible()).to.be.false(); + expect(w.webContents.isFocused()).to.be.false(); + }); + }); + + describe('isCurrentlyAudible() API', () => { + afterEach(closeAllWindows); + it('returns whether audio is playing', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + await w.webContents.executeJavaScript(` + window.context = new AudioContext + // Start in suspended state, because of the + // new web audio api policy. + context.suspend() + window.oscillator = context.createOscillator() + oscillator.connect(context.destination) + oscillator.start() + `); + let p = emittedOnce(w.webContents, '-audio-state-changed'); + w.webContents.executeJavaScript('context.resume()'); + await p; + expect(w.webContents.isCurrentlyAudible()).to.be.true(); + p = emittedOnce(w.webContents, '-audio-state-changed'); + w.webContents.executeJavaScript('oscillator.stop()'); + await p; + expect(w.webContents.isCurrentlyAudible()).to.be.false(); + }); + }); + + describe('openDevTools() API', () => { + afterEach(closeAllWindows); + it('can show window with activation', async () => { + const w = new BrowserWindow({ show: false }); + const focused = emittedOnce(w, 'focus'); + w.show(); + await focused; + expect(w.isFocused()).to.be.true(); + const blurred = emittedOnce(w, 'blur'); + w.webContents.openDevTools({ mode: 'detach', activate: true }); + await Promise.all([ + emittedOnce(w.webContents, 'devtools-opened'), + emittedOnce(w.webContents, 'devtools-focused') + ]); + await blurred; + expect(w.isFocused()).to.be.false(); + }); + + it('can show window without activation', async () => { + const w = new BrowserWindow({ show: false }); + const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened'); + w.webContents.openDevTools({ mode: 'detach', activate: false }); + await devtoolsOpened; + expect(w.webContents.isDevToolsOpened()).to.be.true(); + }); + }); + + describe('before-input-event event', () => { + afterEach(closeAllWindows); + it('can prevent document keyboard events', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + await w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html')); + const keyDown = new Promise(resolve => { + ipcMain.once('keydown', (event, key) => resolve(key)); + }); + w.webContents.once('before-input-event', (event, input) => { + if (input.key === 'a') event.preventDefault(); + }); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' }); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' }); + expect(await keyDown).to.equal('b'); + }); + + it('has the correct properties', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')); + const testBeforeInput = async (opts: any) => { + const modifiers = []; + if (opts.shift) modifiers.push('shift'); + if (opts.control) modifiers.push('control'); + if (opts.alt) modifiers.push('alt'); + if (opts.meta) modifiers.push('meta'); + if (opts.isAutoRepeat) modifiers.push('isAutoRepeat'); + + const p = emittedOnce(w.webContents, 'before-input-event'); + w.webContents.sendInputEvent({ + type: opts.type, + keyCode: opts.keyCode, + modifiers: modifiers as any + }); + const [, input] = await p; + + expect(input.type).to.equal(opts.type); + expect(input.key).to.equal(opts.key); + expect(input.code).to.equal(opts.code); + expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat); + expect(input.shift).to.equal(opts.shift); + expect(input.control).to.equal(opts.control); + expect(input.alt).to.equal(opts.alt); + expect(input.meta).to.equal(opts.meta); + }; + await testBeforeInput({ + type: 'keyDown', + key: 'A', + code: 'KeyA', + keyCode: 'a', + shift: true, + control: true, + alt: true, + meta: true, + isAutoRepeat: true + }); + await testBeforeInput({ + type: 'keyUp', + key: '.', + code: 'Period', + keyCode: '.', + shift: false, + control: true, + alt: true, + meta: false, + isAutoRepeat: false + }); + await testBeforeInput({ + type: 'keyUp', + key: '!', + code: 'Digit1', + keyCode: '1', + shift: true, + control: false, + alt: false, + meta: true, + isAutoRepeat: false + }); + await testBeforeInput({ + type: 'keyUp', + key: 'Tab', + code: 'Tab', + keyCode: 'Tab', + shift: false, + control: true, + alt: false, + meta: false, + isAutoRepeat: true + }); + }); + }); + + // On Mac, zooming isn't done with the mouse wheel. + ifdescribe(process.platform !== 'darwin')('zoom-changed', () => { + afterEach(closeAllWindows); + it('is emitted with the correct zoom-in info', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')); + + const testZoomChanged = async () => { + w.webContents.sendInputEvent({ + type: 'mouseWheel', + x: 300, + y: 300, + deltaX: 0, + deltaY: 1, + wheelTicksX: 0, + wheelTicksY: 1, + modifiers: ['control', 'meta'] + }); + + const [, zoomDirection] = await emittedOnce(w.webContents, 'zoom-changed'); + expect(zoomDirection).to.equal('in'); + }; + + await testZoomChanged(); + }); + + it('is emitted with the correct zoom-out info', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')); + + const testZoomChanged = async () => { + w.webContents.sendInputEvent({ + type: 'mouseWheel', + x: 300, + y: 300, + deltaX: 0, + deltaY: -1, + wheelTicksX: 0, + wheelTicksY: -1, + modifiers: ['control', 'meta'] + }); + + const [, zoomDirection] = await emittedOnce(w.webContents, 'zoom-changed'); + expect(zoomDirection).to.equal('out'); + }; + + await testZoomChanged(); + }); + }); + + describe('sendInputEvent(event)', () => { + let w: BrowserWindow; + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + await w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html')); + }); + afterEach(closeAllWindows); + + it('can send keydown events', async () => { + const keydown = emittedOnce(ipcMain, 'keydown'); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }); + const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keydown; + expect(key).to.equal('a'); + expect(code).to.equal('KeyA'); + expect(keyCode).to.equal(65); + expect(shiftKey).to.be.false(); + expect(ctrlKey).to.be.false(); + expect(altKey).to.be.false(); + }); + + it('can send keydown events with modifiers', async () => { + const keydown = emittedOnce(ipcMain, 'keydown'); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }); + const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keydown; + expect(key).to.equal('Z'); + expect(code).to.equal('KeyZ'); + expect(keyCode).to.equal(90); + expect(shiftKey).to.be.true(); + expect(ctrlKey).to.be.true(); + expect(altKey).to.be.false(); + }); + + it('can send keydown events with special keys', async () => { + const keydown = emittedOnce(ipcMain, 'keydown'); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] }); + const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keydown; + expect(key).to.equal('Tab'); + expect(code).to.equal('Tab'); + expect(keyCode).to.equal(9); + expect(shiftKey).to.be.false(); + expect(ctrlKey).to.be.false(); + expect(altKey).to.be.true(); + }); + + it('can send char events', async () => { + const keypress = emittedOnce(ipcMain, 'keypress'); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }); + w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' }); + const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keypress; + expect(key).to.equal('a'); + expect(code).to.equal('KeyA'); + expect(keyCode).to.equal(65); + expect(shiftKey).to.be.false(); + expect(ctrlKey).to.be.false(); + expect(altKey).to.be.false(); + }); + + it('can send char events with modifiers', async () => { + const keypress = emittedOnce(ipcMain, 'keypress'); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' }); + w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }); + const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keypress; + expect(key).to.equal('Z'); + expect(code).to.equal('KeyZ'); + expect(keyCode).to.equal(90); + expect(shiftKey).to.be.true(); + expect(ctrlKey).to.be.true(); + expect(altKey).to.be.false(); + }); + }); + + describe('insertCSS', () => { + afterEach(closeAllWindows); + it('supports inserting CSS', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + await w.webContents.insertCSS('body { background-repeat: round; }'); + const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")'); + expect(result).to.equal('round'); + }); + + it('supports removing inserted CSS', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + const key = await w.webContents.insertCSS('body { background-repeat: round; }'); + await w.webContents.removeInsertedCSS(key); + const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")'); + expect(result).to.equal('repeat'); + }); + }); + + describe('inspectElement()', () => { + afterEach(closeAllWindows); + it('supports inspecting an element in the devtools', (done) => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + w.webContents.once('devtools-opened', () => { done(); }); + w.webContents.inspectElement(10, 10); + }); + }); + + describe('startDrag({file, icon})', () => { + it('throws errors for a missing file or a missing/empty icon', () => { + const w = new BrowserWindow({ show: false }); + expect(() => { + w.webContents.startDrag({ icon: path.join(fixturesPath, 'assets', 'logo.png') } as any); + }).to.throw('Must specify either \'file\' or \'files\' option'); + + expect(() => { + w.webContents.startDrag({ file: __filename } as any); + }).to.throw('\'icon\' parameter is required'); + + expect(() => { + w.webContents.startDrag({ file: __filename, icon: path.join(mainFixturesPath, 'blank.png') }); + }).to.throw(/Failed to load image from path (.+)/); + }); + }); + + describe('focus APIs', () => { + describe('focus()', () => { + afterEach(closeAllWindows); + it('does not blur the focused window when the web contents is hidden', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.show(); + await w.loadURL('about:blank'); + w.focus(); + const child = new BrowserWindow({ show: false }); + child.loadURL('about:blank'); + child.webContents.focus(); + const currentFocused = w.isFocused(); + const childFocused = child.isFocused(); + child.close(); + expect(currentFocused).to.be.true(); + expect(childFocused).to.be.false(); + }); + }); + + const moveFocusToDevTools = async (win: BrowserWindow) => { + const devToolsOpened = emittedOnce(win.webContents, 'devtools-opened'); + win.webContents.openDevTools({ mode: 'right' }); + await devToolsOpened; + win.webContents.devToolsWebContents!.focus(); + }; + + describe('focus event', () => { + afterEach(closeAllWindows); + + it('is triggered when web contents is focused', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + await moveFocusToDevTools(w); + const focusPromise = emittedOnce(w.webContents, 'focus'); + w.webContents.focus(); + await expect(focusPromise).to.eventually.be.fulfilled(); + }); + + it('is triggered when BrowserWindow is focused', async () => { + const window1 = new BrowserWindow({ show: false }); + const window2 = new BrowserWindow({ show: false }); + + await Promise.all([ + window1.loadURL('about:blank'), + window2.loadURL('about:blank') + ]); + + const focusPromise1 = emittedOnce(window1.webContents, 'focus'); + const focusPromise2 = emittedOnce(window2.webContents, 'focus'); + + window1.showInactive(); + window2.showInactive(); + + window1.focus(); + await expect(focusPromise1).to.eventually.be.fulfilled(); + + window2.focus(); + await expect(focusPromise2).to.eventually.be.fulfilled(); + }); + }); + + describe('blur event', () => { + afterEach(closeAllWindows); + it('is triggered when web contents is blurred', async () => { + const w = new BrowserWindow({ show: true }); + await w.loadURL('about:blank'); + w.webContents.focus(); + const blurPromise = emittedOnce(w.webContents, 'blur'); + await moveFocusToDevTools(w); + await expect(blurPromise).to.eventually.be.fulfilled(); + }); + }); + }); + + describe('getOSProcessId()', () => { + afterEach(closeAllWindows); + it('returns a valid procress id', async () => { + const w = new BrowserWindow({ show: false }); + expect(w.webContents.getOSProcessId()).to.equal(0); + + await w.loadURL('about:blank'); + expect(w.webContents.getOSProcessId()).to.be.above(0); + }); + }); + + describe('getMediaSourceId()', () => { + afterEach(closeAllWindows); + it('returns a valid stream id', () => { + const w = new BrowserWindow({ show: false }); + expect(w.webContents.getMediaSourceId(w.webContents)).to.be.a('string').that.is.not.empty(); + }); + }); + + describe('userAgent APIs', () => { + it('is not empty by default', () => { + const w = new BrowserWindow({ show: false }); + const userAgent = w.webContents.getUserAgent(); + expect(userAgent).to.be.a('string').that.is.not.empty(); + }); + + it('can set the user agent (functions)', () => { + const w = new BrowserWindow({ show: false }); + const userAgent = w.webContents.getUserAgent(); + + w.webContents.setUserAgent('my-user-agent'); + expect(w.webContents.getUserAgent()).to.equal('my-user-agent'); + + w.webContents.setUserAgent(userAgent); + expect(w.webContents.getUserAgent()).to.equal(userAgent); + }); + + it('can set the user agent (properties)', () => { + const w = new BrowserWindow({ show: false }); + const userAgent = w.webContents.userAgent; + + w.webContents.userAgent = 'my-user-agent'; + expect(w.webContents.userAgent).to.equal('my-user-agent'); + + w.webContents.userAgent = userAgent; + expect(w.webContents.userAgent).to.equal(userAgent); + }); + }); + + describe('audioMuted APIs', () => { + it('can set the audio mute level (functions)', () => { + const w = new BrowserWindow({ show: false }); + + w.webContents.setAudioMuted(true); + expect(w.webContents.isAudioMuted()).to.be.true(); + + w.webContents.setAudioMuted(false); + expect(w.webContents.isAudioMuted()).to.be.false(); + }); + + it('can set the audio mute level (functions)', () => { + const w = new BrowserWindow({ show: false }); + + w.webContents.audioMuted = true; + expect(w.webContents.audioMuted).to.be.true(); + + w.webContents.audioMuted = false; + expect(w.webContents.audioMuted).to.be.false(); + }); + }); + + describe('zoom api', () => { + const hostZoomMap: Record = { + host1: 0.3, + host2: 0.7, + host3: 0.2 + }; + + before(() => { + const protocol = session.defaultSession.protocol; + protocol.registerStringProtocol(standardScheme, (request, callback) => { + const response = ``; + callback({ data: response, mimeType: 'text/html' }); + }); + }); + + after(() => { + const protocol = session.defaultSession.protocol; + protocol.unregisterProtocol(standardScheme); + }); + + afterEach(closeAllWindows); + + it('throws on an invalid zoomFactor', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + + expect(() => { + w.webContents.setZoomFactor(0.0); + }).to.throw(/'zoomFactor' must be a double greater than 0.0/); + + expect(() => { + w.webContents.setZoomFactor(-2.0); + }).to.throw(/'zoomFactor' must be a double greater than 0.0/); + }); + + it('can set the correct zoom level (functions)', async () => { + const w = new BrowserWindow({ show: false }); + try { + await w.loadURL('about:blank'); + const zoomLevel = w.webContents.getZoomLevel(); + expect(zoomLevel).to.eql(0.0); + w.webContents.setZoomLevel(0.5); + const newZoomLevel = w.webContents.getZoomLevel(); + expect(newZoomLevel).to.eql(0.5); + } finally { + w.webContents.setZoomLevel(0); + } + }); + + it('can set the correct zoom level (properties)', async () => { + const w = new BrowserWindow({ show: false }); + try { + await w.loadURL('about:blank'); + const zoomLevel = w.webContents.zoomLevel; + expect(zoomLevel).to.eql(0.0); + w.webContents.zoomLevel = 0.5; + const newZoomLevel = w.webContents.zoomLevel; + expect(newZoomLevel).to.eql(0.5); + } finally { + w.webContents.zoomLevel = 0; + } + }); + + it('can set the correct zoom factor (functions)', async () => { + const w = new BrowserWindow({ show: false }); + try { + await w.loadURL('about:blank'); + const zoomFactor = w.webContents.getZoomFactor(); + expect(zoomFactor).to.eql(1.0); + + w.webContents.setZoomFactor(0.5); + const newZoomFactor = w.webContents.getZoomFactor(); + expect(newZoomFactor).to.eql(0.5); + } finally { + w.webContents.setZoomFactor(1.0); + } + }); + + it('can set the correct zoom factor (properties)', async () => { + const w = new BrowserWindow({ show: false }); + try { + await w.loadURL('about:blank'); + const zoomFactor = w.webContents.zoomFactor; + expect(zoomFactor).to.eql(1.0); + + w.webContents.zoomFactor = 0.5; + const newZoomFactor = w.webContents.zoomFactor; + expect(newZoomFactor).to.eql(0.5); + } finally { + w.webContents.zoomFactor = 1.0; + } + }); + + it('can persist zoom level across navigation', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + let finalNavigation = false; + ipcMain.on('set-zoom', (e, host) => { + const zoomLevel = hostZoomMap[host]; + if (!finalNavigation) w.webContents.zoomLevel = zoomLevel; + e.sender.send(`${host}-zoom-set`); + }); + ipcMain.on('host1-zoom-level', (e) => { + try { + const zoomLevel = e.sender.getZoomLevel(); + const expectedZoomLevel = hostZoomMap.host1; + expect(zoomLevel).to.equal(expectedZoomLevel); + if (finalNavigation) { + done(); + } else { + w.loadURL(`${standardScheme}://host2`); + } + } catch (e) { + done(e); + } + }); + ipcMain.once('host2-zoom-level', (e) => { + try { + const zoomLevel = e.sender.getZoomLevel(); + const expectedZoomLevel = hostZoomMap.host2; + expect(zoomLevel).to.equal(expectedZoomLevel); + finalNavigation = true; + w.webContents.goBack(); + } catch (e) { + done(e); + } + }); + w.loadURL(`${standardScheme}://host1`); + }); + + it('can propagate zoom level across same session', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + const w2 = new BrowserWindow({ show: false }); + + defer(() => { + w2.setClosable(true); + w2.close(); + }); + + await w.loadURL(`${standardScheme}://host3`); + w.webContents.zoomLevel = hostZoomMap.host3; + + await w2.loadURL(`${standardScheme}://host3`); + const zoomLevel1 = w.webContents.zoomLevel; + expect(zoomLevel1).to.equal(hostZoomMap.host3); + + const zoomLevel2 = w2.webContents.zoomLevel; + expect(zoomLevel1).to.equal(zoomLevel2); + }); + + it('cannot propagate zoom level across different session', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + const w2 = new BrowserWindow({ + show: false, + webPreferences: { + partition: 'temp' + } + }); + const protocol = w2.webContents.session.protocol; + protocol.registerStringProtocol(standardScheme, (request, callback) => { + callback('hello'); + }); + + defer(() => { + w2.setClosable(true); + w2.close(); + + protocol.unregisterProtocol(standardScheme); + }); + + await w.loadURL(`${standardScheme}://host3`); + w.webContents.zoomLevel = hostZoomMap.host3; + + await w2.loadURL(`${standardScheme}://host3`); + const zoomLevel1 = w.webContents.zoomLevel; + expect(zoomLevel1).to.equal(hostZoomMap.host3); + + const zoomLevel2 = w2.webContents.zoomLevel; + expect(zoomLevel2).to.equal(0); + expect(zoomLevel1).to.not.equal(zoomLevel2); + }); + + it('can persist when it contains iframe', (done) => { + const w = new BrowserWindow({ show: false }); + const server = http.createServer((req, res) => { + setTimeout(() => { + res.end(); + }, 200); + }); + server.listen(0, '127.0.0.1', () => { + const url = 'http://127.0.0.1:' + (server.address() as AddressInfo).port; + const content = ``; + w.webContents.on('did-frame-finish-load', (e, isMainFrame) => { + if (!isMainFrame) { + try { + const zoomLevel = w.webContents.zoomLevel; + expect(zoomLevel).to.equal(2.0); + + w.webContents.zoomLevel = 0; + done(); + } catch (e) { + done(e); + } finally { + server.close(); + } + } + }); + w.webContents.on('dom-ready', () => { + w.webContents.zoomLevel = 2.0; + }); + w.loadURL(`data:text/html,${content}`); + }); + }); + + it('cannot propagate when used with webframe', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + const w2 = new BrowserWindow({ show: false }); + + const temporaryZoomSet = emittedOnce(ipcMain, 'temporary-zoom-set'); + w.loadFile(path.join(fixturesPath, 'pages', 'webframe-zoom.html')); + await temporaryZoomSet; + + const finalZoomLevel = w.webContents.getZoomLevel(); + await w2.loadFile(path.join(fixturesPath, 'pages', 'c.html')); + const zoomLevel1 = w.webContents.zoomLevel; + const zoomLevel2 = w2.webContents.zoomLevel; + + w2.setClosable(true); + w2.close(); + + expect(zoomLevel1).to.equal(finalZoomLevel); + expect(zoomLevel2).to.equal(0); + expect(zoomLevel1).to.not.equal(zoomLevel2); + }); + + describe('with unique domains', () => { + let server: http.Server; + let serverUrl: string; + let crossSiteUrl: string; + + before((done) => { + server = http.createServer((req, res) => { + setTimeout(() => res.end('hey'), 0); + }); + server.listen(0, '127.0.0.1', () => { + serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + crossSiteUrl = `http://localhost:${(server.address() as AddressInfo).port}`; + done(); + }); + }); after(() => { - server.close() - }) + server.close(); + }); + + it('cannot persist zoom level after navigation with webFrame', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + const source = ` + const {ipcRenderer, webFrame} = require('electron') + webFrame.setZoomLevel(0.6) + ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel()) + `; + const zoomLevelPromise = emittedOnce(ipcMain, 'zoom-level-set'); + await w.loadURL(serverUrl); + await w.webContents.executeJavaScript(source); + let [, zoomLevel] = await zoomLevelPromise; + expect(zoomLevel).to.equal(0.6); + const loadPromise = emittedOnce(w.webContents, 'did-finish-load'); + await w.loadURL(crossSiteUrl); + await loadPromise; + zoomLevel = w.webContents.zoomLevel; + expect(zoomLevel).to.equal(0); + }); + }); + }); + + describe('webrtc ip policy api', () => { + afterEach(closeAllWindows); + it('can set and get webrtc ip policies', () => { + const w = new BrowserWindow({ show: false }); + const policies = [ + 'default', + 'default_public_interface_only', + 'default_public_and_private_interfaces', + 'disable_non_proxied_udp' + ]; + policies.forEach((policy) => { + w.webContents.setWebRTCIPHandlingPolicy(policy as any); + expect(w.webContents.getWebRTCIPHandlingPolicy()).to.equal(policy); + }); + }); + }); + + describe('render view deleted events', () => { + let server: http.Server; + let serverUrl: string; + let crossSiteUrl: string; + + before((done) => { + server = http.createServer((req, res) => { + const respond = () => { + if (req.url === '/redirect-cross-site') { + res.setHeader('Location', `${crossSiteUrl}/redirected`); + res.statusCode = 302; + res.end(); + } else if (req.url === '/redirected') { + res.end(''); + } else if (req.url === '/first-window-open') { + res.end(``); + } else if (req.url === '/second-window-open') { + res.end(''); + } else { + res.end(); + } + }; + setTimeout(respond, 0); + }); + server.listen(0, '127.0.0.1', () => { + serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + crossSiteUrl = `http://localhost:${(server.address() as AddressInfo).port}`; + done(); + }); + }); + + after(() => { + server.close(); + }); + + afterEach(closeAllWindows); + + it('does not emit current-render-view-deleted when speculative RVHs are deleted', async () => { + const w = new BrowserWindow({ show: false }); + let currentRenderViewDeletedEmitted = false; + const renderViewDeletedHandler = () => { + currentRenderViewDeletedEmitted = true; + }; + w.webContents.on('current-render-view-deleted' as any, renderViewDeletedHandler); + w.webContents.on('did-finish-load', () => { + w.webContents.removeListener('current-render-view-deleted' as any, renderViewDeletedHandler); + w.close(); + }); + const destroyed = emittedOnce(w.webContents, 'destroyed'); + w.loadURL(`${serverUrl}/redirect-cross-site`); + await destroyed; + expect(currentRenderViewDeletedEmitted).to.be.false('current-render-view-deleted was emitted'); + }); + + it('does not emit current-render-view-deleted when speculative RVHs are deleted', async () => { + const parentWindow = new BrowserWindow({ show: false }); + let currentRenderViewDeletedEmitted = false; + let childWindow: BrowserWindow | null = null; + const destroyed = emittedOnce(parentWindow.webContents, 'destroyed'); + const renderViewDeletedHandler = () => { + currentRenderViewDeletedEmitted = true; + }; + const childWindowCreated = new Promise((resolve) => { + app.once('browser-window-created', (event, window) => { + childWindow = window; + window.webContents.on('current-render-view-deleted' as any, renderViewDeletedHandler); + resolve(); + }); + }); + parentWindow.loadURL(`${serverUrl}/first-window-open`); + await childWindowCreated; + childWindow!.webContents.removeListener('current-render-view-deleted' as any, renderViewDeletedHandler); + parentWindow.close(); + await destroyed; + expect(currentRenderViewDeletedEmitted).to.be.false('child window was destroyed'); + }); + + it('emits current-render-view-deleted if the current RVHs are deleted', async () => { + const w = new BrowserWindow({ show: false }); + let currentRenderViewDeletedEmitted = false; + w.webContents.on('current-render-view-deleted' as any, () => { + currentRenderViewDeletedEmitted = true; + }); + w.webContents.on('did-finish-load', () => { + w.close(); + }); + const destroyed = emittedOnce(w.webContents, 'destroyed'); + w.loadURL(`${serverUrl}/redirect-cross-site`); + await destroyed; + expect(currentRenderViewDeletedEmitted).to.be.true('current-render-view-deleted wasn\'t emitted'); + }); + + it('emits render-view-deleted if any RVHs are deleted', async () => { + const w = new BrowserWindow({ show: false }); + let rvhDeletedCount = 0; + w.webContents.on('render-view-deleted' as any, () => { + rvhDeletedCount++; + }); + w.webContents.on('did-finish-load', () => { + w.close(); + }); + const destroyed = emittedOnce(w.webContents, 'destroyed'); + w.loadURL(`${serverUrl}/redirect-cross-site`); + await destroyed; + const expectedRenderViewDeletedEventCount = 1; + expect(rvhDeletedCount).to.equal(expectedRenderViewDeletedEventCount, 'render-view-deleted wasn\'t emitted the expected nr. of times'); + }); + }); + + describe('setIgnoreMenuShortcuts(ignore)', () => { + afterEach(closeAllWindows); + it('does not throw', () => { + const w = new BrowserWindow({ show: false }); + expect(() => { + w.webContents.setIgnoreMenuShortcuts(true); + w.webContents.setIgnoreMenuShortcuts(false); + }).to.not.throw(); + }); + }); + + const crashPrefs = [ + { + nodeIntegration: true + }, + { + sandbox: true + } + ]; + + const nicePrefs = (o: any) => { + let s = ''; + for (const key of Object.keys(o)) { + s += `${key}=${o[key]}, `; + } + return `(${s.slice(0, s.length - 2)})`; + }; + + for (const prefs of crashPrefs) { + describe(`crash with webPreferences ${nicePrefs(prefs)}`, () => { + let w: BrowserWindow; + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + await w.loadURL('about:blank'); + }); + afterEach(closeAllWindows); + + it('isCrashed() is false by default', () => { + expect(w.webContents.isCrashed()).to.equal(false); + }); + + it('forcefullyCrashRenderer() crashes the process with reason=killed||crashed', async () => { + expect(w.webContents.isCrashed()).to.equal(false); + const crashEvent = emittedOnce(w.webContents, 'render-process-gone'); + w.webContents.forcefullyCrashRenderer(); + const [, details] = await crashEvent; + expect(details.reason === 'killed' || details.reason === 'crashed').to.equal(true, 'reason should be killed || crashed'); + expect(w.webContents.isCrashed()).to.equal(true); + }); + + it('a crashed process is recoverable with reload()', async () => { + expect(w.webContents.isCrashed()).to.equal(false); + w.webContents.forcefullyCrashRenderer(); + w.webContents.reload(); + expect(w.webContents.isCrashed()).to.equal(false); + }); + }); + } + + // Destroying webContents in its event listener is going to crash when + // Electron is built in Debug mode. + describe('destroy()', () => { + let server: http.Server; + let serverUrl: string; + + before((done) => { + server = http.createServer((request, response) => { + switch (request.url) { + case '/net-error': + response.destroy(); + break; + case '/200': + response.end(); + break; + default: + done('unsupported endpoint'); + } + }).listen(0, '127.0.0.1', () => { + serverUrl = 'http://127.0.0.1:' + (server.address() as AddressInfo).port; + done(); + }); + }); + + after(() => { + server.close(); + }); + + const events = [ + { name: 'did-start-loading', url: '/200' }, + { name: 'dom-ready', url: '/200' }, + { name: 'did-stop-loading', url: '/200' }, + { name: 'did-finish-load', url: '/200' }, + // FIXME: Multiple Emit calls inside an observer assume that object + // will be alive till end of the observer. Synchronous `destroy` api + // violates this contract and crashes. + { name: 'did-frame-finish-load', url: '/200' }, + { name: 'did-fail-load', url: '/net-error' } + ]; + for (const e of events) { + it(`should not crash when invoked synchronously inside ${e.name} handler`, async function () { + // This test is flaky on Windows CI and we don't know why, but the + // purpose of this test is to make sure Electron does not crash so it + // is fine to retry this test for a few times. + this.retries(3); + + const contents = (webContents as any).create() as WebContents; + const originalEmit = contents.emit.bind(contents); + contents.emit = (...args) => { return originalEmit(...args); }; + contents.once(e.name as any, () => (contents as any).destroy()); + const destroyed = emittedOnce(contents, 'destroyed'); + contents.loadURL(serverUrl + e.url); + await destroyed; + }); + } + }); + + describe('did-change-theme-color event', () => { + afterEach(closeAllWindows); + it('is triggered with correct theme color', (done) => { + const w = new BrowserWindow({ show: true }); + let count = 0; + w.webContents.on('did-change-theme-color', (e, color) => { + try { + if (count === 0) { + count += 1; + expect(color).to.equal('#FFEEDD'); + w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')); + } else if (count === 1) { + expect(color).to.be.null(); + done(); + } + } catch (e) { + done(e); + } + }); + w.loadFile(path.join(fixturesPath, 'pages', 'theme-color.html')); + }); + }); + + describe('console-message event', () => { + afterEach(closeAllWindows); + it('is triggered with correct log message', (done) => { + const w = new BrowserWindow({ show: true }); + w.webContents.on('console-message', (e, level, message) => { + // Don't just assert as Chromium might emit other logs that we should ignore. + if (message === 'a') { + done(); + } + }); + w.loadFile(path.join(fixturesPath, 'pages', 'a.html')); + }); + }); + + describe('ipc-message event', () => { + afterEach(closeAllWindows); + it('emits when the renderer process sends an asynchronous message', async () => { + const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + await w.webContents.loadURL('about:blank'); + w.webContents.executeJavaScript(` + require('electron').ipcRenderer.send('message', 'Hello World!') + `); - it('works after page load and during subframe load', (done) => { + const [, channel, message] = await emittedOnce(w.webContents, 'ipc-message'); + expect(channel).to.equal('message'); + expect(message).to.equal('Hello World!'); + }); + }); + + describe('ipc-message-sync event', () => { + afterEach(closeAllWindows); + it('emits when the renderer process sends a synchronous message', async () => { + const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + await w.webContents.loadURL('about:blank'); + const promise: Promise<[string, string]> = new Promise(resolve => { + w.webContents.once('ipc-message-sync', (event, channel, arg) => { + event.returnValue = 'foobar' as any; + resolve([channel, arg]); + }); + }); + const result = await w.webContents.executeJavaScript(` + require('electron').ipcRenderer.sendSync('message', 'Hello World!') + `); + + const [channel, message] = await promise; + expect(channel).to.equal('message'); + expect(message).to.equal('Hello World!'); + expect(result).to.equal('foobar'); + }); + }); + + describe('referrer', () => { + afterEach(closeAllWindows); + it('propagates referrer information to new target=_blank windows', (done) => { + const w = new BrowserWindow({ show: false }); + const server = http.createServer((req, res) => { + if (req.url === '/should_have_referrer') { + try { + expect(req.headers.referer).to.equal(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`); + return done(); + } catch (e) { + return done(e); + } finally { + server.close(); + } + } + res.end('link'); + }); + server.listen(0, '127.0.0.1', () => { + const url = 'http://127.0.0.1:' + (server.address() as AddressInfo).port + '/'; w.webContents.once('did-finish-load', () => { - // initiate a sub-frame load, then try and execute script during it - w.webContents.executeJavaScript(` - var iframe = document.createElement('iframe') - iframe.src = '${serverUrl}/slow' - document.body.appendChild(iframe) - `).then(() => { - w.webContents.executeJavaScript('console.log(\'hello\')').then(() => { - done() - }) - }) - }) - w.loadURL(serverUrl) - }) + w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => { + expect(referrer.url).to.equal(url); + expect(referrer.policy).to.equal('strict-origin-when-cross-origin'); + }); + w.webContents.executeJavaScript('a.click()'); + }); + w.loadURL(url); + }); + }); - it('executes after page load', (done) => { - w.webContents.executeJavaScript(`(() => "test")()`).then(result => { - expect(result).to.equal("test") - done() - }) - w.loadURL(serverUrl) - }) - - it('works with result objects that have DOM class prototypes', (done) => { - w.webContents.executeJavaScript('document.location').then(result => { - expect(result.origin).to.equal(serverUrl) - expect(result.protocol).to.equal('http:') - done() + // TODO(jeremy): window.open() in a real browser passes the referrer, but + // our hacked-up window.open() shim doesn't. It should. + xit('propagates referrer information to windows opened with window.open', (done) => { + const w = new BrowserWindow({ show: false }); + const server = http.createServer((req, res) => { + if (req.url === '/should_have_referrer') { + try { + expect(req.headers.referer).to.equal(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`); + return done(); + } catch (e) { + return done(e); + } + } + res.end(''); + }); + server.listen(0, '127.0.0.1', () => { + const url = 'http://127.0.0.1:' + (server.address() as AddressInfo).port + '/'; + w.webContents.once('did-finish-load', () => { + w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => { + expect(referrer.url).to.equal(url); + expect(referrer.policy).to.equal('no-referrer-when-downgrade'); + }); + w.webContents.executeJavaScript('window.open(location.href + "should_have_referrer")'); + }); + w.loadURL(url); + }); + }); + }); + + describe('webframe messages in sandboxed contents', () => { + afterEach(closeAllWindows); + it('responds to executeJavaScript', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const result = await w.webContents.executeJavaScript('37 + 5'); + expect(result).to.equal(42); + }); + }); + + describe('preload-error event', () => { + afterEach(closeAllWindows); + const generateSpecs = (description: string, sandbox: boolean) => { + describe(description, () => { + it('is triggered when unhandled exception is thrown', async () => { + const preload = path.join(fixturesPath, 'module', 'preload-error-exception.js'); + + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox, + preload + } + }); + + const promise = emittedOnce(w.webContents, 'preload-error'); + w.loadURL('about:blank'); + + const [, preloadPath, error] = await promise; + expect(preloadPath).to.equal(preload); + expect(error.message).to.equal('Hello World!'); + }); + + it('is triggered on syntax errors', async () => { + const preload = path.join(fixturesPath, 'module', 'preload-error-syntax.js'); + + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox, + preload + } + }); + + const promise = emittedOnce(w.webContents, 'preload-error'); + w.loadURL('about:blank'); + + const [, preloadPath, error] = await promise; + expect(preloadPath).to.equal(preload); + expect(error.message).to.equal('foobar is not defined'); + }); + + it('is triggered when preload script loading fails', async () => { + const preload = path.join(fixturesPath, 'module', 'preload-invalid.js'); + + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox, + preload + } + }); + + const promise = emittedOnce(w.webContents, 'preload-error'); + w.loadURL('about:blank'); + + const [, preloadPath, error] = await promise; + expect(preloadPath).to.equal(preload); + expect(error.message).to.contain('preload-invalid.js'); + }); + }); + }; + + generateSpecs('without sandbox', false); + generateSpecs('with sandbox', true); + }); + + describe('takeHeapSnapshot()', () => { + afterEach(closeAllWindows); + + it('works with sandboxed renderers', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }); + + await w.loadURL('about:blank'); + + const filePath = path.join(app.getPath('temp'), 'test.heapsnapshot'); + + const cleanup = () => { + try { + fs.unlinkSync(filePath); + } catch (e) { + // ignore error + } + }; + + try { + await w.webContents.takeHeapSnapshot(filePath); + const stats = fs.statSync(filePath); + expect(stats.size).not.to.be.equal(0); + } finally { + cleanup(); + } + }); + + it('fails with invalid file path', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }); + + await w.loadURL('about:blank'); + + const promise = w.webContents.takeHeapSnapshot(''); + return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed'); + }); + }); + + describe('setBackgroundThrottling()', () => { + afterEach(closeAllWindows); + it('does not crash when allowing', () => { + const w = new BrowserWindow({ show: false }); + w.webContents.setBackgroundThrottling(true); + }); + + it('does not crash when called via BrowserWindow', () => { + const w = new BrowserWindow({ show: false }); + + (w as any).setBackgroundThrottling(true); + }); + + it('does not crash when disallowing', () => { + const w = new BrowserWindow({ show: false, webPreferences: { backgroundThrottling: true } }); + + w.webContents.setBackgroundThrottling(false); + }); + }); + + describe('getBackgroundThrottling()', () => { + afterEach(closeAllWindows); + it('works via getter', () => { + const w = new BrowserWindow({ show: false }); + + w.webContents.setBackgroundThrottling(false); + expect(w.webContents.getBackgroundThrottling()).to.equal(false); + + w.webContents.setBackgroundThrottling(true); + expect(w.webContents.getBackgroundThrottling()).to.equal(true); + }); + + it('works via property', () => { + const w = new BrowserWindow({ show: false }); + + w.webContents.backgroundThrottling = false; + expect(w.webContents.backgroundThrottling).to.equal(false); + + w.webContents.backgroundThrottling = true; + expect(w.webContents.backgroundThrottling).to.equal(true); + }); + + it('works via BrowserWindow', () => { + const w = new BrowserWindow({ show: false }); + + (w as any).setBackgroundThrottling(false); + expect((w as any).getBackgroundThrottling()).to.equal(false); + + (w as any).setBackgroundThrottling(true); + expect((w as any).getBackgroundThrottling()).to.equal(true); + }); + }); + + ifdescribe(features.isPrintingEnabled())('getPrinters()', () => { + afterEach(closeAllWindows); + it('can get printer list', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const printers = w.webContents.getPrinters(); + expect(printers).to.be.an('array'); + }); + }); + + ifdescribe(features.isPrintingEnabled())('getPrintersAsync()', () => { + afterEach(closeAllWindows); + it('can get printer list', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const printers = await w.webContents.getPrintersAsync(); + expect(printers).to.be.an('array'); + }); + }); + + ifdescribe(features.isPrintingEnabled())('printToPDF()', () => { + let w: BrowserWindow; + + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('data:text/html,

Hello, World!

'); + }); + + afterEach(closeAllWindows); + + it('rejects on incorrectly typed parameters', async () => { + const badTypes = { + marginsType: 'terrible', + scaleFactor: 'not-a-number', + landscape: [], + pageRanges: { oops: 'im-not-the-right-key' }, + headerFooter: '123', + printSelectionOnly: 1, + printBackground: 2, + pageSize: 'IAmAPageSize' + }; + + // These will hard crash in Chromium unless we type-check + for (const [key, value] of Object.entries(badTypes)) { + const param = { [key]: value }; + await expect(w.webContents.printToPDF(param)).to.eventually.be.rejected(); + } + }); + + it('can print to PDF', async () => { + const data = await w.webContents.printToPDF({}); + expect(data).to.be.an.instanceof(Buffer).that.is.not.empty(); + }); + + it('does not crash when called multiple times in parallel', async () => { + const promises = []; + for (let i = 0; i < 3; i++) { + promises.push(w.webContents.printToPDF({})); + } + + const results = await Promise.all(promises); + for (const data of results) { + expect(data).to.be.an.instanceof(Buffer).that.is.not.empty(); + } + }); + + it('does not crash when called multiple times in sequence', async () => { + const results = []; + for (let i = 0; i < 3; i++) { + const result = await w.webContents.printToPDF({}); + results.push(result); + } + + for (const data of results) { + expect(data).to.be.an.instanceof(Buffer).that.is.not.empty(); + } + }); + + // TODO(codebytere): Re-enable after Chromium fixes upstream v8_scriptormodule_legacy_lifetime crash. + xdescribe('using a large document', () => { + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'print-to-pdf.html')); + }); + + afterEach(closeAllWindows); + + it('respects custom settings', async () => { + const data = await w.webContents.printToPDF({ + pageRanges: { + from: 0, + to: 2 + }, + landscape: true + }); + + const doc = await pdfjs.getDocument(data).promise; + + // Check that correct # of pages are rendered. + expect(doc.numPages).to.equal(3); + + // Check that PDF is generated in landscape mode. + const firstPage = await doc.getPage(1); + const { width, height } = firstPage.getViewport({ scale: 100 }); + expect(width).to.be.greaterThan(height); + }); + }); + }); + + describe('PictureInPicture video', () => { + afterEach(closeAllWindows); + it('works as expected', async function () { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadFile(path.join(fixturesPath, 'api', 'picture-in-picture.html')); + + if (!await w.webContents.executeJavaScript('document.createElement(\'video\').canPlayType(\'video/webm; codecs="vp8.0"\')')) { + this.skip(); + } + + const result = await w.webContents.executeJavaScript( + `runTest(${features.isPictureInPictureEnabled()})`, true); + expect(result).to.be.true(); + }); + }); + + describe('devtools window', () => { + let hasRobotJS = false; + try { + // We have other tests that check if native modules work, if we fail to require + // robotjs let's skip this test to avoid false negatives + require('robotjs'); + hasRobotJS = true; + } catch (err) { /* no-op */ } + + afterEach(closeAllWindows); + + // NB. on macOS, this requires that you grant your terminal the ability to + // control your computer. Open System Preferences > Security & Privacy > + // Privacy > Accessibility and grant your terminal the permission to control + // your computer. + ifit(hasRobotJS)('can receive and handle menu events', async () => { + const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true } }); + w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html')); + + // Ensure the devtools are loaded + w.webContents.closeDevTools(); + const opened = emittedOnce(w.webContents, 'devtools-opened'); + w.webContents.openDevTools(); + await opened; + await emittedOnce(w.webContents.devToolsWebContents!, 'did-finish-load'); + w.webContents.devToolsWebContents!.focus(); + + // Focus an input field + await w.webContents.devToolsWebContents!.executeJavaScript(` + const input = document.createElement('input') + document.body.innerHTML = '' + document.body.appendChild(input) + input.focus() + `); + + // Write something to the clipboard + clipboard.writeText('test value'); + + const pasted = w.webContents.devToolsWebContents!.executeJavaScript(`new Promise(resolve => { + document.querySelector('input').addEventListener('paste', (e) => { + resolve(e.target.value) }) - w.loadURL(serverUrl) - }) - }) - }) -}) + })`); + + // Fake a paste request using robotjs to emulate a REAL keyboard paste event + require('robotjs').keyTap('v', process.platform === 'darwin' ? ['command'] : ['control']); + + const val = await pasted; + + // Once we're done expect the paste to have been successful + expect(val).to.equal('test value', 'value should eventually become the pasted value'); + }); + }); + + describe('Shared Workers', () => { + afterEach(closeAllWindows); + + it('can get multiple shared workers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + + const ready = emittedOnce(ipcMain, 'ready'); + w.loadFile(path.join(fixturesPath, 'api', 'shared-worker', 'shared-worker.html')); + await ready; + + const sharedWorkers = w.webContents.getAllSharedWorkers(); + + expect(sharedWorkers).to.have.lengthOf(2); + expect(sharedWorkers[0].url).to.contain('shared-worker'); + expect(sharedWorkers[1].url).to.contain('shared-worker'); + }); + + it('can inspect a specific shared worker', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + + const ready = emittedOnce(ipcMain, 'ready'); + w.loadFile(path.join(fixturesPath, 'api', 'shared-worker', 'shared-worker.html')); + await ready; + + const sharedWorkers = w.webContents.getAllSharedWorkers(); + + const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened'); + w.webContents.inspectSharedWorkerById(sharedWorkers[0].id); + await devtoolsOpened; + + const devtoolsClosed = emittedOnce(w.webContents, 'devtools-closed'); + w.webContents.closeDevTools(); + await devtoolsClosed; + }); + }); + + describe('login event', () => { + afterEach(closeAllWindows); + + let server: http.Server; + let serverUrl: string; + let serverPort: number; + let proxyServer: http.Server; + let proxyServerPort: number; + + before((done) => { + server = http.createServer((request, response) => { + if (request.url === '/no-auth') { + return response.end('ok'); + } + if (request.headers.authorization) { + response.writeHead(200, { 'Content-type': 'text/plain' }); + return response.end(request.headers.authorization); + } + response + .writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }) + .end('401'); + }).listen(0, '127.0.0.1', () => { + serverPort = (server.address() as AddressInfo).port; + serverUrl = `http://127.0.0.1:${serverPort}`; + done(); + }); + }); + + before((done) => { + proxyServer = http.createServer((request, response) => { + if (request.headers['proxy-authorization']) { + response.writeHead(200, { 'Content-type': 'text/plain' }); + return response.end(request.headers['proxy-authorization']); + } + response + .writeHead(407, { 'Proxy-Authenticate': 'Basic realm="Foo"' }) + .end(); + }).listen(0, '127.0.0.1', () => { + proxyServerPort = (proxyServer.address() as AddressInfo).port; + done(); + }); + }); + + afterEach(async () => { + await session.defaultSession.clearAuthCache(); + }); + + after(() => { + server.close(); + proxyServer.close(); + }); + + it('is emitted when navigating', async () => { + const [user, pass] = ['user', 'pass']; + const w = new BrowserWindow({ show: false }); + let eventRequest: any; + let eventAuthInfo: any; + w.webContents.on('login', (event, request, authInfo, cb) => { + eventRequest = request; + eventAuthInfo = authInfo; + event.preventDefault(); + cb(user, pass); + }); + await w.loadURL(serverUrl); + const body = await w.webContents.executeJavaScript('document.documentElement.textContent'); + expect(body).to.equal(`Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`); + expect(eventRequest.url).to.equal(serverUrl + '/'); + expect(eventAuthInfo.isProxy).to.be.false(); + expect(eventAuthInfo.scheme).to.equal('basic'); + expect(eventAuthInfo.host).to.equal('127.0.0.1'); + expect(eventAuthInfo.port).to.equal(serverPort); + expect(eventAuthInfo.realm).to.equal('Foo'); + }); + + it('is emitted when a proxy requests authorization', async () => { + const customSession = session.fromPartition(`${Math.random()}`); + await customSession.setProxy({ proxyRules: `127.0.0.1:${proxyServerPort}`, proxyBypassRules: '<-loopback>' }); + const [user, pass] = ['user', 'pass']; + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + let eventRequest: any; + let eventAuthInfo: any; + w.webContents.on('login', (event, request, authInfo, cb) => { + eventRequest = request; + eventAuthInfo = authInfo; + event.preventDefault(); + cb(user, pass); + }); + await w.loadURL(`${serverUrl}/no-auth`); + const body = await w.webContents.executeJavaScript('document.documentElement.textContent'); + expect(body).to.equal(`Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`); + expect(eventRequest.url).to.equal(`${serverUrl}/no-auth`); + expect(eventAuthInfo.isProxy).to.be.true(); + expect(eventAuthInfo.scheme).to.equal('basic'); + expect(eventAuthInfo.host).to.equal('127.0.0.1'); + expect(eventAuthInfo.port).to.equal(proxyServerPort); + expect(eventAuthInfo.realm).to.equal('Foo'); + }); + + it('cancels authentication when callback is called with no arguments', async () => { + const w = new BrowserWindow({ show: false }); + w.webContents.on('login', (event, request, authInfo, cb) => { + event.preventDefault(); + cb(); + }); + await w.loadURL(serverUrl); + const body = await w.webContents.executeJavaScript('document.documentElement.textContent'); + expect(body).to.equal('401'); + }); + }); + + describe('page-title-updated event', () => { + afterEach(closeAllWindows); + it('is emitted with a full title for pages with no navigation', async () => { + const bw = new BrowserWindow({ show: false }); + await bw.loadURL('about:blank'); + bw.webContents.executeJavaScript('child = window.open("", "", "show=no"); null'); + const [, child] = await emittedOnce(app, 'web-contents-created'); + bw.webContents.executeJavaScript('child.document.title = "new title"'); + const [, title] = await emittedOnce(child, 'page-title-updated'); + expect(title).to.equal('new title'); + }); + }); + + describe('crashed event', () => { + it('does not crash main process when destroying WebContents in it', (done) => { + const contents = (webContents as any).create({ nodeIntegration: true }); + contents.once('crashed', () => { + contents.destroy(); + done(); + }); + contents.loadURL('about:blank').then(() => contents.forcefullyCrashRenderer()); + }); + }); + + describe('context-menu event', () => { + afterEach(closeAllWindows); + it('emits when right-clicked in page', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')); + + const promise = emittedOnce(w.webContents, 'context-menu'); + + // Simulate right-click to create context-menu event. + const opts = { x: 0, y: 0, button: 'right' as any }; + w.webContents.sendInputEvent({ ...opts, type: 'mouseDown' }); + w.webContents.sendInputEvent({ ...opts, type: 'mouseUp' }); + + const [, params] = await promise; + + expect(params.pageURL).to.equal(w.webContents.getURL()); + expect(params.frame).to.be.an('object'); + expect(params.x).to.be.a('number'); + expect(params.y).to.be.a('number'); + }); + }); + + it('emits a cancelable event before creating a child webcontents', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }); + w.webContents.on('-will-add-new-contents' as any, (event: any, url: any) => { + expect(url).to.equal('about:blank'); + event.preventDefault(); + }); + let wasCalled = false; + w.webContents.on('new-window' as any, () => { + wasCalled = true; + }); + await w.loadURL('about:blank'); + await w.webContents.executeJavaScript('window.open(\'about:blank\')'); + await new Promise((resolve) => { process.nextTick(resolve); }); + expect(wasCalled).to.equal(false); + await closeAllWindows(); + }); +}); diff --git a/spec-main/api-web-contents-view-spec.ts b/spec-main/api-web-contents-view-spec.ts new file mode 100644 index 0000000000000..b8b7b2603049e --- /dev/null +++ b/spec-main/api-web-contents-view-spec.ts @@ -0,0 +1,37 @@ +import { closeWindow } from './window-helpers'; + +import { BaseWindow, WebContentsView } from 'electron/main'; + +describe('WebContentsView', () => { + let w: BaseWindow; + afterEach(() => closeWindow(w as any).then(() => { w = null as unknown as BaseWindow; })); + + it('can be used as content view', () => { + w = new BaseWindow({ show: false }); + w.setContentView(new WebContentsView({})); + }); + + function triggerGCByAllocation () { + const arr = []; + for (let i = 0; i < 1000000; i++) { + arr.push([]); + } + return arr; + } + + it('doesn\'t crash when GCed during allocation', (done) => { + // eslint-disable-next-line no-new + new WebContentsView({}); + setTimeout(() => { + // NB. the crash we're testing for is the lack of a current `v8::Context` + // when emitting an event in WebContents's destructor. V8 is inconsistent + // about whether or not there's a current context during garbage + // collection, and it seems that `v8Util.requestGarbageCollectionForTesting` + // causes a GC in which there _is_ a current context, so the crash isn't + // triggered. Thus, we force a GC by other means: namely, by allocating a + // bunch of stuff. + triggerGCByAllocation(); + done(); + }); + }); +}); diff --git a/spec-main/api-web-frame-main-spec.ts b/spec-main/api-web-frame-main-spec.ts new file mode 100644 index 0000000000000..8890e1402f52c --- /dev/null +++ b/spec-main/api-web-frame-main-spec.ts @@ -0,0 +1,345 @@ +import { expect } from 'chai'; +import * as http from 'http'; +import * as path from 'path'; +import * as url from 'url'; +import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain } from 'electron/main'; +import { closeAllWindows } from './window-helpers'; +import { emittedOnce, emittedNTimes } from './events-helpers'; +import { AddressInfo } from 'net'; +import { ifit, waitUntil } from './spec-helpers'; + +describe('webFrameMain module', () => { + const fixtures = path.resolve(__dirname, '..', 'spec-main', 'fixtures'); + const subframesPath = path.join(fixtures, 'sub-frames'); + + const fileUrl = (filename: string) => url.pathToFileURL(path.join(subframesPath, filename)).href; + + type Server = { server: http.Server, url: string } + + /** Creates an HTTP server whose handler embeds the given iframe src. */ + const createServer = () => new Promise(resolve => { + const server = http.createServer((req, res) => { + const params = new URLSearchParams(url.parse(req.url || '').search || ''); + if (params.has('frameSrc')) { + res.end(``); + } else { + res.end(''); + } + }); + server.listen(0, '127.0.0.1', () => { + const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`; + resolve({ server, url }); + }); + }); + + afterEach(closeAllWindows); + + describe('WebFrame traversal APIs', () => { + let w: BrowserWindow; + let webFrame: WebFrameMain; + + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); + webFrame = w.webContents.mainFrame; + }); + + it('can access top frame', () => { + expect(webFrame.top).to.equal(webFrame); + }); + + it('has no parent on top frame', () => { + expect(webFrame.parent).to.be.null(); + }); + + it('can access immediate frame descendents', () => { + const { frames } = webFrame; + expect(frames).to.have.lengthOf(1); + const subframe = frames[0]; + expect(subframe).not.to.equal(webFrame); + expect(subframe.parent).to.equal(webFrame); + }); + + it('can access deeply nested frames', () => { + const subframe = webFrame.frames[0]; + expect(subframe).not.to.equal(webFrame); + expect(subframe.parent).to.equal(webFrame); + const nestedSubframe = subframe.frames[0]; + expect(nestedSubframe).not.to.equal(webFrame); + expect(nestedSubframe).not.to.equal(subframe); + expect(nestedSubframe.parent).to.equal(subframe); + }); + + it('can traverse all frames in root', () => { + const urls = webFrame.framesInSubtree.map(frame => frame.url); + expect(urls).to.deep.equal([ + fileUrl('frame-with-frame-container.html'), + fileUrl('frame-with-frame.html'), + fileUrl('frame.html') + ]); + }); + + it('can traverse all frames in subtree', () => { + const urls = webFrame.frames[0].framesInSubtree.map(frame => frame.url); + expect(urls).to.deep.equal([ + fileUrl('frame-with-frame.html'), + fileUrl('frame.html') + ]); + }); + + describe('cross-origin', () => { + let serverA = null as unknown as Server; + let serverB = null as unknown as Server; + + before(async () => { + serverA = await createServer(); + serverB = await createServer(); + }); + + after(() => { + serverA.server.close(); + serverB.server.close(); + }); + + it('can access cross-origin frames', async () => { + await w.loadURL(`${serverA.url}?frameSrc=${serverB.url}`); + webFrame = w.webContents.mainFrame; + expect(webFrame.url.startsWith(serverA.url)).to.be.true(); + expect(webFrame.frames[0].url).to.equal(serverB.url); + }); + }); + }); + + describe('WebFrame.url', () => { + it('should report correct address for each subframe', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); + const webFrame = w.webContents.mainFrame; + + expect(webFrame.url).to.equal(fileUrl('frame-with-frame-container.html')); + expect(webFrame.frames[0].url).to.equal(fileUrl('frame-with-frame.html')); + expect(webFrame.frames[0].frames[0].url).to.equal(fileUrl('frame.html')); + }); + }); + + describe('WebFrame IDs', () => { + it('has properties for various identifiers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + await w.loadFile(path.join(subframesPath, 'frame.html')); + const webFrame = w.webContents.mainFrame; + expect(webFrame).to.have.ownProperty('url').that.is.a('string'); + expect(webFrame).to.have.ownProperty('frameTreeNodeId').that.is.a('number'); + expect(webFrame).to.have.ownProperty('name').that.is.a('string'); + expect(webFrame).to.have.ownProperty('osProcessId').that.is.a('number'); + expect(webFrame).to.have.ownProperty('processId').that.is.a('number'); + expect(webFrame).to.have.ownProperty('routingId').that.is.a('number'); + }); + }); + + describe('WebFrame.visibilityState', () => { + // TODO(MarshallOfSound): Fix flaky test + // @flaky-test + it.skip('should match window state', async () => { + const w = new BrowserWindow({ show: true }); + await w.loadURL('about:blank'); + const webFrame = w.webContents.mainFrame; + + expect(webFrame.visibilityState).to.equal('visible'); + w.hide(); + await expect( + waitUntil(() => webFrame.visibilityState === 'hidden') + ).to.eventually.be.fulfilled(); + }); + }); + + describe('WebFrame.executeJavaScript', () => { + it('can inject code into any subframe', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); + const webFrame = w.webContents.mainFrame; + + const getUrl = (frame: WebFrameMain) => frame.executeJavaScript('location.href'); + expect(await getUrl(webFrame)).to.equal(fileUrl('frame-with-frame-container.html')); + expect(await getUrl(webFrame.frames[0])).to.equal(fileUrl('frame-with-frame.html')); + expect(await getUrl(webFrame.frames[0].frames[0])).to.equal(fileUrl('frame.html')); + }); + }); + + describe('WebFrame.reload', () => { + it('reloads a frame', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + await w.loadFile(path.join(subframesPath, 'frame.html')); + const webFrame = w.webContents.mainFrame; + + await webFrame.executeJavaScript('window.TEMP = 1', false); + expect(webFrame.reload()).to.be.true(); + await emittedOnce(w.webContents, 'dom-ready'); + expect(await webFrame.executeJavaScript('window.TEMP', false)).to.be.null(); + }); + }); + + describe('WebFrame.send', () => { + it('works', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(subframesPath, 'preload.js'), + nodeIntegrationInSubFrames: true + } + }); + await w.loadURL('about:blank'); + const webFrame = w.webContents.mainFrame; + const pongPromise = emittedOnce(ipcMain, 'preload-pong'); + webFrame.send('preload-ping'); + const [, routingId] = await pongPromise; + expect(routingId).to.equal(webFrame.routingId); + }); + }); + + describe('RenderFrame lifespan', () => { + let w: BrowserWindow; + + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + }); + + // TODO(jkleinsc) fix this flaky test on linux + ifit(process.platform !== 'linux')('throws upon accessing properties when disposed', async () => { + await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); + const { mainFrame } = w.webContents; + w.destroy(); + // Wait for WebContents, and thus RenderFrameHost, to be destroyed. + await new Promise(resolve => setTimeout(resolve, 0)); + expect(() => mainFrame.url).to.throw(); + }); + + it('persists through cross-origin navigation', async () => { + const server = await createServer(); + // 'localhost' is treated as a separate origin. + const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost'); + await w.loadURL(server.url); + const { mainFrame } = w.webContents; + expect(mainFrame.url).to.equal(server.url); + await w.loadURL(crossOriginUrl); + expect(w.webContents.mainFrame).to.equal(mainFrame); + expect(mainFrame.url).to.equal(crossOriginUrl); + }); + + it('recovers from renderer crash on same-origin', async () => { + const server = await createServer(); + // Keep reference to mainFrame alive throughout crash and recovery. + const { mainFrame } = w.webContents; + await w.webContents.loadURL(server.url); + const crashEvent = emittedOnce(w.webContents, 'render-process-gone'); + w.webContents.forcefullyCrashRenderer(); + await crashEvent; + await w.webContents.loadURL(server.url); + // Log just to keep mainFrame in scope. + console.log('mainFrame.url', mainFrame.url); + }); + + // Fixed by #34411 + it('recovers from renderer crash on cross-origin', async () => { + const server = await createServer(); + // 'localhost' is treated as a separate origin. + const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost'); + // Keep reference to mainFrame alive throughout crash and recovery. + const { mainFrame } = w.webContents; + await w.webContents.loadURL(server.url); + const crashEvent = emittedOnce(w.webContents, 'render-process-gone'); + w.webContents.forcefullyCrashRenderer(); + await crashEvent; + // A short wait seems to be required to reproduce the crash. + await new Promise(resolve => setTimeout(resolve, 100)); + await w.webContents.loadURL(crossOriginUrl); + // Log just to keep mainFrame in scope. + console.log('mainFrame.url', mainFrame.url); + }); + }); + + describe('webFrameMain.fromId', () => { + it('returns undefined for unknown IDs', () => { + expect(webFrameMain.fromId(0, 0)).to.be.undefined(); + }); + + it('can find each frame from navigation events', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + + // frame-with-frame-container.html, frame-with-frame.html, frame.html + const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3); + w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); + + for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) { + const frame = webFrameMain.fromId(frameProcessId, frameRoutingId); + expect(frame).not.to.be.null(); + expect(frame?.processId).to.be.equal(frameProcessId); + expect(frame?.routingId).to.be.equal(frameRoutingId); + expect(frame?.top === frame).to.be.equal(isMainFrame); + } + }); + }); + + describe('"frame-created" event', () => { + it('emits when the main frame is created', async () => { + const w = new BrowserWindow({ show: false }); + const promise = emittedOnce(w.webContents, 'frame-created'); + w.webContents.loadFile(path.join(subframesPath, 'frame.html')); + const [, details] = await promise; + expect(details.frame).to.equal(w.webContents.mainFrame); + }); + + it('emits when nested frames are created', async () => { + const w = new BrowserWindow({ show: false }); + const promise = emittedNTimes(w.webContents, 'frame-created', 2); + w.webContents.loadFile(path.join(subframesPath, 'frame-container.html')); + const [[, mainDetails], [, nestedDetails]] = await promise; + expect(mainDetails.frame).to.equal(w.webContents.mainFrame); + expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]); + }); + + it('is not emitted upon cross-origin navigation', async () => { + const server = await createServer(); + + // HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as + // a separate origin because differing ports aren't enough 🤔 + const secondUrl = `http://localhost:${new URL(server.url).port}`; + + const w = new BrowserWindow({ show: false }); + await w.webContents.loadURL(server.url); + + let frameCreatedEmitted = false; + + w.webContents.once('frame-created', () => { + frameCreatedEmitted = true; + }); + + await w.webContents.loadURL(secondUrl); + + expect(frameCreatedEmitted).to.be.false(); + }); + }); + + describe('"dom-ready" event', () => { + it('emits for top-level frame', async () => { + const w = new BrowserWindow({ show: false }); + const promise = emittedOnce(w.webContents.mainFrame, 'dom-ready'); + w.webContents.loadURL('about:blank'); + await promise; + }); + + it('emits for sub frame', async () => { + const w = new BrowserWindow({ show: false }); + const promise = new Promise(resolve => { + w.webContents.on('frame-created', (e, { frame }) => { + frame.on('dom-ready', () => { + if (frame.name === 'frameA') { + resolve(); + } + }); + }); + }); + w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html')); + await promise; + }); + }); +}); diff --git a/spec-main/api-web-frame-spec.ts b/spec-main/api-web-frame-spec.ts new file mode 100644 index 0000000000000..f7d9d9ffb138a --- /dev/null +++ b/spec-main/api-web-frame-spec.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import { BrowserWindow, ipcMain } from 'electron/main'; +import { closeAllWindows } from './window-helpers'; +import { emittedOnce } from './events-helpers'; + +describe('webFrame module', () => { + const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures'); + + afterEach(closeAllWindows); + + it('can use executeJavaScript', async () => { + const w = new BrowserWindow({ + show: true, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + preload: path.join(fixtures, 'pages', 'world-safe-preload.js') + } + }); + const isSafe = emittedOnce(ipcMain, 'executejs-safe'); + w.loadURL('about:blank'); + const [, wasSafe] = await isSafe; + expect(wasSafe).to.equal(true); + }); + + it('can use executeJavaScript and catch conversion errors', async () => { + const w = new BrowserWindow({ + show: true, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + preload: path.join(fixtures, 'pages', 'world-safe-preload-error.js') + } + }); + const execError = emittedOnce(ipcMain, 'executejs-safe'); + w.loadURL('about:blank'); + const [, error] = await execError; + expect(error).to.not.equal(null, 'Error should not be null'); + expect(error).to.have.property('message', 'Uncaught Error: An object could not be cloned.'); + }); + + it('calls a spellcheck provider', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + await w.loadFile(path.join(fixtures, 'pages', 'webframe-spell-check.html')); + w.focus(); + await w.webContents.executeJavaScript('document.querySelector("input").focus()', true); + + const spellCheckerFeedback = + new Promise<[string[], boolean]>(resolve => { + ipcMain.on('spec-spell-check', (e, words, callbackDefined) => { + if (words.length === 5) { + // The API calls the provider after every completed word. + // The promise is resolved only after this event is received with all words. + resolve([words, callbackDefined]); + } + }); + }); + const inputText = 'spleling test you\'re '; + for (const keyCode of inputText) { + w.webContents.sendInputEvent({ type: 'char', keyCode }); + } + const [words, callbackDefined] = await spellCheckerFeedback; + expect(words.sort()).to.deep.equal(['spleling', 'test', 'you\'re', 'you', 're'].sort()); + expect(callbackDefined).to.be.true(); + }); +}); diff --git a/spec-main/api-web-request-spec.ts b/spec-main/api-web-request-spec.ts new file mode 100644 index 0000000000000..22e58e5fe8be8 --- /dev/null +++ b/spec-main/api-web-request-spec.ts @@ -0,0 +1,560 @@ +import { expect } from 'chai'; +import * as http from 'http'; +import * as qs from 'querystring'; +import * as path from 'path'; +import * as url from 'url'; +import * as WebSocket from 'ws'; +import { ipcMain, protocol, session, WebContents, webContents } from 'electron/main'; +import { AddressInfo, Socket } from 'net'; +import { emittedOnce } from './events-helpers'; + +const fixturesPath = path.resolve(__dirname, 'fixtures'); + +describe('webRequest module', () => { + const ses = session.defaultSession; + const server = http.createServer((req, res) => { + if (req.url === '/serverRedirect') { + res.statusCode = 301; + res.setHeader('Location', 'http://' + req.rawHeaders[1]); + res.end(); + } else if (req.url === '/contentDisposition') { + res.setHeader('content-disposition', [' attachment; filename=aa%E4%B8%ADaa.txt']); + const content = req.url; + res.end(content); + } else { + res.setHeader('Custom', ['Header']); + let content = req.url; + if (req.headers.accept === '*/*;test/header') { + content += 'header/received'; + } + if (req.headers.origin === 'http://new-origin') { + content += 'new/origin'; + } + res.end(content); + } + }); + let defaultURL: string; + + before((done) => { + protocol.registerStringProtocol('cors', (req, cb) => cb('')); + server.listen(0, '127.0.0.1', () => { + const port = (server.address() as AddressInfo).port; + defaultURL = `http://127.0.0.1:${port}/`; + done(); + }); + }); + + after(() => { + server.close(); + protocol.unregisterProtocol('cors'); + }); + + let contents: WebContents = null as unknown as WebContents; + // NB. sandbox: true is used because it makes navigations much (~8x) faster. + before(async () => { + contents = (webContents as any).create({ sandbox: true }); + await contents.loadFile(path.join(fixturesPath, 'pages', 'fetch.html')); + }); + after(() => (contents as any).destroy()); + + async function ajax (url: string, options = {}) { + return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`); + } + + describe('webRequest.onBeforeRequest', () => { + afterEach(() => { + ses.webRequest.onBeforeRequest(null); + }); + + it('can cancel the request', async () => { + ses.webRequest.onBeforeRequest((details, callback) => { + callback({ + cancel: true + }); + }); + await expect(ajax(defaultURL)).to.eventually.be.rejected(); + }); + + it('can filter URLs', async () => { + const filter = { urls: [defaultURL + 'filter/*'] }; + ses.webRequest.onBeforeRequest(filter, (details, callback) => { + callback({ cancel: true }); + }); + const { data } = await ajax(`${defaultURL}nofilter/test`); + expect(data).to.equal('/nofilter/test'); + await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected(); + }); + + it('receives details object', async () => { + ses.webRequest.onBeforeRequest((details, callback) => { + expect(details.id).to.be.a('number'); + expect(details.timestamp).to.be.a('number'); + expect(details.webContentsId).to.be.a('number'); + expect(details.webContents).to.be.an('object'); + expect(details.webContents!.id).to.equal(details.webContentsId); + expect(details.frame).to.be.an('object'); + expect(details.url).to.be.a('string').that.is.equal(defaultURL); + expect(details.method).to.be.a('string').that.is.equal('GET'); + expect(details.resourceType).to.be.a('string').that.is.equal('xhr'); + expect(details.uploadData).to.be.undefined(); + callback({}); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/'); + }); + + it('receives post data in details object', async () => { + const postData = { + name: 'post test', + type: 'string' + }; + ses.webRequest.onBeforeRequest((details, callback) => { + expect(details.url).to.equal(defaultURL); + expect(details.method).to.equal('POST'); + expect(details.uploadData).to.have.lengthOf(1); + const data = qs.parse(details.uploadData[0].bytes.toString()); + expect(data).to.deep.equal(postData); + callback({ cancel: true }); + }); + await expect(ajax(defaultURL, { + method: 'POST', + body: qs.stringify(postData) + })).to.eventually.be.rejected(); + }); + + it('can redirect the request', async () => { + ses.webRequest.onBeforeRequest((details, callback) => { + if (details.url === defaultURL) { + callback({ redirectURL: `${defaultURL}redirect` }); + } else { + callback({}); + } + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/redirect'); + }); + + it('does not crash for redirects', async () => { + ses.webRequest.onBeforeRequest((details, callback) => { + callback({ cancel: false }); + }); + await ajax(defaultURL + 'serverRedirect'); + await ajax(defaultURL + 'serverRedirect'); + }); + + it('works with file:// protocol', async () => { + ses.webRequest.onBeforeRequest((details, callback) => { + callback({ cancel: true }); + }); + const fileURL = url.format({ + pathname: path.join(fixturesPath, 'blank.html').replace(/\\/g, '/'), + protocol: 'file', + slashes: true + }); + await expect(ajax(fileURL)).to.eventually.be.rejected(); + }); + }); + + describe('webRequest.onBeforeSendHeaders', () => { + afterEach(() => { + ses.webRequest.onBeforeSendHeaders(null); + ses.webRequest.onSendHeaders(null); + }); + + it('receives details object', async () => { + ses.webRequest.onBeforeSendHeaders((details, callback) => { + expect(details.requestHeaders).to.be.an('object'); + expect(details.requestHeaders['Foo.Bar']).to.equal('baz'); + callback({}); + }); + const { data } = await ajax(defaultURL, { headers: { 'Foo.Bar': 'baz' } }); + expect(data).to.equal('/'); + }); + + it('can change the request headers', async () => { + ses.webRequest.onBeforeSendHeaders((details, callback) => { + const requestHeaders = details.requestHeaders; + requestHeaders.Accept = '*/*;test/header'; + callback({ requestHeaders: requestHeaders }); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/header/received'); + }); + + it('can change the request headers on a custom protocol redirect', async () => { + protocol.registerStringProtocol('no-cors', (req, callback) => { + if (req.url === 'no-cors://fake-host/redirect') { + callback({ + statusCode: 302, + headers: { + Location: 'no-cors://fake-host' + } + }); + } else { + let content = ''; + if (req.headers.Accept === '*/*;test/header') { + content = 'header-received'; + } + callback(content); + } + }); + + // Note that we need to do navigation every time after a protocol is + // registered or unregistered, otherwise the new protocol won't be + // recognized by current page when NetworkService is used. + await contents.loadFile(path.join(__dirname, 'fixtures', 'pages', 'fetch.html')); + + try { + ses.webRequest.onBeforeSendHeaders((details, callback) => { + const requestHeaders = details.requestHeaders; + requestHeaders.Accept = '*/*;test/header'; + callback({ requestHeaders: requestHeaders }); + }); + const { data } = await ajax('no-cors://fake-host/redirect'); + expect(data).to.equal('header-received'); + } finally { + protocol.unregisterProtocol('no-cors'); + } + }); + + it('can change request origin', async () => { + ses.webRequest.onBeforeSendHeaders((details, callback) => { + const requestHeaders = details.requestHeaders; + requestHeaders.Origin = 'http://new-origin'; + callback({ requestHeaders: requestHeaders }); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/new/origin'); + }); + + it('can capture CORS requests', async () => { + let called = false; + ses.webRequest.onBeforeSendHeaders((details, callback) => { + called = true; + callback({ requestHeaders: details.requestHeaders }); + }); + await ajax('cors://host'); + expect(called).to.be.true(); + }); + + it('resets the whole headers', async () => { + const requestHeaders = { + Test: 'header' + }; + ses.webRequest.onBeforeSendHeaders((details, callback) => { + callback({ requestHeaders: requestHeaders }); + }); + ses.webRequest.onSendHeaders((details) => { + expect(details.requestHeaders).to.deep.equal(requestHeaders); + }); + await ajax(defaultURL); + }); + + it('leaves headers unchanged when no requestHeaders in callback', async () => { + let originalRequestHeaders: Record; + ses.webRequest.onBeforeSendHeaders((details, callback) => { + originalRequestHeaders = details.requestHeaders; + callback({}); + }); + ses.webRequest.onSendHeaders((details) => { + expect(details.requestHeaders).to.deep.equal(originalRequestHeaders); + }); + await ajax(defaultURL); + }); + + it('works with file:// protocol', async () => { + const requestHeaders = { + Test: 'header' + }; + let onSendHeadersCalled = false; + ses.webRequest.onBeforeSendHeaders((details, callback) => { + callback({ requestHeaders: requestHeaders }); + }); + ses.webRequest.onSendHeaders((details) => { + expect(details.requestHeaders).to.deep.equal(requestHeaders); + onSendHeadersCalled = true; + }); + await ajax(url.format({ + pathname: path.join(fixturesPath, 'blank.html').replace(/\\/g, '/'), + protocol: 'file', + slashes: true + })); + expect(onSendHeadersCalled).to.be.true(); + }); + }); + + describe('webRequest.onSendHeaders', () => { + afterEach(() => { + ses.webRequest.onSendHeaders(null); + }); + + it('receives details object', async () => { + ses.webRequest.onSendHeaders((details) => { + expect(details.requestHeaders).to.be.an('object'); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/'); + }); + }); + + describe('webRequest.onHeadersReceived', () => { + afterEach(() => { + ses.webRequest.onHeadersReceived(null); + }); + + it('receives details object', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + expect(details.statusLine).to.equal('HTTP/1.1 200 OK'); + expect(details.statusCode).to.equal(200); + expect(details.responseHeaders!.Custom).to.deep.equal(['Header']); + callback({}); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/'); + }); + + it('can change the response header', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + const responseHeaders = details.responseHeaders!; + responseHeaders.Custom = ['Changed'] as any; + callback({ responseHeaders: responseHeaders }); + }); + const { headers } = await ajax(defaultURL); + expect(headers).to.to.have.property('custom', 'Changed'); + }); + + it('can change response origin', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + const responseHeaders = details.responseHeaders!; + responseHeaders['access-control-allow-origin'] = ['http://new-origin'] as any; + callback({ responseHeaders: responseHeaders }); + }); + const { headers } = await ajax(defaultURL); + expect(headers).to.to.have.property('access-control-allow-origin', 'http://new-origin'); + }); + + it('can change headers of CORS responses', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + const responseHeaders = details.responseHeaders!; + responseHeaders.Custom = ['Changed'] as any; + callback({ responseHeaders: responseHeaders }); + }); + const { headers } = await ajax('cors://host'); + expect(headers).to.to.have.property('custom', 'Changed'); + }); + + it('does not change header by default', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + callback({}); + }); + const { data, headers } = await ajax(defaultURL); + expect(headers).to.to.have.property('custom', 'Header'); + expect(data).to.equal('/'); + }); + + it('does not change content-disposition header by default', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + expect(details.responseHeaders!['content-disposition']).to.deep.equal([' attachment; filename="aa中aa.txt"']); + callback({}); + }); + const { data, headers } = await ajax(defaultURL + 'contentDisposition'); + expect(headers).to.to.have.property('content-disposition', 'attachment; filename=aa%E4%B8%ADaa.txt'); + expect(data).to.equal('/contentDisposition'); + }); + + it('follows server redirect', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + const responseHeaders = details.responseHeaders; + callback({ responseHeaders: responseHeaders }); + }); + const { headers } = await ajax(defaultURL + 'serverRedirect'); + expect(headers).to.to.have.property('custom', 'Header'); + }); + + it('can change the header status', async () => { + ses.webRequest.onHeadersReceived((details, callback) => { + const responseHeaders = details.responseHeaders; + callback({ + responseHeaders: responseHeaders, + statusLine: 'HTTP/1.1 404 Not Found' + }); + }); + const { headers } = await ajax(defaultURL); + expect(headers).to.to.have.property('custom', 'Header'); + }); + }); + + describe('webRequest.onResponseStarted', () => { + afterEach(() => { + ses.webRequest.onResponseStarted(null); + }); + + it('receives details object', async () => { + ses.webRequest.onResponseStarted((details) => { + expect(details.fromCache).to.be.a('boolean'); + expect(details.statusLine).to.equal('HTTP/1.1 200 OK'); + expect(details.statusCode).to.equal(200); + expect(details.responseHeaders!.Custom).to.deep.equal(['Header']); + }); + const { data, headers } = await ajax(defaultURL); + expect(headers).to.to.have.property('custom', 'Header'); + expect(data).to.equal('/'); + }); + }); + + describe('webRequest.onBeforeRedirect', () => { + afterEach(() => { + ses.webRequest.onBeforeRedirect(null); + ses.webRequest.onBeforeRequest(null); + }); + + it('receives details object', async () => { + const redirectURL = defaultURL + 'redirect'; + ses.webRequest.onBeforeRequest((details, callback) => { + if (details.url === defaultURL) { + callback({ redirectURL: redirectURL }); + } else { + callback({}); + } + }); + ses.webRequest.onBeforeRedirect((details) => { + expect(details.fromCache).to.be.a('boolean'); + expect(details.statusLine).to.equal('HTTP/1.1 307 Internal Redirect'); + expect(details.statusCode).to.equal(307); + expect(details.redirectURL).to.equal(redirectURL); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/redirect'); + }); + }); + + describe('webRequest.onCompleted', () => { + afterEach(() => { + ses.webRequest.onCompleted(null); + }); + + it('receives details object', async () => { + ses.webRequest.onCompleted((details) => { + expect(details.fromCache).to.be.a('boolean'); + expect(details.statusLine).to.equal('HTTP/1.1 200 OK'); + expect(details.statusCode).to.equal(200); + }); + const { data } = await ajax(defaultURL); + expect(data).to.equal('/'); + }); + }); + + describe('webRequest.onErrorOccurred', () => { + afterEach(() => { + ses.webRequest.onErrorOccurred(null); + ses.webRequest.onBeforeRequest(null); + }); + + it('receives details object', async () => { + ses.webRequest.onBeforeRequest((details, callback) => { + callback({ cancel: true }); + }); + ses.webRequest.onErrorOccurred((details) => { + expect(details.error).to.equal('net::ERR_BLOCKED_BY_CLIENT'); + }); + await expect(ajax(defaultURL)).to.eventually.be.rejected(); + }); + }); + + describe('WebSocket connections', () => { + it('can be proxyed', async () => { + // Setup server. + const reqHeaders : { [key: string] : any } = {}; + const server = http.createServer((req, res) => { + reqHeaders[req.url!] = req.headers; + res.setHeader('foo1', 'bar1'); + res.end('ok'); + }); + const wss = new WebSocket.Server({ noServer: true }); + wss.on('connection', function connection (ws) { + ws.on('message', function incoming (message) { + if (message === 'foo') { + ws.send('bar'); + } + }); + }); + server.on('upgrade', function upgrade (request, socket, head) { + const pathname = require('url').parse(request.url).pathname; + if (pathname === '/websocket') { + reqHeaders[request.url!] = request.headers; + wss.handleUpgrade(request, socket as Socket, head, function done (ws) { + wss.emit('connection', ws, request); + }); + } + }); + + // Start server. + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const port = String((server.address() as AddressInfo).port); + + // Use a separate session for testing. + const ses = session.fromPartition('WebRequestWebSocket'); + + // Setup listeners. + const receivedHeaders : { [key: string] : any } = {}; + ses.webRequest.onBeforeSendHeaders((details, callback) => { + details.requestHeaders.foo = 'bar'; + callback({ requestHeaders: details.requestHeaders }); + }); + ses.webRequest.onHeadersReceived((details, callback) => { + const pathname = require('url').parse(details.url).pathname; + receivedHeaders[pathname] = details.responseHeaders; + callback({ cancel: false }); + }); + ses.webRequest.onResponseStarted((details) => { + if (details.url.startsWith('ws://')) { + expect(details.responseHeaders!.Connection[0]).be.equal('Upgrade'); + } else if (details.url.startsWith('http')) { + expect(details.responseHeaders!.foo1[0]).be.equal('bar1'); + } + }); + ses.webRequest.onSendHeaders((details) => { + if (details.url.startsWith('ws://')) { + expect(details.requestHeaders.foo).be.equal('bar'); + expect(details.requestHeaders.Upgrade).be.equal('websocket'); + } else if (details.url.startsWith('http')) { + expect(details.requestHeaders.foo).be.equal('bar'); + } + }); + ses.webRequest.onCompleted((details) => { + if (details.url.startsWith('ws://')) { + expect(details.error).be.equal('net::ERR_WS_UPGRADE'); + } else if (details.url.startsWith('http')) { + expect(details.error).be.equal('net::OK'); + } + }); + + const contents = (webContents as any).create({ + session: ses, + nodeIntegration: true, + webSecurity: false, + contextIsolation: false + }); + + // Cleanup. + after(() => { + contents.destroy(); + server.close(); + ses.webRequest.onBeforeRequest(null); + ses.webRequest.onBeforeSendHeaders(null); + ses.webRequest.onHeadersReceived(null); + ses.webRequest.onResponseStarted(null); + ses.webRequest.onSendHeaders(null); + ses.webRequest.onCompleted(null); + }); + + contents.loadFile(path.join(fixturesPath, 'api', 'webrequest.html'), { query: { port } }); + await emittedOnce(ipcMain, 'websocket-success'); + + expect(receivedHeaders['/websocket'].Upgrade[0]).to.equal('websocket'); + expect(receivedHeaders['/'].foo1[0]).to.equal('bar1'); + expect(reqHeaders['/websocket'].foo).to.equal('bar'); + expect(reqHeaders['/'].foo).to.equal('bar'); + }); + }); +}); diff --git a/spec-main/asar-spec.ts b/spec-main/asar-spec.ts new file mode 100644 index 0000000000000..babfcdb8c1a21 --- /dev/null +++ b/spec-main/asar-spec.ts @@ -0,0 +1,127 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as url from 'url'; +import { Worker } from 'worker_threads'; +import { BrowserWindow, ipcMain } from 'electron/main'; +import { closeAllWindows } from './window-helpers'; +import { emittedOnce } from './events-helpers'; + +describe('asar package', () => { + const fixtures = path.join(__dirname, '..', 'spec', 'fixtures'); + const asarDir = path.join(fixtures, 'test.asar'); + + afterEach(closeAllWindows); + + describe('asar protocol', () => { + it('sets __dirname correctly', async function () { + after(function () { + ipcMain.removeAllListeners('dirname'); + }); + + const w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + const p = path.resolve(asarDir, 'web.asar', 'index.html'); + const dirnameEvent = emittedOnce(ipcMain, 'dirname'); + w.loadFile(p); + const [, dirname] = await dirnameEvent; + expect(dirname).to.equal(path.dirname(p)); + }); + + it('loads script tag in html', async function () { + after(function () { + ipcMain.removeAllListeners('ping'); + }); + + const w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + const p = path.resolve(asarDir, 'script.asar', 'index.html'); + const ping = emittedOnce(ipcMain, 'ping'); + w.loadFile(p); + const [, message] = await ping; + expect(message).to.equal('pong'); + }); + + it('loads video tag in html', async function () { + this.timeout(60000); + + after(function () { + ipcMain.removeAllListeners('asar-video'); + }); + + const w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + const p = path.resolve(asarDir, 'video.asar', 'index.html'); + w.loadFile(p); + const [, message, error] = await emittedOnce(ipcMain, 'asar-video'); + if (message === 'ended') { + expect(error).to.be.null(); + } else if (message === 'error') { + throw new Error(error); + } + }); + }); + + describe('worker', () => { + it('Worker can load asar file', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixtures, 'workers', 'load_worker.html')); + + const workerUrl = url.format({ + pathname: path.resolve(fixtures, 'workers', 'workers.asar', 'worker.js').replace(/\\/g, '/'), + protocol: 'file', + slashes: true + }); + const result = await w.webContents.executeJavaScript(`loadWorker('${workerUrl}')`); + expect(result).to.equal('success'); + }); + + it('SharedWorker can load asar file', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixtures, 'workers', 'load_shared_worker.html')); + + const workerUrl = url.format({ + pathname: path.resolve(fixtures, 'workers', 'workers.asar', 'shared_worker.js').replace(/\\/g, '/'), + protocol: 'file', + slashes: true + }); + const result = await w.webContents.executeJavaScript(`loadSharedWorker('${workerUrl}')`); + expect(result).to.equal('success'); + }); + }); + + describe('worker threads', function () { + it('should start worker thread from asar file', function (callback) { + const p = path.join(asarDir, 'worker_threads.asar', 'worker.js'); + const w = new Worker(p); + + w.on('error', (err) => callback(err)); + w.on('message', (message) => { + expect(message).to.equal('ping'); + w.terminate(); + + callback(null); + }); + }); + }); +}); diff --git a/spec-main/autofill-spec.ts b/spec-main/autofill-spec.ts new file mode 100644 index 0000000000000..35a98ed177a75 --- /dev/null +++ b/spec-main/autofill-spec.ts @@ -0,0 +1,28 @@ +import { BrowserWindow } from 'electron'; +import * as path from 'path'; +import { delay } from './spec-helpers'; +import { expect } from 'chai'; +import { closeAllWindows } from './window-helpers'; + +const fixturesPath = path.resolve(__dirname, '..', 'spec-main', 'fixtures'); + +describe('autofill', () => { + afterEach(closeAllWindows); + + it('can be selected via keyboard', async () => { + const w = new BrowserWindow({ show: true }); + await w.loadFile(path.join(fixturesPath, 'pages', 'datalist.html')); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab' }); + const inputText = 'clap'; + for (const keyCode of inputText) { + w.webContents.sendInputEvent({ type: 'char', keyCode }); + await delay(100); + } + + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Down' }); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Enter' }); + + const value = await w.webContents.executeJavaScript("document.querySelector('input').value"); + expect(value).to.equal('Eric Clapton'); + }); +}); diff --git a/spec-main/chromium-spec.ts b/spec-main/chromium-spec.ts index bb3b9999a5e93..3c5253a0a054e 100644 --- a/spec-main/chromium-spec.ts +++ b/spec-main/chromium-spec.ts @@ -1,28 +1,35 @@ -import * as chai from 'chai' -import * as chaiAsPromised from 'chai-as-promised' -import { BrowserWindow, WebContents, session, ipcMain } from 'electron' +import { expect } from 'chai'; +import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main'; import { emittedOnce } from './events-helpers'; import { closeAllWindows } from './window-helpers'; import * as https from 'https'; +import * as http from 'http'; import * as path from 'path'; import * as fs from 'fs'; +import * as url from 'url'; +import * as ChildProcess from 'child_process'; import { EventEmitter } from 'events'; +import { promisify } from 'util'; +import { ifit, ifdescribe, delay, defer } from './spec-helpers'; +import { AddressInfo } from 'net'; +import { PipeTransport } from './pipe-transport'; -const { expect } = chai +const features = process._linkedBinding('electron_common_features'); -chai.use(chaiAsPromised) -const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures') +const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures'); describe('reporting api', () => { - it('sends a report for a deprecation', async () => { - const reports = new EventEmitter + // TODO(nornagon): this started failing a lot on CI. Figure out why and fix + // it. + it.skip('sends a report for a deprecation', async () => { + const reports = new EventEmitter(); // The Reporting API only works on https with valid certs. To dodge having // to set up a trusted certificate, hack the validator. session.defaultSession.setCertificateVerifyProc((req, cb) => { - cb(0) - }) - const certPath = path.join(fixturesPath, 'certificates') + cb(0); + }); + const certPath = path.join(fixturesPath, 'certificates'); const options = { key: fs.readFileSync(path.join(certPath, 'server.key')), cert: fs.readFileSync(path.join(certPath, 'server.pem')), @@ -32,170 +39,2051 @@ describe('reporting api', () => { ], requestCert: true, rejectUnauthorized: false - } + }; const server = https.createServer(options, (req, res) => { if (req.url === '/report') { - let data = '' - req.on('data', (d) => data += d.toString('utf-8')) + let data = ''; + req.on('data', (d) => { data += d.toString('utf-8'); }); req.on('end', () => { - reports.emit('report', JSON.parse(data)) - }) + reports.emit('report', JSON.parse(data)); + }); } res.setHeader('Report-To', JSON.stringify({ group: 'default', max_age: 120, - endpoints: [ {url: `https://localhost:${(server.address() as any).port}/report`} ], - })) - res.setHeader('Content-Type', 'text/html') + endpoints: [{ url: `https://localhost:${(server.address() as any).port}/report` }] + })); + res.setHeader('Content-Type', 'text/html'); // using the deprecated `webkitRequestAnimationFrame` will trigger a // "deprecation" report. - res.end('') - }) - await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + res.end(''); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const bw = new BrowserWindow({ - show: false, - }) + show: false + }); try { - const reportGenerated = emittedOnce(reports, 'report') - const url = `https://localhost:${(server.address() as any).port}/a` - await bw.loadURL(url) - const [report] = await reportGenerated - expect(report).to.be.an('array') - expect(report[0].type).to.equal('deprecation') - expect(report[0].url).to.equal(url) - expect(report[0].body.id).to.equal('PrefixedRequestAnimationFrame') + const reportGenerated = emittedOnce(reports, 'report'); + const url = `https://localhost:${(server.address() as any).port}/a`; + await bw.loadURL(url); + const [report] = await reportGenerated; + expect(report).to.be.an('array'); + expect(report[0].type).to.equal('deprecation'); + expect(report[0].url).to.equal(url); + expect(report[0].body.id).to.equal('PrefixedRequestAnimationFrame'); } finally { - bw.destroy() - server.close() + bw.destroy(); + server.close(); } - }) -}) + }); +}); describe('window.postMessage', () => { afterEach(async () => { - await closeAllWindows() - }) + await closeAllWindows(); + }); it('sets the source and origin correctly', async () => { - const w = new BrowserWindow({show: false, webPreferences: {nodeIntegration: true}}) - w.loadURL(`file://${fixturesPath}/pages/window-open-postMessage-driver.html`) - const [, message] = await emittedOnce(ipcMain, 'complete') - expect(message.data).to.equal('testing') - expect(message.origin).to.equal('file://') - expect(message.sourceEqualsOpener).to.equal(true) - expect(message.eventOrigin).to.equal('file://') - }) -}) + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL(`file://${fixturesPath}/pages/window-open-postMessage-driver.html`); + const [, message] = await emittedOnce(ipcMain, 'complete'); + expect(message.data).to.equal('testing'); + expect(message.origin).to.equal('file://'); + expect(message.sourceEqualsOpener).to.equal(true); + expect(message.eventOrigin).to.equal('file://'); + }); +}); describe('focus handling', () => { - let webviewContents: WebContents = null as unknown as WebContents - let w: BrowserWindow = null as unknown as BrowserWindow + let webviewContents: WebContents = null as unknown as WebContents; + let w: BrowserWindow = null as unknown as BrowserWindow; beforeEach(async () => { w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true, - webviewTag: true + webviewTag: true, + contextIsolation: false } - }) + }); - const webviewReady = emittedOnce(w.webContents, 'did-attach-webview') - await w.loadFile(path.join(fixturesPath, 'pages', 'tab-focus-loop-elements.html')) - const [, wvContents] = await webviewReady - webviewContents = wvContents - await emittedOnce(webviewContents, 'did-finish-load') - w.focus() - }) + const webviewReady = emittedOnce(w.webContents, 'did-attach-webview'); + await w.loadFile(path.join(fixturesPath, 'pages', 'tab-focus-loop-elements.html')); + const [, wvContents] = await webviewReady; + webviewContents = wvContents; + await emittedOnce(webviewContents, 'did-finish-load'); + w.focus(); + }); afterEach(() => { - webviewContents = null as unknown as WebContents - w.destroy() - w = null as unknown as BrowserWindow - }) + webviewContents = null as unknown as WebContents; + w.destroy(); + w = null as unknown as BrowserWindow; + }); const expectFocusChange = async () => { - const [, focusedElementId] = await emittedOnce(ipcMain, 'focus-changed') - return focusedElementId - } + const [, focusedElementId] = await emittedOnce(ipcMain, 'focus-changed'); + return focusedElementId; + }; describe('a TAB press', () => { const tabPressEvent: any = { type: 'keyDown', keyCode: 'Tab' - } + }; it('moves focus to the next focusable item', async () => { - let focusChange = expectFocusChange() - w.webContents.sendInputEvent(tabPressEvent) - let focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-1', `should start focused in element-1, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - w.webContents.sendInputEvent(tabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - w.webContents.sendInputEvent(tabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - webviewContents.sendInputEvent(tabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - webviewContents.sendInputEvent(tabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've moved to element-3, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - w.webContents.sendInputEvent(tabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've looped back to element-1, it's instead in ${focusedElementId}`) - }) - }) + let focusChange = expectFocusChange(); + w.webContents.sendInputEvent(tabPressEvent); + let focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-1', `should start focused in element-1, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + w.webContents.sendInputEvent(tabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + w.webContents.sendInputEvent(tabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + webviewContents.sendInputEvent(tabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + webviewContents.sendInputEvent(tabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've moved to element-3, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + w.webContents.sendInputEvent(tabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've looped back to element-1, it's instead in ${focusedElementId}`); + }); + }); describe('a SHIFT + TAB press', () => { const shiftTabPressEvent: any = { type: 'keyDown', modifiers: ['Shift'], keyCode: 'Tab' - } + }; it('moves focus to the previous focusable item', async () => { - let focusChange = expectFocusChange() - w.webContents.sendInputEvent(shiftTabPressEvent) - let focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-3', `should start focused in element-3, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - w.webContents.sendInputEvent(shiftTabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - webviewContents.sendInputEvent(shiftTabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - webviewContents.sendInputEvent(shiftTabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - w.webContents.sendInputEvent(shiftTabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've moved to element-1, it's instead in ${focusedElementId}`) - - focusChange = expectFocusChange() - w.webContents.sendInputEvent(shiftTabPressEvent) - focusedElementId = await focusChange - expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've looped back to element-3, it's instead in ${focusedElementId}`) - }) - }) -}) + let focusChange = expectFocusChange(); + w.webContents.sendInputEvent(shiftTabPressEvent); + let focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-3', `should start focused in element-3, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + w.webContents.sendInputEvent(shiftTabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + webviewContents.sendInputEvent(shiftTabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + webviewContents.sendInputEvent(shiftTabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + w.webContents.sendInputEvent(shiftTabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've moved to element-1, it's instead in ${focusedElementId}`); + + focusChange = expectFocusChange(); + w.webContents.sendInputEvent(shiftTabPressEvent); + focusedElementId = await focusChange; + expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've looped back to element-3, it's instead in ${focusedElementId}`); + }); + }); +}); + +describe('web security', () => { + afterEach(closeAllWindows); + let server: http.Server; + let serverUrl: string; + before(async () => { + server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + serverUrl = `http://localhost:${(server.address() as any).port}`; + }); + after(() => { + server.close(); + }); + + it('engages CORB when web security is not disabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: true, nodeIntegration: true, contextIsolation: false } }); + const p = emittedOnce(ipcMain, 'success'); + await w.loadURL(`data:text/html,`); + await p; + }); + + // TODO(codebytere): Re-enable after Chromium fixes upstream v8_scriptormodule_legacy_lifetime crash. + xit('bypasses CORB when web security is disabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } }); + const p = emittedOnce(ipcMain, 'success'); + await w.loadURL(`data:text/html, + + `); + await p; + }); + + it('engages CORS when web security is not disabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: true, nodeIntegration: true, contextIsolation: false } }); + const p = emittedOnce(ipcMain, 'response'); + await w.loadURL(`data:text/html,`); + const [, response] = await p; + expect(response).to.equal('failed'); + }); + + it('bypasses CORS when web security is disabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } }); + const p = emittedOnce(ipcMain, 'response'); + await w.loadURL(`data:text/html,`); + const [, response] = await p; + expect(response).to.equal('passed'); + }); + + describe('accessing file://', () => { + async function loadFile (w: BrowserWindow) { + const thisFile = url.format({ + pathname: __filename.replace(/\\/g, '/'), + protocol: 'file', + slashes: true + }); + await w.loadURL(`data:text/html,`); + return await w.webContents.executeJavaScript('loadFile()'); + } + + it('is forbidden when web security is enabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: true } }); + const result = await loadFile(w); + expect(result).to.equal('failed'); + }); + + it('is allowed when web security is disabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + const result = await loadFile(w); + expect(result).to.equal('loaded'); + }); + }); + + describe('wasm-eval csp', () => { + async function loadWasm (csp: string) { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + enableBlinkFeatures: 'WebAssemblyCSP' + } + }); + await w.loadURL(`data:text/html, + + + `); + return await w.webContents.executeJavaScript('loadWasm()'); + } + + it('wasm codegen is disallowed by default', async () => { + const r = await loadWasm(''); + expect(r).to.equal('WebAssembly.instantiate(): Wasm code generation disallowed by embedder'); + }); + + it('wasm codegen is allowed with "wasm-unsafe-eval" csp', async () => { + const r = await loadWasm("'wasm-unsafe-eval'"); + expect(r).to.equal('loaded'); + }); + }); + + it('does not crash when multiple WebContent are created with web security disabled', () => { + const options = { show: false, webPreferences: { webSecurity: false } }; + const w1 = new BrowserWindow(options); + w1.loadURL(serverUrl); + const w2 = new BrowserWindow(options); + w2.loadURL(serverUrl); + }); +}); + +describe('command line switches', () => { + let appProcess: ChildProcess.ChildProcessWithoutNullStreams | undefined; + afterEach(() => { + if (appProcess && !appProcess.killed) { + appProcess.kill(); + appProcess = undefined; + } + }); + describe('--lang switch', () => { + const currentLocale = app.getLocale(); + const testLocale = async (locale: string, result: string, printEnv: boolean = false) => { + const appPath = path.join(fixturesPath, 'api', 'locale-check'); + const args = [appPath, `--set-lang=${locale}`]; + if (printEnv) { + args.push('--print-env'); + } + appProcess = ChildProcess.spawn(process.execPath, args); + + let output = ''; + appProcess.stdout.on('data', (data) => { output += data; }); + let stderr = ''; + appProcess.stderr.on('data', (data) => { stderr += data; }); + + const [code, signal] = await emittedOnce(appProcess, 'exit'); + if (code !== 0) { + throw new Error(`Process exited with code "${code}" signal "${signal}" output "${output}" stderr "${stderr}"`); + } + + output = output.replace(/(\r\n|\n|\r)/gm, ''); + expect(output).to.equal(result); + }; + + it('should set the locale', async () => testLocale('fr', 'fr')); + it('should not set an invalid locale', async () => testLocale('asdfkl', currentLocale)); + + const lcAll = String(process.env.LC_ALL); + ifit(process.platform === 'linux')('current process has a valid LC_ALL env', async () => { + // The LC_ALL env should not be set to DOM locale string. + expect(lcAll).to.not.equal(app.getLocale()); + }); + ifit(process.platform === 'linux')('should not change LC_ALL', async () => testLocale('fr', lcAll, true)); + ifit(process.platform === 'linux')('should not change LC_ALL when setting invalid locale', async () => testLocale('asdfkl', lcAll, true)); + ifit(process.platform === 'linux')('should not change LC_ALL when --lang is not set', async () => testLocale('', lcAll, true)); + }); + + describe('--remote-debugging-pipe switch', () => { + it('should expose CDP via pipe', async () => { + const electronPath = process.execPath; + appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-pipe'], { + stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe'] + }) as ChildProcess.ChildProcessWithoutNullStreams; + const stdio = appProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; + const pipe = new PipeTransport(stdio[3], stdio[4]); + const versionPromise = new Promise(resolve => { pipe.onmessage = resolve; }); + pipe.send({ id: 1, method: 'Browser.getVersion', params: {} }); + const message = (await versionPromise) as any; + expect(message.id).to.equal(1); + expect(message.result.product).to.contain('Chrome'); + expect(message.result.userAgent).to.contain('Electron'); + }); + it('should override --remote-debugging-port switch', async () => { + const electronPath = process.execPath; + appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-pipe', '--remote-debugging-port=0'], { + stdio: ['inherit', 'inherit', 'pipe', 'pipe', 'pipe'] + }) as ChildProcess.ChildProcessWithoutNullStreams; + let stderr = ''; + appProcess.stderr.on('data', (data: string) => { stderr += data; }); + const stdio = appProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; + const pipe = new PipeTransport(stdio[3], stdio[4]); + const versionPromise = new Promise(resolve => { pipe.onmessage = resolve; }); + pipe.send({ id: 1, method: 'Browser.getVersion', params: {} }); + const message = (await versionPromise) as any; + expect(message.id).to.equal(1); + expect(stderr).to.not.include('DevTools listening on'); + }); + it('should shut down Electron upon Browser.close CDP command', async () => { + const electronPath = process.execPath; + appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-pipe'], { + stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe'] + }) as ChildProcess.ChildProcessWithoutNullStreams; + const stdio = appProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; + const pipe = new PipeTransport(stdio[3], stdio[4]); + pipe.send({ id: 1, method: 'Browser.close', params: {} }); + await new Promise(resolve => { appProcess!.on('exit', resolve); }); + }); + }); + + describe('--remote-debugging-port switch', () => { + it('should display the discovery page', (done) => { + const electronPath = process.execPath; + let output = ''; + appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-port=']); + appProcess.stdout.on('data', (data) => { + console.log(data); + }); + + appProcess.stderr.on('data', (data) => { + console.log(data); + output += data; + const m = /DevTools listening on ws:\/\/127.0.0.1:(\d+)\//.exec(output); + if (m) { + appProcess!.stderr.removeAllListeners('data'); + const port = m[1]; + http.get(`http://127.0.0.1:${port}`, (res) => { + try { + expect(res.statusCode).to.eql(200); + expect(parseInt(res.headers['content-length']!)).to.be.greaterThan(0); + done(); + } catch (e) { + done(e); + } finally { + res.destroy(); + } + }); + } + }); + }); + }); +}); + +describe('chromium features', () => { + afterEach(closeAllWindows); + + describe('accessing key names also used as Node.js module names', () => { + it('does not crash', (done) => { + const w = new BrowserWindow({ show: false }); + w.webContents.once('did-finish-load', () => { done(); }); + w.webContents.once('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'external-string.html')); + }); + }); + + describe('loading jquery', () => { + it('does not crash', (done) => { + const w = new BrowserWindow({ show: false }); + w.webContents.once('did-finish-load', () => { done(); }); + w.webContents.once('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'jquery.html')); + }); + }); + + describe('navigator.languages', () => { + it('should return the system locale only', async () => { + const appLocale = app.getLocale(); + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + const languages = await w.webContents.executeJavaScript('navigator.languages'); + expect(languages.length).to.be.greaterThan(0); + expect(languages).to.contain(appLocale); + }); + }); + + describe('navigator.serviceWorker', () => { + it('should register for file scheme', (done) => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + partition: 'sw-file-scheme-spec', + contextIsolation: false + } + }); + w.webContents.on('ipc-message', (event, channel, message) => { + if (channel === 'reload') { + w.webContents.reload(); + } else if (channel === 'error') { + done(message); + } else if (channel === 'response') { + expect(message).to.equal('Hello from serviceWorker!'); + session.fromPartition('sw-file-scheme-spec').clearStorageData({ + storages: ['serviceworkers'] + }).then(() => done()); + } + }); + w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'index.html')); + }); + + it('should register for intercepted file scheme', (done) => { + const customSession = session.fromPartition('intercept-file'); + customSession.protocol.interceptBufferProtocol('file', (request, callback) => { + let file = url.parse(request.url).pathname!; + if (file[0] === '/' && process.platform === 'win32') file = file.slice(1); + + const content = fs.readFileSync(path.normalize(file)); + const ext = path.extname(file); + let type = 'text/html'; + + if (ext === '.js') type = 'application/javascript'; + callback({ data: content, mimeType: type } as any); + }); + + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + session: customSession, + contextIsolation: false + } + }); + w.webContents.on('ipc-message', (event, channel, message) => { + if (channel === 'reload') { + w.webContents.reload(); + } else if (channel === 'error') { + done(`unexpected error : ${message}`); + } else if (channel === 'response') { + expect(message).to.equal('Hello from serviceWorker!'); + customSession.clearStorageData({ + storages: ['serviceworkers'] + }).then(() => { + customSession.protocol.uninterceptProtocol('file'); + done(); + }); + } + }); + w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'index.html')); + }); + + it('should register for custom scheme', (done) => { + const customSession = session.fromPartition('custom-scheme'); + customSession.protocol.registerFileProtocol(serviceWorkerScheme, (request, callback) => { + let file = url.parse(request.url).pathname!; + if (file[0] === '/' && process.platform === 'win32') file = file.slice(1); + + callback({ path: path.normalize(file) } as any); + }); + + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + session: customSession, + contextIsolation: false + } + }); + w.webContents.on('ipc-message', (event, channel, message) => { + if (channel === 'reload') { + w.webContents.reload(); + } else if (channel === 'error') { + done(`unexpected error : ${message}`); + } else if (channel === 'response') { + expect(message).to.equal('Hello from serviceWorker!'); + customSession.clearStorageData({ + storages: ['serviceworkers'] + }).then(() => { + customSession.protocol.uninterceptProtocol(serviceWorkerScheme); + done(); + }); + } + }); + w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'custom-scheme-index.html')); + }); + + it('should not crash when nodeIntegration is enabled', (done) => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: true, + partition: 'sw-file-scheme-worker-spec', + contextIsolation: false + } + }); + + w.webContents.on('ipc-message', (event, channel, message) => { + if (channel === 'reload') { + w.webContents.reload(); + } else if (channel === 'error') { + done(`unexpected error : ${message}`); + } else if (channel === 'response') { + expect(message).to.equal('Hello from serviceWorker!'); + session.fromPartition('sw-file-scheme-worker-spec').clearStorageData({ + storages: ['serviceworkers'] + }).then(() => done()); + } + }); + + w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'index.html')); + }); + }); + + describe('navigator.geolocation', () => { + before(function () { + if (!features.isFakeLocationProviderEnabled()) { + return this.skip(); + } + }); + + it('returns error when permission is denied', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + partition: 'geolocation-spec', + contextIsolation: false + } + }); + const message = emittedOnce(w.webContents, 'ipc-message'); + w.webContents.session.setPermissionRequestHandler((wc, permission, callback) => { + if (permission === 'geolocation') { + callback(false); + } else { + callback(true); + } + }); + w.loadFile(path.join(fixturesPath, 'pages', 'geolocation', 'index.html')); + const [, channel] = await message; + expect(channel).to.equal('success', 'unexpected response from geolocation api'); + }); + }); + + describe('web workers', () => { + let appProcess: ChildProcess.ChildProcessWithoutNullStreams | undefined; + + afterEach(() => { + if (appProcess && !appProcess.killed) { + appProcess.kill(); + appProcess = undefined; + } + }); + + it('Worker with nodeIntegrationInWorker has access to self.module.paths', async () => { + const appPath = path.join(__dirname, 'fixtures', 'apps', 'self-module-paths'); + + appProcess = ChildProcess.spawn(process.execPath, [appPath]); + + const [code] = await emittedOnce(appProcess, 'exit'); + expect(code).to.equal(0); + }); + }); + + describe('form submit', () => { + let server: http.Server; + let serverUrl: string; + + before(async () => { + server = http.createServer((req, res) => { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); + res.setHeader('Content-Type', 'application/json'); + req.on('end', () => { + res.end(`body:${body}`); + }); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + serverUrl = `http://localhost:${(server.address() as any).port}`; + }); + after(async () => { + server.close(); + await closeAllWindows(); + }); + + [true, false].forEach((isSandboxEnabled) => + describe(`sandbox=${isSandboxEnabled}`, () => { + it('posts data in the same window', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: isSandboxEnabled + } + }); + + await w.loadFile(path.join(fixturesPath, 'pages', 'form-with-data.html')); + + const loadPromise = emittedOnce(w.webContents, 'did-finish-load'); + + w.webContents.executeJavaScript(` + const form = document.querySelector('form') + form.action = '${serverUrl}'; + form.submit(); + `); + + await loadPromise; + + const res = await w.webContents.executeJavaScript('document.body.innerText'); + expect(res).to.equal('body:greeting=hello'); + }); + + it('posts data to a new window with target=_blank', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: isSandboxEnabled + } + }); + + await w.loadFile(path.join(fixturesPath, 'pages', 'form-with-data.html')); + + const windowCreatedPromise = emittedOnce(app, 'browser-window-created'); + + w.webContents.executeJavaScript(` + const form = document.querySelector('form') + form.action = '${serverUrl}'; + form.target = '_blank'; + form.submit(); + `); + + const [, newWin] = await windowCreatedPromise; + + const res = await newWin.webContents.executeJavaScript('document.body.innerText'); + expect(res).to.equal('body:greeting=hello'); + }); + }) + ); + }); + + describe('window.open', () => { + for (const show of [true, false]) { + it(`shows the child regardless of parent visibility when parent {show=${show}}`, async () => { + const w = new BrowserWindow({ show }); + + // toggle visibility + if (show) { + w.hide(); + } else { + w.show(); + } + + defer(() => { w.close(); }); + + const newWindow = emittedOnce(w.webContents, 'new-window'); + w.loadFile(path.join(fixturesPath, 'pages', 'window-open.html')); + const [,,,, options] = await newWindow; + expect(options.show).to.equal(true); + }); + } + + it('disables node integration when it is disabled on the parent window for chrome devtools URLs', async () => { + // NB. webSecurity is disabled because native window.open() is not + // allowed to load devtools:// URLs. + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webSecurity: false } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript(` + { b = window.open('devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no'); null } + `); + const [, contents] = await emittedOnce(app, 'web-contents-created'); + const typeofProcessGlobal = await contents.executeJavaScript('typeof process'); + expect(typeofProcessGlobal).to.equal('undefined'); + }); + + it('can disable node integration when it is enabled on the parent window', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript(` + { b = window.open('about:blank', '', 'nodeIntegration=no,show=no'); null } + `); + const [, contents] = await emittedOnce(app, 'web-contents-created'); + const typeofProcessGlobal = await contents.executeJavaScript('typeof process'); + expect(typeofProcessGlobal).to.equal('undefined'); + }); + + // TODO(jkleinsc) fix this flaky test on WOA + ifit(process.platform !== 'win32' || process.arch !== 'arm64')('disables JavaScript when it is disabled on the parent window', async () => { + const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true } }); + w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html')); + const windowUrl = require('url').format({ + pathname: `${fixturesPath}/pages/window-no-javascript.html`, + protocol: 'file', + slashes: true + }); + w.webContents.executeJavaScript(` + { b = window.open(${JSON.stringify(windowUrl)}, '', 'javascript=no,show=no'); null } + `); + const [, contents] = await emittedOnce(app, 'web-contents-created'); + await emittedOnce(contents, 'did-finish-load'); + // Click link on page + contents.sendInputEvent({ type: 'mouseDown', clickCount: 1, x: 1, y: 1 }); + contents.sendInputEvent({ type: 'mouseUp', clickCount: 1, x: 1, y: 1 }); + const [, window] = await emittedOnce(app, 'browser-window-created'); + const preferences = window.webContents.getLastWebPreferences(); + expect(preferences.javascript).to.be.false(); + }); + + it('defines a window.location getter', async () => { + let targetURL: string; + if (process.platform === 'win32') { + targetURL = `file:///${fixturesPath.replace(/\\/g, '/')}/pages/base-page.html`; + } else { + targetURL = `file://${fixturesPath}/pages/base-page.html`; + } + const w = new BrowserWindow({ show: false }); + w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html')); + w.webContents.executeJavaScript(`{ b = window.open(${JSON.stringify(targetURL)}); null }`); + const [, window] = await emittedOnce(app, 'browser-window-created'); + await emittedOnce(window.webContents, 'did-finish-load'); + expect(await w.webContents.executeJavaScript('b.location.href')).to.equal(targetURL); + }); + + it('defines a window.location setter', async () => { + const w = new BrowserWindow({ show: false }); + w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html')); + w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }'); + const [, { webContents }] = await emittedOnce(app, 'browser-window-created'); + await emittedOnce(webContents, 'did-finish-load'); + // When it loads, redirect + w.webContents.executeJavaScript(`{ b.location = ${JSON.stringify(`file://${fixturesPath}/pages/base-page.html`)}; null }`); + await emittedOnce(webContents, 'did-finish-load'); + }); + + it('defines a window.location.href setter', async () => { + const w = new BrowserWindow({ show: false }); + w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html')); + w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }'); + const [, { webContents }] = await emittedOnce(app, 'browser-window-created'); + await emittedOnce(webContents, 'did-finish-load'); + // When it loads, redirect + w.webContents.executeJavaScript(`{ b.location.href = ${JSON.stringify(`file://${fixturesPath}/pages/base-page.html`)}; null }`); + await emittedOnce(webContents, 'did-finish-load'); + }); + + it('open a blank page when no URL is specified', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('{ b = window.open(); null }'); + const [, { webContents }] = await emittedOnce(app, 'browser-window-created'); + await emittedOnce(webContents, 'did-finish-load'); + expect(await w.webContents.executeJavaScript('b.location.href')).to.equal('about:blank'); + }); + + it('open a blank page when an empty URL is specified', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('{ b = window.open(\'\'); null }'); + const [, { webContents }] = await emittedOnce(app, 'browser-window-created'); + await emittedOnce(webContents, 'did-finish-load'); + expect(await w.webContents.executeJavaScript('b.location.href')).to.equal('about:blank'); + }); + + it('does not throw an exception when the frameName is a built-in object property', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('{ b = window.open(\'\', \'__proto__\'); null }'); + const [, , frameName] = await emittedOnce(w.webContents, 'new-window'); + + expect(frameName).to.equal('__proto__'); + }); + }); + + describe('window.opener', () => { + it('is null for main window', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + w.loadFile(path.join(fixturesPath, 'pages', 'window-opener.html')); + const [, channel, opener] = await emittedOnce(w.webContents, 'ipc-message'); + expect(channel).to.equal('opener'); + expect(opener).to.equal(null); + }); + }); + + describe('navigator.mediaDevices', () => { + afterEach(closeAllWindows); + afterEach(() => { + session.defaultSession.setPermissionCheckHandler(null); + session.defaultSession.setPermissionRequestHandler(null); + }); + + it('can return labels of enumerated devices', async () => { + const w = new BrowserWindow({ show: false }); + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const labels = await w.webContents.executeJavaScript('navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => d.label))'); + expect(labels.some((l: any) => l)).to.be.true(); + }); + + it('does not return labels of enumerated devices when permission denied', async () => { + session.defaultSession.setPermissionCheckHandler(() => false); + const w = new BrowserWindow({ show: false }); + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const labels = await w.webContents.executeJavaScript('navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => d.label))'); + expect(labels.some((l: any) => l)).to.be.false(); + }); + + it('returns the same device ids across reloads', async () => { + const ses = session.fromPartition('persist:media-device-id'); + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + session: ses, + contextIsolation: false + } + }); + w.loadFile(path.join(fixturesPath, 'pages', 'media-id-reset.html')); + const [, firstDeviceIds] = await emittedOnce(ipcMain, 'deviceIds'); + const [, secondDeviceIds] = await emittedOnce(ipcMain, 'deviceIds', () => w.webContents.reload()); + expect(firstDeviceIds).to.deep.equal(secondDeviceIds); + }); + + it('can return new device id when cookie storage is cleared', async () => { + const ses = session.fromPartition('persist:media-device-id'); + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + session: ses, + contextIsolation: false + } + }); + w.loadFile(path.join(fixturesPath, 'pages', 'media-id-reset.html')); + const [, firstDeviceIds] = await emittedOnce(ipcMain, 'deviceIds'); + await ses.clearStorageData({ storages: ['cookies'] }); + const [, secondDeviceIds] = await emittedOnce(ipcMain, 'deviceIds', () => w.webContents.reload()); + expect(firstDeviceIds).to.not.deep.equal(secondDeviceIds); + }); + + it('provides a securityOrigin to the request handler', async () => { + session.defaultSession.setPermissionRequestHandler( + (wc, permission, callback, details) => { + if (details.securityOrigin !== undefined) { + callback(true); + } else { + callback(false); + } + } + ); + const w = new BrowserWindow({ show: false }); + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const labels = await w.webContents.executeJavaScript(`navigator.mediaDevices.getUserMedia({ + video: { + mandatory: { + chromeMediaSource: "desktop", + minWidth: 1280, + maxWidth: 1280, + minHeight: 720, + maxHeight: 720 + } + } + }).then((stream) => stream.getVideoTracks())`); + expect(labels.some((l: any) => l)).to.be.true(); + }); + }); + + describe('window.opener access', () => { + const scheme = 'app'; + + const fileUrl = `file://${fixturesPath}/pages/window-opener-location.html`; + const httpUrl1 = `${scheme}://origin1`; + const httpUrl2 = `${scheme}://origin2`; + const fileBlank = `file://${fixturesPath}/pages/blank.html`; + const httpBlank = `${scheme}://origin1/blank`; + + const table = [ + { parent: fileBlank, child: httpUrl1, nodeIntegration: false, openerAccessible: false }, + { parent: fileBlank, child: httpUrl1, nodeIntegration: true, openerAccessible: false }, + + // {parent: httpBlank, child: fileUrl, nodeIntegration: false, openerAccessible: false}, // can't window.open() + // {parent: httpBlank, child: fileUrl, nodeIntegration: true, openerAccessible: false}, // can't window.open() + + // NB. this is different from Chrome's behavior, which isolates file: urls from each other + { parent: fileBlank, child: fileUrl, nodeIntegration: false, openerAccessible: true }, + { parent: fileBlank, child: fileUrl, nodeIntegration: true, openerAccessible: true }, + + { parent: httpBlank, child: httpUrl1, nodeIntegration: false, openerAccessible: true }, + { parent: httpBlank, child: httpUrl1, nodeIntegration: true, openerAccessible: true }, + + { parent: httpBlank, child: httpUrl2, nodeIntegration: false, openerAccessible: false }, + { parent: httpBlank, child: httpUrl2, nodeIntegration: true, openerAccessible: false } + ]; + const s = (url: string) => url.startsWith('file') ? 'file://...' : url; + + before(() => { + protocol.registerFileProtocol(scheme, (request, callback) => { + if (request.url.includes('blank')) { + callback(`${fixturesPath}/pages/blank.html`); + } else { + callback(`${fixturesPath}/pages/window-opener-location.html`); + } + }); + }); + after(() => { + protocol.unregisterProtocol(scheme); + }); + afterEach(closeAllWindows); + + describe('when opened from main window', () => { + for (const { parent, child, nodeIntegration, openerAccessible } of table) { + for (const sandboxPopup of [false, true]) { + const description = `when parent=${s(parent)} opens child=${s(child)} with nodeIntegration=${nodeIntegration} sandboxPopup=${sandboxPopup}, child should ${openerAccessible ? '' : 'not '}be able to access opener`; + it(description, async () => { + const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.webContents.setWindowOpenHandler(() => ({ + action: 'allow', + overrideBrowserWindowOptions: { + webPreferences: { + sandbox: sandboxPopup + } + } + })); + await w.loadURL(parent); + const childOpenerLocation = await w.webContents.executeJavaScript(`new Promise(resolve => { + window.addEventListener('message', function f(e) { + resolve(e.data) + }) + window.open(${JSON.stringify(child)}, "", "show=no,nodeIntegration=${nodeIntegration ? 'yes' : 'no'}") + })`); + if (openerAccessible) { + expect(childOpenerLocation).to.be.a('string'); + } else { + expect(childOpenerLocation).to.be.null(); + } + }); + } + } + }); + + describe('when opened from ', () => { + for (const { parent, child, nodeIntegration, openerAccessible } of table) { + const description = `when parent=${s(parent)} opens child=${s(child)} with nodeIntegration=${nodeIntegration}, child should ${openerAccessible ? '' : 'not '}be able to access opener`; + it(description, async () => { + // This test involves three contexts: + // 1. The root BrowserWindow in which the test is run, + // 2. A belonging to the root window, + // 3. A window opened by calling window.open() from within the . + // We are testing whether context (3) can access context (2) under various conditions. + + // This is context (1), the base window for the test. + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } }); + await w.loadURL('about:blank'); + + const parentCode = `new Promise((resolve) => { + // This is context (3), a child window of the WebView. + const child = window.open(${JSON.stringify(child)}, "", "show=no,contextIsolation=no,nodeIntegration=yes") + window.addEventListener("message", e => { + resolve(e.data) + }) + })`; + const childOpenerLocation = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => { + // This is context (2), a WebView which will call window.open() + const webview = new WebView() + webview.setAttribute('nodeintegration', '${nodeIntegration ? 'on' : 'off'}') + webview.setAttribute('webpreferences', 'contextIsolation=no') + webview.setAttribute('allowpopups', 'on') + webview.src = ${JSON.stringify(parent + '?p=' + encodeURIComponent(child))} + webview.addEventListener('dom-ready', async () => { + webview.executeJavaScript(${JSON.stringify(parentCode)}).then(resolve, reject) + }) + document.body.appendChild(webview) + })`); + if (openerAccessible) { + expect(childOpenerLocation).to.be.a('string'); + } else { + expect(childOpenerLocation).to.be.null(); + } + }); + } + }); + }); + + describe('storage', () => { + describe('custom non standard schemes', () => { + const protocolName = 'storage'; + let contents: WebContents; + before(() => { + protocol.registerFileProtocol(protocolName, (request, callback) => { + const parsedUrl = url.parse(request.url); + let filename; + switch (parsedUrl.pathname) { + case '/localStorage' : filename = 'local_storage.html'; break; + case '/sessionStorage' : filename = 'session_storage.html'; break; + case '/WebSQL' : filename = 'web_sql.html'; break; + case '/indexedDB' : filename = 'indexed_db.html'; break; + case '/cookie' : filename = 'cookie.html'; break; + default : filename = ''; + } + callback({ path: `${fixturesPath}/pages/storage/${filename}` }); + }); + }); + + after(() => { + protocol.unregisterProtocol(protocolName); + }); + + beforeEach(() => { + contents = (webContents as any).create({ + nodeIntegration: true, + contextIsolation: false + }); + }); + + afterEach(() => { + (contents as any).destroy(); + contents = null as any; + }); + + it('cannot access localStorage', async () => { + const response = emittedOnce(ipcMain, 'local-storage-response'); + contents.loadURL(protocolName + '://host/localStorage'); + const [, error] = await response; + expect(error).to.equal('Failed to read the \'localStorage\' property from \'Window\': Access is denied for this document.'); + }); + + it('cannot access sessionStorage', async () => { + const response = emittedOnce(ipcMain, 'session-storage-response'); + contents.loadURL(`${protocolName}://host/sessionStorage`); + const [, error] = await response; + expect(error).to.equal('Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.'); + }); + + it('cannot access WebSQL database', async () => { + const response = emittedOnce(ipcMain, 'web-sql-response'); + contents.loadURL(`${protocolName}://host/WebSQL`); + const [, error] = await response; + expect(error).to.equal('Failed to execute \'openDatabase\' on \'Window\': Access to the WebDatabase API is denied in this context.'); + }); + + it('cannot access indexedDB', async () => { + const response = emittedOnce(ipcMain, 'indexed-db-response'); + contents.loadURL(`${protocolName}://host/indexedDB`); + const [, error] = await response; + expect(error).to.equal('Failed to execute \'open\' on \'IDBFactory\': access to the Indexed Database API is denied in this context.'); + }); + + it('cannot access cookie', async () => { + const response = emittedOnce(ipcMain, 'cookie-response'); + contents.loadURL(`${protocolName}://host/cookie`); + const [, error] = await response; + expect(error).to.equal('Failed to set the \'cookie\' property on \'Document\': Access is denied for this document.'); + }); + }); + + describe('can be accessed', () => { + let server: http.Server; + let serverUrl: string; + let serverCrossSiteUrl: string; + before((done) => { + server = http.createServer((req, res) => { + const respond = () => { + if (req.url === '/redirect-cross-site') { + res.setHeader('Location', `${serverCrossSiteUrl}/redirected`); + res.statusCode = 302; + res.end(); + } else if (req.url === '/redirected') { + res.end(''); + } else { + res.end(); + } + }; + setTimeout(respond, 0); + }); + server.listen(0, '127.0.0.1', () => { + serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + serverCrossSiteUrl = `http://localhost:${(server.address() as AddressInfo).port}`; + done(); + }); + }); + + after(() => { + server.close(); + server = null as any; + }); + + afterEach(closeAllWindows); + + const testLocalStorageAfterXSiteRedirect = (testTitle: string, extraPreferences = {}) => { + it(testTitle, async () => { + const w = new BrowserWindow({ + show: false, + ...extraPreferences + }); + let redirected = false; + w.webContents.on('crashed', () => { + expect.fail('renderer crashed / was killed'); + }); + w.webContents.on('did-redirect-navigation', (event, url) => { + expect(url).to.equal(`${serverCrossSiteUrl}/redirected`); + redirected = true; + }); + await w.loadURL(`${serverUrl}/redirect-cross-site`); + expect(redirected).to.be.true('didnt redirect'); + }); + }; + + testLocalStorageAfterXSiteRedirect('after a cross-site redirect'); + testLocalStorageAfterXSiteRedirect('after a cross-site redirect in sandbox mode', { sandbox: true }); + }); + + describe('enableWebSQL webpreference', () => { + const origin = `${standardScheme}://fake-host`; + const filePath = path.join(fixturesPath, 'pages', 'storage', 'web_sql.html'); + const sqlPartition = 'web-sql-preference-test'; + const sqlSession = session.fromPartition(sqlPartition); + const securityError = 'An attempt was made to break through the security policy of the user agent.'; + let contents: WebContents, w: BrowserWindow; + + before(() => { + sqlSession.protocol.registerFileProtocol(standardScheme, (request, callback) => { + callback({ path: filePath }); + }); + }); + + after(() => { + sqlSession.protocol.unregisterProtocol(standardScheme); + }); + + afterEach(async () => { + if (contents) { + (contents as any).destroy(); + contents = null as any; + } + await closeAllWindows(); + (w as any) = null; + }); + + it('default value allows websql', async () => { + contents = (webContents as any).create({ + session: sqlSession, + nodeIntegration: true, + contextIsolation: false + }); + contents.loadURL(origin); + const [, error] = await emittedOnce(ipcMain, 'web-sql-response'); + expect(error).to.be.null(); + }); + + it('when set to false can disallow websql', async () => { + contents = (webContents as any).create({ + session: sqlSession, + nodeIntegration: true, + enableWebSQL: false, + contextIsolation: false + }); + contents.loadURL(origin); + const [, error] = await emittedOnce(ipcMain, 'web-sql-response'); + expect(error).to.equal(securityError); + }); + + it('when set to false does not disable indexedDB', async () => { + contents = (webContents as any).create({ + session: sqlSession, + nodeIntegration: true, + enableWebSQL: false, + contextIsolation: false + }); + contents.loadURL(origin); + const [, error] = await emittedOnce(ipcMain, 'web-sql-response'); + expect(error).to.equal(securityError); + const dbName = 'random'; + const result = await contents.executeJavaScript(` + new Promise((resolve, reject) => { + try { + let req = window.indexedDB.open('${dbName}'); + req.onsuccess = (event) => { + let db = req.result; + resolve(db.name); + } + req.onerror = (event) => { resolve(event.target.code); } + } catch (e) { + resolve(e.message); + } + }); + `); + expect(result).to.equal(dbName); + }); + + it('child webContents can override when the embedder has allowed websql', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + webviewTag: true, + session: sqlSession, + contextIsolation: false + } + }); + w.webContents.loadURL(origin); + const [, error] = await emittedOnce(ipcMain, 'web-sql-response'); + expect(error).to.be.null(); + const webviewResult = emittedOnce(ipcMain, 'web-sql-response'); + await w.webContents.executeJavaScript(` + new Promise((resolve, reject) => { + const webview = new WebView(); + webview.setAttribute('src', '${origin}'); + webview.setAttribute('webpreferences', 'enableWebSQL=0,contextIsolation=no'); + webview.setAttribute('partition', '${sqlPartition}'); + webview.setAttribute('nodeIntegration', 'on'); + document.body.appendChild(webview); + webview.addEventListener('dom-ready', () => resolve()); + }); + `); + const [, childError] = await webviewResult; + expect(childError).to.equal(securityError); + }); + + it('child webContents cannot override when the embedder has disallowed websql', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + enableWebSQL: false, + webviewTag: true, + session: sqlSession, + contextIsolation: false + } + }); + w.webContents.loadURL('data:text/html,'); + const webviewResult = emittedOnce(ipcMain, 'web-sql-response'); + await w.webContents.executeJavaScript(` + new Promise((resolve, reject) => { + const webview = new WebView(); + webview.setAttribute('src', '${origin}'); + webview.setAttribute('webpreferences', 'enableWebSQL=1,contextIsolation=no'); + webview.setAttribute('partition', '${sqlPartition}'); + webview.setAttribute('nodeIntegration', 'on'); + document.body.appendChild(webview); + webview.addEventListener('dom-ready', () => resolve()); + }); + `); + const [, childError] = await webviewResult; + expect(childError).to.equal(securityError); + }); + + it('child webContents can use websql when the embedder has allowed websql', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + webviewTag: true, + session: sqlSession, + contextIsolation: false + } + }); + w.webContents.loadURL(origin); + const [, error] = await emittedOnce(ipcMain, 'web-sql-response'); + expect(error).to.be.null(); + const webviewResult = emittedOnce(ipcMain, 'web-sql-response'); + await w.webContents.executeJavaScript(` + new Promise((resolve, reject) => { + const webview = new WebView(); + webview.setAttribute('src', '${origin}'); + webview.setAttribute('webpreferences', 'enableWebSQL=1,contextIsolation=no'); + webview.setAttribute('partition', '${sqlPartition}'); + webview.setAttribute('nodeIntegration', 'on'); + document.body.appendChild(webview); + webview.addEventListener('dom-ready', () => resolve()); + }); + `); + const [, childError] = await webviewResult; + expect(childError).to.be.null(); + }); + }); + }); + + ifdescribe(features.isPDFViewerEnabled())('PDF Viewer', () => { + const pdfSource = url.format({ + pathname: path.join(__dirname, 'fixtures', 'cat.pdf').replace(/\\/g, '/'), + protocol: 'file', + slashes: true + }); + + it('successfully loads a PDF file', async () => { + const w = new BrowserWindow({ show: false }); + + w.loadURL(pdfSource); + await emittedOnce(w.webContents, 'did-finish-load'); + }); + + it('opens when loading a pdf resource as top level navigation', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL(pdfSource); + const [, contents] = await emittedOnce(app, 'web-contents-created'); + await emittedOnce(contents, 'did-navigate'); + expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html'); + }); + + it('opens when loading a pdf resource in a iframe', async () => { + const w = new BrowserWindow({ show: false }); + w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'pdf-in-iframe.html')); + const [, contents] = await emittedOnce(app, 'web-contents-created'); + await emittedOnce(contents, 'did-navigate'); + expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html'); + }); + }); + + describe('window.history', () => { + describe('window.history.pushState', () => { + it('should push state after calling history.pushState() from the same url', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + // History should have current page by now. + expect((w.webContents as any).length()).to.equal(1); + + const waitCommit = emittedOnce(w.webContents, 'navigation-entry-committed'); + w.webContents.executeJavaScript('window.history.pushState({}, "")'); + await waitCommit; + // Initial page + pushed state. + expect((w.webContents as any).length()).to.equal(2); + }); + }); + }); + + describe('chrome://media-internals', () => { + it('loads the page successfully', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('chrome://media-internals'); + const pageExists = await w.webContents.executeJavaScript( + "window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')" + ); + expect(pageExists).to.be.true(); + }); + }); + + describe('chrome://webrtc-internals', () => { + it('loads the page successfully', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('chrome://webrtc-internals'); + const pageExists = await w.webContents.executeJavaScript( + "window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')" + ); + expect(pageExists).to.be.true(); + }); + }); + + describe('document.hasFocus', () => { + it('has correct value when multiple windows are opened', async () => { + const w1 = new BrowserWindow({ show: true }); + const w2 = new BrowserWindow({ show: true }); + const w3 = new BrowserWindow({ show: false }); + await w1.loadFile(path.join(__dirname, 'fixtures', 'blank.html')); + await w2.loadFile(path.join(__dirname, 'fixtures', 'blank.html')); + await w3.loadFile(path.join(__dirname, 'fixtures', 'blank.html')); + expect(webContents.getFocusedWebContents().id).to.equal(w2.webContents.id); + let focus = false; + focus = await w1.webContents.executeJavaScript( + 'document.hasFocus()' + ); + expect(focus).to.be.false(); + focus = await w2.webContents.executeJavaScript( + 'document.hasFocus()' + ); + expect(focus).to.be.true(); + focus = await w3.webContents.executeJavaScript( + 'document.hasFocus()' + ); + expect(focus).to.be.false(); + }); + }); + + describe('navigator.userAgentData', () => { + // These tests are done on an http server because navigator.userAgentData + // requires a secure context. + let server: http.Server; + let serverUrl: string; + before(async () => { + server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + serverUrl = `http://localhost:${(server.address() as any).port}`; + }); + after(() => { + server.close(); + }); + + describe('is not empty', () => { + it('by default', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL(serverUrl); + const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform'); + expect(platform).not.to.be.empty(); + }); + + it('when there is a session-wide UA override', async () => { + const ses = session.fromPartition(`${Math.random()}`); + ses.setUserAgent('foobar'); + const w = new BrowserWindow({ show: false, webPreferences: { session: ses } }); + await w.loadURL(serverUrl); + const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform'); + expect(platform).not.to.be.empty(); + }); + + it('when there is a WebContents-specific UA override', async () => { + const w = new BrowserWindow({ show: false }); + w.webContents.setUserAgent('foo'); + await w.loadURL(serverUrl); + const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform'); + expect(platform).not.to.be.empty(); + }); + + it('when there is a WebContents-specific UA override at load time', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL(serverUrl, { + userAgent: 'foo' + }); + const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform'); + expect(platform).not.to.be.empty(); + }); + }); + + describe('brand list', () => { + it('contains chromium', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL(serverUrl); + const brands = await w.webContents.executeJavaScript('navigator.userAgentData.brands'); + expect(brands.map((b: any) => b.brand)).to.include('Chromium'); + }); + }); + }); +}); + +describe('font fallback', () => { + async function getRenderedFonts (html: string) { + const w = new BrowserWindow({ show: false }); + try { + await w.loadURL(`data:text/html,${html}`); + w.webContents.debugger.attach(); + const sendCommand = (method: string, commandParams?: any) => w.webContents.debugger.sendCommand(method, commandParams); + const { nodeId } = (await sendCommand('DOM.getDocument')).root.children[0]; + await sendCommand('CSS.enable'); + const { fonts } = await sendCommand('CSS.getPlatformFontsForNode', { nodeId }); + return fonts; + } finally { + w.close(); + } + } + + it('should use Helvetica for sans-serif on Mac, and Arial on Windows and Linux', async () => { + const html = 'test'; + const fonts = await getRenderedFonts(html); + expect(fonts).to.be.an('array'); + expect(fonts).to.have.length(1); + if (process.platform === 'win32') { + expect(fonts[0].familyName).to.equal('Arial'); + } else if (process.platform === 'darwin') { + expect(fonts[0].familyName).to.equal('Helvetica'); + } else if (process.platform === 'linux') { + expect(fonts[0].familyName).to.equal('DejaVu Sans'); + } // I think this depends on the distro? We don't specify a default. + }); + + ifit(process.platform !== 'linux')('should fall back to Japanese font for sans-serif Japanese script', async function () { + const html = ` + + + + + test 智史 + + `; + const fonts = await getRenderedFonts(html); + expect(fonts).to.be.an('array'); + expect(fonts).to.have.length(1); + if (process.platform === 'win32') { expect(fonts[0].familyName).to.be.oneOf(['Meiryo', 'Yu Gothic']); } else if (process.platform === 'darwin') { expect(fonts[0].familyName).to.equal('Hiragino Kaku Gothic ProN'); } + }); +}); + +describe('iframe using HTML fullscreen API while window is OS-fullscreened', () => { + const fullscreenChildHtml = promisify(fs.readFile)( + path.join(fixturesPath, 'pages', 'fullscreen-oopif.html') + ); + let w: BrowserWindow, server: http.Server; + + before(() => { + server = http.createServer(async (_req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.write(await fullscreenChildHtml); + res.end(); + }); + + server.listen(8989, '127.0.0.1'); + }); + + beforeEach(() => { + w = new BrowserWindow({ + show: true, + fullscreen: true, + webPreferences: { + nodeIntegration: true, + nodeIntegrationInSubFrames: true, + contextIsolation: false + } + }); + }); + + afterEach(async () => { + await closeAllWindows(); + (w as any) = null; + server.close(); + }); + + it('can fullscreen from out-of-process iframes (OOPIFs)', async () => { + const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange'); + const html = + ''; + w.loadURL(`data:text/html,${html}`); + await fullscreenChange; + + const fullscreenWidth = await w.webContents.executeJavaScript( + "document.querySelector('iframe').offsetWidth" + ); + expect(fullscreenWidth > 0).to.be.true(); + + await w.webContents.executeJavaScript( + "document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')" + ); + + await delay(500); + + const width = await w.webContents.executeJavaScript( + "document.querySelector('iframe').offsetWidth" + ); + expect(width).to.equal(0); + }); + + // TODO(jkleinsc) fix this flaky test on WOA + ifit(process.platform !== 'win32' || process.arch !== 'arm64')('can fullscreen from in-process iframes', async () => { + const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange'); + w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html')); + await fullscreenChange; + + const fullscreenWidth = await w.webContents.executeJavaScript( + "document.querySelector('iframe').offsetWidth" + ); + expect(fullscreenWidth > 0).to.true(); + + await w.webContents.executeJavaScript('document.exitFullscreen()'); + const width = await w.webContents.executeJavaScript( + "document.querySelector('iframe').offsetWidth" + ); + expect(width).to.equal(0); + }); +}); + +describe('navigator.serial', () => { + let w: BrowserWindow; + before(async () => { + w = new BrowserWindow({ + show: false + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + }); + + const getPorts: any = () => { + return w.webContents.executeJavaScript(` + navigator.serial.requestPort().then(port => port.toString()).catch(err => err.toString()); + `, true); + }; + + after(closeAllWindows); + afterEach(() => { + session.defaultSession.setPermissionCheckHandler(null); + session.defaultSession.removeAllListeners('select-serial-port'); + }); + + it('does not return a port if select-serial-port event is not defined', async () => { + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const port = await getPorts(); + expect(port).to.equal('NotFoundError: No port selected by the user.'); + }); + + it('does not return a port when permission denied', async () => { + w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + callback(portList[0].portId); + }); + session.defaultSession.setPermissionCheckHandler(() => false); + const port = await getPorts(); + expect(port).to.equal('NotFoundError: No port selected by the user.'); + }); + + it('does not crash when select-serial-port is called with an invalid port', async () => { + w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + callback('i-do-not-exist'); + }); + const port = await getPorts(); + expect(port).to.equal('NotFoundError: No port selected by the user.'); + }); + + it('returns a port when select-serial-port event is defined', async () => { + let havePorts = false; + w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + if (portList.length > 0) { + havePorts = true; + callback(portList[0].portId); + } else { + callback(''); + } + }); + const port = await getPorts(); + if (havePorts) { + expect(port).to.equal('[object SerialPort]'); + } else { + expect(port).to.equal('NotFoundError: No port selected by the user.'); + } + }); + + it('navigator.serial.getPorts() returns values', async () => { + let havePorts = false; + + w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + if (portList.length > 0) { + havePorts = true; + callback(portList[0].portId); + } else { + callback(''); + } + }); + await getPorts(); + if (havePorts) { + const grantedPorts = await w.webContents.executeJavaScript('navigator.serial.getPorts()'); + expect(grantedPorts).to.not.be.empty(); + } + }); +}); + +describe('navigator.clipboard', () => { + let w: BrowserWindow; + before(async () => { + w = new BrowserWindow({ + webPreferences: { + enableBlinkFeatures: 'Serial' + } + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + }); + + const readClipboard: any = () => { + return w.webContents.executeJavaScript(` + navigator.clipboard.read().then(clipboard => clipboard.toString()).catch(err => err.message); + `, true); + }; + + after(closeAllWindows); + afterEach(() => { + session.defaultSession.setPermissionRequestHandler(null); + }); + + it('returns clipboard contents when a PermissionRequestHandler is not defined', async () => { + const clipboard = await readClipboard(); + expect(clipboard).to.not.equal('Read permission denied.'); + }); + + it('returns an error when permission denied', async () => { + session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => { + if (permission === 'clipboard-read') { + callback(false); + } else { + callback(true); + } + }); + const clipboard = await readClipboard(); + expect(clipboard).to.equal('Read permission denied.'); + }); + + it('returns clipboard contents when permission is granted', async () => { + session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => { + if (permission === 'clipboard-read') { + callback(true); + } else { + callback(false); + } + }); + const clipboard = await readClipboard(); + expect(clipboard).to.not.equal('Read permission denied.'); + }); +}); + +ifdescribe((process.platform !== 'linux' || app.isUnityRunning()))('navigator.setAppBadge/clearAppBadge', () => { + let w: BrowserWindow; + + const expectedBadgeCount = 42; + + const fireAppBadgeAction: any = (action: string, value: any) => { + return w.webContents.executeJavaScript(` + navigator.${action}AppBadge(${value}).then(() => 'success').catch(err => err.message)`); + }; + + // For some reason on macOS changing the badge count doesn't happen right away, so wait + // until it changes. + async function waitForBadgeCount (value: number) { + let badgeCount = app.getBadgeCount(); + while (badgeCount !== value) { + await new Promise(resolve => setTimeout(resolve, 10)); + badgeCount = app.getBadgeCount(); + } + return badgeCount; + } + + describe('in the renderer', () => { + before(async () => { + w = new BrowserWindow({ + show: false + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + }); + + after(() => { + app.badgeCount = 0; + closeAllWindows(); + }); + + it('setAppBadge can set a numerical value', async () => { + const result = await fireAppBadgeAction('set', expectedBadgeCount); + expect(result).to.equal('success'); + expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount); + }); + + it('setAppBadge can set an empty(dot) value', async () => { + const result = await fireAppBadgeAction('set'); + expect(result).to.equal('success'); + expect(waitForBadgeCount(0)).to.eventually.equal(0); + }); + + it('clearAppBadge can clear a value', async () => { + let result = await fireAppBadgeAction('set', expectedBadgeCount); + expect(result).to.equal('success'); + expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount); + result = await fireAppBadgeAction('clear'); + expect(result).to.equal('success'); + expect(waitForBadgeCount(0)).to.eventually.equal(0); + }); + }); + + describe('in a service worker', () => { + beforeEach(async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + partition: 'sw-file-scheme-spec', + contextIsolation: false + } + }); + }); + + afterEach(() => { + app.badgeCount = 0; + closeAllWindows(); + }); + + it('setAppBadge can be called in a ServiceWorker', (done) => { + w.webContents.on('ipc-message', (event, channel, message) => { + if (channel === 'reload') { + w.webContents.reload(); + } else if (channel === 'error') { + done(message); + } else if (channel === 'response') { + expect(message).to.equal('SUCCESS setting app badge'); + expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount); + session.fromPartition('sw-file-scheme-spec').clearStorageData({ + storages: ['serviceworkers'] + }).then(() => done()); + } + }); + w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'badge-index.html'), { search: '?setBadge' }); + }); + + it('clearAppBadge can be called in a ServiceWorker', (done) => { + w.webContents.on('ipc-message', (event, channel, message) => { + if (channel === 'reload') { + w.webContents.reload(); + } else if (channel === 'setAppBadge') { + expect(message).to.equal('SUCCESS setting app badge'); + expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount); + } else if (channel === 'error') { + done(message); + } else if (channel === 'response') { + expect(message).to.equal('SUCCESS clearing app badge'); + expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount); + session.fromPartition('sw-file-scheme-spec').clearStorageData({ + storages: ['serviceworkers'] + }).then(() => done()); + } + }); + w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))); + w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'badge-index.html'), { search: '?clearBadge' }); + }); + }); +}); + +describe('navigator.bluetooth', () => { + let w: BrowserWindow; + before(async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + enableBlinkFeatures: 'WebBluetooth' + } + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + }); + + after(closeAllWindows); + + it('can request bluetooth devices', async () => { + const bluetooth = await w.webContents.executeJavaScript(` + navigator.bluetooth.requestDevice({ acceptAllDevices: true}).then(device => "Found a device!").catch(err => err.message);`, true); + expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']); + }); +}); + +describe('navigator.hid', () => { + let w: BrowserWindow; + let server: http.Server; + let serverUrl: string; + before(async () => { + w = new BrowserWindow({ + show: false + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + serverUrl = `http://localhost:${(server.address() as any).port}`; + }); + + const getDevices: any = () => { + return w.webContents.executeJavaScript(` + navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString()); + `, true); + }; + + after(() => { + server.close(); + closeAllWindows(); + }); + afterEach(() => { + session.defaultSession.setPermissionCheckHandler(null); + session.defaultSession.setDevicePermissionHandler(null); + session.defaultSession.removeAllListeners('select-hid-device'); + }); + + it('does not return a device if select-hid-device event is not defined', async () => { + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const device = await getDevices(); + expect(device).to.equal(''); + }); + + it('does not return a device when permission denied', async () => { + let selectFired = false; + w.webContents.session.on('select-hid-device', (event, details, callback) => { + selectFired = true; + callback(); + }); + session.defaultSession.setPermissionCheckHandler(() => false); + const device = await getDevices(); + expect(selectFired).to.be.false(); + expect(device).to.equal(''); + }); + + it('returns a device when select-hid-device event is defined', async () => { + let haveDevices = false; + let selectFired = false; + w.webContents.session.on('select-hid-device', (event, details, callback) => { + expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number'); + selectFired = true; + if (details.deviceList.length > 0) { + haveDevices = true; + callback(details.deviceList[0].deviceId); + } else { + callback(); + } + }); + const device = await getDevices(); + expect(selectFired).to.be.true(); + if (haveDevices) { + expect(device).to.contain('[object HIDDevice]'); + } else { + expect(device).to.equal(''); + } + if (process.arch === 'arm64' || process.arch === 'arm') { + // arm CI returns HID devices - this block may need to change if CI hardware changes. + expect(haveDevices).to.be.true(); + // Verify that navigation will clear device permissions + const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()'); + expect(grantedDevices).to.not.be.empty(); + w.loadURL(serverUrl); + const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate'); + const frame = webFrameMain.fromId(frameProcessId, frameRoutingId); + expect(frame).to.not.be.empty(); + if (frame) { + const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.hid.getDevices()'); + expect(grantedDevicesOnNewPage).to.be.empty(); + } + } + }); + + it('returns a device when DevicePermissionHandler is defined', async () => { + let haveDevices = false; + let selectFired = false; + let gotDevicePerms = false; + w.webContents.session.on('select-hid-device', (event, details, callback) => { + selectFired = true; + if (details.deviceList.length > 0) { + const foundDevice = details.deviceList.find((device) => { + if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') { + haveDevices = true; + return true; + } + }); + if (foundDevice) { + callback(foundDevice.deviceId); + return; + } + } + callback(); + }); + session.defaultSession.setDevicePermissionHandler(() => { + gotDevicePerms = true; + return true; + }); + await w.webContents.executeJavaScript('navigator.hid.getDevices();', true); + const device = await getDevices(); + expect(selectFired).to.be.true(); + if (haveDevices) { + expect(device).to.contain('[object HIDDevice]'); + expect(gotDevicePerms).to.be.true(); + } else { + expect(device).to.equal(''); + } + }); +}); diff --git a/spec-main/crash-spec.ts b/spec-main/crash-spec.ts new file mode 100644 index 0000000000000..eb9718dac5acd --- /dev/null +++ b/spec-main/crash-spec.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const fixturePath = path.resolve(__dirname, 'fixtures', 'crash-cases'); + +let children: cp.ChildProcessWithoutNullStreams[] = []; + +const runFixtureAndEnsureCleanExit = (args: string[]) => { + let out = ''; + const child = cp.spawn(process.execPath, args); + children.push(child); + child.stdout.on('data', (chunk: Buffer) => { + out += chunk.toString(); + }); + child.stderr.on('data', (chunk: Buffer) => { + out += chunk.toString(); + }); + return new Promise((resolve) => { + child.on('exit', (code, signal) => { + if (code !== 0 || signal !== null) { + console.error(out); + } + expect(signal).to.equal(null, 'exit signal should be null'); + expect(code).to.equal(0, 'should have exited with code 0'); + children = children.filter(c => c !== child); + resolve(); + }); + }); +}; + +describe('crash cases', () => { + afterEach(() => { + for (const child of children) { + child.kill(); + } + expect(children).to.have.lengthOf(0, 'all child processes should have exited cleanly'); + children.length = 0; + }); + const cases = fs.readdirSync(fixturePath); + + for (const crashCase of cases) { + it(`the "${crashCase}" case should not crash`, () => { + const fixture = path.resolve(fixturePath, crashCase); + const argsFile = path.resolve(fixture, 'electron.args'); + const args = [fixture]; + if (fs.existsSync(argsFile)) { + args.push(...fs.readFileSync(argsFile, 'utf8').trim().split('\n')); + } + return runFixtureAndEnsureCleanExit(args); + }); + } +}); diff --git a/spec-main/events-helpers.ts b/spec-main/events-helpers.ts index 2e5252c098734..4cb3d231db77e 100644 --- a/spec-main/events-helpers.ts +++ b/spec-main/events-helpers.ts @@ -10,29 +10,46 @@ */ export const waitForEvent = (target: EventTarget, eventName: string) => { return new Promise(resolve => { - target.addEventListener(eventName, resolve, { once: true }) - }) -} + target.addEventListener(eventName, resolve, { once: true }); + }); +}; /** * @param {!EventEmitter} emitter * @param {string} eventName * @return {!Promise} With Event as the first item. */ -export const emittedOnce = (emitter: NodeJS.EventEmitter, eventName: string) => { - return emittedNTimes(emitter, eventName, 1).then(([result]) => result) -} +export const emittedOnce = (emitter: NodeJS.EventEmitter, eventName: string, trigger?: () => void) => { + return emittedNTimes(emitter, eventName, 1, trigger).then(([result]) => result); +}; -export const emittedNTimes = (emitter: NodeJS.EventEmitter, eventName: string, times: number) => { - const events: any[][] = [] - return new Promise(resolve => { +export const emittedNTimes = async (emitter: NodeJS.EventEmitter, eventName: string, times: number, trigger?: () => void) => { + const events: any[][] = []; + const p = new Promise(resolve => { const handler = (...args: any[]) => { - events.push(args) + events.push(args); if (events.length === times) { - emitter.removeListener(eventName, handler) - resolve(events) + emitter.removeListener(eventName, handler); + resolve(events); } - } - emitter.on(eventName, handler) - }) -} + }; + emitter.on(eventName, handler); + }); + if (trigger) { + await Promise.resolve(trigger()); + } + return p; +}; + +export const emittedUntil = async (emitter: NodeJS.EventEmitter, eventName: string, untilFn: Function) => { + const p = new Promise(resolve => { + const handler = (...args: any[]) => { + if (untilFn(...args)) { + emitter.removeListener(eventName, handler); + resolve(args); + } + }; + emitter.on(eventName, handler); + }); + return p; +}; diff --git a/spec-main/extensions-spec.ts b/spec-main/extensions-spec.ts index f18ec31f658fa..b2dc6d7f96c21 100644 --- a/spec-main/extensions-spec.ts +++ b/spec-main/extensions-spec.ts @@ -1,90 +1,704 @@ -import { expect } from 'chai' -import { session, BrowserWindow, ipcMain } from 'electron' -import { closeAllWindows } from './window-helpers' -import * as http from 'http' -import { AddressInfo } from 'net' -import * as path from 'path' -import { ifdescribe } from './spec-helpers' -import { emittedOnce } from './events-helpers' - -const fixtures = path.join(__dirname, 'fixtures') - -ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => { +import { expect } from 'chai'; +import { app, session, BrowserWindow, ipcMain, WebContents, Extension, Session } from 'electron/main'; +import { closeAllWindows, closeWindow } from './window-helpers'; +import * as http from 'http'; +import { AddressInfo } from 'net'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as WebSocket from 'ws'; +import { emittedOnce, emittedNTimes, emittedUntil } from './events-helpers'; +import { ifit } from './spec-helpers'; + +const uuid = require('uuid'); + +const fixtures = path.join(__dirname, 'fixtures'); + +describe('chrome extensions', () => { + const emptyPage = ''; + // NB. extensions are only allowed on http://, https:// and ftp:// (!) urls by default. - let server: http.Server - let url: string + let server: http.Server; + let url: string; + let port: string; before(async () => { - server = http.createServer((req, res) => res.end()) - await new Promise(resolve => server.listen(0, '127.0.0.1', () => { - url = `http://127.0.0.1:${(server.address() as AddressInfo).port}` - resolve() - })) - }) + server = http.createServer((req, res) => { + if (req.url === '/cors') { + res.setHeader('Access-Control-Allow-Origin', 'http://example.com'); + } + res.end(emptyPage); + }); + + const wss = new WebSocket.Server({ noServer: true }); + wss.on('connection', function connection (ws) { + ws.on('message', function incoming (message) { + if (message === 'foo') { + ws.send('bar'); + } + }); + }); + + await new Promise(resolve => server.listen(0, '127.0.0.1', () => { + port = String((server.address() as AddressInfo).port); + url = `http://127.0.0.1:${port}`; + resolve(); + })); + }); after(() => { - server.close() - }) + server.close(); + }); + afterEach(closeAllWindows); + afterEach(() => { + session.defaultSession.getAllExtensions().forEach((e: any) => { + session.defaultSession.removeExtension(e.id); + }); + }); + + it('does not crash when using chrome.management', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } }); + await w.loadURL('about:blank'); + + const promise = emittedOnce(app, 'web-contents-created'); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page')); + const args: any = await promise; + const wc: Electron.WebContents = args[1]; + await expect(wc.executeJavaScript(` + (() => { + return new Promise((resolve) => { + chrome.management.getSelf((info) => { + resolve(info); + }); + }) + })(); + `)).to.eventually.have.property('id'); + }); + + it('can open WebSQLDatabase in a background page', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } }); + await w.loadURL('about:blank'); + + const promise = emittedOnce(app, 'web-contents-created'); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page')); + const args: any = await promise; + const wc: Electron.WebContents = args[1]; + await expect(wc.executeJavaScript('(()=>{try{openDatabase("t", "1.0", "test", 2e5);return true;}catch(e){throw e}})()')).to.not.be.rejected(); + }); + + function fetch (contents: WebContents, url: string) { + return contents.executeJavaScript(`fetch(${JSON.stringify(url)})`); + } + + it('bypasses CORS in requests made from extensions', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } }); + const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page')); + await w.loadURL(`${extension.url}bare-page.html`); + await expect(fetch(w.webContents, `${url}/cors`)).to.not.be.rejectedWith(TypeError); + }); - afterEach(closeAllWindows) it('loads an extension', async () => { // NB. we have to use a persist: session (i.e. non-OTR) because the // extension registry is redirected to the main session. so installing an // extension in an in-memory session results in it being installed in the // default session. + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w.loadURL(url); + const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor'); + expect(bg).to.equal('red'); + }); + + it('does not crash when loading an extension with missing manifest', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const promise = customSession.loadExtension(path.join(fixtures, 'extensions', 'missing-manifest')); + await expect(promise).to.eventually.be.rejectedWith(/Manifest file is missing or unreadable/); + }); + + it('does not crash when failing to load an extension', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const promise = customSession.loadExtension(path.join(fixtures, 'extensions', 'load-error')); + await expect(promise).to.eventually.be.rejected(); + }); + + it('serializes a loaded extension', async () => { + const extensionPath = path.join(fixtures, 'extensions', 'red-bg'); + const manifest = JSON.parse(fs.readFileSync(path.join(extensionPath, 'manifest.json'), 'utf-8')); + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const extension = await customSession.loadExtension(extensionPath); + expect(extension.id).to.be.a('string'); + expect(extension.name).to.be.a('string'); + expect(extension.path).to.be.a('string'); + expect(extension.version).to.be.a('string'); + expect(extension.url).to.be.a('string'); + expect(extension.manifest).to.deep.equal(manifest); + }); + + it('removes an extension', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')); + { + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w.loadURL(url); + const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor'); + expect(bg).to.equal('red'); + } + customSession.removeExtension(id); + { + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w.loadURL(url); + const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor'); + expect(bg).to.equal(''); + } + }); + + it('emits extension lifecycle events', async () => { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadChromeExtension(path.join(fixtures, 'extensions', 'red-bg')) - const w = new BrowserWindow({show: false, webPreferences: {session: customSession}}) - await w.loadURL(url) - const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') - expect(bg).to.equal('red') - }) + + const loadedPromise = emittedOnce(customSession, 'extension-loaded'); + const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')); + const [, loadedExtension] = await loadedPromise; + const [, readyExtension] = await emittedUntil(customSession, 'extension-ready', (event: Event, extension: Extension) => { + return extension.name !== 'Chromium PDF Viewer' && extension.name !== 'CryptoTokenExtension'; + }); + + expect(loadedExtension).to.deep.equal(extension); + expect(readyExtension).to.deep.equal(extension); + + const unloadedPromise = emittedOnce(customSession, 'extension-unloaded'); + await customSession.removeExtension(extension.id); + const [, unloadedExtension] = await unloadedPromise; + expect(unloadedExtension).to.deep.equal(extension); + }); + + it('lists loaded extensions in getAllExtensions', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')); + expect(customSession.getAllExtensions()).to.deep.equal([e]); + customSession.removeExtension(e.id); + expect(customSession.getAllExtensions()).to.deep.equal([]); + }); + + it('gets an extension by id', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')); + expect(customSession.getExtension(e.id)).to.deep.equal(e); + }); it('confines an extension to the session it was loaded in', async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadChromeExtension(path.join(fixtures, 'extensions', 'red-bg')) - const w = new BrowserWindow({show: false}) // not in the session - await w.loadURL(url) - const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') - expect(bg).to.equal('') - }) + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')); + const w = new BrowserWindow({ show: false }); // not in the session + await w.loadURL(url); + const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor'); + expect(bg).to.equal(''); + }); + + it('loading an extension in a temporary session throws an error', async () => { + const customSession = session.fromPartition(uuid.v4()); + await expect(customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session'); + }); + + describe('chrome.i18n', () => { + let w: BrowserWindow; + let extension: Extension; + const exec = async (name: string) => { + const p = emittedOnce(ipcMain, 'success'); + await w.webContents.executeJavaScript(`exec('${name}')`); + const [, result] = await p; + return result; + }; + beforeEach(async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n')); + w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } }); + await w.loadURL(url); + }); + it('getAcceptLanguages()', async () => { + const result = await exec('getAcceptLanguages'); + expect(result).to.be.an('array').and.deep.equal(['en-US', 'en']); + }); + it('getMessage()', async () => { + const result = await exec('getMessage'); + expect(result.id).to.be.a('string').and.equal(extension.id); + expect(result.name).to.be.a('string').and.equal('chrome-i18n'); + }); + }); describe('chrome.runtime', () => { - let content: any - before(async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadChromeExtension(path.join(fixtures, 'extensions', 'chrome-runtime')) - const w = new BrowserWindow({show: false, webPreferences: { session: customSession }}) + let w: BrowserWindow; + const exec = async (name: string) => { + const p = emittedOnce(ipcMain, 'success'); + await w.webContents.executeJavaScript(`exec('${name}')`); + const [, result] = await p; + return result; + }; + beforeEach(async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime')); + w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } }); + await w.loadURL(url); + }); + it('getManifest()', async () => { + const result = await exec('getManifest'); + expect(result).to.be.an('object').with.property('name', 'chrome-runtime'); + }); + it('id', async () => { + const result = await exec('id'); + expect(result).to.be.a('string').with.lengthOf(32); + }); + it('getURL()', async () => { + const result = await exec('getURL'); + expect(result).to.be.a('string').and.match(/^chrome-extension:\/\/.*main.js$/); + }); + it('getPlatformInfo()', async () => { + const result = await exec('getPlatformInfo'); + expect(result).to.be.an('object'); + expect(result.os).to.be.a('string'); + expect(result.arch).to.be.a('string'); + expect(result.nacl_arch).to.be.a('string'); + }); + }); + + describe('chrome.storage', () => { + it('stores and retrieves a key', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage')); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } }); try { - await w.loadURL(url) - content = JSON.parse(await w.webContents.executeJavaScript('document.documentElement.textContent')) - expect(content).to.be.an('object') + const p = emittedOnce(ipcMain, 'storage-success'); + await w.loadURL(url); + const [, v] = await p; + expect(v).to.equal('value'); } finally { - w.destroy() + w.destroy(); } - }) - it('getManifest()', () => { - expect(content.manifest).to.be.an('object').with.property('name', 'chrome-runtime') - }) - it('id', () => { - expect(content.id).to.be.a('string').with.lengthOf(32) - }) - it('getURL()', () => { - expect(content.url).to.be.a('string').and.match(/^chrome-extension:\/\/.*main.js$/) - }) - }) + }); + }); - describe('chrome.storage', () => { - it('stores and retrieves a key', async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadChromeExtension(path.join(fixtures, 'extensions', 'chrome-storage')) - const w = new BrowserWindow({show: false, webPreferences: { session: customSession, nodeIntegration: true }}) + describe('chrome.webRequest', () => { + function fetch (contents: WebContents, url: string) { + return contents.executeJavaScript(`fetch(${JSON.stringify(url)})`); + } + + let customSession: Session; + let w: BrowserWindow; + + beforeEach(() => { + customSession = session.fromPartition(`persist:${uuid.v4()}`); + w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true, contextIsolation: true } }); + }); + + describe('onBeforeRequest', () => { + it('can cancel http requests', async () => { + await w.loadURL(url); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest')); + await expect(fetch(w.webContents, url)).to.eventually.be.rejectedWith('Failed to fetch'); + }); + + it('does not cancel http requests when no extension loaded', async () => { + await w.loadURL(url); + await expect(fetch(w.webContents, url)).to.not.be.rejectedWith('Failed to fetch'); + }); + }); + + it('does not take precedence over Electron webRequest - http', async () => { + return new Promise((resolve) => { + (async () => { + customSession.webRequest.onBeforeRequest((details, callback) => { + resolve(); + callback({ cancel: true }); + }); + await w.loadURL(url); + + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest')); + fetch(w.webContents, url); + })(); + }); + }); + + it('does not take precedence over Electron webRequest - WebSocket', () => { + return new Promise((resolve) => { + (async () => { + customSession.webRequest.onBeforeSendHeaders(() => { + resolve(); + }); + await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port } }); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss')); + })(); + }); + }); + + describe('WebSocket', () => { + it('can be proxied', async () => { + await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port } }); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss')); + customSession.webRequest.onSendHeaders((details) => { + if (details.url.startsWith('ws://')) { + expect(details.requestHeaders.foo).be.equal('bar'); + } + }); + }); + }); + }); + + describe('chrome.tabs', () => { + let customSession: Session; + before(async () => { + customSession = session.fromPartition(`persist:${uuid.v4()}`); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api')); + }); + + it('executeScript', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }); + await w.loadURL(url); + + const message = { method: 'executeScript', args: ['1 + 2'] }; + w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`); + + const [,, responseString] = await emittedOnce(w.webContents, 'console-message'); + const response = JSON.parse(responseString); + + expect(response).to.equal(3); + }); + + it('connect', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }); + await w.loadURL(url); + + const portName = uuid.v4(); + const message = { method: 'connectTab', args: [portName] }; + w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`); + + const [,, responseString] = await emittedOnce(w.webContents, 'console-message'); + const response = responseString.split(','); + expect(response[0]).to.equal(portName); + expect(response[1]).to.equal('howdy'); + }); + + it('sendMessage receives the response', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }); + await w.loadURL(url); + + const message = { method: 'sendMessage', args: ['Hello World!'] }; + w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`); + + const [,, responseString] = await emittedOnce(w.webContents, 'console-message'); + const response = JSON.parse(responseString); + + expect(response.message).to.equal('Hello World!'); + expect(response.tabId).to.equal(w.webContents.id); + }); + + it('update', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }); + await w.loadURL(url); + + const w2 = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w2.loadURL('about:blank'); + + const w2Navigated = emittedOnce(w2.webContents, 'did-navigate'); + + const message = { method: 'update', args: [w2.webContents.id, { url }] }; + w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`); + + const [,, responseString] = await emittedOnce(w.webContents, 'console-message'); + const response = JSON.parse(responseString); + + await w2Navigated; + + expect(new URL(w2.getURL()).toString()).to.equal(new URL(url).toString()); + + expect(response.id).to.equal(w2.webContents.id); + }); + }); + + describe('background pages', () => { + it('loads a lazy background page when sending a message', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } }); try { - const p = emittedOnce(ipcMain, 'storage-success') - await w.loadURL(url) - const [, v] = await p - expect(v).to.equal('value') + w.loadURL(url); + const [, resp] = await emittedOnce(ipcMain, 'bg-page-message-response'); + expect(resp.message).to.deep.equal({ some: 'message' }); + expect(resp.sender.id).to.be.a('string'); + expect(resp.sender.origin).to.equal(url); + expect(resp.sender.url).to.equal(url + '/'); } finally { - w.destroy() + w.destroy(); } - }) - }) -}) + }); + + it('can use extension.getBackgroundPage from a ui page', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w.loadURL(`chrome-extension://${id}/page-get-background.html`); + const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise'); + expect(receivedMessage).to.deep.equal({ some: 'message' }); + }); + + it('can use extension.getBackgroundPage from a ui page', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w.loadURL(`chrome-extension://${id}/page-get-background.html`); + const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise'); + expect(receivedMessage).to.deep.equal({ some: 'message' }); + }); + + it('can use runtime.getBackgroundPage from a ui page', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')); + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }); + await w.loadURL(`chrome-extension://${id}/page-runtime-get-background.html`); + const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise'); + expect(receivedMessage).to.deep.equal({ some: 'message' }); + }); + + it('has session in background page', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); + const promise = emittedOnce(app, 'web-contents-created'); + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page')); + const [, bgPageContents] = await promise; + expect(bgPageContents.getType()).to.equal('backgroundPage'); + await emittedOnce(bgPageContents, 'did-finish-load'); + expect(bgPageContents.getURL()).to.equal(`chrome-extension://${id}/_generated_background_page.html`); + expect(bgPageContents.session).to.not.equal(undefined); + }); + + it('can open devtools of background page', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); + const promise = emittedOnce(app, 'web-contents-created'); + await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page')); + const [, bgPageContents] = await promise; + expect(bgPageContents.getType()).to.equal('backgroundPage'); + bgPageContents.openDevTools(); + bgPageContents.closeDevTools(); + }); + }); + + describe('devtools extensions', () => { + let showPanelTimeoutId: any = null; + afterEach(() => { + if (showPanelTimeoutId) clearTimeout(showPanelTimeoutId); + }); + const showLastDevToolsPanel = (w: BrowserWindow) => { + w.webContents.once('devtools-opened', () => { + const show = () => { + if (w == null || w.isDestroyed()) return; + const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined }; + if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) { + return; + } + + const showLastPanel = () => { + // this is executed in the devtools context, where UI is a global + const { UI } = (window as any); + const tabs = UI.inspectorView.tabbedPane.tabs; + const lastPanelId = tabs[tabs.length - 1].id; + UI.inspectorView.showPanel(lastPanelId); + }; + devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => { + showPanelTimeoutId = setTimeout(show, 100); + }); + }; + showPanelTimeoutId = setTimeout(show, 100); + }); + }; + + // TODO(jkleinsc) fix this flaky test on WOA + ifit(process.platform !== 'win32' || process.arch !== 'arm64')('loads a devtools extension', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension')); + const winningMessage = emittedOnce(ipcMain, 'winning'); + const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } }); + await w.loadURL(url); + w.webContents.openDevTools(); + showLastDevToolsPanel(w); + await winningMessage; + }); + }); + + describe('chrome extension content scripts', () => { + const fixtures = path.resolve(__dirname, 'fixtures'); + const extensionPath = path.resolve(fixtures, 'extensions'); + + const addExtension = (name: string) => session.defaultSession.loadExtension(path.resolve(extensionPath, name)); + const removeAllExtensions = () => { + Object.keys(session.defaultSession.getAllExtensions()).map(extName => { + session.defaultSession.removeExtension(extName); + }); + }; + + let responseIdCounter = 0; + const executeJavaScriptInFrame = (webContents: WebContents, frameRoutingId: number, code: string) => { + return new Promise(resolve => { + const responseId = responseIdCounter++; + ipcMain.once(`executeJavaScriptInFrame_${responseId}`, (event, result) => { + resolve(result); + }); + webContents.send('executeJavaScriptInFrame', frameRoutingId, code, responseId); + }); + }; + + const generateTests = (sandboxEnabled: boolean, contextIsolationEnabled: boolean) => { + describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'} and context isolation ${contextIsolationEnabled ? 'enabled' : 'disabled'}`, () => { + let w: BrowserWindow; + + describe('supports "run_at" option', () => { + beforeEach(async () => { + await closeWindow(w); + w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + webPreferences: { + contextIsolation: contextIsolationEnabled, + sandbox: sandboxEnabled + } + }); + }); + + afterEach(() => { + removeAllExtensions(); + return closeWindow(w).then(() => { w = null as unknown as BrowserWindow; }); + }); + + it('should run content script at document_start', async () => { + await addExtension('content-script-document-start'); + w.webContents.once('dom-ready', async () => { + const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor'); + expect(result).to.equal('red'); + }); + w.loadURL(url); + }); + + it('should run content script at document_idle', async () => { + await addExtension('content-script-document-idle'); + w.loadURL(url); + const result = await w.webContents.executeJavaScript('document.body.style.backgroundColor'); + expect(result).to.equal('red'); + }); + + it('should run content script at document_end', async () => { + await addExtension('content-script-document-end'); + w.webContents.once('did-finish-load', async () => { + const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor'); + expect(result).to.equal('red'); + }); + w.loadURL(url); + }); + }); + + // TODO(nornagon): real extensions don't load on file: urls, so this + // test needs to be updated to serve its content over http. + describe.skip('supports "all_frames" option', () => { + const contentScript = path.resolve(fixtures, 'extensions/content-script'); + + // Computed style values + const COLOR_RED = 'rgb(255, 0, 0)'; + const COLOR_BLUE = 'rgb(0, 0, 255)'; + const COLOR_TRANSPARENT = 'rgba(0, 0, 0, 0)'; + + before(() => { + session.defaultSession.loadExtension(contentScript); + }); + + after(() => { + session.defaultSession.removeExtension('content-script-test'); + }); + + beforeEach(() => { + w = new BrowserWindow({ + show: false, + webPreferences: { + // enable content script injection in subframes + nodeIntegrationInSubFrames: true, + preload: path.join(contentScript, 'all_frames-preload.js') + } + }); + }); + + afterEach(() => + closeWindow(w).then(() => { + w = null as unknown as BrowserWindow; + }) + ); + + it('applies matching rules in subframes', async () => { + const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2); + w.loadFile(path.join(contentScript, 'frame-with-frame.html')); + const frameEvents = await detailsPromise; + await Promise.all( + frameEvents.map(async frameEvent => { + const [, isMainFrame, , frameRoutingId] = frameEvent; + const result: any = await executeJavaScriptInFrame( + w.webContents, + frameRoutingId, + `(() => { + const a = document.getElementById('all_frames_enabled') + const b = document.getElementById('all_frames_disabled') + return { + enabledColor: getComputedStyle(a).backgroundColor, + disabledColor: getComputedStyle(b).backgroundColor + } + })()` + ); + expect(result.enabledColor).to.equal(COLOR_RED); + if (isMainFrame) { + expect(result.disabledColor).to.equal(COLOR_BLUE); + } else { + expect(result.disabledColor).to.equal(COLOR_TRANSPARENT); // null color + } + }) + ); + }); + }); + }); + }; + + generateTests(false, false); + generateTests(false, true); + generateTests(true, false); + generateTests(true, true); + }); + + describe('extension ui pages', () => { + afterEach(() => { + session.defaultSession.getAllExtensions().forEach(e => { + session.defaultSession.removeExtension(e.id); + }); + }); + + it('loads a ui page of an extension', async () => { + const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page')); + const w = new BrowserWindow({ show: false }); + await w.loadURL(`chrome-extension://${id}/bare-page.html`); + const textContent = await w.webContents.executeJavaScript('document.body.textContent'); + expect(textContent).to.equal('ui page loaded ok\n'); + }); + + it('can load resources', async () => { + const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page')); + const w = new BrowserWindow({ show: false }); + await w.loadURL(`chrome-extension://${id}/page-script-load.html`); + const textContent = await w.webContents.executeJavaScript('document.body.textContent'); + expect(textContent).to.equal('script loaded ok\n'); + }); + }); + + describe('manifest v3', () => { + it('registers background service worker', async () => { + const customSession = session.fromPartition(`persist:${uuid.v4()}`); + const registrationPromise = new Promise(resolve => { + customSession.serviceWorkers.once('registration-completed', (event, { scope }) => resolve(scope)); + }); + const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker')); + const scope = await registrationPromise; + expect(scope).equals(extension.url); + }); + }); +}); diff --git a/spec-main/fixtures/api/beforeunload-empty-string.html b/spec-main/fixtures/api/beforeunload-empty-string.html new file mode 100644 index 0000000000000..671a2ec991895 --- /dev/null +++ b/spec-main/fixtures/api/beforeunload-empty-string.html @@ -0,0 +1,14 @@ + + + + + diff --git a/spec-main/fixtures/api/beforeunload-false-prevent3.html b/spec-main/fixtures/api/beforeunload-false-prevent3.html new file mode 100644 index 0000000000000..98a069ee3bd46 --- /dev/null +++ b/spec-main/fixtures/api/beforeunload-false-prevent3.html @@ -0,0 +1,17 @@ + + + + + diff --git a/spec-main/fixtures/api/beforeunload-false.html b/spec-main/fixtures/api/beforeunload-false.html new file mode 100644 index 0000000000000..d9504a8c7ef3e --- /dev/null +++ b/spec-main/fixtures/api/beforeunload-false.html @@ -0,0 +1,14 @@ + + + + + diff --git a/spec-main/fixtures/api/beforeunload-undefined.html b/spec-main/fixtures/api/beforeunload-undefined.html new file mode 100644 index 0000000000000..e6eb2c13b28b8 --- /dev/null +++ b/spec-main/fixtures/api/beforeunload-undefined.html @@ -0,0 +1,9 @@ + + + + + + diff --git a/spec-main/fixtures/api/context-bridge/can-bind-preload.js b/spec-main/fixtures/api/context-bridge/can-bind-preload.js new file mode 100644 index 0000000000000..262c6e80057d6 --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/can-bind-preload.js @@ -0,0 +1,13 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +console.info(contextBridge); + +let bound = false; +try { + contextBridge.exposeInMainWorld('test', {}); + bound = true; +} catch { + // Ignore +} + +ipcRenderer.send('context-bridge-bound', bound); diff --git a/spec-main/fixtures/api/context-bridge/context-bridge-mutability/index.html b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/index.html new file mode 100644 index 0000000000000..980a3d7f32c30 --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/index.html @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/spec-main/fixtures/api/context-bridge/context-bridge-mutability/main.js b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/main.js new file mode 100644 index 0000000000000..622ef6acbf9c4 --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/main.js @@ -0,0 +1,20 @@ +const { app, BrowserWindow } = require('electron'); +const path = require('path'); + +let win; +app.whenReady().then(function () { + win = new BrowserWindow({ + webPreferences: { + contextIsolation: true, + preload: path.join(__dirname, 'preload.js') + } + }); + + win.loadFile('index.html'); + + win.webContents.on('console-message', (event, level, message) => { + console.log(message); + }); + + win.webContents.on('did-finish-load', () => app.quit()); +}); diff --git a/spec-main/fixtures/api/context-bridge/context-bridge-mutability/package.json b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/package.json new file mode 100644 index 0000000000000..d1fc13838e5fe --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/package.json @@ -0,0 +1,4 @@ +{ + "name": "context-bridge-mutability", + "main": "main.js" +} \ No newline at end of file diff --git a/spec-main/fixtures/api/context-bridge/context-bridge-mutability/preload.js b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/preload.js new file mode 100644 index 0000000000000..e3d3d9abfad5b --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/context-bridge-mutability/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('str', 'some-text'); +contextBridge.exposeInMainWorld('obj', { prop: 'obj-prop' }); +contextBridge.exposeInMainWorld('arr', [1, 2, 3, 4]); diff --git a/spec/fixtures/api/site-instance-overrides/index.html b/spec-main/fixtures/api/context-bridge/empty.html similarity index 100% rename from spec/fixtures/api/site-instance-overrides/index.html rename to spec-main/fixtures/api/context-bridge/empty.html diff --git a/spec-main/fixtures/api/custom-protocol-shutdown.js b/spec-main/fixtures/api/custom-protocol-shutdown.js new file mode 100644 index 0000000000000..b59a936a61232 --- /dev/null +++ b/spec-main/fixtures/api/custom-protocol-shutdown.js @@ -0,0 +1,18 @@ +const { app, webContents, protocol, session } = require('electron'); + +protocol.registerSchemesAsPrivileged([ + { scheme: 'test', privileges: { standard: true, secure: true } } +]); + +app.whenReady().then(function () { + const ses = session.fromPartition('persist:test-standard-shutdown'); + const web = webContents.create({ session: ses }); + + ses.protocol.registerStringProtocol('test', (request, callback) => { + callback('Hello World!'); + }); + + web.loadURL('test://abc/hello.txt'); + + web.on('did-finish-load', () => app.quit()); +}); diff --git a/spec-main/fixtures/api/ipc-main-listeners/main.js b/spec-main/fixtures/api/ipc-main-listeners/main.js new file mode 100644 index 0000000000000..49e3c6f0f1106 --- /dev/null +++ b/spec-main/fixtures/api/ipc-main-listeners/main.js @@ -0,0 +1,8 @@ +const { app, ipcMain } = require('electron'); + +app.whenReady().then(() => { + process.stdout.write(JSON.stringify(ipcMain.eventNames())); + process.stdout.end(); + + app.quit(); +}); diff --git a/spec-main/fixtures/api/ipc-main-listeners/package.json b/spec-main/fixtures/api/ipc-main-listeners/package.json new file mode 100644 index 0000000000000..9400a77a8c7a5 --- /dev/null +++ b/spec-main/fixtures/api/ipc-main-listeners/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-ipc-main-listeners", + "main": "main.js" +} diff --git a/spec/fixtures/api/native-window-open-native-addon.html b/spec-main/fixtures/api/native-window-open-native-addon.html similarity index 90% rename from spec/fixtures/api/native-window-open-native-addon.html rename to spec-main/fixtures/api/native-window-open-native-addon.html index 27c24a60b7550..c1cd5cea755a1 100644 --- a/spec/fixtures/api/native-window-open-native-addon.html +++ b/spec-main/fixtures/api/native-window-open-native-addon.html @@ -7,7 +7,7 @@ let requireError try { - echo = require('echo') + echo = require('@electron-ci/echo') } catch (error) { requireError = error } diff --git a/spec-main/fixtures/api/net-log/main.js b/spec-main/fixtures/api/net-log/main.js index 040aece614dc6..b5463acfdbbf1 100644 --- a/spec-main/fixtures/api/net-log/main.js +++ b/spec-main/fixtures/api/net-log/main.js @@ -1,31 +1,31 @@ -const { app, net, session } = require('electron') +const { app, net, session } = require('electron'); if (process.env.TEST_DUMP_FILE) { - app.commandLine.appendSwitch('log-net-log', process.env.TEST_DUMP_FILE) + app.commandLine.appendSwitch('log-net-log', process.env.TEST_DUMP_FILE); } function request () { return new Promise((resolve) => { - const req = net.request(process.env.TEST_REQUEST_URL) + const req = net.request(process.env.TEST_REQUEST_URL); req.on('response', () => { - resolve() - }) - req.end() - }) + resolve(); + }); + req.end(); + }); } -app.on('ready', async () => { - const netLog = session.defaultSession.netLog +app.whenReady().then(async () => { + const netLog = session.defaultSession.netLog; if (process.env.TEST_DUMP_FILE_DYNAMIC) { - await netLog.startLogging(process.env.TEST_DUMP_FILE_DYNAMIC) + await netLog.startLogging(process.env.TEST_DUMP_FILE_DYNAMIC); } - await request() + await request(); if (process.env.TEST_MANUAL_STOP) { - await netLog.stopLogging() + await netLog.stopLogging(); } - app.quit() -}) + app.quit(); +}); diff --git a/spec-main/fixtures/api/net-log/package.json b/spec-main/fixtures/api/net-log/package.json index 59272007963f9..4b99b89a4c282 100644 --- a/spec-main/fixtures/api/net-log/package.json +++ b/spec-main/fixtures/api/net-log/package.json @@ -1,4 +1,4 @@ { - "name": "net-log", + "name": "electron-test-net-log", "main": "main.js" } diff --git a/spec-main/fixtures/api/new-window-preload.js b/spec-main/fixtures/api/new-window-preload.js new file mode 100644 index 0000000000000..090804a618b18 --- /dev/null +++ b/spec-main/fixtures/api/new-window-preload.js @@ -0,0 +1,6 @@ +const { ipcRenderer, webFrame } = require('electron'); + +ipcRenderer.send('answer', { + argv: process.argv +}); +window.close(); diff --git a/spec-main/fixtures/api/print-to-pdf.html b/spec-main/fixtures/api/print-to-pdf.html new file mode 100644 index 0000000000000..783378b373d3f --- /dev/null +++ b/spec-main/fixtures/api/print-to-pdf.html @@ -0,0 +1,2382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + The world’s leading software development platform · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Skip to + content + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + + +
+
+ + + +
+
+
+
+

Built for developers

+

+ GitHub is a development platform inspired by the way you work. From open source to business, you can host and review code, manage projects, and + build software alongside 50 million developers. +

+
+
+
+ + + + + +
+ +
+ +
+
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+

Get started with GitHub Enterprise

+

+ Take collaboration to the next level with security and administrative features built for teams. +

+
+
+
+
+ + + + + + + + + + + +

Enterprise

+

Deploy to your environment or the cloud.

+
+ Start + a free trial +
+
+
+ + + + + + + +

Talk to us

+

Need help?

+
+ Contact + Sales +
+
+
+
+
+ + +
+ +
+
+
+
+ NEW +
+
+

+ GitHub is now free for teams +

+

+ GitHub Free gives teams private repositories with unlimited collaborators at no cost. GitHub Team is + now reduced to $4 per user/month. +

+
+
+

+ Try GitHub Free +

+
+
+ +
+ +
+
+

+ More than 2.9 million businesses and organizations use GitHub +

+
    +
  • Airbnb
  • +
  • SAP
  • +
  • IBM
  • +
  • Google
  • +
  • PayPal
  • +
  • Bloomberg
  • +
  • Spotify
  • +
  • Swift
  • +
  • Facebook
  • +
  • Node.js
  • +
  • NASA
  • +
  • Walmart
  • +
+
+
+ + + +
+
+

+ Security and administration +

+

+ Your business needs, met +

+

+ From flexible hosting to granular access controls, we’ve got your security requirements covered. +

+

+ + How GitHub Enterprise works + Learn how GitHub Enterprise works + +

+ +
+
+ Security and administration +
+
+

Code security

+

+ Prevent problems before they happen. Protected branches, signed commits, and required status checks + protect your work and help you maintain a high standard for your code. +

+

Access controlled

+

+ Encourage teams to work together while limiting access to those who need it with granular permissions + and authentication through SAML/SSO and LDAP. +

+
+
+ +
+
+ + + + + +
+
+

Hosted where you need it

+

Securely and reliably host your work on GitHub using GitHub Enterprise Cloud. + Or deploy GitHub Enterprise Server in your own data centers or in a private cloud using Amazon Web + Services, Azure, or Google Cloud Platform.

+ + Compare plans + Contact + Sales for more information +
+
+ +
+
+ +
+
+
+

+ Integrations +

+

+ Build on GitHub +

+

+ Customize your process with GitHub apps and an intuitive API. Integrate the tools you already use or + discover new favorites to create a happier, more efficient way of working. +

+

+ Learn + about integrations +

+
+ +
+
Slack
+
ZenHub
+
Travis CI
+
Atom
+
Circle CI
+
Google
+
Code Climate
+
+ +
+

+ Sometimes, there’s more than one tool for the job. Why not try something new? +

+

+ + Browse GitHub Marketplace + +

+
+
+
+ +
+
+

+ Community +

+

+ Welcome home,
developers +

+

+ GitHub is home to the world’s largest community of developers and their projects... +

+
+ + + + + +
+ +
+
+

+ Get started for free — join the millions of developers already using GitHub to share their code, work + together, and build amazing things. +

+
+
+ + + + + +
+
+ +
+ +
+ + + + + + +
+ + + You can’t perform that action at this time. +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spec-main/fixtures/api/safe-storage/decrypt-app/main.js b/spec-main/fixtures/api/safe-storage/decrypt-app/main.js new file mode 100644 index 0000000000000..95608a41e86ce --- /dev/null +++ b/spec-main/fixtures/api/safe-storage/decrypt-app/main.js @@ -0,0 +1,13 @@ +const { app, safeStorage, ipcMain } = require('electron'); +const { promises: fs } = require('fs'); +const path = require('path'); + +const pathToEncryptedString = path.resolve(__dirname, '..', 'encrypted.txt'); +const readFile = fs.readFile; + +app.whenReady().then(async () => { + const encryptedString = await readFile(pathToEncryptedString); + const decrypted = safeStorage.decryptString(encryptedString); + console.log(decrypted); + app.quit(); +}); diff --git a/spec-main/fixtures/api/safe-storage/decrypt-app/package.json b/spec-main/fixtures/api/safe-storage/decrypt-app/package.json new file mode 100644 index 0000000000000..cb5d6ffc41906 --- /dev/null +++ b/spec-main/fixtures/api/safe-storage/decrypt-app/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-safe-storage", + "main": "main.js" +} diff --git a/spec-main/fixtures/api/safe-storage/encrypt-app/main.js b/spec-main/fixtures/api/safe-storage/encrypt-app/main.js new file mode 100644 index 0000000000000..da1a19a3daab5 --- /dev/null +++ b/spec-main/fixtures/api/safe-storage/encrypt-app/main.js @@ -0,0 +1,12 @@ +const { app, safeStorage, ipcMain } = require('electron'); +const { promises: fs } = require('fs'); +const path = require('path'); + +const pathToEncryptedString = path.resolve(__dirname, '..', 'encrypted.txt'); +const writeFile = fs.writeFile; + +app.whenReady().then(async () => { + const encrypted = safeStorage.encryptString('plaintext'); + const encryptedString = await writeFile(pathToEncryptedString, encrypted); + app.quit(); +}); diff --git a/spec-main/fixtures/api/safe-storage/encrypt-app/package.json b/spec-main/fixtures/api/safe-storage/encrypt-app/package.json new file mode 100644 index 0000000000000..2fa2fabcd1e84 --- /dev/null +++ b/spec-main/fixtures/api/safe-storage/encrypt-app/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-safe-storage", + "main": "main.js" +} diff --git a/spec/fixtures/api/sandbox.html b/spec-main/fixtures/api/sandbox.html similarity index 92% rename from spec/fixtures/api/sandbox.html rename to spec-main/fixtures/api/sandbox.html index c0ae7c279fb08..7eeb32f33657f 100644 --- a/spec/fixtures/api/sandbox.html +++ b/spec-main/fixtures/api/sandbox.html @@ -48,14 +48,15 @@ }, 'exit-event': () => { const {ipcRenderer} = require('electron') + const currentLocation = location.href.slice(); process.on('exit', () => { - ipcRenderer.send('answer', location.href) + ipcRenderer.send('answer', currentLocation) }) location.assign('http://www.google.com') }, 'window-open': () => { addEventListener('load', () => { - popup = open(window.location.href, 'popup!', 'top=60,left=50,width=500,height=600') + const popup = open(window.location.href, 'popup!', 'top=60,left=50,width=500,height=600') popup.addEventListener('DOMContentLoaded', () => { popup.document.write('

scripting from opener

') popup.callback() @@ -82,7 +83,7 @@ }, 'verify-ipc-sender': () => { const {ipcRenderer} = require('electron') - popup = open() + const popup = open() ipcRenderer.once('verified', () => { ipcRenderer.send('parent-answer') }) diff --git a/spec/fixtures/api/send-sync-message.html b/spec-main/fixtures/api/send-sync-message.html similarity index 100% rename from spec/fixtures/api/send-sync-message.html rename to spec-main/fixtures/api/send-sync-message.html diff --git a/spec-main/fixtures/api/service-workers/index.html b/spec-main/fixtures/api/service-workers/index.html new file mode 100644 index 0000000000000..bb90197451664 --- /dev/null +++ b/spec-main/fixtures/api/service-workers/index.html @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/spec-main/fixtures/api/service-workers/logs.html b/spec-main/fixtures/api/service-workers/logs.html new file mode 100644 index 0000000000000..d61005f344537 --- /dev/null +++ b/spec-main/fixtures/api/service-workers/logs.html @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/spec-main/fixtures/api/service-workers/sw-logs.js b/spec-main/fixtures/api/service-workers/sw-logs.js new file mode 100644 index 0000000000000..04e620f913a4a --- /dev/null +++ b/spec-main/fixtures/api/service-workers/sw-logs.js @@ -0,0 +1,6 @@ +self.addEventListener('install', function (event) { + console.log('log log'); + console.info('info log'); + console.warn('warn log'); + console.error('error log'); +}); diff --git a/spec-main/fixtures/api/service-workers/sw.js b/spec-main/fixtures/api/service-workers/sw.js new file mode 100644 index 0000000000000..7c3ad71f2bbdf --- /dev/null +++ b/spec-main/fixtures/api/service-workers/sw.js @@ -0,0 +1,3 @@ +self.addEventListener('install', function (event) { + console.log('Installed'); +}); diff --git a/spec-main/fixtures/api/test-menu-null/main.js b/spec-main/fixtures/api/test-menu-null/main.js new file mode 100644 index 0000000000000..13b9c0cd4756f --- /dev/null +++ b/spec-main/fixtures/api/test-menu-null/main.js @@ -0,0 +1,16 @@ +const { app, BrowserWindow } = require('electron'); + +let win; +app.whenReady().then(function () { + win = new BrowserWindow({}); + win.setMenu(null); + + setTimeout(() => { + if (win.isMenuBarVisible()) { + console.log('Window has a menu'); + } else { + console.log('Window has no menu'); + } + app.quit(); + }); +}); diff --git a/spec-main/fixtures/api/test-menu-null/package.json b/spec-main/fixtures/api/test-menu-null/package.json new file mode 100644 index 0000000000000..bfb7944df5c83 --- /dev/null +++ b/spec-main/fixtures/api/test-menu-null/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-menu", + "main": "main.js" +} diff --git a/spec-main/fixtures/api/test-menu-visibility/main.js b/spec-main/fixtures/api/test-menu-visibility/main.js new file mode 100644 index 0000000000000..9f5ffa2d0ec89 --- /dev/null +++ b/spec-main/fixtures/api/test-menu-visibility/main.js @@ -0,0 +1,19 @@ +const { app, BrowserWindow } = require('electron'); + +let win; +// This test uses "app.once('ready')" while the |test-menu-null| test uses +// "app.whenReady()", the 2 APIs have slight difference on timing to cover +// more cases. +app.once('ready', function () { + win = new BrowserWindow({}); + win.setMenuBarVisibility(false); + + setTimeout(() => { + if (win.isMenuBarVisible()) { + console.log('Window has a menu'); + } else { + console.log('Window has no menu'); + } + app.quit(); + }); +}); diff --git a/spec-main/fixtures/api/test-menu-visibility/package.json b/spec-main/fixtures/api/test-menu-visibility/package.json new file mode 100644 index 0000000000000..bfb7944df5c83 --- /dev/null +++ b/spec-main/fixtures/api/test-menu-visibility/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-menu", + "main": "main.js" +} diff --git a/spec-main/fixtures/api/webrequest.html b/spec-main/fixtures/api/webrequest.html new file mode 100644 index 0000000000000..6cc9f0bd18908 --- /dev/null +++ b/spec-main/fixtures/api/webrequest.html @@ -0,0 +1,27 @@ + diff --git a/spec-main/fixtures/api/window-open-preload.js b/spec-main/fixtures/api/window-open-preload.js new file mode 100644 index 0000000000000..10c7c7085ed97 --- /dev/null +++ b/spec-main/fixtures/api/window-open-preload.js @@ -0,0 +1,13 @@ +const { ipcRenderer, webFrame } = require('electron'); + +setImmediate(function () { + if (window.location.toString() === 'bar://page/') { + const windowOpenerIsNull = window.opener == null; + ipcRenderer.send('answer', { + nodeIntegration: webFrame.getWebPreference('nodeIntegration'), + typeofProcess: typeof global.process, + windowOpenerIsNull + }); + window.close(); + } +}); diff --git a/spec-main/fixtures/apps/background-color-transparent/index.html b/spec-main/fixtures/apps/background-color-transparent/index.html new file mode 100644 index 0000000000000..f8f5ff4c32697 --- /dev/null +++ b/spec-main/fixtures/apps/background-color-transparent/index.html @@ -0,0 +1,15 @@ + + + + + test-color-window + + + + + + diff --git a/spec-main/fixtures/apps/background-color-transparent/main.js b/spec-main/fixtures/apps/background-color-transparent/main.js new file mode 100644 index 0000000000000..8d1da05c6c1db --- /dev/null +++ b/spec-main/fixtures/apps/background-color-transparent/main.js @@ -0,0 +1,59 @@ +const { app, BrowserWindow, desktopCapturer, ipcMain } = require('electron'); +const getColors = require('get-image-colors'); + +const colors = {}; + +// Fetch the test window. +const getWindow = async () => { + const sources = await desktopCapturer.getSources({ types: ['window'] }); + const filtered = sources.filter(s => s.name === 'test-color-window'); + + if (filtered.length === 0) { + throw new Error('Could not find test window'); + } + + return filtered[0]; +}; + +async function createWindow () { + const mainWindow = new BrowserWindow({ + frame: false, + transparent: true, + vibrancy: 'under-window', + webPreferences: { + contextIsolation: false, + nodeIntegration: true + } + }); + + await mainWindow.loadFile('index.html'); + + // Get initial green background color. + const window = await getWindow(); + const buf = window.thumbnail.toPNG(); + const result = await getColors(buf, { count: 1, type: 'image/png' }); + colors.green = result[0].hex(); +} + +ipcMain.on('set-transparent', async () => { + // Get updated background color. + const window = await getWindow(); + const buf = window.thumbnail.toPNG(); + const result = await getColors(buf, { count: 1, type: 'image/png' }); + colors.transparent = result[0].hex(); + + const { green, transparent } = colors; + process.exit(green === transparent ? 1 : 0); +}); + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/spec-main/fixtures/apps/background-color-transparent/package.json b/spec-main/fixtures/apps/background-color-transparent/package.json new file mode 100644 index 0000000000000..0ea4852f44c3f --- /dev/null +++ b/spec-main/fixtures/apps/background-color-transparent/package.json @@ -0,0 +1,4 @@ +{ + "name": "background-color-transparent", + "main": "main.js" +} diff --git a/spec-main/fixtures/apps/background-color-transparent/renderer.js b/spec-main/fixtures/apps/background-color-transparent/renderer.js new file mode 100644 index 0000000000000..1312f5f129052 --- /dev/null +++ b/spec-main/fixtures/apps/background-color-transparent/renderer.js @@ -0,0 +1,9 @@ +const { ipcRenderer } = require('electron'); + +window.setTimeout(async (_) => { + document.body.style.background = 'transparent'; + + window.setTimeout(async (_) => { + ipcRenderer.send('set-transparent'); + }, 2000); +}, 3000); diff --git a/spec-main/fixtures/apps/crash/main.js b/spec-main/fixtures/apps/crash/main.js new file mode 100644 index 0000000000000..5adb5873fe250 --- /dev/null +++ b/spec-main/fixtures/apps/crash/main.js @@ -0,0 +1,65 @@ +const { app, BrowserWindow, crashReporter } = require('electron'); +const path = require('path'); +const childProcess = require('child_process'); + +app.setVersion('0.1.0'); + +const url = app.commandLine.getSwitchValue('crash-reporter-url'); +const uploadToServer = !app.commandLine.hasSwitch('no-upload'); +const setExtraParameters = app.commandLine.hasSwitch('set-extra-parameters-in-renderer'); +const addGlobalParam = app.commandLine.getSwitchValue('add-global-param')?.split(':'); + +crashReporter.start({ + productName: 'Zombies', + companyName: 'Umbrella Corporation', + compress: false, + uploadToServer, + submitURL: url, + ignoreSystemCrashHandler: true, + extra: { + mainProcessSpecific: 'mps' + }, + globalExtra: addGlobalParam[0] ? { [addGlobalParam[0]]: addGlobalParam[1] } : {} +}); + +app.whenReady().then(() => { + const crashType = app.commandLine.getSwitchValue('crash-type'); + + if (crashType === 'main') { + process.crash(); + } else if (crashType === 'renderer') { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + if (setExtraParameters) { + w.webContents.executeJavaScript(` + require('electron').crashReporter.addExtraParameter('rendererSpecific', 'rs'); + require('electron').crashReporter.addExtraParameter('addedThenRemoved', 'to-be-removed'); + require('electron').crashReporter.removeExtraParameter('addedThenRemoved'); + `); + } + w.webContents.executeJavaScript('process.crash()'); + w.webContents.on('render-process-gone', () => process.exit(0)); + } else if (crashType === 'sandboxed-renderer') { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: path.resolve(__dirname, 'sandbox-preload.js'), + contextIsolation: false + } + }); + w.loadURL(`about:blank?set_extra=${setExtraParameters ? 1 : 0}`); + w.webContents.on('render-process-gone', () => process.exit(0)); + } else if (crashType === 'node') { + const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`); + const version = app.getVersion(); + const crashPath = path.join(__dirname, 'node-crash.js'); + const child = childProcess.fork(crashPath, [url, version, crashesDir], { silent: true }); + child.on('exit', () => process.exit(0)); + } else { + console.error(`Unrecognized crash type: '${crashType}'`); + process.exit(1); + } +}); + +setTimeout(() => app.exit(), 30000); diff --git a/spec-main/fixtures/apps/crash/node-crash.js b/spec-main/fixtures/apps/crash/node-crash.js new file mode 100644 index 0000000000000..1d3d45c6c4e5e --- /dev/null +++ b/spec-main/fixtures/apps/crash/node-crash.js @@ -0,0 +1,11 @@ +if (process.platform === 'linux') { + process.crashReporter.start({ + submitURL: process.argv[2], + productName: 'Zombies', + compress: false, + globalExtra: { + _version: process.argv[3] + } + }); +} +process.nextTick(() => process.crash()); diff --git a/spec-main/fixtures/apps/crash/package.json b/spec-main/fixtures/apps/crash/package.json new file mode 100644 index 0000000000000..fefcef88bea69 --- /dev/null +++ b/spec-main/fixtures/apps/crash/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-crash", + "main": "main.js" +} diff --git a/spec-main/fixtures/apps/crash/sandbox-preload.js b/spec-main/fixtures/apps/crash/sandbox-preload.js new file mode 100644 index 0000000000000..a1aca9dc3e073 --- /dev/null +++ b/spec-main/fixtures/apps/crash/sandbox-preload.js @@ -0,0 +1,10 @@ +const { crashReporter } = require('electron'); + +const params = new URLSearchParams(location.search); +if (params.get('set_extra') === '1') { + crashReporter.addExtraParameter('rendererSpecific', 'rs'); + crashReporter.addExtraParameter('addedThenRemoved', 'to-be-removed'); + crashReporter.removeExtraParameter('addedThenRemoved'); +} + +process.crash(); diff --git a/spec-main/fixtures/apps/libuv-hang/index.html b/spec-main/fixtures/apps/libuv-hang/index.html new file mode 100644 index 0000000000000..a3534d419a6f4 --- /dev/null +++ b/spec-main/fixtures/apps/libuv-hang/index.html @@ -0,0 +1,13 @@ + + + + + + + Hello World! + + +

Hello World!

+ + + diff --git a/spec-main/fixtures/apps/libuv-hang/main.js b/spec-main/fixtures/apps/libuv-hang/main.js new file mode 100644 index 0000000000000..4ca4ef15d9024 --- /dev/null +++ b/spec-main/fixtures/apps/libuv-hang/main.js @@ -0,0 +1,36 @@ +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); + +async function createWindow () { + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }); + + await mainWindow.loadFile('index.html'); +} + +app.whenReady().then(() => { + createWindow(); + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +let count = 0; +ipcMain.handle('reload-successful', () => { + if (count === 2) { + app.quit(); + } else { + count++; + return count; + } +}); + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/spec-main/fixtures/apps/libuv-hang/preload.js b/spec-main/fixtures/apps/libuv-hang/preload.js new file mode 100644 index 0000000000000..a5840f557f562 --- /dev/null +++ b/spec-main/fixtures/apps/libuv-hang/preload.js @@ -0,0 +1,16 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('api', { + ipcRenderer, + run: async () => { + const { promises: fs } = require('fs'); + for (let i = 0; i < 10; i++) { + const list = await fs.readdir('.', { withFileTypes: true }); + for (const file of list) { + if (file.isFile()) { + await fs.readFile(file.name, 'utf-8'); + } + } + } + } +}); diff --git a/spec-main/fixtures/apps/libuv-hang/renderer.js b/spec-main/fixtures/apps/libuv-hang/renderer.js new file mode 100644 index 0000000000000..5f0a2b58b5145 --- /dev/null +++ b/spec-main/fixtures/apps/libuv-hang/renderer.js @@ -0,0 +1,8 @@ +const count = localStorage.getItem('count'); + +const { run, ipcRenderer } = window.api; + +run().then(async () => { + const count = await ipcRenderer.invoke('reload-successful'); + if (count < 3) location.reload(); +}).catch(console.log); diff --git a/spec-main/fixtures/apps/open-new-window-from-link/index.html b/spec-main/fixtures/apps/open-new-window-from-link/index.html new file mode 100644 index 0000000000000..b7584564e0010 --- /dev/null +++ b/spec-main/fixtures/apps/open-new-window-from-link/index.html @@ -0,0 +1,11 @@ + + + + + + Hello World! + + + Open New Window + + diff --git a/spec-main/fixtures/apps/open-new-window-from-link/main.js b/spec-main/fixtures/apps/open-new-window-from-link/main.js new file mode 100644 index 0000000000000..7d41b3bb29ee5 --- /dev/null +++ b/spec-main/fixtures/apps/open-new-window-from-link/main.js @@ -0,0 +1,64 @@ +const { app, BrowserWindow } = require('electron'); +const path = require('path'); + +async function createWindow () { + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + x: 100, + y: 100, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: false, + nodeIntegration: true + } + }); + + await mainWindow.loadFile('index.html'); + + const rect = await mainWindow.webContents.executeJavaScript('JSON.parse(JSON.stringify(document.querySelector("a").getBoundingClientRect()))'); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + + function click (x, y, options) { + x = Math.floor(x); + y = Math.floor(y); + mainWindow.webContents.sendInputEvent({ + type: 'mouseDown', + button: 'left', + x, + y, + clickCount: 1, + ...options + }); + + mainWindow.webContents.sendInputEvent({ + type: 'mouseUp', + button: 'left', + x, + y, + clickCount: 1, + ...options + }); + } + + click(x, y, { modifiers: ['shift'] }); +} + +app.whenReady().then(() => { + app.on('web-contents-created', (e, wc) => { + wc.on('render-process-gone', (e, details) => { + console.error(details); + process.exit(1); + }); + + wc.on('did-finish-load', () => { + const title = wc.getTitle(); + if (title === 'Window From Link') { + process.exit(0); + } + }); + }); + + createWindow(); +}); diff --git a/spec-main/fixtures/apps/open-new-window-from-link/new-window-page.html b/spec-main/fixtures/apps/open-new-window-from-link/new-window-page.html new file mode 100644 index 0000000000000..ac919774f3b27 --- /dev/null +++ b/spec-main/fixtures/apps/open-new-window-from-link/new-window-page.html @@ -0,0 +1,11 @@ + + + + + + Window From Link + + + I'm a window opened from a link! + + diff --git a/spec-main/fixtures/apps/open-new-window-from-link/package.json b/spec-main/fixtures/apps/open-new-window-from-link/package.json new file mode 100644 index 0000000000000..ff9319a62ae99 --- /dev/null +++ b/spec-main/fixtures/apps/open-new-window-from-link/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-open-new-window-from-link", + "main": "main.js" +} diff --git a/spec-main/fixtures/apps/open-new-window-from-link/preload.js b/spec-main/fixtures/apps/open-new-window-from-link/preload.js new file mode 100644 index 0000000000000..edb91c4291072 --- /dev/null +++ b/spec-main/fixtures/apps/open-new-window-from-link/preload.js @@ -0,0 +1,3 @@ +window.addEventListener('click', e => { + console.log('click', e); +}); diff --git a/spec-main/fixtures/apps/remote-control/main.js b/spec-main/fixtures/apps/remote-control/main.js new file mode 100644 index 0000000000000..b8d89ff724853 --- /dev/null +++ b/spec-main/fixtures/apps/remote-control/main.js @@ -0,0 +1,32 @@ +const { app } = require('electron'); +const http = require('http'); +const v8 = require('v8'); + +if (app.commandLine.hasSwitch('boot-eval')) { + // eslint-disable-next-line no-eval + eval(app.commandLine.getSwitchValue('boot-eval')); +} + +app.whenReady().then(() => { + const server = http.createServer((req, res) => { + const chunks = []; + req.on('data', chunk => { chunks.push(chunk); }); + req.on('end', () => { + const js = Buffer.concat(chunks).toString('utf8'); + (async () => { + try { + const result = await Promise.resolve(eval(js)); // eslint-disable-line no-eval + res.end(v8.serialize({ result })); + } catch (e) { + res.end(v8.serialize({ error: e.stack })); + } + })(); + }); + }).listen(0, '127.0.0.1', () => { + process.stdout.write(`Listening: ${server.address().port}\n`); + }); +}); + +setTimeout(() => { + process.exit(0); +}, 30000); diff --git a/spec-main/fixtures/apps/remote-control/package.json b/spec-main/fixtures/apps/remote-control/package.json new file mode 100644 index 0000000000000..1d473fd35e0e8 --- /dev/null +++ b/spec-main/fixtures/apps/remote-control/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-remote-control", + "main": "main.js" +} diff --git a/spec-main/fixtures/apps/self-module-paths/index.html b/spec-main/fixtures/apps/self-module-paths/index.html new file mode 100644 index 0000000000000..d1c74e8175047 --- /dev/null +++ b/spec-main/fixtures/apps/self-module-paths/index.html @@ -0,0 +1,7 @@ + + + +

Hello World!

+ + + diff --git a/spec-main/fixtures/apps/self-module-paths/main.js b/spec-main/fixtures/apps/self-module-paths/main.js new file mode 100644 index 0000000000000..3baae49b7a79e --- /dev/null +++ b/spec-main/fixtures/apps/self-module-paths/main.js @@ -0,0 +1,34 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); + +function createWindow () { + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + show: false, + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: true, + contextIsolation: false + } + }); + + mainWindow.loadFile('index.html'); +} + +ipcMain.handle('module-paths', (e, success) => { + process.exit(success ? 0 : 1); +}); + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/spec-main/fixtures/apps/self-module-paths/package.json b/spec-main/fixtures/apps/self-module-paths/package.json new file mode 100644 index 0000000000000..dd5fbf228960e --- /dev/null +++ b/spec-main/fixtures/apps/self-module-paths/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-self-module-paths", + "main": "main.js" +} diff --git a/spec-main/fixtures/apps/self-module-paths/renderer.js b/spec-main/fixtures/apps/self-module-paths/renderer.js new file mode 100644 index 0000000000000..5148845343972 --- /dev/null +++ b/spec-main/fixtures/apps/self-module-paths/renderer.js @@ -0,0 +1,12 @@ +const { ipcRenderer } = require('electron'); + +const worker = new Worker('worker.js'); + +worker.onmessage = (event) => { + const workerPaths = event.data.sort().toString(); + const rendererPaths = self.module.paths.sort().toString(); + const validModulePaths = workerPaths === rendererPaths && workerPaths !== 0; + + ipcRenderer.invoke('module-paths', validModulePaths); + worker.terminate(); +}; diff --git a/spec-main/fixtures/apps/self-module-paths/worker.js b/spec-main/fixtures/apps/self-module-paths/worker.js new file mode 100644 index 0000000000000..2f1ecffdbcfea --- /dev/null +++ b/spec-main/fixtures/apps/self-module-paths/worker.js @@ -0,0 +1 @@ +self.postMessage(self.module.paths); diff --git a/spec-main/fixtures/apps/xwindow-icon/icon.png b/spec-main/fixtures/apps/xwindow-icon/icon.png new file mode 100644 index 0000000000000..bc527dda8ae2c Binary files /dev/null and b/spec-main/fixtures/apps/xwindow-icon/icon.png differ diff --git a/spec-main/fixtures/apps/xwindow-icon/main.js b/spec-main/fixtures/apps/xwindow-icon/main.js new file mode 100644 index 0000000000000..0da98ad277ce9 --- /dev/null +++ b/spec-main/fixtures/apps/xwindow-icon/main.js @@ -0,0 +1,13 @@ +const { app, BrowserWindow } = require('electron'); +const path = require('path'); + +app.whenReady().then(() => { + const w = new BrowserWindow({ + show: false, + icon: path.join(__dirname, 'icon.png') + }); + w.webContents.on('did-finish-load', () => { + app.quit(); + }); + w.loadURL('about:blank'); +}); diff --git a/spec-main/fixtures/apps/xwindow-icon/package.json b/spec-main/fixtures/apps/xwindow-icon/package.json new file mode 100644 index 0000000000000..595e59b0f0f46 --- /dev/null +++ b/spec-main/fixtures/apps/xwindow-icon/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-xwindow-icon", + "main": "main.js" + } \ No newline at end of file diff --git a/spec-main/fixtures/auto-update/check-with-headers/index.js b/spec-main/fixtures/auto-update/check-with-headers/index.js new file mode 100644 index 0000000000000..987a586f9cc46 --- /dev/null +++ b/spec-main/fixtures/auto-update/check-with-headers/index.js @@ -0,0 +1,26 @@ +process.on('uncaughtException', (err) => { + console.error(err); + process.exit(1); +}); + +const { autoUpdater } = require('electron'); + +autoUpdater.on('error', (err) => { + console.error(err); + process.exit(1); +}); + +const feedUrl = process.argv[1]; + +autoUpdater.setFeedURL({ + url: feedUrl, + headers: { + 'X-test': 'this-is-a-test' + } +}); + +autoUpdater.checkForUpdates(); + +autoUpdater.on('update-not-available', () => { + process.exit(0); +}); diff --git a/spec-main/fixtures/auto-update/check-with-headers/package.json b/spec-main/fixtures/auto-update/check-with-headers/package.json new file mode 100644 index 0000000000000..05b06e42b92f0 --- /dev/null +++ b/spec-main/fixtures/auto-update/check-with-headers/package.json @@ -0,0 +1,5 @@ +{ + "name": "electron-test-initial-app", + "version": "1.0.0", + "main": "./index.js" +} \ No newline at end of file diff --git a/spec-main/fixtures/auto-update/check/index.js b/spec-main/fixtures/auto-update/check/index.js new file mode 100644 index 0000000000000..7b89a831a6859 --- /dev/null +++ b/spec-main/fixtures/auto-update/check/index.js @@ -0,0 +1,23 @@ +process.on('uncaughtException', (err) => { + console.error(err); + process.exit(1); +}); + +const { autoUpdater } = require('electron'); + +autoUpdater.on('error', (err) => { + console.error(err); + process.exit(1); +}); + +const feedUrl = process.argv[1]; + +autoUpdater.setFeedURL({ + url: feedUrl +}); + +autoUpdater.checkForUpdates(); + +autoUpdater.on('update-not-available', () => { + process.exit(0); +}); diff --git a/spec-main/fixtures/auto-update/check/package.json b/spec-main/fixtures/auto-update/check/package.json new file mode 100644 index 0000000000000..05b06e42b92f0 --- /dev/null +++ b/spec-main/fixtures/auto-update/check/package.json @@ -0,0 +1,5 @@ +{ + "name": "electron-test-initial-app", + "version": "1.0.0", + "main": "./index.js" +} \ No newline at end of file diff --git a/spec-main/fixtures/auto-update/initial/index.js b/spec-main/fixtures/auto-update/initial/index.js new file mode 100644 index 0000000000000..1f1c2571b1645 --- /dev/null +++ b/spec-main/fixtures/auto-update/initial/index.js @@ -0,0 +1,18 @@ +process.on('uncaughtException', (err) => { + console.error(err); + process.exit(1); +}); + +const { autoUpdater } = require('electron'); + +const feedUrl = process.argv[1]; + +console.log('Setting Feed URL'); + +autoUpdater.setFeedURL({ + url: feedUrl +}); + +console.log('Feed URL Set:', feedUrl); + +process.exit(0); diff --git a/spec-main/fixtures/auto-update/initial/package.json b/spec-main/fixtures/auto-update/initial/package.json new file mode 100644 index 0000000000000..05b06e42b92f0 --- /dev/null +++ b/spec-main/fixtures/auto-update/initial/package.json @@ -0,0 +1,5 @@ +{ + "name": "electron-test-initial-app", + "version": "1.0.0", + "main": "./index.js" +} \ No newline at end of file diff --git a/spec-main/fixtures/auto-update/update-json/index.js b/spec-main/fixtures/auto-update/update-json/index.js new file mode 100644 index 0000000000000..1595696131f9d --- /dev/null +++ b/spec-main/fixtures/auto-update/update-json/index.js @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); + +process.on('uncaughtException', (err) => { + console.error(err); + process.exit(1); +}); + +const { app, autoUpdater } = require('electron'); + +autoUpdater.on('error', (err) => { + console.error(err); + process.exit(1); +}); + +const urlPath = path.resolve(__dirname, '../../../../url.txt'); +let feedUrl = process.argv[1]; +if (!feedUrl || !feedUrl.startsWith('http')) { + feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`; +} else { + fs.writeFileSync(urlPath, `${feedUrl}/updated`); +} + +autoUpdater.setFeedURL({ + url: feedUrl, + serverType: 'json' +}); + +autoUpdater.checkForUpdates(); + +autoUpdater.on('update-available', () => { + console.log('Update Available'); +}); + +autoUpdater.on('update-downloaded', () => { + console.log('Update Downloaded'); + autoUpdater.quitAndInstall(); +}); + +autoUpdater.on('update-not-available', () => { + console.error('No update available'); + process.exit(1); +}); diff --git a/spec/fixtures/auto-update/check/package.json b/spec-main/fixtures/auto-update/update-json/package.json similarity index 100% rename from spec/fixtures/auto-update/check/package.json rename to spec-main/fixtures/auto-update/update-json/package.json diff --git a/spec-main/fixtures/auto-update/update/index.js b/spec-main/fixtures/auto-update/update/index.js new file mode 100644 index 0000000000000..e391362f12f04 --- /dev/null +++ b/spec-main/fixtures/auto-update/update/index.js @@ -0,0 +1,42 @@ +const fs = require('fs'); +const path = require('path'); + +process.on('uncaughtException', (err) => { + console.error(err); + process.exit(1); +}); + +const { app, autoUpdater } = require('electron'); + +autoUpdater.on('error', (err) => { + console.error(err); + process.exit(1); +}); + +const urlPath = path.resolve(__dirname, '../../../../url.txt'); +let feedUrl = process.argv[1]; +if (!feedUrl || !feedUrl.startsWith('http')) { + feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`; +} else { + fs.writeFileSync(urlPath, `${feedUrl}/updated`); +} + +autoUpdater.setFeedURL({ + url: feedUrl +}); + +autoUpdater.checkForUpdates(); + +autoUpdater.on('update-available', () => { + console.log('Update Available'); +}); + +autoUpdater.on('update-downloaded', () => { + console.log('Update Downloaded'); + autoUpdater.quitAndInstall(); +}); + +autoUpdater.on('update-not-available', () => { + console.error('No update available'); + process.exit(1); +}); diff --git a/spec/fixtures/auto-update/initial/package.json b/spec-main/fixtures/auto-update/update/package.json similarity index 100% rename from spec/fixtures/auto-update/initial/package.json rename to spec-main/fixtures/auto-update/update/package.json diff --git a/spec-main/fixtures/blank.png b/spec-main/fixtures/blank.png new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/spec/fixtures/assets/cat.pdf b/spec-main/fixtures/cat.pdf similarity index 100% rename from spec/fixtures/assets/cat.pdf rename to spec-main/fixtures/cat.pdf diff --git a/spec-main/fixtures/chromium/other-window.js b/spec-main/fixtures/chromium/other-window.js new file mode 100644 index 0000000000000..5e516e9dfabcc --- /dev/null +++ b/spec-main/fixtures/chromium/other-window.js @@ -0,0 +1,21 @@ +const { app, BrowserWindow } = require('electron'); + +const ints = (...args) => args.map(a => parseInt(a, 10)); + +const [x, y, width, height] = ints(...process.argv.slice(2)); + +let w; + +app.whenReady().then(() => { + w = new BrowserWindow({ + x, + y, + width, + height + }); + console.log('__ready__'); +}); + +process.on('SIGTERM', () => { + process.exit(0); +}); diff --git a/spec-main/fixtures/chromium/spellchecker.html b/spec-main/fixtures/chromium/spellchecker.html new file mode 100644 index 0000000000000..f785d1e8ff35f --- /dev/null +++ b/spec-main/fixtures/chromium/spellchecker.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/spec-main/fixtures/chromium/visibilitystate.html b/spec-main/fixtures/chromium/visibilitystate.html new file mode 100644 index 0000000000000..7b1576b9d7e18 --- /dev/null +++ b/spec-main/fixtures/chromium/visibilitystate.html @@ -0,0 +1,18 @@ + + + + + + + Document + + + + + diff --git a/spec-main/fixtures/crash-cases/api-browser-destroy/index.js b/spec-main/fixtures/crash-cases/api-browser-destroy/index.js new file mode 100644 index 0000000000000..1e87b8d177a42 --- /dev/null +++ b/spec-main/fixtures/crash-cases/api-browser-destroy/index.js @@ -0,0 +1,25 @@ +const { app, BrowserWindow, BrowserView } = require('electron'); +const { expect } = require('chai'); + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true + } + }); + const view = new BrowserView(); + mainWindow.addBrowserView(view); + view.webContents.destroy(); + view.setBounds({ x: 0, y: 0, width: 0, height: 0 }); + const bounds = view.getBounds(); + expect(bounds).to.deep.equal({ x: 0, y: 0, width: 0, height: 0 }); + view.setBackgroundColor('#56cc5b10'); +} + +app.on('ready', () => { + createWindow(); + setTimeout(() => app.quit()); +}); diff --git a/spec-main/fixtures/crash-cases/early-in-memory-session-create/index.js b/spec-main/fixtures/crash-cases/early-in-memory-session-create/index.js new file mode 100644 index 0000000000000..295a1cd5a5d8d --- /dev/null +++ b/spec-main/fixtures/crash-cases/early-in-memory-session-create/index.js @@ -0,0 +1,8 @@ +const { app, session } = require('electron'); + +app.on('ready', () => { + session.fromPartition('in-memory'); + setImmediate(() => { + process.exit(0); + }); +}); diff --git a/spec-main/fixtures/crash-cases/fs-promises-renderer-crash/index.html b/spec-main/fixtures/crash-cases/fs-promises-renderer-crash/index.html new file mode 100644 index 0000000000000..9721dac6ed821 --- /dev/null +++ b/spec-main/fixtures/crash-cases/fs-promises-renderer-crash/index.html @@ -0,0 +1,17 @@ + + + + + diff --git a/spec-main/fixtures/crash-cases/fs-promises-renderer-crash/index.js b/spec-main/fixtures/crash-cases/fs-promises-renderer-crash/index.js new file mode 100644 index 0000000000000..4c74ba39f74d6 --- /dev/null +++ b/spec-main/fixtures/crash-cases/fs-promises-renderer-crash/index.js @@ -0,0 +1,28 @@ +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); + +app.whenReady().then(() => { + let reloadCount = 0; + const win = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + win.loadFile('index.html'); + + win.webContents.on('render-process-gone', () => { + process.exit(1); + }); + + win.webContents.on('did-finish-load', () => { + if (reloadCount > 2) { + setImmediate(() => app.quit()); + } else { + reloadCount += 1; + win.webContents.send('reload', path.join(__dirname, '..', '..', 'cat.pdf')); + } + }); +}); diff --git a/spec-main/fixtures/crash-cases/in-memory-session-double-free/index.js b/spec-main/fixtures/crash-cases/in-memory-session-double-free/index.js new file mode 100644 index 0000000000000..e2445690cd2f3 --- /dev/null +++ b/spec-main/fixtures/crash-cases/in-memory-session-double-free/index.js @@ -0,0 +1,7 @@ +const { app, BrowserWindow } = require('electron'); + +app.on('ready', async () => { + const win = new BrowserWindow({ show: false, webPreferences: { partition: '123321' } }); + await win.loadURL('data:text/html,'); + setTimeout(() => app.quit()); +}); diff --git a/spec-main/fixtures/crash-cases/js-execute-iframe/index.html b/spec-main/fixtures/crash-cases/js-execute-iframe/index.html new file mode 100644 index 0000000000000..96a82f6749f25 --- /dev/null +++ b/spec-main/fixtures/crash-cases/js-execute-iframe/index.html @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/spec-main/fixtures/crash-cases/js-execute-iframe/index.js b/spec-main/fixtures/crash-cases/js-execute-iframe/index.js new file mode 100644 index 0000000000000..d024d4eea4b14 --- /dev/null +++ b/spec-main/fixtures/crash-cases/js-execute-iframe/index.js @@ -0,0 +1,51 @@ +const { app, BrowserWindow } = require('electron'); +const net = require('net'); +const path = require('path'); + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + nodeIntegrationInSubFrames: true + } + }); + + mainWindow.loadFile('index.html'); +} + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); + +const server = net.createServer((c) => { + console.log('client connected'); + + c.on('end', () => { + console.log('client disconnected'); + app.quit(); + }); + + c.write('hello\r\n'); + c.pipe(c); +}); + +server.on('error', (err) => { + throw err; +}); + +const p = process.platform === 'win32' + ? path.join('\\\\?\\pipe', process.cwd(), 'myctl') + : '/tmp/echo.sock'; + +server.listen(p, () => { + console.log('server bound'); +}); diff --git a/spec-main/fixtures/crash-cases/js-execute-iframe/page2.html b/spec-main/fixtures/crash-cases/js-execute-iframe/page2.html new file mode 100644 index 0000000000000..755d755c42e30 --- /dev/null +++ b/spec-main/fixtures/crash-cases/js-execute-iframe/page2.html @@ -0,0 +1,4 @@ + + + HELLO + \ No newline at end of file diff --git a/spec-main/fixtures/crash-cases/native-window-open-exit/index.html b/spec-main/fixtures/crash-cases/native-window-open-exit/index.html new file mode 100644 index 0000000000000..ee82a9694ed55 --- /dev/null +++ b/spec-main/fixtures/crash-cases/native-window-open-exit/index.html @@ -0,0 +1,3 @@ + + MAIN PAGE + \ No newline at end of file diff --git a/spec-main/fixtures/crash-cases/native-window-open-exit/index.js b/spec-main/fixtures/crash-cases/native-window-open-exit/index.js new file mode 100644 index 0000000000000..4ef61051800ae --- /dev/null +++ b/spec-main/fixtures/crash-cases/native-window-open-exit/index.js @@ -0,0 +1,40 @@ +const { app, ipcMain, BrowserWindow } = require('electron'); +const path = require('path'); +const http = require('http'); + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + webSecurity: false, + preload: path.join(__dirname, 'preload.js') + } + }); + + mainWindow.loadFile('index.html'); + mainWindow.webContents.on('render-process-gone', () => { + process.exit(1); + }); +} + +const server = http.createServer((_req, res) => { + res.end('hello'); +}).listen(7001, '127.0.0.1'); + +app.whenReady().then(() => { + createWindow(); + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); + +ipcMain.on('test-done', () => { + console.log('test passed'); + server.close(); + process.exit(0); +}); diff --git a/spec-main/fixtures/crash-cases/native-window-open-exit/preload.js b/spec-main/fixtures/crash-cases/native-window-open-exit/preload.js new file mode 100644 index 0000000000000..aace1d035a0da --- /dev/null +++ b/spec-main/fixtures/crash-cases/native-window-open-exit/preload.js @@ -0,0 +1,6 @@ +const { ipcRenderer } = require('electron'); + +window.addEventListener('DOMContentLoaded', () => { + window.open('127.0.0.1:7001', '_blank'); + setTimeout(() => ipcRenderer.send('test-done')); +}); diff --git a/spec-main/fixtures/crash-cases/quit-on-crashed-event/index.js b/spec-main/fixtures/crash-cases/quit-on-crashed-event/index.js new file mode 100644 index 0000000000000..a095bf69f490c --- /dev/null +++ b/spec-main/fixtures/crash-cases/quit-on-crashed-event/index.js @@ -0,0 +1,19 @@ +const { app, BrowserWindow } = require('electron'); + +app.once('ready', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: false, + nodeIntegration: true + } + }); + w.webContents.once('render-process-gone', (_, details) => { + if (details.reason === 'crashed') { + process.exit(0); + } else { + process.exit(details.exitCode); + } + }); + await w.webContents.loadURL('chrome://checkcrash'); +}); diff --git a/spec-main/fixtures/crash-cases/safe-storage/index.js b/spec-main/fixtures/crash-cases/safe-storage/index.js new file mode 100644 index 0000000000000..151751820a8d5 --- /dev/null +++ b/spec-main/fixtures/crash-cases/safe-storage/index.js @@ -0,0 +1,39 @@ +const { app, safeStorage } = require('electron'); +const { expect } = require('chai'); + +(async () => { + if (!app.isReady()) { + // isEncryptionAvailable() returns false before the app is ready on + // Linux: https://github.com/electron/electron/issues/32206 + // and + // Windows: https://github.com/electron/electron/issues/33640. + expect(safeStorage.isEncryptionAvailable()).to.equal(process.platform === 'darwin'); + if (safeStorage.isEncryptionAvailable()) { + const plaintext = 'plaintext'; + const ciphertext = safeStorage.encryptString(plaintext); + expect(Buffer.isBuffer(ciphertext)).to.equal(true); + expect(safeStorage.decryptString(ciphertext)).to.equal(plaintext); + } else { + expect(() => safeStorage.encryptString('plaintext')).to.throw(/safeStorage cannot be used before app is ready/); + expect(() => safeStorage.decryptString(Buffer.from(''))).to.throw(/safeStorage cannot be used before app is ready/); + } + } + await app.whenReady(); + // isEncryptionAvailable() will always return false on CI due to a mocked + // dbus as mentioned above. + expect(safeStorage.isEncryptionAvailable()).to.equal(process.platform !== 'linux'); + if (safeStorage.isEncryptionAvailable()) { + const plaintext = 'plaintext'; + const ciphertext = safeStorage.encryptString(plaintext); + expect(Buffer.isBuffer(ciphertext)).to.equal(true); + expect(safeStorage.decryptString(ciphertext)).to.equal(plaintext); + } else { + expect(() => safeStorage.encryptString('plaintext')).to.throw(/Encryption is not available/); + expect(() => safeStorage.decryptString(Buffer.from(''))).to.throw(/Decryption is not available/); + } +})() + .then(app.quit) + .catch((err) => { + console.error(err); + app.exit(1); + }); diff --git a/spec-main/fixtures/crash-cases/setimmediate-renderer-crash/index.js b/spec-main/fixtures/crash-cases/setimmediate-renderer-crash/index.js new file mode 100644 index 0000000000000..7960616214020 --- /dev/null +++ b/spec-main/fixtures/crash-cases/setimmediate-renderer-crash/index.js @@ -0,0 +1,22 @@ +const { app, BrowserWindow } = require('electron'); +const path = require('path'); + +app.whenReady().then(() => { + const win = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + preload: path.resolve(__dirname, 'preload.js') + } + }); + + win.loadURL('about:blank'); + + win.webContents.on('render-process-gone', () => { + process.exit(1); + }); + + win.webContents.on('did-finish-load', () => { + setTimeout(() => app.quit()); + }); +}); diff --git a/spec-main/fixtures/crash-cases/setimmediate-renderer-crash/preload.js b/spec-main/fixtures/crash-cases/setimmediate-renderer-crash/preload.js new file mode 100644 index 0000000000000..8e8afb06af723 --- /dev/null +++ b/spec-main/fixtures/crash-cases/setimmediate-renderer-crash/preload.js @@ -0,0 +1,3 @@ +setImmediate(() => { + throw new Error('oh no'); +}); diff --git a/spec-main/fixtures/crash-cases/setimmediate-window-open-crash/index.html b/spec-main/fixtures/crash-cases/setimmediate-window-open-crash/index.html new file mode 100644 index 0000000000000..f0c803fc08c8e --- /dev/null +++ b/spec-main/fixtures/crash-cases/setimmediate-window-open-crash/index.html @@ -0,0 +1,21 @@ + + + + + diff --git a/spec-main/fixtures/crash-cases/setimmediate-window-open-crash/index.js b/spec-main/fixtures/crash-cases/setimmediate-window-open-crash/index.js new file mode 100644 index 0000000000000..b9cfe83aec46e --- /dev/null +++ b/spec-main/fixtures/crash-cases/setimmediate-window-open-crash/index.js @@ -0,0 +1,20 @@ +const { app, BrowserWindow } = require('electron'); + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + mainWindow.on('close', () => { + app.quit(); + }); + + mainWindow.loadFile('index.html'); +} + +app.whenReady().then(() => { + createWindow(); +}); diff --git a/spec-main/fixtures/crash-cases/transparent-window-get-background-color/index.js b/spec-main/fixtures/crash-cases/transparent-window-get-background-color/index.js new file mode 100644 index 0000000000000..5742ffa171e0c --- /dev/null +++ b/spec-main/fixtures/crash-cases/transparent-window-get-background-color/index.js @@ -0,0 +1,14 @@ +const { app, BrowserWindow } = require('electron'); + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + transparent: true + }); + mainWindow.getBackgroundColor(); +} + +app.on('ready', () => { + createWindow(); + setTimeout(() => app.quit()); +}); diff --git a/spec-main/fixtures/crash-cases/webcontents-create-leak-exit/index.js b/spec-main/fixtures/crash-cases/webcontents-create-leak-exit/index.js new file mode 100644 index 0000000000000..d601c18fc14eb --- /dev/null +++ b/spec-main/fixtures/crash-cases/webcontents-create-leak-exit/index.js @@ -0,0 +1,6 @@ +const { app, webContents } = require('electron'); +app.whenReady().then(function () { + webContents.create({}); + + app.quit(); +}); diff --git a/spec-main/fixtures/crash-cases/webcontentsview-create-leak-exit/index.js b/spec-main/fixtures/crash-cases/webcontentsview-create-leak-exit/index.js new file mode 100644 index 0000000000000..3211bfd40b00f --- /dev/null +++ b/spec-main/fixtures/crash-cases/webcontentsview-create-leak-exit/index.js @@ -0,0 +1,6 @@ +const { WebContentsView, app } = require('electron'); +app.whenReady().then(function () { + new WebContentsView({}) // eslint-disable-line + + app.quit(); +}); diff --git a/spec-main/fixtures/crash-cases/webview-attach-destroyed/index.js b/spec-main/fixtures/crash-cases/webview-attach-destroyed/index.js new file mode 100644 index 0000000000000..ea6ee7c8b8a84 --- /dev/null +++ b/spec-main/fixtures/crash-cases/webview-attach-destroyed/index.js @@ -0,0 +1,9 @@ +const { app, BrowserWindow } = require('electron'); + +app.whenReady().then(() => { + const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } }); + w.loadURL('data:text/html,'); + app.on('web-contents-created', () => { + w.close(); + }); +}); diff --git a/spec-main/fixtures/crash-cases/webview-contents-error-on-creation/index.js b/spec-main/fixtures/crash-cases/webview-contents-error-on-creation/index.js new file mode 100644 index 0000000000000..cbfc7eb98b488 --- /dev/null +++ b/spec-main/fixtures/crash-cases/webview-contents-error-on-creation/index.js @@ -0,0 +1,14 @@ +const { app, BrowserWindow } = require('electron'); + +app.whenReady().then(() => { + const mainWindow = new BrowserWindow({ + show: false + }); + mainWindow.loadFile('about:blank'); + + app.on('web-contents-created', () => { + throw new Error(); + }); + + app.quit(); +}); diff --git a/spec/fixtures/devtools-extensions/bad-manifest/manifest.json b/spec-main/fixtures/devtools-extensions/bad-manifest/manifest.json similarity index 100% rename from spec/fixtures/devtools-extensions/bad-manifest/manifest.json rename to spec-main/fixtures/devtools-extensions/bad-manifest/manifest.json diff --git a/spec/fixtures/devtools-extensions/foo/_locales/en/messages.json b/spec-main/fixtures/devtools-extensions/foo/_locales/en/messages.json similarity index 100% rename from spec/fixtures/devtools-extensions/foo/_locales/en/messages.json rename to spec-main/fixtures/devtools-extensions/foo/_locales/en/messages.json diff --git a/spec-main/fixtures/devtools-extensions/foo/devtools.js b/spec-main/fixtures/devtools-extensions/foo/devtools.js new file mode 100644 index 0000000000000..637f3411d6de1 --- /dev/null +++ b/spec-main/fixtures/devtools-extensions/foo/devtools.js @@ -0,0 +1,2 @@ +/* global chrome */ +chrome.devtools.panels.create('Foo', 'foo.png', 'index.html'); diff --git a/spec-main/fixtures/devtools-extensions/foo/foo.html b/spec-main/fixtures/devtools-extensions/foo/foo.html new file mode 100644 index 0000000000000..9a00a78cfe0fd --- /dev/null +++ b/spec-main/fixtures/devtools-extensions/foo/foo.html @@ -0,0 +1,8 @@ + + + + + foo + + + diff --git a/spec-main/fixtures/devtools-extensions/foo/index.html b/spec-main/fixtures/devtools-extensions/foo/index.html new file mode 100644 index 0000000000000..df7c456474a42 --- /dev/null +++ b/spec-main/fixtures/devtools-extensions/foo/index.html @@ -0,0 +1,11 @@ + + + + + + + + + a custom devtools extension + + diff --git a/spec-main/fixtures/devtools-extensions/foo/manifest.json b/spec-main/fixtures/devtools-extensions/foo/manifest.json new file mode 100644 index 0000000000000..65e16e074b2cf --- /dev/null +++ b/spec-main/fixtures/devtools-extensions/foo/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "foo", + "permissions": [ + "storage" + ], + "version": "1.0", + "devtools_page": "foo.html", + "default_locale": "en" +} diff --git a/spec-main/fixtures/devtools-extensions/foo/panel.js b/spec-main/fixtures/devtools-extensions/foo/panel.js new file mode 100644 index 0000000000000..8b877b389cabd --- /dev/null +++ b/spec-main/fixtures/devtools-extensions/foo/panel.js @@ -0,0 +1,79 @@ +/* global chrome */ +function testStorageClear (callback) { + chrome.storage.sync.clear(function () { + chrome.storage.sync.get(null, function (syncItems) { + chrome.storage.local.clear(function () { + chrome.storage.local.get(null, function (localItems) { + callback(syncItems, localItems); + }); + }); + }); + }); +} + +function testStorageRemove (callback) { + chrome.storage.sync.remove('bar', function () { + chrome.storage.sync.get({ foo: 'baz' }, function (syncItems) { + chrome.storage.local.remove(['hello'], function () { + chrome.storage.local.get(null, function (localItems) { + callback(syncItems, localItems); + }); + }); + }); + }); +} + +function testStorageSet (callback) { + chrome.storage.sync.set({ foo: 'bar', bar: 'foo' }, function () { + chrome.storage.sync.get({ foo: 'baz', bar: 'fooo' }, function (syncItems) { + chrome.storage.local.set({ hello: 'world', world: 'hello' }, function () { + chrome.storage.local.get(null, function (localItems) { + callback(syncItems, localItems); + }); + }); + }); + }); +} + +function testStorage (callback) { + testStorageSet(function (syncForSet, localForSet) { + testStorageRemove(function (syncForRemove, localForRemove) { + testStorageClear(function (syncForClear, localForClear) { + callback( + syncForSet, localForSet, + syncForRemove, localForRemove, + syncForClear, localForClear + ); + }); + }); + }); +} + +testStorage(function ( + syncForSet, localForSet, + syncForRemove, localForRemove, + syncForClear, localForClear +) { + setTimeout(() => { + const message = JSON.stringify({ + runtimeId: chrome.runtime.id, + tabId: chrome.devtools.inspectedWindow.tabId, + i18nString: chrome.i18n.getMessage('foo', ['bar', 'baz']), + storageItems: { + local: { + set: localForSet, + remove: localForRemove, + clear: localForClear + }, + sync: { + set: syncForSet, + remove: syncForRemove, + clear: syncForClear + } + } + }); + + const sendMessage = `require('electron').ipcRenderer.send('answer', ${message})`; + window.chrome.devtools.inspectedWindow.eval(sendMessage, function () {}); + }); +}); diff --git a/spec-main/fixtures/dogs-running.txt b/spec-main/fixtures/dogs-running.txt new file mode 100644 index 0000000000000..66d80ebc6def2 --- /dev/null +++ b/spec-main/fixtures/dogs-running.txt @@ -0,0 +1 @@ +Dogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs runningDogs running \ No newline at end of file diff --git a/spec-main/fixtures/extensions/chrome-api/background.js b/spec-main/fixtures/extensions/chrome-api/background.js new file mode 100644 index 0000000000000..e3f2129a1bc0a --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-api/background.js @@ -0,0 +1,34 @@ +/* global chrome */ + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + const { method, args = [] } = message; + const tabId = sender.tab.id; + + switch (method) { + case 'sendMessage': { + const [message] = args; + chrome.tabs.sendMessage(tabId, { message, tabId }, undefined, sendResponse); + break; + } + + case 'executeScript': { + const [code] = args; + chrome.tabs.executeScript(tabId, { code }, ([result]) => sendResponse(result)); + break; + } + + case 'connectTab': { + const [name] = args; + const port = chrome.tabs.connect(tabId, { name }); + port.postMessage('howdy'); + break; + } + + case 'update': { + const [tabId, props] = args; + chrome.tabs.update(tabId, props, sendResponse); + } + } + // Respond asynchronously + return true; +}); diff --git a/spec-main/fixtures/extensions/chrome-api/main.js b/spec-main/fixtures/extensions/chrome-api/main.js new file mode 100644 index 0000000000000..14331534b7f91 --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-api/main.js @@ -0,0 +1,52 @@ +/* global chrome */ + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + sendResponse(message); +}); + +const testMap = { + connect () { + let success = false; + try { + chrome.runtime.connect(chrome.runtime.id); + chrome.runtime.connect(chrome.runtime.id, { name: 'content-script' }); + chrome.runtime.connect({ name: 'content-script' }); + success = true; + } finally { + console.log(JSON.stringify(success)); + } + }, + getManifest () { + const manifest = chrome.runtime.getManifest(); + console.log(JSON.stringify(manifest)); + }, + sendMessage (message) { + chrome.runtime.sendMessage({ method: 'sendMessage', args: [message] }, response => { + console.log(JSON.stringify(response)); + }); + }, + executeScript (code) { + chrome.runtime.sendMessage({ method: 'executeScript', args: [code] }, response => { + console.log(JSON.stringify(response)); + }); + }, + connectTab (name) { + chrome.runtime.onConnect.addListener(port => { + port.onMessage.addListener(message => { + console.log([port.name, message].join()); + }); + }); + chrome.runtime.sendMessage({ method: 'connectTab', args: [name] }); + }, + update (tabId, props) { + chrome.runtime.sendMessage({ method: 'update', args: [tabId, props] }, response => { + console.log(JSON.stringify(response)); + }); + } +}; + +const dispatchTest = (event) => { + const { method, args = [] } = JSON.parse(event.data); + testMap[method](...args); +}; +window.addEventListener('message', dispatchTest, false); diff --git a/spec-main/fixtures/extensions/chrome-api/manifest.json b/spec-main/fixtures/extensions/chrome-api/manifest.json new file mode 100644 index 0000000000000..10e82e9ffd379 --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-api/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "chrome-api", + "version": "1.0", + "content_scripts": [ + { + "matches": [""], + "js": ["main.js"], + "run_at": "document_start" + } + ], + "background": { + "scripts": ["background.js"], + "persistent": false + }, + "permissions": [ + "" + ], + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/chrome-i18n/_locales/en/messages.json b/spec-main/fixtures/extensions/chrome-i18n/_locales/en/messages.json new file mode 100644 index 0000000000000..802922cff5dc6 --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-i18n/_locales/en/messages.json @@ -0,0 +1,6 @@ +{ + "extName": { + "message": "chrome-i18n", + "description": "Extension name." + } +} diff --git a/spec-main/fixtures/extensions/chrome-i18n/main.js b/spec-main/fixtures/extensions/chrome-i18n/main.js new file mode 100644 index 0000000000000..77657ebf4e385 --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-i18n/main.js @@ -0,0 +1,33 @@ +/* eslint-disable */ + +function evalInMainWorld(fn) { + const script = document.createElement('script') + script.textContent = `((${fn})())` + document.documentElement.appendChild(script) +} + +async function exec(name) { + let result + switch (name) { + case 'getMessage': + result = { + id: chrome.i18n.getMessage('@@extension_id'), + name: chrome.i18n.getMessage('extName'), + } + break + case 'getAcceptLanguages': + result = await new Promise(resolve => chrome.i18n.getAcceptLanguages(resolve)) + break + } + + const funcStr = `() => { require('electron').ipcRenderer.send('success', ${JSON.stringify(result)}) }` + evalInMainWorld(funcStr) +} + +window.addEventListener('message', event => { + exec(event.data.name) +}) + +evalInMainWorld(() => { + window.exec = name => window.postMessage({ name }) +}) diff --git a/spec-main/fixtures/extensions/chrome-i18n/manifest.json b/spec-main/fixtures/extensions/chrome-i18n/manifest.json new file mode 100644 index 0000000000000..e6401fdc3d07a --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-i18n/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "__MSG_extName__", + "default_locale": "en", + "version": "1.0", + "content_scripts": [ + { + "matches": [""], + "js": ["main.js"], + "run_at": "document_start" + } + ], + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/chrome-runtime/background.js b/spec-main/fixtures/extensions/chrome-runtime/background.js new file mode 100644 index 0000000000000..7a81b6c8e749c --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-runtime/background.js @@ -0,0 +1,12 @@ +/* global chrome */ + +chrome.runtime.onMessage.addListener((message, sender, reply) => { + switch (message) { + case 'getPlatformInfo': + chrome.runtime.getPlatformInfo(reply); + break; + } + + // Respond asynchronously + return true; +}); diff --git a/spec-main/fixtures/extensions/chrome-runtime/main.js b/spec-main/fixtures/extensions/chrome-runtime/main.js index 9caebefa5e974..c830b951dddb7 100644 --- a/spec-main/fixtures/extensions/chrome-runtime/main.js +++ b/spec-main/fixtures/extensions/chrome-runtime/main.js @@ -1,5 +1,39 @@ -document.documentElement.textContent = JSON.stringify({ - manifest: chrome.runtime.getManifest(), - id: chrome.runtime.id, - url: chrome.runtime.getURL('main.js'), +/* eslint-disable */ + +function evalInMainWorld(fn) { + const script = document.createElement('script') + script.textContent = `((${fn})())` + document.documentElement.appendChild(script) +} + +async function exec(name) { + let result + switch (name) { + case 'getManifest': + result = chrome.runtime.getManifest() + break + case 'id': + result = chrome.runtime.id + break + case 'getURL': + result = chrome.runtime.getURL('main.js') + break + case 'getPlatformInfo': { + result = await new Promise(resolve => { + chrome.runtime.sendMessage(name, resolve) + }) + break + } + } + + const funcStr = `() => { require('electron').ipcRenderer.send('success', ${JSON.stringify(result)}) }` + evalInMainWorld(funcStr) +} + +window.addEventListener('message', event => { + exec(event.data.name) +}) + +evalInMainWorld(() => { + window.exec = name => window.postMessage({ name }) }) diff --git a/spec-main/fixtures/extensions/chrome-runtime/manifest.json b/spec-main/fixtures/extensions/chrome-runtime/manifest.json index 3428ef156ebe7..9fca66254304b 100644 --- a/spec-main/fixtures/extensions/chrome-runtime/manifest.json +++ b/spec-main/fixtures/extensions/chrome-runtime/manifest.json @@ -5,8 +5,12 @@ { "matches": [""], "js": ["main.js"], - "run_at": "document_start" + "run_at": "document_end" } ], + "background": { + "scripts": ["background.js"], + "persistent": false + }, "manifest_version": 2 } diff --git a/spec-main/fixtures/extensions/chrome-storage/main.js b/spec-main/fixtures/extensions/chrome-storage/main.js index 91beec17c649f..b8804809d95fb 100644 --- a/spec-main/fixtures/extensions/chrome-storage/main.js +++ b/spec-main/fixtures/extensions/chrome-storage/main.js @@ -1,5 +1,6 @@ -chrome.storage.local.set({key: 'value'}, () => { - chrome.storage.local.get(['key'], ({key}) => { +/* eslint-disable */ +chrome.storage.local.set({ key: 'value' }, () => { + chrome.storage.local.get(['key'], ({ key }) => { const script = document.createElement('script') script.textContent = `require('electron').ipcRenderer.send('storage-success', ${JSON.stringify(key)})` document.documentElement.appendChild(script) diff --git a/spec-main/fixtures/extensions/chrome-webRequest-wss/background.js b/spec-main/fixtures/extensions/chrome-webRequest-wss/background.js new file mode 100644 index 0000000000000..94ddee976b1ee --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-webRequest-wss/background.js @@ -0,0 +1,12 @@ +/* global chrome */ + +chrome.webRequest.onBeforeSendHeaders.addListener( + (details) => { + if (details.requestHeaders) { + details.requestHeaders.foo = 'bar'; + } + return { cancel: false, requestHeaders: details.requestHeaders }; + }, + { urls: ['*://127.0.0.1:*'] }, + ['blocking'] +); diff --git a/spec-main/fixtures/extensions/chrome-webRequest-wss/manifest.json b/spec-main/fixtures/extensions/chrome-webRequest-wss/manifest.json new file mode 100644 index 0000000000000..c1723d2118850 --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-webRequest-wss/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "chrome-webRequest", + "version": "1.0", + "background": { + "scripts": ["background.js"], + "persistent": true + }, + "permissions": ["webRequest", "webRequestBlocking", ""], + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/chrome-webRequest/background.js b/spec-main/fixtures/extensions/chrome-webRequest/background.js new file mode 100644 index 0000000000000..ba6f9f3f610ea --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-webRequest/background.js @@ -0,0 +1,9 @@ +/* global chrome */ + +chrome.webRequest.onBeforeRequest.addListener( + (details) => { + return { cancel: true }; + }, + { urls: ['*://127.0.0.1:*'] }, + ['blocking'] +); diff --git a/spec-main/fixtures/extensions/chrome-webRequest/manifest.json b/spec-main/fixtures/extensions/chrome-webRequest/manifest.json new file mode 100644 index 0000000000000..c1723d2118850 --- /dev/null +++ b/spec-main/fixtures/extensions/chrome-webRequest/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "chrome-webRequest", + "version": "1.0", + "background": { + "scripts": ["background.js"], + "persistent": true + }, + "permissions": ["webRequest", "webRequestBlocking", ""], + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/content-script-document-end/end.js b/spec-main/fixtures/extensions/content-script-document-end/end.js new file mode 100644 index 0000000000000..2ce126d3f81bf --- /dev/null +++ b/spec-main/fixtures/extensions/content-script-document-end/end.js @@ -0,0 +1 @@ +document.documentElement.style.backgroundColor = 'red'; diff --git a/spec/fixtures/extensions/content-script-document-end/manifest.json b/spec-main/fixtures/extensions/content-script-document-end/manifest.json similarity index 100% rename from spec/fixtures/extensions/content-script-document-end/manifest.json rename to spec-main/fixtures/extensions/content-script-document-end/manifest.json diff --git a/spec-main/fixtures/extensions/content-script-document-idle/idle.js b/spec-main/fixtures/extensions/content-script-document-idle/idle.js new file mode 100644 index 0000000000000..b01f93ba591ab --- /dev/null +++ b/spec-main/fixtures/extensions/content-script-document-idle/idle.js @@ -0,0 +1 @@ +document.body.style.backgroundColor = 'red'; diff --git a/spec/fixtures/extensions/content-script-document-idle/manifest.json b/spec-main/fixtures/extensions/content-script-document-idle/manifest.json similarity index 100% rename from spec/fixtures/extensions/content-script-document-idle/manifest.json rename to spec-main/fixtures/extensions/content-script-document-idle/manifest.json diff --git a/spec/fixtures/extensions/content-script-document-start/manifest.json b/spec-main/fixtures/extensions/content-script-document-start/manifest.json similarity index 100% rename from spec/fixtures/extensions/content-script-document-start/manifest.json rename to spec-main/fixtures/extensions/content-script-document-start/manifest.json diff --git a/spec-main/fixtures/extensions/content-script-document-start/start.js b/spec-main/fixtures/extensions/content-script-document-start/start.js new file mode 100644 index 0000000000000..2ce126d3f81bf --- /dev/null +++ b/spec-main/fixtures/extensions/content-script-document-start/start.js @@ -0,0 +1 @@ +document.documentElement.style.backgroundColor = 'red'; diff --git a/spec/fixtures/extensions/content-script/all_frames-disabled.css b/spec-main/fixtures/extensions/content-script/all_frames-disabled.css similarity index 100% rename from spec/fixtures/extensions/content-script/all_frames-disabled.css rename to spec-main/fixtures/extensions/content-script/all_frames-disabled.css diff --git a/spec/fixtures/extensions/content-script/all_frames-enabled.css b/spec-main/fixtures/extensions/content-script/all_frames-enabled.css similarity index 100% rename from spec/fixtures/extensions/content-script/all_frames-enabled.css rename to spec-main/fixtures/extensions/content-script/all_frames-enabled.css diff --git a/spec-main/fixtures/extensions/content-script/all_frames-preload.js b/spec-main/fixtures/extensions/content-script/all_frames-preload.js new file mode 100644 index 0000000000000..424124917ac0e --- /dev/null +++ b/spec-main/fixtures/extensions/content-script/all_frames-preload.js @@ -0,0 +1,14 @@ +const { ipcRenderer, webFrame } = require('electron'); + +if (process.isMainFrame) { + // https://github.com/electron/electron/issues/17252 + ipcRenderer.on('executeJavaScriptInFrame', (event, frameRoutingId, code, responseId) => { + const frame = webFrame.findFrameByRoutingId(frameRoutingId); + if (!frame) { + throw new Error(`Can't find frame for routing ID ${frameRoutingId}`); + } + frame.executeJavaScript(code, false).then(result => { + event.sender.send(`executeJavaScriptInFrame_${responseId}`, result); + }); + }); +} diff --git a/spec/fixtures/extensions/content-script/frame-with-frame.html b/spec-main/fixtures/extensions/content-script/frame-with-frame.html similarity index 100% rename from spec/fixtures/extensions/content-script/frame-with-frame.html rename to spec-main/fixtures/extensions/content-script/frame-with-frame.html diff --git a/spec/fixtures/extensions/content-script/frame.html b/spec-main/fixtures/extensions/content-script/frame.html similarity index 100% rename from spec/fixtures/extensions/content-script/frame.html rename to spec-main/fixtures/extensions/content-script/frame.html diff --git a/spec/fixtures/extensions/content-script/manifest.json b/spec-main/fixtures/extensions/content-script/manifest.json similarity index 100% rename from spec/fixtures/extensions/content-script/manifest.json rename to spec-main/fixtures/extensions/content-script/manifest.json diff --git a/spec-main/fixtures/extensions/devtools-extension/foo.html b/spec-main/fixtures/extensions/devtools-extension/foo.html new file mode 100644 index 0000000000000..d5df384f53b49 --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/foo.html @@ -0,0 +1,9 @@ + + + + + foo + + + + diff --git a/spec-main/fixtures/extensions/devtools-extension/foo.js b/spec-main/fixtures/extensions/devtools-extension/foo.js new file mode 100644 index 0000000000000..3196c3e4ab403 --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/foo.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line +chrome.devtools.panels.create('Foo', 'icon.png', 'index.html') diff --git a/spec-main/fixtures/extensions/devtools-extension/index.html b/spec-main/fixtures/extensions/devtools-extension/index.html new file mode 100644 index 0000000000000..dde48b4c80858 --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/index.html @@ -0,0 +1,5 @@ + + + a custom devtools extension + + diff --git a/spec-main/fixtures/extensions/devtools-extension/index.js b/spec-main/fixtures/extensions/devtools-extension/index.js new file mode 100644 index 0000000000000..0cd6e38ed9564 --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/index.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line +chrome.devtools.inspectedWindow.eval(`require("electron").ipcRenderer.send("winning")`, (result, exc) => { + console.log(result, exc); +}); diff --git a/spec-main/fixtures/extensions/devtools-extension/manifest.json b/spec-main/fixtures/extensions/devtools-extension/manifest.json new file mode 100644 index 0000000000000..bf5acfc24f36a --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "version": "1.0", + "devtools_page": "foo.html", + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/lazy-background-page/background.js b/spec-main/fixtures/extensions/lazy-background-page/background.js new file mode 100644 index 0000000000000..5fcd9fdad481a --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/background.js @@ -0,0 +1,5 @@ +/* eslint-disable no-undef */ +chrome.runtime.onMessage.addListener((message, sender, reply) => { + window.receivedMessage = message; + reply({ message, sender }); +}); diff --git a/spec-main/fixtures/extensions/lazy-background-page/content_script.js b/spec-main/fixtures/extensions/lazy-background-page/content_script.js new file mode 100644 index 0000000000000..f8f0bf6f8a510 --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/content_script.js @@ -0,0 +1,6 @@ +/* eslint-disable no-undef */ +chrome.runtime.sendMessage({ some: 'message' }, (response) => { + const script = document.createElement('script'); + script.textContent = `require('electron').ipcRenderer.send('bg-page-message-response', ${JSON.stringify(response)})`; + document.documentElement.appendChild(script); +}); diff --git a/spec-main/fixtures/extensions/lazy-background-page/get-background-page.js b/spec-main/fixtures/extensions/lazy-background-page/get-background-page.js new file mode 100644 index 0000000000000..a54c8391f513e --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/get-background-page.js @@ -0,0 +1,7 @@ +/* global chrome */ +window.completionPromise = new Promise((resolve) => { + window.completionPromiseResolve = resolve; +}); +chrome.runtime.sendMessage({ some: 'message' }, (response) => { + window.completionPromiseResolve(chrome.extension.getBackgroundPage().receivedMessage); +}); diff --git a/spec-main/fixtures/extensions/lazy-background-page/manifest.json b/spec-main/fixtures/extensions/lazy-background-page/manifest.json new file mode 100644 index 0000000000000..f80e5b928784d --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/manifest.json @@ -0,0 +1,16 @@ +{ + "name": "lazy-background-page", + "version": "1.0", + "background": { + "scripts": ["background.js"], + "persistent": false + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content_script.js"], + "run_at": "document_start" + } + ], + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/lazy-background-page/page-get-background.html b/spec-main/fixtures/extensions/lazy-background-page/page-get-background.html new file mode 100644 index 0000000000000..ab983bfd34c21 --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/page-get-background.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html b/spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html new file mode 100644 index 0000000000000..eee3ba694e8db --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/lazy-background-page/runtime-get-background-page.js b/spec-main/fixtures/extensions/lazy-background-page/runtime-get-background-page.js new file mode 100644 index 0000000000000..59716d5501dd4 --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/runtime-get-background-page.js @@ -0,0 +1,9 @@ +/* global chrome */ +window.completionPromise = new Promise((resolve) => { + window.completionPromiseResolve = resolve; +}); +chrome.runtime.sendMessage({ some: 'message' }, (response) => { + chrome.runtime.getBackgroundPage((bgPage) => { + window.completionPromiseResolve(bgPage.receivedMessage); + }); +}); diff --git a/spec-main/fixtures/extensions/load-error/manifest.json b/spec-main/fixtures/extensions/load-error/manifest.json new file mode 100644 index 0000000000000..29a9cd7975e3d --- /dev/null +++ b/spec-main/fixtures/extensions/load-error/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "load-error", + "version": "1.0", + "icons": { + "16": "/images/error.png" + }, + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/missing-manifest/main.js b/spec-main/fixtures/extensions/missing-manifest/main.js new file mode 100644 index 0000000000000..d1e760d7b9d07 --- /dev/null +++ b/spec-main/fixtures/extensions/missing-manifest/main.js @@ -0,0 +1 @@ +console.log('oh no where is my manifest'); diff --git a/spec-main/fixtures/extensions/mv3-service-worker/background.js b/spec-main/fixtures/extensions/mv3-service-worker/background.js new file mode 100644 index 0000000000000..c4d4a3003587d --- /dev/null +++ b/spec-main/fixtures/extensions/mv3-service-worker/background.js @@ -0,0 +1 @@ +console.log('service worker installed'); diff --git a/spec-main/fixtures/extensions/mv3-service-worker/manifest.json b/spec-main/fixtures/extensions/mv3-service-worker/manifest.json new file mode 100644 index 0000000000000..bc81e3a6a510b --- /dev/null +++ b/spec-main/fixtures/extensions/mv3-service-worker/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "MV3 Service Worker", + "description": "Test for extension service worker support.", + "version": "1.0", + "manifest_version": 3, + "background": { + "service_worker": "background.js" + } +} diff --git a/spec-main/fixtures/extensions/persistent-background-page/background.js b/spec-main/fixtures/extensions/persistent-background-page/background.js new file mode 100644 index 0000000000000..2a49b2bd9e686 --- /dev/null +++ b/spec-main/fixtures/extensions/persistent-background-page/background.js @@ -0,0 +1 @@ +/* eslint-disable no-undef */ diff --git a/spec-main/fixtures/extensions/persistent-background-page/manifest.json b/spec-main/fixtures/extensions/persistent-background-page/manifest.json new file mode 100644 index 0000000000000..bbfe42a5d94b6 --- /dev/null +++ b/spec-main/fixtures/extensions/persistent-background-page/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "persistent-background-page", + "version": "1.0", + "background": { + "scripts": ["background.js"], + "persistent": true + }, + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/red-bg/main.js b/spec-main/fixtures/extensions/red-bg/main.js index 787b050adc8cf..2ce126d3f81bf 100644 --- a/spec-main/fixtures/extensions/red-bg/main.js +++ b/spec-main/fixtures/extensions/red-bg/main.js @@ -1 +1 @@ -document.documentElement.style.backgroundColor = 'red' +document.documentElement.style.backgroundColor = 'red'; diff --git a/spec-main/fixtures/extensions/ui-page/bare-page.html b/spec-main/fixtures/extensions/ui-page/bare-page.html new file mode 100644 index 0000000000000..25735dccac8bd --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/bare-page.html @@ -0,0 +1,2 @@ + +ui page loaded ok diff --git a/spec-main/fixtures/extensions/ui-page/manifest.json b/spec-main/fixtures/extensions/ui-page/manifest.json new file mode 100644 index 0000000000000..7344d6270a0a0 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "ui-page", + "version": "1.0", + "manifest_version": 2, + "permissions": [""] +} diff --git a/spec-main/fixtures/extensions/ui-page/page-get-background.html b/spec-main/fixtures/extensions/ui-page/page-get-background.html new file mode 100644 index 0000000000000..ab983bfd34c21 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/page-get-background.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/ui-page/page-script-load.html b/spec-main/fixtures/extensions/ui-page/page-script-load.html new file mode 100644 index 0000000000000..02a37b4bc0bc5 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/page-script-load.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/ui-page/script.js b/spec-main/fixtures/extensions/ui-page/script.js new file mode 100644 index 0000000000000..1941b7484aa92 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/script.js @@ -0,0 +1 @@ +document.write('script loaded ok'); diff --git a/spec-main/fixtures/module/declare-buffer.js b/spec-main/fixtures/module/declare-buffer.js new file mode 100644 index 0000000000000..575d5c55ba709 --- /dev/null +++ b/spec-main/fixtures/module/declare-buffer.js @@ -0,0 +1,2 @@ +const Buffer = 'declared Buffer'; +module.exports = Buffer; diff --git a/spec-main/fixtures/module/declare-global.js b/spec-main/fixtures/module/declare-global.js new file mode 100644 index 0000000000000..8f49833aa9177 --- /dev/null +++ b/spec-main/fixtures/module/declare-global.js @@ -0,0 +1,2 @@ +const global = 'declared global'; +module.exports = global; diff --git a/spec-main/fixtures/module/declare-process.js b/spec-main/fixtures/module/declare-process.js new file mode 100644 index 0000000000000..5cc35a3cb10b0 --- /dev/null +++ b/spec-main/fixtures/module/declare-process.js @@ -0,0 +1,2 @@ +const process = 'declared process'; +module.exports = process; diff --git a/spec-main/fixtures/module/echo-renamed.js b/spec-main/fixtures/module/echo-renamed.js new file mode 100644 index 0000000000000..6dfa05f914a3c --- /dev/null +++ b/spec-main/fixtures/module/echo-renamed.js @@ -0,0 +1,7 @@ +let echo; +try { + echo = require('@electron-ci/echo'); +} catch (e) { + process.exit(1); +} +process.exit(echo(0)); diff --git a/spec-main/fixtures/module/echo.js b/spec-main/fixtures/module/echo.js new file mode 100644 index 0000000000000..ae1b31dd9bed9 --- /dev/null +++ b/spec-main/fixtures/module/echo.js @@ -0,0 +1,6 @@ +process.on('uncaughtException', function (err) { + process.send(err.message); +}); + +const echo = require('@electron-ci/echo'); +process.send(echo('ok')); diff --git a/spec-main/fixtures/module/preload-sandbox.js b/spec-main/fixtures/module/preload-sandbox.js new file mode 100644 index 0000000000000..d774c54301f31 --- /dev/null +++ b/spec-main/fixtures/module/preload-sandbox.js @@ -0,0 +1,64 @@ +(function () { + const { setImmediate } = require('timers'); + const { ipcRenderer } = require('electron'); + window.ipcRenderer = ipcRenderer; + window.setImmediate = setImmediate; + window.require = require; + + function invoke (code) { + try { + return code(); + } catch { + return null; + } + } + + process.once('loaded', () => { + ipcRenderer.send('process-loaded'); + }); + + if (location.protocol === 'file:') { + window.test = 'preload'; + window.process = process; + if (process.env.sandboxmain) { + window.test = { + osSandbox: !process.argv.includes('--no-sandbox'), + hasCrash: typeof process.crash === 'function', + hasHang: typeof process.hang === 'function', + creationTime: invoke(() => process.getCreationTime()), + heapStatistics: invoke(() => process.getHeapStatistics()), + blinkMemoryInfo: invoke(() => process.getBlinkMemoryInfo()), + processMemoryInfo: invoke(() => process.getProcessMemoryInfo() ? {} : null), + systemMemoryInfo: invoke(() => process.getSystemMemoryInfo()), + systemVersion: invoke(() => process.getSystemVersion()), + cpuUsage: invoke(() => process.getCPUUsage()), + ioCounters: invoke(() => process.getIOCounters()), + uptime: invoke(() => process.uptime()), + env: process.env, + execPath: process.execPath, + pid: process.pid, + arch: process.arch, + platform: process.platform, + sandboxed: process.sandboxed, + contextIsolated: process.contextIsolated, + type: process.type, + version: process.version, + versions: process.versions, + contextId: process.contextId + }; + } + } else if (location.href !== 'about:blank') { + addEventListener('DOMContentLoaded', () => { + ipcRenderer.on('touch-the-opener', () => { + let errorMessage = null; + try { + const openerDoc = opener.document; // eslint-disable-line no-unused-vars + } catch (error) { + errorMessage = error.message; + } + ipcRenderer.send('answer', errorMessage); + }); + ipcRenderer.send('child-loaded', window.opener == null, document.body.innerHTML, location.href); + }); + } +})(); diff --git a/spec-main/fixtures/module/print-crash-parameters.js b/spec-main/fixtures/module/print-crash-parameters.js new file mode 100644 index 0000000000000..94fde0fbd5c78 --- /dev/null +++ b/spec-main/fixtures/module/print-crash-parameters.js @@ -0,0 +1,2 @@ +process.crashReporter.addExtraParameter('hello', 'world'); +process.stdout.write(JSON.stringify(process.crashReporter.getParameters()) + '\n'); diff --git a/spec/fixtures/module/test.coffee b/spec-main/fixtures/module/test.coffee similarity index 100% rename from spec/fixtures/module/test.coffee rename to spec-main/fixtures/module/test.coffee diff --git a/spec-main/fixtures/module/uv-dlopen.js b/spec-main/fixtures/module/uv-dlopen.js new file mode 100644 index 0000000000000..2f1e3413f3255 --- /dev/null +++ b/spec-main/fixtures/module/uv-dlopen.js @@ -0,0 +1 @@ +require('@electron-ci/uv-dlopen'); diff --git a/spec/fixtures/native-addon/echo/binding.cc b/spec-main/fixtures/native-addon/echo/binding.cc similarity index 100% rename from spec/fixtures/native-addon/echo/binding.cc rename to spec-main/fixtures/native-addon/echo/binding.cc diff --git a/spec/fixtures/native-addon/echo/binding.gyp b/spec-main/fixtures/native-addon/echo/binding.gyp similarity index 100% rename from spec/fixtures/native-addon/echo/binding.gyp rename to spec-main/fixtures/native-addon/echo/binding.gyp diff --git a/spec-main/fixtures/native-addon/echo/lib/echo.js b/spec-main/fixtures/native-addon/echo/lib/echo.js new file mode 100644 index 0000000000000..3d204f5e239cf --- /dev/null +++ b/spec-main/fixtures/native-addon/echo/lib/echo.js @@ -0,0 +1 @@ +module.exports = require('../build/Release/echo.node').Print; diff --git a/spec-main/fixtures/native-addon/echo/package.json b/spec-main/fixtures/native-addon/echo/package.json new file mode 100644 index 0000000000000..74956d42a0e27 --- /dev/null +++ b/spec-main/fixtures/native-addon/echo/package.json @@ -0,0 +1,5 @@ +{ + "main": "./lib/echo.js", + "name": "@electron-ci/echo", + "version": "0.0.1" +} diff --git a/spec-main/fixtures/native-addon/uv-dlopen/binding.gyp b/spec-main/fixtures/native-addon/uv-dlopen/binding.gyp new file mode 100644 index 0000000000000..6d8d0d39b0dde --- /dev/null +++ b/spec-main/fixtures/native-addon/uv-dlopen/binding.gyp @@ -0,0 +1,13 @@ +{ + "targets": [ + { + "target_name": "test_module", + "sources": [ "main.cpp" ], + }, + { + "target_name": "libfoo", + "type": "shared_library", + "sources": [ "foo.cpp" ] + } + ] +} \ No newline at end of file diff --git a/spec-main/fixtures/native-addon/uv-dlopen/foo.cpp b/spec-main/fixtures/native-addon/uv-dlopen/foo.cpp new file mode 100644 index 0000000000000..00beb974c1a72 --- /dev/null +++ b/spec-main/fixtures/native-addon/uv-dlopen/foo.cpp @@ -0,0 +1,2 @@ +extern "C" +void foo() { } \ No newline at end of file diff --git a/spec-main/fixtures/native-addon/uv-dlopen/index.js b/spec-main/fixtures/native-addon/uv-dlopen/index.js new file mode 100644 index 0000000000000..d401b4e391c11 --- /dev/null +++ b/spec-main/fixtures/native-addon/uv-dlopen/index.js @@ -0,0 +1,16 @@ +const testLoadLibrary = require('./build/Release/test_module'); + +const lib = (() => { + switch (process.platform) { + case 'linux': + return `${__dirname}/build/Release/foo.so`; + case 'darwin': + return `${__dirname}/build/Release/foo.dylib`; + case 'win32': + return `${__dirname}/build/Release/libfoo.dll`; + default: + throw new Error('unsupported os'); + } +})(); + +testLoadLibrary(lib); diff --git a/spec-main/fixtures/native-addon/uv-dlopen/main.cpp b/spec-main/fixtures/native-addon/uv-dlopen/main.cpp new file mode 100644 index 0000000000000..4d34850a6254c --- /dev/null +++ b/spec-main/fixtures/native-addon/uv-dlopen/main.cpp @@ -0,0 +1,42 @@ +#include +#include + +namespace test_module { + +napi_value TestLoadLibrary(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv; + napi_status status; + status = napi_get_cb_info(env, info, &argc, &argv, NULL, NULL); + if (status != napi_ok) napi_fatal_error(NULL, 0, NULL, 0); + + char lib_path[256]; + status = napi_get_value_string_utf8(env, argv, lib_path, 256, NULL); + if (status != napi_ok) napi_fatal_error(NULL, 0, NULL, 0); + + uv_lib_t lib; + auto uv_status = uv_dlopen(lib_path, &lib); + if (uv_status == 0) { + napi_value result; + status = napi_get_boolean(env, true, &result); + if (status != napi_ok) napi_fatal_error(NULL, 0, NULL, 0); + return result; + } else { + status = napi_throw_error(env, NULL, uv_dlerror(&lib)); + if (status != napi_ok) napi_fatal_error(NULL, 0, NULL, 0); + } +} + +napi_value Init(napi_env env, napi_value exports) { + napi_value method; + napi_status status; + status = napi_create_function(env, "testLoadLibrary", NAPI_AUTO_LENGTH, + TestLoadLibrary, NULL, &method); + if (status != napi_ok) + return NULL; + return method; +} + +NAPI_MODULE(TestLoadLibrary, Init); + +} // namespace test_module \ No newline at end of file diff --git a/spec-main/fixtures/native-addon/uv-dlopen/package.json b/spec-main/fixtures/native-addon/uv-dlopen/package.json new file mode 100644 index 0000000000000..f6844acfeb0f4 --- /dev/null +++ b/spec-main/fixtures/native-addon/uv-dlopen/package.json @@ -0,0 +1,5 @@ +{ + "name": "@electron-ci/uv-dlopen", + "version": "0.0.1", + "main": "index.js" +} diff --git a/spec-main/fixtures/pages/datalist.html b/spec-main/fixtures/pages/datalist.html new file mode 100644 index 0000000000000..8fc56ecbf14b1 --- /dev/null +++ b/spec-main/fixtures/pages/datalist.html @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/spec-main/fixtures/pages/fetch.html b/spec-main/fixtures/pages/fetch.html new file mode 100644 index 0000000000000..9e2ef6409579c --- /dev/null +++ b/spec-main/fixtures/pages/fetch.html @@ -0,0 +1,15 @@ + + + + + diff --git a/spec-main/fixtures/pages/half-background-color.html b/spec-main/fixtures/pages/half-background-color.html new file mode 100644 index 0000000000000..07e5ccdcadf90 --- /dev/null +++ b/spec-main/fixtures/pages/half-background-color.html @@ -0,0 +1,20 @@ + + + + + + + + +
+ + diff --git a/spec-main/fixtures/pages/jquery-3.6.0.min.js b/spec-main/fixtures/pages/jquery-3.6.0.min.js new file mode 100644 index 0000000000000..c4c6022f2982e --- /dev/null +++ b/spec-main/fixtures/pages/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 + + + + + + diff --git a/spec-main/fixtures/pages/overlay.html b/spec-main/fixtures/pages/overlay.html new file mode 100644 index 0000000000000..e866725574d55 --- /dev/null +++ b/spec-main/fixtures/pages/overlay.html @@ -0,0 +1,84 @@ + + + + + + + + + +
+
+ Title goes here + +
+
+
+ + + diff --git a/spec-main/fixtures/pages/pdf-in-iframe.html b/spec-main/fixtures/pages/pdf-in-iframe.html new file mode 100644 index 0000000000000..e42aa85490a02 --- /dev/null +++ b/spec-main/fixtures/pages/pdf-in-iframe.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/frame-container-webview.html b/spec-main/fixtures/sub-frames/frame-container-webview.html similarity index 81% rename from spec/fixtures/sub-frames/frame-container-webview.html rename to spec-main/fixtures/sub-frames/frame-container-webview.html index aabc9e87e1a19..98c30790890b3 100644 --- a/spec/fixtures/sub-frames/frame-container-webview.html +++ b/spec-main/fixtures/sub-frames/frame-container-webview.html @@ -8,6 +8,6 @@ This is the root page with a webview - + diff --git a/spec/fixtures/sub-frames/frame-container.html b/spec-main/fixtures/sub-frames/frame-container.html similarity index 83% rename from spec/fixtures/sub-frames/frame-container.html rename to spec-main/fixtures/sub-frames/frame-container.html index f731555a5ddaf..48e1e12150be4 100644 --- a/spec/fixtures/sub-frames/frame-container.html +++ b/spec-main/fixtures/sub-frames/frame-container.html @@ -8,6 +8,6 @@ This is the root page - + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/frame-with-frame-container-webview.html b/spec-main/fixtures/sub-frames/frame-with-frame-container-webview.html similarity index 100% rename from spec/fixtures/sub-frames/frame-with-frame-container-webview.html rename to spec-main/fixtures/sub-frames/frame-with-frame-container-webview.html diff --git a/spec/fixtures/sub-frames/frame-with-frame-container.html b/spec-main/fixtures/sub-frames/frame-with-frame-container.html similarity index 100% rename from spec/fixtures/sub-frames/frame-with-frame-container.html rename to spec-main/fixtures/sub-frames/frame-with-frame-container.html diff --git a/spec/fixtures/sub-frames/frame-with-frame.html b/spec-main/fixtures/sub-frames/frame-with-frame.html similarity index 84% rename from spec/fixtures/sub-frames/frame-with-frame.html rename to spec-main/fixtures/sub-frames/frame-with-frame.html index 9d99fef71b332..3f46a8adab9d3 100644 --- a/spec/fixtures/sub-frames/frame-with-frame.html +++ b/spec-main/fixtures/sub-frames/frame-with-frame.html @@ -8,6 +8,6 @@ This is a frame, is has one child - + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/frame.html b/spec-main/fixtures/sub-frames/frame.html similarity index 100% rename from spec/fixtures/sub-frames/frame.html rename to spec-main/fixtures/sub-frames/frame.html diff --git a/spec-main/fixtures/sub-frames/preload.js b/spec-main/fixtures/sub-frames/preload.js new file mode 100644 index 0000000000000..52e87657f1d79 --- /dev/null +++ b/spec-main/fixtures/sub-frames/preload.js @@ -0,0 +1,9 @@ +const { ipcRenderer, webFrame } = require('electron'); + +window.isolatedGlobal = true; + +ipcRenderer.send('preload-ran', window.location.href, webFrame.routingId); + +ipcRenderer.on('preload-ping', () => { + ipcRenderer.send('preload-pong', webFrame.routingId); +}); diff --git a/spec-main/fixtures/sub-frames/test.js b/spec-main/fixtures/sub-frames/test.js new file mode 100644 index 0000000000000..e921523b1b42e --- /dev/null +++ b/spec-main/fixtures/sub-frames/test.js @@ -0,0 +1 @@ +console.log('hello'); diff --git a/spec-main/fixtures/sub-frames/webview-iframe-preload.js b/spec-main/fixtures/sub-frames/webview-iframe-preload.js new file mode 100644 index 0000000000000..af76f1e03d237 --- /dev/null +++ b/spec-main/fixtures/sub-frames/webview-iframe-preload.js @@ -0,0 +1,15 @@ +const { ipcRenderer } = require('electron'); + +if (process.isMainFrame) { + window.addEventListener('DOMContentLoaded', () => { + const webview = document.createElement('webview'); + webview.src = 'about:blank'; + webview.setAttribute('webpreferences', 'contextIsolation=no'); + webview.addEventListener('did-finish-load', () => { + ipcRenderer.send('webview-loaded'); + }, { once: true }); + document.body.appendChild(webview); + }); +} else { + ipcRenderer.send('preload-in-frame'); +} diff --git a/spec-main/fixtures/version-bumper/fixture_support.md b/spec-main/fixtures/version-bumper/fixture_support.md new file mode 100644 index 0000000000000..0ca1e58ad4bde --- /dev/null +++ b/spec-main/fixtures/version-bumper/fixture_support.md @@ -0,0 +1,122 @@ +# Electron Support + +## Finding Support + +If you have a security concern, +please see the [security document](https://github.com/electron/electron/tree/master/SECURITY.md). + +If you're looking for programming help, +for answers to questions, +or to join in discussion with other developers who use Electron, +you can interact with the community in these locations: + +* [`Electron's Discord`](https://discord.com/invite/electron) has channels for: + * Getting help + * Ecosystem apps like [Electron Forge](https://github.com/electron-userland/electron-forge) and [Electron Fiddle](https://github.com/electron/fiddle) + * Sharing ideas with other Electron app developers + * And more! +* [`electron`](https://discuss.atom.io/c/electron) category on the Atom forums +* `#atom-shell` channel on Freenode +* `#electron` channel on [Atom's Slack](https://discuss.atom.io/t/join-us-on-slack/16638?source_topic_id=25406) +* [`electron-ru`](https://telegram.me/electron_ru) *(Russian)* +* [`electron-br`](https://electron-br.slack.com) *(Brazilian Portuguese)* +* [`electron-kr`](https://electron-kr.github.io/electron-kr) *(Korean)* +* [`electron-jp`](https://electron-jp.slack.com) *(Japanese)* +* [`electron-tr`](https://electron-tr.herokuapp.com) *(Turkish)* +* [`electron-id`](https://electron-id.slack.com) *(Indonesia)* +* [`electron-pl`](https://electronpl.github.io) *(Poland)* + +If you'd like to contribute to Electron, +see the [contributing document](https://github.com/electron/electron/blob/master/CONTRIBUTING.md). + +If you've found a bug in a [supported version](#supported-versions) of Electron, +please report it with the [issue tracker](../development/issues.md). + +[awesome-electron](https://github.com/sindresorhus/awesome-electron) +is a community-maintained list of useful example apps, +tools and resources. + +## Supported Versions + +The latest three *stable* major versions are supported by the Electron team. +For example, if the latest release is 6.1.x, then the 5.0.x as well +as the 4.2.x series are supported. We only support the latest minor release +for each stable release series. This means that in the case of a security fix +6.1.x will receive the fix, but we will not release a new version of 6.0.x. + +The latest stable release unilaterally receives all fixes from `master`, +and the version prior to that receives the vast majority of those fixes +as time and bandwidth warrants. The oldest supported release line will receive +only security fixes directly. + +All supported release lines will accept external pull requests to backport +fixes previously merged to `master`, though this may be on a case-by-case +basis for some older supported lines. All contested decisions around release +line backports will be resolved by the [Releases Working Group](https://github.com/electron/governance/tree/master/wg-releases) as an agenda item at their weekly meeting the week the backport PR is raised. + +When an API is changed or removed in a way that breaks existing functionality, the +previous functionality will be supported for a minimum of two major versions when +possible before being removed. For example, if a function takes three arguments, +and that number is reduced to two in major version 10, the three-argument version would +continue to work until, at minimum, major version 12. Past the minimum two-version +threshold, we will attempt to support backwards compatibility beyond two versions +until the maintainers feel the maintenance burden is too high to continue doing so. + +### Currently supported versions + +* 4.x.y +* 3.x.y +* 2.x.y +* 1.x.y + +### End-of-life + +When a release branch reaches the end of its support cycle, the series +will be deprecated in NPM and a final end-of-support release will be +made. This release will add a warning to inform that an unsupported +version of Electron is in use. + +These steps are to help app developers learn when a branch they're +using becomes unsupported, but without being excessively intrusive +to end users. + +If an application has exceptional circumstances and needs to stay +on an unsupported series of Electron, developers can silence the +end-of-support warning by omitting the final release from the app's +`package.json` `devDependencies`. For example, since the 1-6-x series +ended with an end-of-support 1.6.18 release, developers could choose +to stay in the 1-6-x series without warnings with `devDependency` of +`"electron": 1.6.0 - 1.6.17`. + +## Supported Platforms + +Following platforms are supported by Electron: + +### macOS + +Only 64bit binaries are provided for macOS, and the minimum macOS version +supported is macOS 10.11 (El Capitan). + +Native support for Apple Silicon (`arm64`) devices was added in Electron 11.0.0. + +### Windows + +Windows 7 and later are supported, older operating systems are not supported +(and do not work). + +Both `ia32` (`x86`) and `x64` (`amd64`) binaries are provided for Windows. +[Native support for Windows on Arm (`arm64`) devices was added in Electron 6.0.8.](windows-arm.md). +Running apps packaged with previous versions is possible using the ia32 binary. + +### Linux + +The prebuilt binaries of Electron are built on Ubuntu 18.04. + +Whether the prebuilt binary can run on a distribution depends on whether the +distribution includes the libraries that Electron is linked to on the building +platform, so only Ubuntu 18.04 is guaranteed to work, but following platforms +are also verified to be able to run the prebuilt binaries of Electron: + +* Ubuntu 14.04 and newer +* Fedora 24 and newer +* Debian 8 and newer diff --git a/spec-main/fixtures/webview/fullscreen/frame.html b/spec-main/fixtures/webview/fullscreen/frame.html new file mode 100644 index 0000000000000..c92571eef4a8b --- /dev/null +++ b/spec-main/fixtures/webview/fullscreen/frame.html @@ -0,0 +1,12 @@ + +
+ WebView +
+ + diff --git a/spec-main/fixtures/webview/fullscreen/main.html b/spec-main/fixtures/webview/fullscreen/main.html new file mode 100644 index 0000000000000..aeb460578f97d --- /dev/null +++ b/spec-main/fixtures/webview/fullscreen/main.html @@ -0,0 +1,12 @@ + + + + diff --git a/spec-main/guest-window-manager-spec.ts b/spec-main/guest-window-manager-spec.ts new file mode 100644 index 0000000000000..974e8fce452c2 --- /dev/null +++ b/spec-main/guest-window-manager-spec.ts @@ -0,0 +1,303 @@ +import { BrowserWindow } from 'electron'; +import { writeFileSync, readFileSync } from 'fs'; +import { resolve } from 'path'; +import { expect, assert } from 'chai'; +import { closeAllWindows } from './window-helpers'; +const { emittedOnce } = require('./events-helpers'); + +function genSnapshot (browserWindow: BrowserWindow, features: string) { + return new Promise((resolve) => { + browserWindow.webContents.on('new-window', (...args: any[]) => { + resolve([features, ...args]); + }); + browserWindow.webContents.executeJavaScript(`window.open('about:blank', 'frame-name', '${features}') && true`); + }); +} + +describe('new-window event', () => { + const snapshotFileName = 'native-window-open.snapshot.txt'; + const browserWindowOptions = { + show: false, + width: 200, + title: 'cool', + backgroundColor: 'blue', + focusable: false, + webPreferences: { + sandbox: true + } + }; + + const snapshotFile = resolve(__dirname, 'fixtures', 'snapshots', snapshotFileName); + let browserWindow: BrowserWindow; + let existingSnapshots: any[]; + + before(() => { + existingSnapshots = parseSnapshots(readFileSync(snapshotFile, { encoding: 'utf8' })); + }); + + beforeEach((done) => { + browserWindow = new BrowserWindow(browserWindowOptions); + browserWindow.loadURL('about:blank'); + browserWindow.on('ready-to-show', () => { done(); }); + }); + + afterEach(closeAllWindows); + + const newSnapshots: any[] = []; + [ + 'top=5,left=10,resizable=no', + 'zoomFactor=2,resizable=0,x=0,y=10', + 'backgroundColor=gray,webPreferences=0,x=100,y=100', + 'x=50,y=20,title=sup', + 'show=false,top=1,left=1' + ].forEach((features, index) => { + /** + * ATTN: If this test is failing, you likely just need to change + * `shouldOverwriteSnapshot` to true and then evaluate the snapshot diff + * to see if the change is harmless. + */ + it(`matches snapshot for ${features}`, async () => { + const newSnapshot = await genSnapshot(browserWindow, features); + newSnapshots.push(newSnapshot); + // TODO: The output when these fail could be friendlier. + expect(stringifySnapshots(newSnapshot)).to.equal(stringifySnapshots(existingSnapshots[index])); + }); + }); + + after(() => { + const shouldOverwriteSnapshot = false; + if (shouldOverwriteSnapshot) writeFileSync(snapshotFile, stringifySnapshots(newSnapshots, true)); + }); +}); + +describe('webContents.setWindowOpenHandler', () => { + let browserWindow: BrowserWindow; + beforeEach(async () => { + browserWindow = new BrowserWindow({ show: false }); + await browserWindow.loadURL('about:blank'); + }); + + afterEach(closeAllWindows); + + it('does not fire window creation events if the handler callback throws an error', (done) => { + const error = new Error('oh no'); + const listeners = process.listeners('uncaughtException'); + process.removeAllListeners('uncaughtException'); + process.on('uncaughtException', (thrown) => { + try { + expect(thrown).to.equal(error); + done(); + } catch (e) { + done(e); + } finally { + process.removeAllListeners('uncaughtException'); + listeners.forEach((listener) => process.on('uncaughtException', listener)); + } + }); + + browserWindow.webContents.on('new-window', () => { + assert.fail('new-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.on('did-create-window', () => { + assert.fail('did-create-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true"); + + browserWindow.webContents.setWindowOpenHandler(() => { + throw error; + }); + }); + + it('does not fire window creation events if the handler callback returns a bad result', async () => { + const bad = new Promise((resolve) => { + browserWindow.webContents.setWindowOpenHandler(() => { + setTimeout(resolve); + return [1, 2, 3] as any; + }); + }); + + browserWindow.webContents.on('new-window', () => { + assert.fail('new-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.on('did-create-window', () => { + assert.fail('did-create-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true"); + + await bad; + }); + + it('does not fire window creation events if an override returns action: deny', async () => { + const denied = new Promise((resolve) => { + browserWindow.webContents.setWindowOpenHandler(() => { + setTimeout(resolve); + return { action: 'deny' }; + }); + }); + browserWindow.webContents.on('new-window', () => { + assert.fail('new-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.on('did-create-window', () => { + assert.fail('did-create-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true"); + + await denied; + }); + + it('is called when clicking on a target=_blank link', async () => { + const denied = new Promise((resolve) => { + browserWindow.webContents.setWindowOpenHandler(() => { + setTimeout(resolve); + return { action: 'deny' }; + }); + }); + browserWindow.webContents.on('new-window', () => { + assert.fail('new-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.on('did-create-window', () => { + assert.fail('did-create-window should not be called with an overridden window.open'); + }); + + await browserWindow.webContents.loadURL('data:text/html,link'); + browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 }); + browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 }); + + await denied; + }); + + it('is called when shift-clicking on a link', async () => { + const denied = new Promise((resolve) => { + browserWindow.webContents.setWindowOpenHandler(() => { + setTimeout(resolve); + return { action: 'deny' }; + }); + }); + browserWindow.webContents.on('new-window', () => { + assert.fail('new-window should not be called with an overridden window.open'); + }); + + browserWindow.webContents.on('did-create-window', () => { + assert.fail('did-create-window should not be called with an overridden window.open'); + }); + + await browserWindow.webContents.loadURL('data:text/html,link'); + browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] }); + browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] }); + + await denied; + }); + + it('fires handler with correct params', async () => { + const testFrameName = 'test-frame-name'; + const testFeatures = 'top=10&left=10&something-unknown&show=no'; + const testUrl = 'app://does-not-exist/'; + const details = await new Promise(resolve => { + browserWindow.webContents.setWindowOpenHandler((details) => { + setTimeout(() => resolve(details)); + return { action: 'deny' }; + }); + + browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`); + }); + const { url, frameName, features, disposition, referrer } = details; + expect(url).to.equal(testUrl); + expect(frameName).to.equal(testFrameName); + expect(features).to.equal(testFeatures); + expect(disposition).to.equal('new-window'); + expect(referrer).to.deep.equal({ + policy: 'strict-origin-when-cross-origin', + url: '' + }); + }); + + it('includes post body', async () => { + const details = await new Promise(resolve => { + browserWindow.webContents.setWindowOpenHandler((details) => { + setTimeout(() => resolve(details)); + return { action: 'deny' }; + }); + + browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(` +
+ +
+ + `)}`); + }); + const { url, frameName, features, disposition, referrer, postBody } = details; + expect(url).to.equal('http://example.com/'); + expect(frameName).to.equal(''); + expect(features).to.deep.equal(''); + expect(disposition).to.equal('foreground-tab'); + expect(referrer).to.deep.equal({ + policy: 'strict-origin-when-cross-origin', + url: '' + }); + expect(postBody).to.deep.equal({ + contentType: 'application/x-www-form-urlencoded', + data: [{ + type: 'rawData', + bytes: Buffer.from('key=value') + }] + }); + }); + + it('does fire window creation events if an override returns action: allow', async () => { + browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' })); + + setImmediate(() => { + browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true"); + }); + + await Promise.all([ + emittedOnce(browserWindow.webContents, 'did-create-window'), + emittedOnce(browserWindow.webContents, 'new-window') + ]); + }); + + it('can change webPreferences of child windows', (done) => { + browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } })); + + browserWindow.webContents.on('did-create-window', async (childWindow) => { + await childWindow.webContents.executeJavaScript("document.write('hello')"); + const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize"); + expect(size).to.equal('30px'); + done(); + }); + + browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true"); + }); + + it('does not hang parent window when denying window.open', async () => { + browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' })); + browserWindow.webContents.executeJavaScript("window.open('https://127.0.0.1')"); + expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42); + }); +}); + +function stringifySnapshots (snapshots: any, pretty = false) { + return JSON.stringify(snapshots, (key, value) => { + if (['sender', 'webContents'].includes(key)) { + return '[WebContents]'; + } + if (key === 'processId' && typeof value === 'number') { + return 'placeholder-process-id'; + } + if (key === 'returnValue') { + return 'placeholder-guest-contents-id'; + } + return value; + }, pretty ? 2 : undefined); +} + +function parseSnapshots (snapshotsJson: string) { + return JSON.parse(snapshotsJson); +} diff --git a/spec-main/index.js b/spec-main/index.js index 1ad1442bbe36a..6579025ee7579 100644 --- a/spec-main/index.js +++ b/spec-main/index.js @@ -1,95 +1,147 @@ -const Module = require('module') -const path = require('path') -const v8 = require('v8') +const path = require('path'); +const v8 = require('v8'); -Module.globalPaths.push(path.resolve(__dirname, '../spec/node_modules')) +module.paths.push(path.resolve(__dirname, '../spec/node_modules')); + +// Extra module paths which can be used to load Mocha reporters +if (process.env.ELECTRON_TEST_EXTRA_MODULE_PATHS) { + for (const modulePath of process.env.ELECTRON_TEST_EXTRA_MODULE_PATHS.split(':')) { + module.paths.push(modulePath); + } +} + +// Add search paths for loaded spec files +require('../spec/global-paths')(module.paths); // We want to terminate on errors, not throw up a dialog process.on('uncaughtException', (err) => { - console.error('Unhandled exception in main spec runner:', err) - process.exit(1) -}) + console.error('Unhandled exception in main spec runner:', err); + process.exit(1); +}); // Tell ts-node which tsconfig to use -process.env.TS_NODE_PROJECT = path.resolve(__dirname, '../tsconfig.spec.json') -process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true' +process.env.TS_NODE_PROJECT = path.resolve(__dirname, '../tsconfig.spec.json'); +process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; -const { app, protocol } = require('electron') +const { app, protocol } = require('electron'); -v8.setFlagsFromString('--expose_gc') -app.commandLine.appendSwitch('js-flags', '--expose_gc') +v8.setFlagsFromString('--expose_gc'); +app.commandLine.appendSwitch('js-flags', '--expose_gc'); // Prevent the spec runner quiting when the first window closes -app.on('window-all-closed', () => null) -// TODO: This API should _probably_ only be enabled for the specific test that needs it -// not the entire test suite -app.commandLine.appendSwitch('ignore-certificate-errors') +app.on('window-all-closed', () => null); -global.standardScheme = 'app' +// Use fake device for Media Stream to replace actual camera and microphone. +app.commandLine.appendSwitch('use-fake-device-for-media-stream'); +app.commandLine.appendSwitch('host-rules', 'MAP localhost2 127.0.0.1'); + +global.standardScheme = 'app'; +global.zoomScheme = 'zoom'; +global.serviceWorkerScheme = 'sw'; protocol.registerSchemesAsPrivileged([ - { scheme: global.standardScheme, privileges: { standard: true, secure: true } }, + { scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } }, + { scheme: global.zoomScheme, privileges: { standard: true, secure: true } }, + { scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } }, { scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } }, { scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } }, { scheme: 'no-cors', privileges: { supportFetchAPI: true } }, - { scheme: 'no-fetch', privileges: { corsEnabled: true } } -]) + { scheme: 'no-fetch', privileges: { corsEnabled: true } }, + { scheme: 'stream', privileges: { standard: true, stream: true } }, + { scheme: 'foo', privileges: { standard: true } }, + { scheme: 'bar', privileges: { standard: true } } +]); -app.whenReady().then(() => { - require('ts-node/register') +app.whenReady().then(async () => { + require('ts-node/register'); const argv = require('yargs') .boolean('ci') + .array('files') .string('g').alias('g', 'grep') .boolean('i').alias('i', 'invert') - .argv - - const isCi = !!argv.ci - global.isCI = isCi + .argv; - const Mocha = require('mocha') - const mochaOptions = {} + const Mocha = require('mocha'); + const mochaOptions = {}; if (process.env.MOCHA_REPORTER) { - mochaOptions.reporter = process.env.MOCHA_REPORTER + mochaOptions.reporter = process.env.MOCHA_REPORTER; } if (process.env.MOCHA_MULTI_REPORTERS) { mochaOptions.reporterOptions = { reporterEnabled: process.env.MOCHA_MULTI_REPORTERS - } + }; } - const mocha = new Mocha(mochaOptions) + const mocha = new Mocha(mochaOptions); + + // The cleanup method is registered this way rather than through an + // `afterEach` at the top level so that it can run before other `afterEach` + // methods. + // + // The order of events is: + // 1. test completes, + // 2. `defer()`-ed methods run, in reverse order, + // 3. regular `afterEach` hooks run. + const { runCleanupFunctions } = require('./spec-helpers'); + mocha.suite.on('suite', function attach (suite) { + suite.afterEach('cleanup', runCleanupFunctions); + suite.on('suite', attach); + }); if (!process.env.MOCHA_REPORTER) { - mocha.ui('bdd').reporter('tap') + mocha.ui('bdd').reporter('tap'); } - mocha.timeout(isCi ? 30000 : 10000) - - if (argv.grep) mocha.grep(argv.grep) - if (argv.invert) mocha.invert() - - // Read all test files. - const walker = require('walkdir').walk(__dirname, { - no_recurse: true - }) - - // This allows you to run specific modules only: - // npm run test -match=menu - const moduleMatch = process.env.npm_config_match - ? new RegExp(process.env.npm_config_match, 'g') - : null - - walker.on('file', (file) => { - if (/-spec\.[tj]s$/.test(file) && - (!moduleMatch || moduleMatch.test(file))) { - mocha.addFile(file) + const mochaTimeout = process.env.MOCHA_TIMEOUT || 30000; + mocha.timeout(mochaTimeout); + + if (argv.grep) mocha.grep(argv.grep); + if (argv.invert) mocha.invert(); + + const filter = (file) => { + if (!/-spec\.[tj]s$/.test(file)) { + return false; + } + + // This allows you to run specific modules only: + // npm run test -match=menu + const moduleMatch = process.env.npm_config_match + ? new RegExp(process.env.npm_config_match, 'g') + : null; + if (moduleMatch && !moduleMatch.test(file)) { + return false; } - }) - - walker.on('end', () => { - const cb = () => { - // Ensure the callback is called after runner is defined - process.nextTick(() => { - process.exit(runner.failures) - }) + + const baseElectronDir = path.resolve(__dirname, '..'); + if (argv.files && !argv.files.includes(path.relative(baseElectronDir, file))) { + return false; } - const runner = mocha.run(cb) - }) -}) + + return true; + }; + + const getFiles = require('../spec/static/get-files'); + const testFiles = await getFiles(__dirname, { filter }); + testFiles.sort().forEach((file) => { + mocha.addFile(file); + }); + + const cb = () => { + // Ensure the callback is called after runner is defined + process.nextTick(() => { + process.exit(runner.failures); + }); + }; + + // Set up chai in the correct order + const chai = require('chai'); + chai.use(require('chai-as-promised')); + chai.use(require('dirty-chai')); + + // Show full object diff + // https://github.com/chaijs/chai/issues/469 + chai.config.truncateThreshold = 0; + + const runner = mocha.run(cb); +}).catch((err) => { + console.error('An error occurred while running the spec-main spec runner'); + console.error(err); + process.exit(1); +}); diff --git a/spec-main/internal-spec.ts b/spec-main/internal-spec.ts new file mode 100644 index 0000000000000..45d5f6605b1b7 --- /dev/null +++ b/spec-main/internal-spec.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; + +describe('feature-string parsing', () => { + it('is indifferent to whitespace around keys and values', () => { + const { parseCommaSeparatedKeyValue } = require('../lib/browser/parse-features-string'); + const checkParse = (string: string, parsed: Record) => { + const features = parseCommaSeparatedKeyValue(string); + expect(features).to.deep.equal(parsed); + }; + checkParse('a=yes,c=d', { a: true, c: 'd' }); + checkParse('a=yes ,c=d', { a: true, c: 'd' }); + checkParse('a=yes, c=d', { a: true, c: 'd' }); + checkParse('a=yes , c=d', { a: true, c: 'd' }); + checkParse(' a=yes , c=d', { a: true, c: 'd' }); + checkParse(' a= yes , c=d', { a: true, c: 'd' }); + checkParse(' a = yes , c=d', { a: true, c: 'd' }); + checkParse(' a = yes , c =d', { a: true, c: 'd' }); + checkParse(' a = yes , c = d', { a: true, c: 'd' }); + checkParse(' a = yes , c = d ', { a: true, c: 'd' }); + }); +}); diff --git a/spec-main/logging-spec.ts b/spec-main/logging-spec.ts new file mode 100644 index 0000000000000..a98ed4b9532a3 --- /dev/null +++ b/spec-main/logging-spec.ts @@ -0,0 +1,192 @@ +import { app } from 'electron'; +import { expect } from 'chai'; +import { emittedOnce } from './events-helpers'; +import { startRemoteControlApp, ifdescribe } from './spec-helpers'; + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as uuid from 'uuid'; + +function isTestingBindingAvailable () { + try { + process._linkedBinding('electron_common_testing'); + return true; + } catch { + return false; + } +} + +// This test depends on functions that are only available when DCHECK_IS_ON. +ifdescribe(isTestingBindingAvailable())('logging', () => { + it('does not log by default', async () => { + // ELECTRON_ENABLE_LOGGING is turned on in the appveyor config. + const { ELECTRON_ENABLE_LOGGING: _, ...envWithoutEnableLogging } = process.env; + const rc = await startRemoteControlApp([], { env: envWithoutEnableLogging }); + const stderrComplete = new Promise(resolve => { + let stderr = ''; + rc.process.stderr!.on('data', function listener (chunk) { + stderr += chunk.toString('utf8'); + }); + rc.process.on('close', () => { resolve(stderr); }); + }); + const [hasLoggingSwitch, hasLoggingVar] = await rc.remotely(() => { + // Make sure we're actually capturing stderr by logging a known value to + // stderr. + console.error('SENTINEL'); + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { process.exit(0); }); + return [require('electron').app.commandLine.hasSwitch('enable-logging'), !!process.env.ELECTRON_ENABLE_LOGGING]; + }); + expect(hasLoggingSwitch).to.be.false(); + expect(hasLoggingVar).to.be.false(); + const stderr = await stderrComplete; + // stderr should include the sentinel but not the LOG() message. + expect(stderr).to.match(/SENTINEL/); + expect(stderr).not.to.match(/TEST_LOG/); + }); + + it('logs to stderr when --enable-logging is passed', async () => { + const rc = await startRemoteControlApp(['--enable-logging']); + const stderrComplete = new Promise(resolve => { + let stderr = ''; + rc.process.stderr!.on('data', function listener (chunk) { + stderr += chunk.toString('utf8'); + }); + rc.process.on('close', () => { resolve(stderr); }); + }); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + const stderr = await stderrComplete; + expect(stderr).to.match(/TEST_LOG/); + }); + + it('logs to stderr when ELECTRON_ENABLE_LOGGING is set', async () => { + const rc = await startRemoteControlApp([], { env: { ...process.env, ELECTRON_ENABLE_LOGGING: '1' } }); + const stderrComplete = new Promise(resolve => { + let stderr = ''; + rc.process.stderr!.on('data', function listener (chunk) { + stderr += chunk.toString('utf8'); + }); + rc.process.on('close', () => { resolve(stderr); }); + }); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + const stderr = await stderrComplete; + expect(stderr).to.match(/TEST_LOG/); + }); + + it('logs to a file in the user data dir when --enable-logging=file is passed', async () => { + const rc = await startRemoteControlApp(['--enable-logging=file']); + const userDataDir = await rc.remotely(() => { + const { app } = require('electron'); + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { app.quit(); }); + return app.getPath('userData'); + }); + await emittedOnce(rc.process, 'exit'); + const logFilePath = path.join(userDataDir, 'electron_debug.log'); + const stat = await fs.stat(logFilePath); + expect(stat.isFile()).to.be.true(); + const contents = await fs.readFile(logFilePath, 'utf8'); + expect(contents).to.match(/TEST_LOG/); + }); + + it('logs to a file in the user data dir when ELECTRON_ENABLE_LOGGING=file is set', async () => { + const rc = await startRemoteControlApp([], { env: { ...process.env, ELECTRON_ENABLE_LOGGING: 'file' } }); + const userDataDir = await rc.remotely(() => { + const { app } = require('electron'); + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { app.quit(); }); + return app.getPath('userData'); + }); + await emittedOnce(rc.process, 'exit'); + const logFilePath = path.join(userDataDir, 'electron_debug.log'); + const stat = await fs.stat(logFilePath); + expect(stat.isFile()).to.be.true(); + const contents = await fs.readFile(logFilePath, 'utf8'); + expect(contents).to.match(/TEST_LOG/); + }); + + it('logs to the given file when --log-file is passed', async () => { + const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4()); + const rc = await startRemoteControlApp(['--enable-logging', '--log-file=' + logFilePath]); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + await emittedOnce(rc.process, 'exit'); + const stat = await fs.stat(logFilePath); + expect(stat.isFile()).to.be.true(); + const contents = await fs.readFile(logFilePath, 'utf8'); + expect(contents).to.match(/TEST_LOG/); + }); + + it('logs to the given file when ELECTRON_LOG_FILE is set', async () => { + const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4()); + const rc = await startRemoteControlApp([], { env: { ...process.env, ELECTRON_ENABLE_LOGGING: '1', ELECTRON_LOG_FILE: logFilePath } }); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + await emittedOnce(rc.process, 'exit'); + const stat = await fs.stat(logFilePath); + expect(stat.isFile()).to.be.true(); + const contents = await fs.readFile(logFilePath, 'utf8'); + expect(contents).to.match(/TEST_LOG/); + }); + + it('does not lose early log messages when logging to a given file with --log-file', async () => { + const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4()); + const rc = await startRemoteControlApp(['--enable-logging', '--log-file=' + logFilePath, '--boot-eval=process._linkedBinding(\'electron_common_testing\').log(0, \'EARLY_LOG\')']); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'LATER_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + await emittedOnce(rc.process, 'exit'); + const stat = await fs.stat(logFilePath); + expect(stat.isFile()).to.be.true(); + const contents = await fs.readFile(logFilePath, 'utf8'); + expect(contents).to.match(/EARLY_LOG/); + expect(contents).to.match(/LATER_LOG/); + }); + + it('enables logging when switch is appended during first tick', async () => { + const rc = await startRemoteControlApp(['--boot-eval=require(\'electron\').app.commandLine.appendSwitch(\'--enable-logging\')']); + const stderrComplete = new Promise(resolve => { + let stderr = ''; + rc.process.stderr!.on('data', function listener (chunk) { + stderr += chunk.toString('utf8'); + }); + rc.process.on('close', () => { resolve(stderr); }); + }); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + const stderr = await stderrComplete; + expect(stderr).to.match(/TEST_LOG/); + }); + + it('respects --log-level', async () => { + const rc = await startRemoteControlApp(['--enable-logging', '--log-level=1']); + const stderrComplete = new Promise(resolve => { + let stderr = ''; + rc.process.stderr!.on('data', function listener (chunk) { + stderr += chunk.toString('utf8'); + }); + rc.process.on('close', () => { resolve(stderr); }); + }); + rc.remotely(() => { + process._linkedBinding('electron_common_testing').log(0, 'TEST_INFO_LOG'); + process._linkedBinding('electron_common_testing').log(1, 'TEST_WARNING_LOG'); + setTimeout(() => { require('electron').app.quit(); }); + }); + const stderr = await stderrComplete; + expect(stderr).to.match(/TEST_WARNING_LOG/); + expect(stderr).not.to.match(/TEST_INFO_LOG/); + }); +}); diff --git a/spec-main/modules-spec.ts b/spec-main/modules-spec.ts new file mode 100644 index 0000000000000..a83797961fcf7 --- /dev/null +++ b/spec-main/modules-spec.ts @@ -0,0 +1,186 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as fs from 'fs'; +import { BrowserWindow } from 'electron/main'; +import { ifdescribe, ifit } from './spec-helpers'; +import { closeAllWindows } from './window-helpers'; +import { emittedOnce } from './events-helpers'; +import * as childProcess from 'child_process'; + +const Module = require('module'); + +const features = process._linkedBinding('electron_common_features'); +const nativeModulesEnabled = !process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS; + +describe('modules support', () => { + const fixtures = path.join(__dirname, 'fixtures'); + + describe('third-party module', () => { + ifdescribe(nativeModulesEnabled)('echo', () => { + afterEach(closeAllWindows); + it('can be required in renderer', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + await expect( + w.webContents.executeJavaScript( + "{ require('@electron-ci/echo'); null }" + ) + ).to.be.fulfilled(); + }); + + ifit(features.isRunAsNodeEnabled())('can be required in node binary', async function () { + const child = childProcess.fork(path.join(fixtures, 'module', 'echo.js')); + const [msg] = await emittedOnce(child, 'message'); + expect(msg).to.equal('ok'); + }); + + ifit(process.platform === 'win32')('can be required if electron.exe is renamed', () => { + const testExecPath = path.join(path.dirname(process.execPath), 'test.exe'); + fs.copyFileSync(process.execPath, testExecPath); + try { + const fixture = path.join(fixtures, 'module', 'echo-renamed.js'); + expect(fs.existsSync(fixture)).to.be.true(); + const child = childProcess.spawnSync(testExecPath, [fixture]); + expect(child.status).to.equal(0); + } finally { + fs.unlinkSync(testExecPath); + } + }); + }); + + const enablePlatforms: NodeJS.Platform[] = [ + 'linux', + 'darwin', + 'win32' + ]; + ifdescribe(nativeModulesEnabled && enablePlatforms.includes(process.platform))('module that use uv_dlopen', () => { + it('can be required in renderer', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + await expect(w.webContents.executeJavaScript('{ require(\'@electron-ci/uv-dlopen\'); null }')).to.be.fulfilled(); + }); + + ifit(features.isRunAsNodeEnabled())('can be required in node binary', async function () { + const child = childProcess.fork(path.join(fixtures, 'module', 'uv-dlopen.js')); + await new Promise(resolve => child.once('exit', (exitCode) => { + expect(exitCode).to.equal(0); + resolve(); + })); + }); + }); + + describe('q', () => { + describe('Q.when', () => { + it('emits the fullfil callback', (done) => { + const Q = require('q'); + Q(true).then((val: boolean) => { + expect(val).to.be.true(); + done(); + }); + }); + }); + }); + + describe('coffeescript', () => { + it('can be registered and used to require .coffee files', () => { + expect(() => { + require('coffeescript').register(); + }).to.not.throw(); + expect(require('./fixtures/module/test.coffee')).to.be.true(); + }); + }); + }); + + describe('global variables', () => { + describe('process', () => { + it('can be declared in a module', () => { + expect(require('./fixtures/module/declare-process')).to.equal('declared process'); + }); + }); + + describe('global', () => { + it('can be declared in a module', () => { + expect(require('./fixtures/module/declare-global')).to.equal('declared global'); + }); + }); + + describe('Buffer', () => { + it('can be declared in a module', () => { + expect(require('./fixtures/module/declare-buffer')).to.equal('declared Buffer'); + }); + }); + }); + + describe('Module._nodeModulePaths', () => { + // Work around the hack in spec/global-paths. + beforeEach(() => { + Module.ignoreGlobalPathsHack = true; + }); + + afterEach(() => { + Module.ignoreGlobalPathsHack = false; + }); + + describe('when the path is inside the resources path', () => { + it('does not include paths outside of the resources path', () => { + let modulePath = process.resourcesPath; + expect(Module._nodeModulePaths(modulePath)).to.deep.equal([ + path.join(process.resourcesPath, 'node_modules') + ]); + + modulePath = process.resourcesPath + '-foo'; + const nodeModulePaths = Module._nodeModulePaths(modulePath); + expect(nodeModulePaths).to.include(path.join(modulePath, 'node_modules')); + expect(nodeModulePaths).to.include(path.join(modulePath, '..', 'node_modules')); + + modulePath = path.join(process.resourcesPath, 'foo'); + expect(Module._nodeModulePaths(modulePath)).to.deep.equal([ + path.join(process.resourcesPath, 'foo', 'node_modules'), + path.join(process.resourcesPath, 'node_modules') + ]); + + modulePath = path.join(process.resourcesPath, 'node_modules', 'foo'); + expect(Module._nodeModulePaths(modulePath)).to.deep.equal([ + path.join(process.resourcesPath, 'node_modules', 'foo', 'node_modules'), + path.join(process.resourcesPath, 'node_modules') + ]); + + modulePath = path.join(process.resourcesPath, 'node_modules', 'foo', 'bar'); + expect(Module._nodeModulePaths(modulePath)).to.deep.equal([ + path.join(process.resourcesPath, 'node_modules', 'foo', 'bar', 'node_modules'), + path.join(process.resourcesPath, 'node_modules', 'foo', 'node_modules'), + path.join(process.resourcesPath, 'node_modules') + ]); + + modulePath = path.join(process.resourcesPath, 'node_modules', 'foo', 'node_modules', 'bar'); + expect(Module._nodeModulePaths(modulePath)).to.deep.equal([ + path.join(process.resourcesPath, 'node_modules', 'foo', 'node_modules', 'bar', 'node_modules'), + path.join(process.resourcesPath, 'node_modules', 'foo', 'node_modules'), + path.join(process.resourcesPath, 'node_modules') + ]); + }); + }); + + describe('when the path is outside the resources path', () => { + it('includes paths outside of the resources path', () => { + const modulePath = path.resolve('/foo'); + expect(Module._nodeModulePaths(modulePath)).to.deep.equal([ + path.join(modulePath, 'node_modules'), + path.resolve('/node_modules') + ]); + }); + }); + }); + + describe('require', () => { + describe('when loaded URL is not file: protocol', () => { + afterEach(closeAllWindows); + it('searches for module under app directory', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + const result = await w.webContents.executeJavaScript('typeof require("q").when'); + expect(result).to.equal('function'); + }); + }); + }); +}); diff --git a/spec-main/node-spec.ts b/spec-main/node-spec.ts new file mode 100644 index 0000000000000..af6c11360f202 --- /dev/null +++ b/spec-main/node-spec.ts @@ -0,0 +1,381 @@ +import { expect } from 'chai'; +import * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; +import { emittedOnce } from './events-helpers'; +import { ifdescribe, ifit } from './spec-helpers'; +import { webContents, WebContents } from 'electron/main'; + +const features = process._linkedBinding('electron_common_features'); +const mainFixturesPath = path.resolve(__dirname, 'fixtures'); + +describe('node feature', () => { + const fixtures = path.join(__dirname, '..', 'spec', 'fixtures'); + describe('child_process', () => { + describe('child_process.fork', () => { + it('Works in browser process', async () => { + const child = childProcess.fork(path.join(fixtures, 'module', 'ping.js')); + const message = emittedOnce(child, 'message'); + child.send('message'); + const [msg] = await message; + expect(msg).to.equal('message'); + }); + }); + }); + + it('does not hang when using the fs module in the renderer process', async () => { + const appPath = path.join(mainFixturesPath, 'apps', 'libuv-hang', 'main.js'); + const appProcess = childProcess.spawn(process.execPath, [appPath], { + cwd: path.join(mainFixturesPath, 'apps', 'libuv-hang'), + stdio: 'inherit' + }); + const [code] = await emittedOnce(appProcess, 'close'); + expect(code).to.equal(0); + }); + + describe('contexts', () => { + describe('setTimeout called under Chromium event loop in browser process', () => { + it('Can be scheduled in time', (done) => { + setTimeout(done, 0); + }); + + it('Can be promisified', (done) => { + util.promisify(setTimeout)(0).then(done); + }); + }); + + describe('setInterval called under Chromium event loop in browser process', () => { + it('can be scheduled in time', (done) => { + let interval: any = null; + let clearing = false; + const clear = () => { + if (interval === null || clearing) return; + + // interval might trigger while clearing (remote is slow sometimes) + clearing = true; + clearInterval(interval); + clearing = false; + interval = null; + done(); + }; + interval = setInterval(clear, 10); + }); + }); + }); + + describe('NODE_OPTIONS', () => { + let child: childProcess.ChildProcessWithoutNullStreams; + let exitPromise: Promise; + + it('Fails for options disallowed by Node.js itself', (done) => { + after(async () => { + const [code, signal] = await exitPromise; + expect(signal).to.equal(null); + + // Exit code 9 indicates cli flag parsing failure + expect(code).to.equal(9); + child.kill(); + }); + + const env = Object.assign({}, process.env, { NODE_OPTIONS: '--v8-options' }); + child = childProcess.spawn(process.execPath, { env }); + exitPromise = emittedOnce(child, 'exit'); + + let output = ''; + let success = false; + const cleanup = () => { + child.stderr.removeListener('data', listener); + child.stdout.removeListener('data', listener); + }; + + const listener = (data: Buffer) => { + output += data; + if (/electron: --v8-options is not allowed in NODE_OPTIONS/m.test(output)) { + success = true; + cleanup(); + done(); + } + }; + + child.stderr.on('data', listener); + child.stdout.on('data', listener); + child.on('exit', () => { + if (!success) { + cleanup(); + done(new Error(`Unexpected output: ${output.toString()}`)); + } + }); + }); + + it('Disallows crypto-related options', (done) => { + after(() => { + child.kill(); + }); + + const env = Object.assign({}, process.env, { NODE_OPTIONS: '--use-openssl-ca' }); + child = childProcess.spawn(process.execPath, ['--enable-logging'], { env }); + + let output = ''; + const cleanup = () => { + child.stderr.removeListener('data', listener); + child.stdout.removeListener('data', listener); + }; + + const listener = (data: Buffer) => { + output += data; + if (/The NODE_OPTION --use-openssl-ca is not supported in Electron/m.test(output)) { + cleanup(); + done(); + } + }; + + child.stderr.on('data', listener); + child.stdout.on('data', listener); + }); + + it('does allow --require in non-packaged apps', async () => { + const appPath = path.join(fixtures, 'module', 'noop.js'); + const env = Object.assign({}, process.env, { + NODE_OPTIONS: `--require=${path.join(fixtures, 'module', 'fail.js')}` + }); + // App should exit with code 1. + const child = childProcess.spawn(process.execPath, [appPath], { env }); + const [code] = await emittedOnce(child, 'exit'); + expect(code).to.equal(1); + }); + + it('does not allow --require in packaged apps', async () => { + const appPath = path.join(fixtures, 'module', 'noop.js'); + const env = Object.assign({}, process.env, { + ELECTRON_FORCE_IS_PACKAGED: 'true', + NODE_OPTIONS: `--require=${path.join(fixtures, 'module', 'fail.js')}` + }); + // App should exit with code 0. + const child = childProcess.spawn(process.execPath, [appPath], { env }); + const [code] = await emittedOnce(child, 'exit'); + expect(code).to.equal(0); + }); + }); + + ifdescribe(features.isRunAsNodeEnabled())('Node.js cli flags', () => { + let child: childProcess.ChildProcessWithoutNullStreams; + let exitPromise: Promise; + + it('Prohibits crypto-related flags in ELECTRON_RUN_AS_NODE mode', (done) => { + after(async () => { + const [code, signal] = await exitPromise; + expect(signal).to.equal(null); + expect(code).to.equal(9); + child.kill(); + }); + + child = childProcess.spawn(process.execPath, ['--force-fips'], { + env: { ELECTRON_RUN_AS_NODE: 'true' } + }); + exitPromise = emittedOnce(child, 'exit'); + + let output = ''; + const cleanup = () => { + child.stderr.removeListener('data', listener); + child.stdout.removeListener('data', listener); + }; + + const listener = (data: Buffer) => { + output += data; + if (/.*The Node.js cli flag --force-fips is not supported in Electron/m.test(output)) { + cleanup(); + done(); + } + }; + + child.stderr.on('data', listener); + child.stdout.on('data', listener); + }); + }); + + describe('process.stdout', () => { + it('is a real Node stream', () => { + expect((process.stdout as any)._type).to.not.be.undefined(); + }); + }); + + describe('fs.readFile', () => { + it('can accept a FileHandle as the Path argument', async () => { + const filePathForHandle = path.resolve(mainFixturesPath, 'dogs-running.txt'); + const fileHandle = await fs.promises.open(filePathForHandle, 'r'); + + const file = await fs.promises.readFile(fileHandle, { encoding: 'utf8' }); + expect(file).to.not.be.empty(); + await fileHandle.close(); + }); + }); + + ifdescribe(features.isRunAsNodeEnabled())('inspector', () => { + let child: childProcess.ChildProcessWithoutNullStreams; + let exitPromise: Promise | null; + + afterEach(async () => { + if (child && exitPromise) { + const [code, signal] = await exitPromise; + expect(signal).to.equal(null); + expect(code).to.equal(0); + } else if (child) { + child.kill(); + } + child = null as any; + exitPromise = null as any; + }); + + it('Supports starting the v8 inspector with --inspect/--inspect-brk', (done) => { + child = childProcess.spawn(process.execPath, ['--inspect-brk', path.join(fixtures, 'module', 'run-as-node.js')], { + env: { ELECTRON_RUN_AS_NODE: 'true' } + }); + + let output = ''; + const cleanup = () => { + child.stderr.removeListener('data', listener); + child.stdout.removeListener('data', listener); + }; + + const listener = (data: Buffer) => { + output += data; + if (/Debugger listening on ws:/m.test(output)) { + cleanup(); + done(); + } + }; + + child.stderr.on('data', listener); + child.stdout.on('data', listener); + }); + + it('Supports starting the v8 inspector with --inspect and a provided port', async () => { + child = childProcess.spawn(process.execPath, ['--inspect=17364', path.join(fixtures, 'module', 'run-as-node.js')], { + env: { ELECTRON_RUN_AS_NODE: 'true' } + }); + exitPromise = emittedOnce(child, 'exit'); + + let output = ''; + const listener = (data: Buffer) => { output += data; }; + const cleanup = () => { + child.stderr.removeListener('data', listener); + child.stdout.removeListener('data', listener); + }; + + child.stderr.on('data', listener); + child.stdout.on('data', listener); + await emittedOnce(child, 'exit'); + cleanup(); + if (/^Debugger listening on ws:/m.test(output)) { + expect(output.trim()).to.contain(':17364', 'should be listening on port 17364'); + } else { + throw new Error(`Unexpected output: ${output.toString()}`); + } + }); + + it('Does not start the v8 inspector when --inspect is after a -- argument', async () => { + child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'noop.js'), '--', '--inspect']); + exitPromise = emittedOnce(child, 'exit'); + + let output = ''; + const listener = (data: Buffer) => { output += data; }; + child.stderr.on('data', listener); + child.stdout.on('data', listener); + await emittedOnce(child, 'exit'); + if (output.trim().startsWith('Debugger listening on ws://')) { + throw new Error('Inspector was started when it should not have been'); + } + }); + + // IPC Electron child process not supported on Windows. + ifit(process.platform !== 'win32')('does not crash when quitting with the inspector connected', function (done) { + child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'delay-exit'), '--inspect=0'], { + stdio: ['ipc'] + }) as childProcess.ChildProcessWithoutNullStreams; + exitPromise = emittedOnce(child, 'exit'); + + const cleanup = () => { + child.stderr.removeListener('data', listener); + child.stdout.removeListener('data', listener); + }; + + let output = ''; + const success = false; + function listener (data: Buffer) { + output += data; + console.log(data.toString()); // NOTE: temporary debug logging to try to catch flake. + const match = /^Debugger listening on (ws:\/\/.+:\d+\/.+)\n/m.exec(output.trim()); + if (match) { + cleanup(); + // NOTE: temporary debug logging to try to catch flake. + child.stderr.on('data', (m) => console.log(m.toString())); + child.stdout.on('data', (m) => console.log(m.toString())); + const w = (webContents as any).create({}) as WebContents; + w.loadURL('about:blank') + .then(() => w.executeJavaScript(`new Promise(resolve => { + const connection = new WebSocket(${JSON.stringify(match[1])}) + connection.onopen = () => { + connection.onclose = () => resolve() + connection.close() + } + })`)) + .then(() => { + (w as any).destroy(); + child.send('plz-quit'); + done(); + }); + } + } + + child.stderr.on('data', listener); + child.stdout.on('data', listener); + child.on('exit', () => { + if (!success) cleanup(); + }); + }); + + it('Supports js binding', async () => { + child = childProcess.spawn(process.execPath, ['--inspect', path.join(fixtures, 'module', 'inspector-binding.js')], { + env: { ELECTRON_RUN_AS_NODE: 'true' }, + stdio: ['ipc'] + }) as childProcess.ChildProcessWithoutNullStreams; + exitPromise = emittedOnce(child, 'exit'); + + const [{ cmd, debuggerEnabled, success }] = await emittedOnce(child, 'message'); + expect(cmd).to.equal('assert'); + expect(debuggerEnabled).to.be.true(); + expect(success).to.be.true(); + }); + }); + + it('Can find a module using a package.json main field', () => { + const result = childProcess.spawnSync(process.execPath, [path.resolve(fixtures, 'api', 'electron-main-module', 'app.asar')], { stdio: 'inherit' }); + expect(result.status).to.equal(0); + }); + + ifit(features.isRunAsNodeEnabled())('handles Promise timeouts correctly', async () => { + const scriptPath = path.join(fixtures, 'module', 'node-promise-timer.js'); + const child = childProcess.spawn(process.execPath, [scriptPath], { + env: { ELECTRON_RUN_AS_NODE: 'true' } + }); + const [code, signal] = await emittedOnce(child, 'exit'); + expect(code).to.equal(0); + expect(signal).to.equal(null); + child.kill(); + }); + + it('performs microtask checkpoint correctly', (done) => { + const f3 = async () => { + return new Promise((resolve, reject) => { + reject(new Error('oops')); + }); + }; + + process.once('unhandledRejection', () => done('catch block is delayed to next tick')); + + setTimeout(() => { + f3().catch(() => done()); + }); + }); +}); diff --git a/spec-main/package.json b/spec-main/package.json index 8a802f6bfdf2d..1635e32313cda 100644 --- a/spec-main/package.json +++ b/spec-main/package.json @@ -2,5 +2,21 @@ "name": "electron-test-main", "productName": "Electron Test Main", "main": "index.js", - "version": "0.1.0" + "version": "0.1.0", + "devDependencies": { + "@electron-ci/echo": "file:./fixtures/native-addon/echo", + "@electron-ci/uv-dlopen": "file:./fixtures/native-addon/uv-dlopen/", + "@types/sinon": "^9.0.4", + "@types/ws": "^7.2.0", + "busboy": "^0.3.1", + "q": "^1.5.1", + "sinon": "^9.0.1", + "ws": "^7.4.6" + }, + "dependencies": { + "chai-as-promised": "^7.1.1", + "dirty-chai": "^2.0.1", + "get-image-colors": "^4.0.0", + "pdfjs-dist": "^2.2.228" + } } diff --git a/spec-main/pipe-transport.ts b/spec-main/pipe-transport.ts new file mode 100644 index 0000000000000..044df4138e52c --- /dev/null +++ b/spec-main/pipe-transport.ts @@ -0,0 +1,37 @@ +// A small pipe transport for talking to Electron over CDP. +export class PipeTransport { + private _pipeWrite: NodeJS.WritableStream | null; + private _pendingMessage = ''; + + onmessage?: (message: string) => void; + + constructor (pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) { + this._pipeWrite = pipeWrite; + pipeRead.on('data', buffer => this._dispatch(buffer)); + } + + send (message: Object) { + this._pipeWrite!.write(JSON.stringify(message)); + this._pipeWrite!.write('\0'); + } + + _dispatch (buffer: Buffer) { + let end = buffer.indexOf('\0'); + if (end === -1) { + this._pendingMessage += buffer.toString(); + return; + } + const message = this._pendingMessage + buffer.toString(undefined, 0, end); + if (this.onmessage) { this.onmessage.call(null, JSON.parse(message)); } + + let start = end + 1; + end = buffer.indexOf('\0', start); + while (end !== -1) { + const message = buffer.toString(undefined, start, end); + if (this.onmessage) { this.onmessage.call(null, JSON.parse(message)); } + start = end + 1; + end = buffer.indexOf('\0', start); + } + this._pendingMessage = buffer.toString(undefined, start); + } +} diff --git a/spec-main/release-notes-spec.ts b/spec-main/release-notes-spec.ts new file mode 100644 index 0000000000000..a2fd06b93f70a --- /dev/null +++ b/spec-main/release-notes-spec.ts @@ -0,0 +1,214 @@ +import { GitProcess, IGitExecutionOptions, IGitResult } from 'dugite'; +import { expect } from 'chai'; +import * as notes from '../script/release/notes/notes.js'; +import * as path from 'path'; +import * as sinon from 'sinon'; + +/* Fake a Dugite GitProcess that only returns the specific + commits that we want to test */ + +class Commit { + sha1: string; + subject: string; + constructor (sha1: string, subject: string) { + this.sha1 = sha1; + this.subject = subject; + } +} + +class GitFake { + branches: { + [key: string]: Commit[], + }; + + constructor () { + this.branches = {}; + } + + setBranch (name: string, commits: Array): void { + this.branches[name] = commits; + } + + // find the newest shared commit between branches a and b + mergeBase (a: string, b:string): string { + for (const commit of [...this.branches[a].reverse()]) { + if (this.branches[b].map((commit: Commit) => commit.sha1).includes(commit.sha1)) { + return commit.sha1; + } + } + console.error('test error: branches not related'); + return ''; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + exec (args: string[], path: string, options?: IGitExecutionOptions | undefined): Promise { + let stdout = ''; + const stderr = ''; + const exitCode = 0; + + if (args.length === 3 && args[0] === 'merge-base') { + // expected form: `git merge-base branchName1 branchName2` + const a: string = args[1]!; + const b: string = args[2]!; + stdout = this.mergeBase(a, b); + } else if (args.length === 3 && args[0] === 'log' && args[1] === '--format=%H') { + // exepcted form: `git log --format=%H branchName + const branch: string = args[2]!; + stdout = this.branches[branch].map((commit: Commit) => commit.sha1).join('\n'); + } else if (args.length > 1 && args[0] === 'log' && args.includes('--format=%H,%s')) { + // expected form: `git log --format=%H,%s sha1..branchName + const [start, branch] = args[args.length - 1].split('..'); + const lines : string[] = []; + let started = false; + for (const commit of this.branches[branch]) { + started = started || commit.sha1 === start; + if (started) { + lines.push(`${commit.sha1},${commit.subject}` /* %H,%s */); + } + } + stdout = lines.join('\n'); + } else if (args.length === 6 && + args[0] === 'branch' && + args[1] === '--all' && + args[2] === '--contains' && + args[3].endsWith('-x-y')) { + // "what branch is this tag in?" + // git branch --all --contains ${ref} --sort version:refname + stdout = args[3]; + } else { + console.error('unhandled GitProcess.exec():', args); + } + + return Promise.resolve({ exitCode, stdout, stderr }); + } +} + +describe('release notes', () => { + const sandbox = sinon.createSandbox(); + const gitFake = new GitFake(); + + const oldBranch = '8-x-y'; + const newBranch = '9-x-y'; + + // commits shared by both oldBranch and newBranch + const sharedHistory = [ + new Commit('2abea22b4bffa1626a521711bacec7cd51425818', "fix: explicitly cancel redirects when mode is 'error' (#20686)"), + new Commit('467409458e716c68b35fa935d556050ca6bed1c4', 'build: add support for automated minor releases (#20620)') // merge-base + ]; + + // these commits came after newBranch was created + const newBreaking = new Commit('2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98', 'refactor: use v8 serialization for ipc (#20214)'); + const newFeat = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: allow GUID parameter to avoid systray demotion on Windows (#21891)'); + const newFix = new Commit('0600420bac25439fc2067d51c6aaa4ee11770577', "fix: don't allow window to go behind menu bar on mac (#22828)"); + const oldFix = new Commit('f77bd19a70ac2d708d17ddbe4dc12745ca3a8577', 'fix: prevent menu gc during popup (#20785)'); + + // a bug that's fixed in both branches by separate PRs + const newTropFix = new Commit('a6ff42c190cb5caf8f3e217748e49183a951491b', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22750)'); + const oldTropFix = new Commit('8751f485c5a6c8c78990bfd55a4350700f81f8cd', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22749)'); + + // a PR that has unusual note formatting + const sublist = new Commit('61dc1c88fd34a3e8fff80c80ed79d0455970e610', 'fix: client area inset calculation when maximized for framless windows (#25052) (#25216)'); + + before(() => { + // location of relase-notes' octokit reply cache + const fixtureDir = path.resolve(__dirname, 'fixtures', 'release-notes'); + process.env.NOTES_CACHE_PATH = path.resolve(fixtureDir, 'cache'); + }); + + beforeEach(() => { + const wrapper = (args: string[], path: string, options?: IGitExecutionOptions | undefined) => gitFake.exec(args, path, options); + sandbox.replace(GitProcess, 'exec', wrapper); + + gitFake.setBranch(oldBranch, [...sharedHistory, oldFix]); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('trop annotations', () => { + it('shows sibling branches', async function () { + const version = 'v9.0.0'; + gitFake.setBranch(oldBranch, [...sharedHistory, oldTropFix]); + gitFake.setBranch(newBranch, [...sharedHistory, newTropFix]); + const results: any = await notes.get(oldBranch, newBranch, version); + expect(results.fix).to.have.lengthOf(1); + console.log(results.fix); + expect(results.fix[0].trops).to.have.keys('8-x-y', '9-x-y'); + }); + }); + + // use case: A malicious contributor could edit the text of their 'Notes:' + // in the PR body after a PR's been merged and the maintainers have moved on. + // So instead always use the release-clerk PR comment + it('uses the release-clerk text', async function () { + // realText source: ${fixtureDir}/electron-electron-issue-21891-comments + const realText = 'Added GUID parameter to Tray API to avoid system tray icon demotion on Windows'; + const testCommit = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: lole u got troled hard (#21891)'); + const version = 'v9.0.0'; + + gitFake.setBranch(newBranch, [...sharedHistory, testCommit]); + const results: any = await notes.get(oldBranch, newBranch, version); + expect(results.feat).to.have.lengthOf(1); + expect(results.feat[0].hash).to.equal(testCommit.sha1); + expect(results.feat[0].note).to.equal(realText); + }); + + describe('rendering', () => { + it('removes redundant bullet points', async function () { + const testCommit = sublist; + const version = 'v10.1.1'; + + gitFake.setBranch(newBranch, [...sharedHistory, testCommit]); + const results: any = await notes.get(oldBranch, newBranch, version); + const rendered: any = await notes.render(results); + + expect(rendered).to.not.include('* *'); + }); + + it('indents sublists', async function () { + const testCommit = sublist; + const version = 'v10.1.1'; + + gitFake.setBranch(newBranch, [...sharedHistory, testCommit]); + const results: any = await notes.get(oldBranch, newBranch, version); + const rendered: any = await notes.render(results); + + expect(rendered).to.include([ + '* Fixed the following issues for frameless when maximized on Windows:', + ' * fix unreachable task bar when auto hidden with position top', + ' * fix 1px extending to secondary monitor', + ' * fix 1px overflowing into taskbar at certain resolutions', + ' * fix white line on top of window under 4k resolutions. [#25216]'].join('\n')); + }); + }); + // test that when you feed in different semantic commit types, + // the parser returns them in the results' correct category + describe('semantic commit', () => { + const version = 'v9.0.0'; + + it("honors 'feat' type", async function () { + const testCommit = newFeat; + gitFake.setBranch(newBranch, [...sharedHistory, testCommit]); + const results: any = await notes.get(oldBranch, newBranch, version); + expect(results.feat).to.have.lengthOf(1); + expect(results.feat[0].hash).to.equal(testCommit.sha1); + }); + + it("honors 'fix' type", async function () { + const testCommit = newFix; + gitFake.setBranch(newBranch, [...sharedHistory, testCommit]); + const results: any = await notes.get(oldBranch, newBranch, version); + expect(results.fix).to.have.lengthOf(1); + expect(results.fix[0].hash).to.equal(testCommit.sha1); + }); + + it("honors 'BREAKING CHANGE' message", async function () { + const testCommit = newBreaking; + gitFake.setBranch(newBranch, [...sharedHistory, testCommit]); + const results: any = await notes.get(oldBranch, newBranch, version); + expect(results.breaking).to.have.lengthOf(1); + expect(results.breaking[0].hash).to.equal(testCommit.sha1); + }); + }); +}); diff --git a/spec-main/screen-helpers.ts b/spec-main/screen-helpers.ts new file mode 100644 index 0000000000000..26fbcb8ea2b08 --- /dev/null +++ b/spec-main/screen-helpers.ts @@ -0,0 +1,87 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { screen, desktopCapturer, NativeImage } from 'electron'; + +const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures'); + +/** Chroma key green. */ +export const CHROMA_COLOR_HEX = '#00b140'; + +/** + * Capture the screen at the given point. + * + * NOTE: Not yet supported on Linux in CI due to empty sources list. + */ +export const captureScreen = async (point: Electron.Point = { x: 0, y: 0 }): Promise => { + const display = screen.getDisplayNearestPoint(point); + const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: display.size }); + // Toggle to save screen captures for debugging. + const DEBUG_CAPTURE = false; + if (DEBUG_CAPTURE) { + for (const source of sources) { + await fs.promises.writeFile(path.join(fixtures, `screenshot_${source.display_id}.png`), source.thumbnail.toPNG()); + } + } + const screenCapture = sources.find(source => source.display_id === `${display.id}`); + // Fails when HDR is enabled on Windows. + // https://bugs.chromium.org/p/chromium/issues/detail?id=1247730 + if (!screenCapture) { + const displayIds = sources.map(source => source.display_id); + throw new Error(`Unable to find screen capture for display '${display.id}'\n\tAvailable displays: ${displayIds.join(', ')}`); + } + return screenCapture.thumbnail; +}; + +const formatHexByte = (val: number): string => { + const str = val.toString(16); + return str.length === 2 ? str : `0${str}`; +}; + +/** + * Get the hex color at the given pixel coordinate in an image. + */ +export const getPixelColor = (image: Electron.NativeImage, point: Electron.Point): string => { + const pixel = image.crop({ ...point, width: 1, height: 1 }); + // TODO(samuelmaddock): NativeImage.toBitmap() should return the raw pixel + // color, but it sometimes differs. Why is that? + const [b, g, r] = pixel.toBitmap(); + return `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`; +}; + +const hexToRgba = (hexColor: string) => { + const match = hexColor.match(/^#([0-9a-fA-F]{6,8})$/); + if (!match) return; + + const colorStr = match[1]; + return [ + parseInt(colorStr.substring(0, 2), 16), + parseInt(colorStr.substring(2, 4), 16), + parseInt(colorStr.substring(4, 6), 16), + parseInt(colorStr.substring(6, 8), 16) || 0xFF + ]; +}; + +/** Calculate euclidian distance between colors. */ +const colorDistance = (hexColorA: string, hexColorB: string) => { + const colorA = hexToRgba(hexColorA); + const colorB = hexToRgba(hexColorB); + if (!colorA || !colorB) return -1; + return Math.sqrt( + Math.pow(colorB[0] - colorA[0], 2) + + Math.pow(colorB[1] - colorA[1], 2) + + Math.pow(colorB[2] - colorA[2], 2) + ); +}; + +/** + * Determine if colors are similar based on distance. This can be useful when + * comparing colors which may differ based on lossy compression. + */ +export const areColorsSimilar = ( + hexColorA: string, + hexColorB: string, + distanceThreshold = 90 +): boolean => { + const distance = colorDistance(hexColorA, hexColorB); + return distance <= distanceThreshold; +}; diff --git a/spec-main/security-warnings-spec.ts b/spec-main/security-warnings-spec.ts new file mode 100644 index 0000000000000..22b96975914b8 --- /dev/null +++ b/spec-main/security-warnings-spec.ts @@ -0,0 +1,228 @@ +import { expect } from 'chai'; +import * as http from 'http'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as url from 'url'; + +import { BrowserWindow, WebPreferences } from 'electron/main'; + +import { closeWindow } from './window-helpers'; +import { AddressInfo } from 'net'; +import { emittedUntil } from './events-helpers'; +import { delay } from './spec-helpers'; + +const messageContainsSecurityWarning = (event: Event, level: number, message: string) => { + return message.indexOf('Electron Security Warning') > -1; +}; + +const isLoaded = (event: Event, level: number, message: string) => { + return (message === 'loaded'); +}; + +describe('security warnings', () => { + let server: http.Server; + let w: BrowserWindow; + let useCsp = true; + let serverUrl: string; + + before((done) => { + // Create HTTP Server + server = http.createServer((request, response) => { + const uri = url.parse(request.url!).pathname!; + let filename = path.join(__dirname, '..', 'spec', 'fixtures', 'pages', uri); + + fs.stat(filename, (error, stats) => { + if (error) { + response.writeHead(404, { 'Content-Type': 'text/plain' }); + response.end(); + return; + } + + if (stats.isDirectory()) { + filename += '/index.html'; + } + + fs.readFile(filename, 'binary', (err, file) => { + if (err) { + response.writeHead(404, { 'Content-Type': 'text/plain' }); + response.end(); + return; + } + + const cspHeaders = [ + ...(useCsp ? ['script-src \'self\' \'unsafe-inline\''] : []) + ]; + response.writeHead(200, { 'Content-Security-Policy': cspHeaders }); + response.write(file, 'binary'); + response.end(); + }); + }); + }).listen(0, '127.0.0.1', () => { + serverUrl = `http://localhost2:${(server.address() as AddressInfo).port}`; + done(); + }); + }); + + after(() => { + // Close server + server.close(); + server = null as unknown as any; + }); + + afterEach(async () => { + useCsp = true; + await closeWindow(w); + w = null as unknown as any; + }); + + it('should warn about Node.js integration with remote content', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + w.loadURL(`${serverUrl}/base-page-security.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('Node.js Integration with Remote Content'); + }); + + it('should not warn about Node.js integration with remote content from localhost', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true + } + }); + + w.loadURL(`${serverUrl}/base-page-security-onload-message.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', isLoaded); + expect(message).to.not.include('Node.js Integration with Remote Content'); + }); + + const generateSpecs = (description: string, webPreferences: WebPreferences) => { + describe(description, () => { + it('should warn about disabled webSecurity', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + webSecurity: false, + ...webPreferences + } + }); + + w.loadURL(`${serverUrl}/base-page-security.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('Disabled webSecurity'); + }); + + it('should warn about insecure Content-Security-Policy', async () => { + w = new BrowserWindow({ + show: false, + webPreferences + }); + + useCsp = false; + w.loadURL(`${serverUrl}/base-page-security.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('Insecure Content-Security-Policy'); + }); + + it('should not warn about secure Content-Security-Policy', async () => { + w = new BrowserWindow({ + show: false, + webPreferences + }); + + useCsp = true; + w.loadURL(`${serverUrl}/base-page-security.html`); + let didNotWarn = true; + w.webContents.on('console-message', () => { + didNotWarn = false; + }); + await delay(500); + expect(didNotWarn).to.equal(true); + }); + + it('should warn about allowRunningInsecureContent', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + allowRunningInsecureContent: true, + ...webPreferences + } + }); + + w.loadURL(`${serverUrl}/base-page-security.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('allowRunningInsecureContent'); + }); + + it('should warn about experimentalFeatures', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + experimentalFeatures: true, + ...webPreferences + } + }); + + w.loadURL(`${serverUrl}/base-page-security.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('experimentalFeatures'); + }); + + it('should warn about enableBlinkFeatures', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + enableBlinkFeatures: 'my-cool-feature', + ...webPreferences + } + }); + + w.loadURL(`${serverUrl}/base-page-security.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('enableBlinkFeatures'); + }); + + it('should warn about allowpopups', async () => { + w = new BrowserWindow({ + show: false, + webPreferences + }); + + w.loadURL(`${serverUrl}/webview-allowpopups.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('allowpopups'); + }); + + it('should warn about insecure resources', async () => { + w = new BrowserWindow({ + show: false, + webPreferences + }); + + w.loadURL(`${serverUrl}/insecure-resources.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.include('Insecure Resources'); + }); + + it('should not warn about loading insecure-resources.html from localhost', async () => { + w = new BrowserWindow({ + show: false, + webPreferences + }); + + w.loadURL(`${serverUrl}/insecure-resources.html`); + const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning); + expect(message).to.not.include('insecure-resources.html'); + }); + }); + }; + + generateSpecs('without sandbox', { contextIsolation: false }); + generateSpecs('with sandbox', { sandbox: true, contextIsolation: false }); +}); diff --git a/spec-main/spec-helpers.ts b/spec-main/spec-helpers.ts index d75e663a9bc54..434f68636f206 100644 --- a/spec-main/spec-helpers.ts +++ b/spec-main/spec-helpers.ts @@ -1,2 +1,147 @@ -export const ifit = (condition: boolean) => (condition ? it : it.skip) -export const ifdescribe = (condition: boolean) => (condition ? describe : describe.skip) \ No newline at end of file +import * as childProcess from 'child_process'; +import * as path from 'path'; +import * as http from 'http'; +import * as v8 from 'v8'; +import { SuiteFunction, TestFunction } from 'mocha'; + +const addOnly = (fn: Function): T => { + const wrapped = (...args: any[]) => { + return fn(...args); + }; + (wrapped as any).only = wrapped; + (wrapped as any).skip = wrapped; + return wrapped as any; +}; + +export const ifit = (condition: boolean) => (condition ? it : addOnly(it.skip)); +export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly(describe.skip)); + +export const delay = (time: number = 0) => new Promise(resolve => setTimeout(resolve, time)); + +type CleanupFunction = (() => void) | (() => Promise) +const cleanupFunctions: CleanupFunction[] = []; +export async function runCleanupFunctions () { + for (const cleanup of cleanupFunctions) { + const r = cleanup(); + if (r instanceof Promise) { await r; } + } + cleanupFunctions.length = 0; +} + +export function defer (f: CleanupFunction) { + cleanupFunctions.unshift(f); +} + +class RemoteControlApp { + process: childProcess.ChildProcess; + port: number; + + constructor (proc: childProcess.ChildProcess, port: number) { + this.process = proc; + this.port = port; + } + + remoteEval = (js: string): Promise => { + return new Promise((resolve, reject) => { + const req = http.request({ + host: '127.0.0.1', + port: this.port, + method: 'POST' + }, res => { + const chunks = [] as Buffer[]; + res.on('data', chunk => { chunks.push(chunk); }); + res.on('end', () => { + const ret = v8.deserialize(Buffer.concat(chunks)); + if (Object.prototype.hasOwnProperty.call(ret, 'error')) { + reject(new Error(`remote error: ${ret.error}\n\nTriggered at:`)); + } else { + resolve(ret.result); + } + }); + }); + req.write(js); + req.end(); + }); + } + + remotely = (script: Function, ...args: any[]): Promise => { + return this.remoteEval(`(${script})(...${JSON.stringify(args)})`); + } +} + +export async function startRemoteControlApp (extraArgs: string[] = [], options?: childProcess.SpawnOptionsWithoutStdio) { + const appPath = path.join(__dirname, 'fixtures', 'apps', 'remote-control'); + const appProcess = childProcess.spawn(process.execPath, [appPath, ...extraArgs], options); + appProcess.stderr.on('data', d => { + process.stderr.write(d); + }); + const port = await new Promise(resolve => { + appProcess.stdout.on('data', d => { + const m = /Listening: (\d+)/.exec(d.toString()); + if (m && m[1] != null) { + resolve(Number(m[1])); + } + }); + }); + defer(() => { appProcess.kill('SIGINT'); }); + return new RemoteControlApp(appProcess, port); +} + +export function waitUntil ( + callback: () => boolean, + opts: { rate?: number, timeout?: number } = {} +) { + const { rate = 10, timeout = 10000 } = opts; + return new Promise((resolve, reject) => { + let intervalId: NodeJS.Timeout | undefined; // eslint-disable-line prefer-const + let timeoutId: NodeJS.Timeout | undefined; + + const cleanup = () => { + if (intervalId) clearInterval(intervalId); + if (timeoutId) clearTimeout(timeoutId); + }; + + const check = () => { + let result; + + try { + result = callback(); + } catch (e) { + cleanup(); + reject(e); + return; + } + + if (result === true) { + cleanup(); + resolve(); + return true; + } + }; + + if (check()) { + return; + } + + intervalId = setInterval(check, rate); + + timeoutId = setTimeout(() => { + timeoutId = undefined; + cleanup(); + reject(new Error(`waitUntil timed out after ${timeout}ms`)); + }, timeout); + }); +} + +export async function repeatedly ( + fn: () => Promise, + opts?: { until?: (x: T) => boolean, timeLimit?: number } +) { + const { until = (x: T) => !!x, timeLimit = 10000 } = opts ?? {}; + const begin = +new Date(); + while (true) { + const ret = await fn(); + if (until(ret)) { return ret; } + if (+new Date() - begin > timeLimit) { throw new Error(`repeatedly timed out (limit=${timeLimit})`); } + } +} diff --git a/spec-main/spellchecker-spec.ts b/spec-main/spellchecker-spec.ts new file mode 100644 index 0000000000000..3cc058158729b --- /dev/null +++ b/spec-main/spellchecker-spec.ts @@ -0,0 +1,238 @@ +import { BrowserWindow, Session, session } from 'electron/main'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as http from 'http'; +import { AddressInfo } from 'net'; +import { closeWindow } from './window-helpers'; +import { emittedOnce } from './events-helpers'; +import { ifit, ifdescribe, delay } from './spec-helpers'; + +const features = process._linkedBinding('electron_common_features'); +const v8Util = process._linkedBinding('electron_common_v8_util'); + +ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', function () { + this.timeout((process.env.IS_ASAN ? 200 : 20) * 1000); + + let w: BrowserWindow; + + async function rightClick () { + const contextMenuPromise = emittedOnce(w.webContents, 'context-menu'); + w.webContents.sendInputEvent({ + type: 'mouseDown', + button: 'right', + x: 43, + y: 42 + }); + return (await contextMenuPromise)[1] as Electron.ContextMenuParams; + } + + // When the page is just loaded, the spellchecker might not be ready yet. Since + // there is no event to know the state of spellchecker, the only reliable way + // to detect spellchecker is to keep checking with a busy loop. + async function rightClickUntil (fn: (params: Electron.ContextMenuParams) => boolean) { + const now = Date.now(); + const timeout = (process.env.IS_ASAN ? 180 : 10) * 1000; + let contextMenuParams = await rightClick(); + while (!fn(contextMenuParams) && (Date.now() - now < timeout)) { + await delay(100); + contextMenuParams = await rightClick(); + } + return contextMenuParams; + } + + // Setup a server to download hunspell dictionary. + const server = http.createServer((req, res) => { + // The provided is minimal dict for testing only, full list of words can + // be found at src/third_party/hunspell_dictionaries/xx_XX.dic. + fs.readFile(path.join(__dirname, '/../../third_party/hunspell_dictionaries/xx-XX-3-0.bdic'), function (err, data) { + if (err) { + console.error('Failed to read dictionary file'); + res.writeHead(404); + res.end(JSON.stringify(err)); + return; + } + res.writeHead(200); + res.end(data); + }); + }); + before((done) => { + server.listen(0, '127.0.0.1', () => done()); + }); + after(() => server.close()); + + const fixtures = path.resolve(__dirname, '../spec/fixtures'); + const preload = path.join(fixtures, 'module', 'preload-electron.js'); + + const generateSpecs = (description: string, sandbox: boolean) => { + describe(description, () => { + beforeEach(async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + partition: `unique-spell-${Date.now()}`, + contextIsolation: false, + preload, + sandbox + } + }); + w.webContents.session.setSpellCheckerDictionaryDownloadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`); + w.webContents.session.setSpellCheckerLanguages(['en-US']); + await w.loadFile(path.resolve(__dirname, './fixtures/chromium/spellchecker.html')); + }); + + afterEach(async () => { + await closeWindow(w); + }); + + // Context menu test can not run on Windows. + const shouldRun = process.platform !== 'win32'; + + ifit(shouldRun)('should detect correctly spelled words as correct', async () => { + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typography"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + const contextMenuParams = await rightClickUntil((contextMenuParams) => contextMenuParams.selectionText.length > 0); + expect(contextMenuParams.misspelledWord).to.eq(''); + expect(contextMenuParams.dictionarySuggestions).to.have.lengthOf(0); + }); + + ifit(shouldRun)('should detect incorrectly spelled words as incorrect', async () => { + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + const contextMenuParams = await rightClickUntil((contextMenuParams) => contextMenuParams.misspelledWord.length > 0); + expect(contextMenuParams.misspelledWord).to.eq('typograpy'); + expect(contextMenuParams.dictionarySuggestions).to.have.length.of.at.least(1); + }); + + ifit(shouldRun)('should detect incorrectly spelled words as incorrect after disabling all languages and re-enabling', async () => { + w.webContents.session.setSpellCheckerLanguages([]); + await delay(500); + w.webContents.session.setSpellCheckerLanguages(['en-US']); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + const contextMenuParams = await rightClickUntil((contextMenuParams) => contextMenuParams.misspelledWord.length > 0); + expect(contextMenuParams.misspelledWord).to.eq('typograpy'); + expect(contextMenuParams.dictionarySuggestions).to.have.length.of.at.least(1); + }); + + ifit(shouldRun)('should expose webFrame spellchecker correctly', async () => { + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + await rightClickUntil((contextMenuParams) => contextMenuParams.misspelledWord.length > 0); + + const callWebFrameFn = (expr: string) => w.webContents.executeJavaScript(`electron.webFrame.${expr}`); + + expect(await callWebFrameFn('isWordMisspelled("typography")')).to.equal(false); + expect(await callWebFrameFn('isWordMisspelled("typograpy")')).to.equal(true); + expect(await callWebFrameFn('getWordSuggestions("typography")')).to.be.empty(); + expect(await callWebFrameFn('getWordSuggestions("typograpy")')).to.not.be.empty(); + }); + + describe('spellCheckerEnabled', () => { + it('is enabled by default', async () => { + expect(w.webContents.session.spellCheckerEnabled).to.be.true(); + }); + + ifit(shouldRun)('can be dynamically changed', async () => { + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + await rightClickUntil((contextMenuParams) => contextMenuParams.misspelledWord.length > 0); + + const callWebFrameFn = (expr: string) => w.webContents.executeJavaScript(`electron.webFrame.${expr}`); + + w.webContents.session.spellCheckerEnabled = false; + v8Util.runUntilIdle(); + expect(w.webContents.session.spellCheckerEnabled).to.be.false(); + // spellCheckerEnabled is sent to renderer asynchronously and there is + // no event notifying when it is finished, so wait a little while to + // ensure the setting has been changed in renderer. + await delay(500); + expect(await callWebFrameFn('isWordMisspelled("typograpy")')).to.equal(false); + + w.webContents.session.spellCheckerEnabled = true; + v8Util.runUntilIdle(); + expect(w.webContents.session.spellCheckerEnabled).to.be.true(); + await delay(500); + expect(await callWebFrameFn('isWordMisspelled("typograpy")')).to.equal(true); + }); + }); + + describe('custom dictionary word list API', () => { + let ses: Session; + + beforeEach(async () => { + // ensure a new session runs on each test run + ses = session.fromPartition(`persist:customdictionary-test-${Date.now()}`); + }); + + afterEach(async () => { + if (ses) { + await ses.clearStorageData(); + ses = null as any; + } + }); + + describe('ses.listWordsFromSpellCheckerDictionary', () => { + it('should successfully list words in custom dictionary', async () => { + const words = ['foo', 'bar', 'baz']; + const results = words.map(word => ses.addWordToSpellCheckerDictionary(word)); + expect(results).to.eql([true, true, true]); + + const wordList = await ses.listWordsInSpellCheckerDictionary(); + expect(wordList).to.have.deep.members(words); + }); + + it('should return an empty array if no words are added', async () => { + const wordList = await ses.listWordsInSpellCheckerDictionary(); + expect(wordList).to.have.length(0); + }); + }); + + describe('ses.addWordToSpellCheckerDictionary', () => { + it('should successfully add word to custom dictionary', async () => { + const result = ses.addWordToSpellCheckerDictionary('foobar'); + expect(result).to.equal(true); + const wordList = await ses.listWordsInSpellCheckerDictionary(); + expect(wordList).to.eql(['foobar']); + }); + + it('should fail for an empty string', async () => { + const result = ses.addWordToSpellCheckerDictionary(''); + expect(result).to.equal(false); + const wordList = await ses.listWordsInSpellCheckerDictionary; + expect(wordList).to.have.length(0); + }); + + // remove API will always return false because we can't add words + it('should fail for non-persistent sessions', async () => { + const tempSes = session.fromPartition('temporary'); + const result = tempSes.addWordToSpellCheckerDictionary('foobar'); + expect(result).to.equal(false); + }); + }); + + describe('ses.removeWordFromSpellCheckerDictionary', () => { + it('should successfully remove words to custom dictionary', async () => { + const result1 = ses.addWordToSpellCheckerDictionary('foobar'); + expect(result1).to.equal(true); + const wordList1 = await ses.listWordsInSpellCheckerDictionary(); + expect(wordList1).to.eql(['foobar']); + const result2 = ses.removeWordFromSpellCheckerDictionary('foobar'); + expect(result2).to.equal(true); + const wordList2 = await ses.listWordsInSpellCheckerDictionary(); + expect(wordList2).to.have.length(0); + }); + + it('should fail for words not in custom dictionary', () => { + const result2 = ses.removeWordFromSpellCheckerDictionary('foobar'); + expect(result2).to.equal(false); + }); + }); + }); + }); + }; + + generateSpecs('without sandbox', false); + generateSpecs('with sandbox', true); +}); diff --git a/spec-main/types-spec.ts b/spec-main/types-spec.ts index b0de8df6f133a..2ccf650aedc4f 100644 --- a/spec-main/types-spec.ts +++ b/spec-main/types-spec.ts @@ -1,10 +1,11 @@ -import { expect } from 'chai' +import { expect } from 'chai'; describe('bundled @types/node', () => { it('should match the major version of bundled node', () => { - expect(require('../npm/package.json').dependencies).to.have.property('@types/node') - const range = require('../npm/package.json').dependencies['@types/node'] - expect(range).to.match(/^\^.+/, 'should allow any type dep in a major range') - expect(range.slice(1).split('.')[0]).to.equal(process.versions.node.split('.')[0]) - }) -}) + expect(require('../npm/package.json').dependencies).to.have.property('@types/node'); + const range = require('../npm/package.json').dependencies['@types/node']; + expect(range).to.match(/^\^.+/, 'should allow any type dep in a major range'); + // TODO(codebytere): re-enable after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/52594 is merged. + // expect(range.slice(1).split('.')[0]).to.equal(process.versions.node.split('.')[0]); + }); +}); diff --git a/spec-main/version-bump-spec.ts b/spec-main/version-bump-spec.ts index db8b7272d42ba..ef37d871b77d4 100644 --- a/spec-main/version-bump-spec.ts +++ b/spec-main/version-bump-spec.ts @@ -1,7 +1,62 @@ -import { expect } from 'chai' -import { nextVersion } from '../script/release/version-bumper' -import * as utils from '../script/release/version-utils' +import { expect } from 'chai'; +import { GitProcess, IGitExecutionOptions, IGitResult } from 'dugite'; +import { nextVersion, shouldUpdateSupported, updateSupported } from '../script/release/version-bumper'; +import * as utils from '../script/release/version-utils'; +import * as sinon from 'sinon'; import { ifdescribe } from './spec-helpers'; +const { promises: fs } = require('fs'); +const path = require('path'); + +const fixtureDir = path.resolve(__dirname, 'fixtures', 'version-bumper', 'fixture_support.md'); +const readFile = fs.readFile; +const writeFile = fs.writeFile; + +class GitFake { + branches: { + [key: string]: string[], + }; + + constructor () { + this.branches = {}; + } + + setBranch (channel: string): void { + this.branches[channel] = []; + } + + setVersion (channel: string, latestTag: string): void { + const tags = [latestTag]; + if (channel === 'alpha') { + const versionStrs = latestTag.split(`${channel}.`); + const latest = parseInt(versionStrs[1]); + + for (let i = latest; i >= 1; i--) { + tags.push(`${versionStrs[0]}${channel}.${latest - i}`); + } + } + + this.branches[channel] = tags; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + exec (args: string[], path: string, options?: IGitExecutionOptions | undefined): Promise { + let stdout = ''; + const stderr = ''; + const exitCode = 0; + + // handle for promoting from current master HEAD + let branch = 'stable'; + const v = (args[2] === 'HEAD') ? 'stable' : args[3]; + if (v.includes('nightly')) branch = 'nightly'; + if (v.includes('alpha')) branch = 'alpha'; + if (v.includes('beta')) branch = 'beta'; + + if (!this.branches[branch]) this.setBranch(branch); + + stdout = this.branches[branch].join('\n'); + return Promise.resolve({ exitCode, stdout, stderr }); + } +} describe('version-bumper', () => { describe('makeVersion', () => { @@ -10,114 +65,282 @@ describe('version-bumper', () => { major: 2, minor: 0, patch: 0 - } + }; - const version = utils.makeVersion(components, '.') - expect(version).to.equal('2.0.0') - }) + const version = utils.makeVersion(components, '.'); + expect(version).to.equal('2.0.0'); + }); it('makes a version with a period delimeter and a partial pre', () => { const components = { major: 2, minor: 0, patch: 0, - pre: [ 'nightly', 12345678 ] - } + pre: ['nightly', 12345678] + }; - const version = utils.makeVersion(components, '.', utils.preType.PARTIAL) - expect(version).to.equal('2.0.0.12345678') - }) + const version = utils.makeVersion(components, '.', utils.preType.PARTIAL); + expect(version).to.equal('2.0.0.12345678'); + }); it('makes a version with a period delimeter and a full pre', () => { const components = { major: 2, minor: 0, patch: 0, - pre: [ 'nightly', 12345678 ] + pre: ['nightly', 12345678] + }; + + const version = utils.makeVersion(components, '.', utils.preType.FULL); + expect(version).to.equal('2.0.0-nightly.12345678'); + }); + }); + + describe('updateSupported', () => { + let restore: any; + before(async () => { + restore = await readFile(fixtureDir, 'utf8'); + }); + + afterEach(async () => { + await writeFile(fixtureDir, restore, 'utf8'); + }); + + it('updates correctly when a new stable version is promoted from beta', async () => { + const version = '4.0.0'; + const currentVersion = '4.0.0-beta.29'; + if (shouldUpdateSupported('stable', currentVersion, version)) { + await updateSupported(version, fixtureDir); + } + const contents = await readFile(fixtureDir, 'utf8'); + + expect(contents).to.contain('4.x.y\n* 3.x.y\n* 2.x.y'); + }); + + it('should not update when a new stable patch version is promoted', async () => { + const version = '3.0.1'; + const currentVersion = '3.0.0'; + if (shouldUpdateSupported('stable', currentVersion, version)) { + await updateSupported(version, fixtureDir); + } + const contents = await readFile(fixtureDir, 'utf8'); + + expect(contents).to.contain('3.x.y\n* 2.x.y\n* 1.x.y'); + }); + + it('should not update when a new stable minor version is promoted', async () => { + const version = '3.1.0'; + const currentVersion = '3.0.0'; + if (shouldUpdateSupported('minor', currentVersion, version)) { + await updateSupported(version, fixtureDir); + } + const contents = await readFile(fixtureDir, 'utf8'); + + expect(contents).to.contain('3.x.y\n* 2.x.y\n* 1.x.y'); + }); + + it('should not update when a new beta.1 version is promoted', async () => { + const version = '5.0.0-beta.1'; + const currentVersion = '4.0.0-beta.29'; + if (shouldUpdateSupported('beta', currentVersion, version)) { + await updateSupported(version, fixtureDir); + } + const contents = await readFile(fixtureDir, 'utf8'); + + expect(contents).to.contain('3.x.y\n* 2.x.y\n* 1.x.y'); + }); + + it('should not update when a new beta.12 version is promoted', async () => { + const version = '4.0.0-beta.12'; + const currentVersion = '4.0.0-beta.11'; + if (shouldUpdateSupported('beta', currentVersion, version)) { + await updateSupported(version, fixtureDir); } + const contents = await readFile(fixtureDir, 'utf8'); - const version = utils.makeVersion(components, '.', utils.preType.FULL) - expect(version).to.equal('2.0.0-nightly.12345678') - }) - }) + expect(contents).to.contain('3.x.y\n* 2.x.y\n* 1.x.y'); + }); + + it('should update when a new major nightly version is promoted', async () => { + const version = '4.0.0-nightly.19950901'; + const currentVersion = '3.0.0-nightly.19950828'; + if (shouldUpdateSupported('nightly', currentVersion, version)) { + await updateSupported(version, fixtureDir); + } + const contents = await readFile(fixtureDir, 'utf8'); + + expect(contents).to.contain('4.x.y\n* 3.x.y\n* 2.x.y'); + }); + + it('should not update when a new nightly version is promoted', async () => { + const version = '3.0.0-nightly.19950901'; + const currentVersion = '3.0.0-nightly.19950828'; + if (shouldUpdateSupported('nightly', currentVersion, version)) { + await updateSupported(version, fixtureDir); + } + const contents = await readFile(fixtureDir, 'utf8'); + + expect(contents).to.contain('3.x.y\n* 2.x.y\n* 1.x.y'); + }); + }); // On macOS Circle CI we don't have a real git environment due to running // gclient sync on a linux machine. These tests therefore don't run as expected. - ifdescribe(!(process.platform === 'linux' && process.arch === 'arm') && !(isCI && process.platform === 'darwin'))('nextVersion', () => { - const nightlyPattern = /[0-9.]*(-nightly.(\d{4})(\d{2})(\d{2}))$/g - const betaPattern = /[0-9.]*(-beta[0-9.]*)/g - - it('bumps to nightly from stable', async () => { - const version = 'v2.0.0' - const next = await nextVersion('nightly', version) - const matches = next.match(nightlyPattern) - expect(matches).to.have.lengthOf(1) - }) - - it('bumps to nightly from beta', async () => { - const version = 'v2.0.0-beta.1' - const next = await nextVersion('nightly', version) - const matches = next.match(nightlyPattern) - expect(matches).to.have.lengthOf(1) - }) - - it('bumps to nightly from nightly', async () => { - const version = 'v2.0.0-nightly.19950901' - const next = await nextVersion('nightly', version) - const matches = next.match(nightlyPattern) - expect(matches).to.have.lengthOf(1) - }) - - it('throws error when bumping to beta from stable', () => { - const version = 'v2.0.0' - return expect( - nextVersion('beta', version) - ).to.be.rejectedWith('Cannot bump to beta from stable.') - }) - - it('bumps to beta from nightly', async () => { - const version = 'v2.0.0-nightly.19950901' - const next = await nextVersion('beta', version) - const matches = next.match(betaPattern) - expect(matches).to.have.lengthOf(1) - }) - - it('bumps to beta from beta', async () => { - const version = 'v2.0.0-beta.8' - const next = await nextVersion('beta', version) - expect(next).to.equal('2.0.0-beta.9') - }) - - it('bumps to stable from beta', async () => { - const version = 'v2.0.0-beta.1' - const next = await nextVersion('stable', version) - expect(next).to.equal('2.0.0') - }) - - it('bumps to stable from stable', async () => { - const version = 'v2.0.0' - const next = await nextVersion('stable', version) - expect(next).to.equal('2.0.1') - }) - - it('bumps to stable from nightly', async () => { - const version = 'v2.0.0-nightly.19950901' - const next = await nextVersion('stable', version) - expect(next).to.equal('2.0.0') - }) - - it('throws on an invalid version', () => { - const version = 'vI.AM.INVALID' - return expect( - nextVersion('beta', version) - ).to.be.rejectedWith(`Invalid current version: ${version}`) - }) + ifdescribe(!(process.platform === 'linux' && process.arch.indexOf('arm') === 0) && process.platform !== 'darwin')('nextVersion', () => { + describe('bump versions', () => { + const nightlyPattern = /[0-9.]*(-nightly.(\d{4})(\d{2})(\d{2}))$/g; + const betaPattern = /[0-9.]*(-beta[0-9.]*)/g; + + it('bumps to nightly from stable', async () => { + const version = 'v2.0.0'; + const next = await nextVersion('nightly', version); + const matches = next.match(nightlyPattern); + expect(matches).to.have.lengthOf(1); + }); + + it('bumps to nightly from beta', async () => { + const version = 'v2.0.0-beta.1'; + const next = await nextVersion('nightly', version); + const matches = next.match(nightlyPattern); + expect(matches).to.have.lengthOf(1); + }); + + it('bumps to nightly from nightly', async () => { + const version = 'v2.0.0-nightly.19950901'; + const next = await nextVersion('nightly', version); + const matches = next.match(nightlyPattern); + expect(matches).to.have.lengthOf(1); + }); + + it('bumps to a nightly version above our switch from N-0-x to N-x-y branch names', async () => { + const version = 'v2.0.0-nightly.19950901'; + const next = await nextVersion('nightly', version); + // If it starts with v8 then we didn't bump above the 8-x-y branch + expect(next.startsWith('v8')).to.equal(false); + }); + + it('throws error when bumping to beta from stable', () => { + const version = 'v2.0.0'; + return expect( + nextVersion('beta', version) + ).to.be.rejectedWith('Cannot bump to beta from stable.'); + }); + + // TODO ELECTRON 15: Re-enable after Electron 15 alpha has released + it.skip('bumps to beta from nightly', async () => { + const version = 'v2.0.0-nightly.19950901'; + const next = await nextVersion('beta', version); + const matches = next.match(betaPattern); + expect(matches).to.have.lengthOf(1); + }); + + it('bumps to beta from beta', async () => { + const version = 'v2.0.0-beta.8'; + const next = await nextVersion('beta', version); + expect(next).to.equal('2.0.0-beta.9'); + }); - it('throws on an invalid bump type', () => { - const version = 'v2.0.0' + it('bumps to beta from beta if the previous beta is at least beta.10', async () => { + const version = 'v6.0.0-beta.15'; + const next = await nextVersion('beta', version); + expect(next).to.equal('6.0.0-beta.16'); + }); + + it('bumps to stable from beta', async () => { + const version = 'v2.0.0-beta.1'; + const next = await nextVersion('stable', version); + expect(next).to.equal('2.0.0'); + }); + + it('bumps to stable from stable', async () => { + const version = 'v2.0.0'; + const next = await nextVersion('stable', version); + expect(next).to.equal('2.0.1'); + }); + + it('bumps to minor from stable', async () => { + const version = 'v2.0.0'; + const next = await nextVersion('minor', version); + expect(next).to.equal('2.1.0'); + }); + + it('bumps to stable from nightly', async () => { + const version = 'v2.0.0-nightly.19950901'; + const next = await nextVersion('stable', version); + expect(next).to.equal('2.0.0'); + }); + + it('throws on an invalid version', () => { + const version = 'vI.AM.INVALID'; + return expect( + nextVersion('beta', version) + ).to.be.rejectedWith(`Invalid current version: ${version}`); + }); + + it('throws on an invalid bump type', () => { + const version = 'v2.0.0'; + return expect( + nextVersion('WRONG', version) + ).to.be.rejectedWith('Invalid bump type.'); + }); + }); + }); + + // If we don't plan on continuing to support an alpha channel past Electron 15, + // these tests will be removed. Otherwise, integrate into the bump versions tests + describe('bump versions - alpha channel', () => { + const alphaPattern = /[0-9.]*(-alpha[0-9.]*)/g; + const betaPattern = /[0-9.]*(-beta[0-9.]*)/g; + + const sandbox = sinon.createSandbox(); + const gitFake = new GitFake(); + + beforeEach(() => { + const wrapper = (args: string[], path: string, options?: IGitExecutionOptions | undefined) => gitFake.exec(args, path, options); + sandbox.replace(GitProcess, 'exec', wrapper); + }); + + afterEach(() => { + gitFake.branches = {}; + sandbox.restore(); + }); + + it('bumps to alpha from nightly', async () => { + const version = 'v2.0.0-nightly.19950901'; + gitFake.setVersion('nightly', version); + const next = await nextVersion('alpha', version); + const matches = next.match(alphaPattern); + expect(matches).to.have.lengthOf(1); + }); + + it('throws error when bumping to alpha from stable', () => { + const version = 'v2.0.0'; return expect( - nextVersion('WRONG', version) - ).to.be.rejectedWith('Invalid bump type.') - }) - }) -}) \ No newline at end of file + nextVersion('alpha', version) + ).to.be.rejectedWith('Cannot bump to alpha from stable.'); + }); + + it('bumps to alpha from alpha', async () => { + const version = 'v2.0.0-alpha.8'; + gitFake.setVersion('alpha', version); + const next = await nextVersion('alpha', version); + expect(next).to.equal('2.0.0-alpha.9'); + }); + + it('bumps to alpha from alpha if the previous alpha is at least alpha.10', async () => { + const version = 'v6.0.0-alpha.15'; + gitFake.setVersion('alpha', version); + const next = await nextVersion('alpha', version); + expect(next).to.equal('6.0.0-alpha.16'); + }); + + it('bumps to beta from alpha', async () => { + const version = 'v2.0.0-alpha.8'; + gitFake.setVersion('alpha', version); + const next = await nextVersion('beta', version); + const matches = next.match(betaPattern); + expect(matches).to.have.lengthOf(1); + expect(next).to.equal('2.0.0-beta.1'); + }); + }); +}); diff --git a/spec-main/video-helpers.js b/spec-main/video-helpers.js new file mode 100644 index 0000000000000..d5538c8eab15a --- /dev/null +++ b/spec-main/video-helpers.js @@ -0,0 +1,498 @@ +/* +https://github.com/antimatter15/whammy +The MIT License (MIT) + +Copyright (c) 2015 Kevin Kwok + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +function atob (str) { + return Buffer.from(str, 'base64').toString('binary'); +} + +// in this case, frames has a very specific meaning, which will be +// detailed once i finish writing the code + +function ToWebM (frames) { + const info = checkFrames(frames); + + // max duration by cluster in milliseconds + const CLUSTER_MAX_DURATION = 30000; + + const EBML = [ + { + id: 0x1a45dfa3, // EBML + data: [ + { + data: 1, + id: 0x4286 // EBMLVersion + }, + { + data: 1, + id: 0x42f7 // EBMLReadVersion + }, + { + data: 4, + id: 0x42f2 // EBMLMaxIDLength + }, + { + data: 8, + id: 0x42f3 // EBMLMaxSizeLength + }, + { + data: 'webm', + id: 0x4282 // DocType + }, + { + data: 2, + id: 0x4287 // DocTypeVersion + }, + { + data: 2, + id: 0x4285 // DocTypeReadVersion + } + ] + }, + { + id: 0x18538067, // Segment + data: [ + { + id: 0x1549a966, // Info + data: [ + { + data: 1e6, // do things in millisecs (num of nanosecs for duration scale) + id: 0x2ad7b1 // TimecodeScale + }, + { + data: 'whammy', + id: 0x4d80 // MuxingApp + }, + { + data: 'whammy', + id: 0x5741 // WritingApp + }, + { + data: doubleToString(info.duration), + id: 0x4489 // Duration + } + ] + }, + { + id: 0x1654ae6b, // Tracks + data: [ + { + id: 0xae, // TrackEntry + data: [ + { + data: 1, + id: 0xd7 // TrackNumber + }, + { + data: 1, + id: 0x73c5 // TrackUID + }, + { + data: 0, + id: 0x9c // FlagLacing + }, + { + data: 'und', + id: 0x22b59c // Language + }, + { + data: 'V_VP8', + id: 0x86 // CodecID + }, + { + data: 'VP8', + id: 0x258688 // CodecName + }, + { + data: 1, + id: 0x83 // TrackType + }, + { + id: 0xe0, // Video + data: [ + { + data: info.width, + id: 0xb0 // PixelWidth + }, + { + data: info.height, + id: 0xba // PixelHeight + } + ] + } + ] + } + ] + }, + { + id: 0x1c53bb6b, // Cues + data: [ + // cue insertion point + ] + } + + // cluster insertion point + ] + } + ]; + + const segment = EBML[1]; + const cues = segment.data[2]; + + // Generate clusters (max duration) + let frameNumber = 0; + let clusterTimecode = 0; + while (frameNumber < frames.length) { + const cuePoint = { + id: 0xbb, // CuePoint + data: [ + { + data: Math.round(clusterTimecode), + id: 0xb3 // CueTime + }, + { + id: 0xb7, // CueTrackPositions + data: [ + { + data: 1, + id: 0xf7 // CueTrack + }, + { + data: 0, // to be filled in when we know it + size: 8, + id: 0xf1 // CueClusterPosition + } + ] + } + ] + }; + + cues.data.push(cuePoint); + + const clusterFrames = []; + let clusterDuration = 0; + do { + clusterFrames.push(frames[frameNumber]); + clusterDuration += frames[frameNumber].duration; + frameNumber++; + } while (frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION); + + let clusterCounter = 0; + const cluster = { + id: 0x1f43b675, // Cluster + data: [ + { + data: Math.round(clusterTimecode), + id: 0xe7 // Timecode + } + ].concat(clusterFrames.map(function (webp) { + const block = makeSimpleBlock({ + discardable: 0, + frame: webp.data.slice(4), + invisible: 0, + keyframe: 1, + lacing: 0, + trackNum: 1, + timecode: Math.round(clusterCounter) + }); + clusterCounter += webp.duration; + return { + data: block, + id: 0xa3 + }; + })) + }; + + // Add cluster to segment + segment.data.push(cluster); + clusterTimecode += clusterDuration; + } + + // First pass to compute cluster positions + let position = 0; + for (let i = 0; i < segment.data.length; i++) { + if (i >= 3) { + cues.data[i - 3].data[1].data[1].data = position; + } + const data = generateEBML([segment.data[i]]); + position += data.size || data.byteLength || data.length; + if (i !== 2) { // not cues + // Save results to avoid having to encode everything twice + segment.data[i] = data; + } + } + + return generateEBML(EBML); +} + +// sums the lengths of all the frames and gets the duration, woo + +function checkFrames (frames) { + const width = frames[0].width; + const height = frames[0].height; + let duration = frames[0].duration; + for (let i = 1; i < frames.length; i++) { + if (frames[i].width !== width) throw new Error('Frame ' + (i + 1) + ' has a different width'); + if (frames[i].height !== height) throw new Error('Frame ' + (i + 1) + ' has a different height'); + if (frames[i].duration < 0 || frames[i].duration > 0x7fff) throw new Error('Frame ' + (i + 1) + ' has a weird duration (must be between 0 and 32767)'); + duration += frames[i].duration; + } + return { + duration: duration, + width: width, + height: height + }; +} + +function numToBuffer (num) { + const parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); +} + +function numToFixedBuffer (num, size) { + const parts = new Uint8Array(size); + for (let i = size - 1; i >= 0; i--) { + parts[i] = num & 0xff; + num = num >> 8; + } + return parts; +} + +function strToBuffer (str) { + // return new Blob([str]); + + const arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; + // this is slower + // return new Uint8Array(str.split('').map(function(e){ + // return e.charCodeAt(0) + // })) +} + +// sorry this is ugly, and sort of hard to understand exactly why this was done +// at all really, but the reason is that there's some code below that i dont really +// feel like understanding, and this is easier than using my brain. + +function bitsToBuffer (bits) { + const data = []; + const pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (let i = 0; i < bits.length; i += 8) { + data.push(parseInt(bits.substr(i, 8), 2)); + } + return new Uint8Array(data); +} + +function generateEBML (json) { + const ebml = []; + for (let i = 0; i < json.length; i++) { + if (!('id' in json[i])) { + // already encoded blob or byteArray + ebml.push(json[i]); + continue; + } + + let data = json[i].data; + if (typeof data === 'object') data = generateEBML(data); + if (typeof data === 'number') data = ('size' in json[i]) ? numToFixedBuffer(data, json[i].size) : bitsToBuffer(data.toString(2)); + if (typeof data === 'string') data = strToBuffer(data); + + const len = data.size || data.byteLength || data.length; + const zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); + const sizeStr = len.toString(2); + const padded = (new Array((zeroes * 7 + 7 + 1) - sizeStr.length)).join('0') + sizeStr; + const size = (new Array(zeroes)).join('0') + '1' + padded; + + // i actually dont quite understand what went on up there, so I'm not really + // going to fix this, i'm probably just going to write some hacky thing which + // converts that string into a buffer-esque thing + + ebml.push(numToBuffer(json[i].id)); + ebml.push(bitsToBuffer(size)); + ebml.push(data); + } + + // convert ebml to an array + const buffer = toFlatArray(ebml); + return new Uint8Array(buffer); +} + +function toFlatArray (arr, outBuffer) { + if (outBuffer == null) { + outBuffer = []; + } + for (let i = 0; i < arr.length; i++) { + if (typeof arr[i] === 'object') { + // an array + toFlatArray(arr[i], outBuffer); + } else { + // a simple element + outBuffer.push(arr[i]); + } + } + return outBuffer; +} + +function makeSimpleBlock (data) { + let flags = 0; + if (data.keyframe) flags |= 128; + if (data.invisible) flags |= 8; + if (data.lacing) flags |= (data.lacing << 1); + if (data.discardable) flags |= 1; + if (data.trackNum > 127) { + throw new Error('TrackNumber > 127 not supported'); + } + const out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function (e) { + return String.fromCharCode(e); + }).join('') + data.frame; + + return out; +} + +// here's something else taken verbatim from weppy, awesome rite? + +function parseWebP (riff) { + const VP8 = riff.RIFF[0].WEBP[0]; + + const frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header + const c = []; + for (let i = 0; i < 4; i++) c[i] = VP8.charCodeAt(frameStart + 3 + i); + + // the code below is literally copied verbatim from the bitstream spec + let tmp = (c[1] << 8) | c[0]; + const width = tmp & 0x3FFF; + const horizontalScale = tmp >> 14; + tmp = (c[3] << 8) | c[2]; + const height = tmp & 0x3FFF; + const verticalScale = tmp >> 14; + return { + width: width, + height: height, + data: VP8, + riff: riff + }; +} + +// i think i'm going off on a riff by pretending this is some known +// idiom which i'm making a casual and brilliant pun about, but since +// i can't find anything on google which conforms to this idiomatic +// usage, I'm assuming this is just a consequence of some psychotic +// break which makes me make up puns. well, enough riff-raff (aha a +// rescue of sorts), this function was ripped wholesale from weppy + +function parseRIFF (string) { + let offset = 0; + const chunks = {}; + + while (offset < string.length) { + const id = string.substr(offset, 4); + chunks[id] = chunks[id] || []; + if (id === 'RIFF' || id === 'LIST') { + const len = parseInt(string.substr(offset + 4, 4).split('').map(function (i) { + const unpadded = i.charCodeAt(0).toString(2); + return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; + }).join(''), 2); + const data = string.substr(offset + 4 + 4, len); + offset += 4 + 4 + len; + chunks[id].push(parseRIFF(data)); + } else if (id === 'WEBP') { + // Use (offset + 8) to skip past "VP8 "/"VP8L"/"VP8X" field after "WEBP" + chunks[id].push(string.substr(offset + 8)); + offset = string.length; + } else { + // Unknown chunk type; push entire payload + chunks[id].push(string.substr(offset + 4)); + offset = string.length; + } + } + return chunks; +} + +// here's a little utility function that acts as a utility for other functions +// basically, the only purpose is for encoding "Duration", which is encoded as +// a double (considerably more difficult to encode than an integer) +function doubleToString (num) { + return [].slice.call( + new Uint8Array( + ( + new Float64Array([num]) // create a float64 array + ).buffer) // extract the array buffer + , 0) // convert the Uint8Array into a regular array + .map(function (e) { // since it's a regular array, we can now use map + return String.fromCharCode(e); // encode all the bytes individually + }) + .reverse() // correct the byte endianness (assume it's little endian for now) + .join(''); // join the bytes in holy matrimony as a string +} + +function WhammyVideo (speed, quality = 0.8) { // a more abstract-ish API + this.frames = []; + this.duration = 1000 / speed; + this.quality = quality; +} + +/** + * + * @param {string} frame + * @param {number} [duration] + */ +WhammyVideo.prototype.add = function (frame, duration) { + if (typeof duration !== 'undefined' && this.duration) throw new Error("you can't pass a duration if the fps is set"); + if (typeof duration === 'undefined' && !this.duration) throw new Error("if you don't have the fps set, you need to have durations here."); + if (frame.canvas) { // CanvasRenderingContext2D + frame = frame.canvas; + } + if (frame.toDataURL) { + // frame = frame.toDataURL('image/webp', this.quality); + // quickly store image data so we don't block cpu. encode in compile method. + frame = frame.getContext('2d').getImageData(0, 0, frame.width, frame.height); + } else if (typeof frame !== 'string') { + throw new Error('frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string'); + } + if (typeof frame === 'string' && !(/^data:image\/webp;base64,/ig).test(frame)) { + throw new Error('Input must be formatted properly as a base64 encoded DataURI of type image/webp'); + } + this.frames.push({ + image: frame, + duration: duration || this.duration + }); +}; + +WhammyVideo.prototype.compile = function (callback) { + const webm = new ToWebM(this.frames.map(function (frame) { + const webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); + webp.duration = frame.duration; + return webp; + })); + callback(webm); +}; + +export const WebmGenerator = WhammyVideo; diff --git a/spec-main/visibility-state-spec.ts b/spec-main/visibility-state-spec.ts new file mode 100644 index 0000000000000..fc0b692aa2650 --- /dev/null +++ b/spec-main/visibility-state-spec.ts @@ -0,0 +1,184 @@ +import { expect } from 'chai'; +import * as cp from 'child_process'; +import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain } from 'electron/main'; +import * as path from 'path'; + +import { emittedOnce } from './events-helpers'; +import { closeWindow } from './window-helpers'; +import { ifdescribe, delay } from './spec-helpers'; + +// visibilityState specs pass on linux with a real window manager but on CI +// the environment does not let these specs pass +ifdescribe(process.platform !== 'linux')('document.visibilityState', () => { + let w: BrowserWindow; + + afterEach(() => { + return closeWindow(w); + }); + + const load = () => w.loadFile(path.resolve(__dirname, 'fixtures', 'chromium', 'visibilitystate.html')); + + const itWithOptions = (name: string, options: BrowserWindowConstructorOptions, fn: Mocha.Func) => { + return it(name, async function (...args) { + w = new BrowserWindow({ + ...options, + paintWhenInitiallyHidden: false, + webPreferences: { + ...(options.webPreferences || {}), + nodeIntegration: true, + contextIsolation: false + } + }); + await Promise.resolve(fn.apply(this, args)); + }); + }; + + itWithOptions('should be visible when the window is initially shown by default', {}, async () => { + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('visible'); + }); + + itWithOptions('should be visible when the window is initially shown', { + show: true + }, async () => { + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('visible'); + }); + + itWithOptions('should be hidden when the window is initially hidden', { + show: false + }, async () => { + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('hidden'); + }); + + itWithOptions('should be visible when the window is initially hidden but shown before the page is loaded', { + show: false + }, async () => { + w.show(); + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('visible'); + }); + + itWithOptions('should be hidden when the window is initially shown but hidden before the page is loaded', { + show: true + }, async () => { + // TODO(MarshallOfSound): Figure out if we can work around this 1 tick issue for users + if (process.platform === 'darwin') { + // Wait for a tick, the window being "shown" takes 1 tick on macOS + await delay(10000); + } + w.hide(); + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('hidden'); + }); + + itWithOptions('should be toggle between visible and hidden as the window is hidden and shown', {}, async () => { + load(); + const [, initialState] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(initialState).to.equal('visible'); + w.hide(); + await emittedOnce(ipcMain, 'visibility-change-hidden'); + w.show(); + await emittedOnce(ipcMain, 'visibility-change-visible'); + }); + + itWithOptions('should become hidden when a window is minimized', {}, async () => { + load(); + const [, initialState] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(initialState).to.equal('visible'); + w.minimize(); + await emittedOnce(ipcMain, 'visibility-change-hidden', () => w.minimize()); + }); + + itWithOptions('should become visible when a window is restored', {}, async () => { + load(); + const [, initialState] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(initialState).to.equal('visible'); + w.minimize(); + await emittedOnce(ipcMain, 'visibility-change-hidden'); + w.restore(); + await emittedOnce(ipcMain, 'visibility-change-visible'); + }); + + describe('on platforms that support occlusion detection', () => { + let child: cp.ChildProcess; + + before(function () { + if (process.platform !== 'darwin') this.skip(); + }); + + const makeOtherWindow = (opts: { x: number; y: number; width: number; height: number; }) => { + child = cp.spawn(process.execPath, [path.resolve(__dirname, 'fixtures', 'chromium', 'other-window.js'), `${opts.x}`, `${opts.y}`, `${opts.width}`, `${opts.height}`]); + return new Promise(resolve => { + child.stdout!.on('data', (chunk) => { + if (chunk.toString().includes('__ready__')) resolve(); + }); + }); + }; + + afterEach(() => { + if (child && !child.killed) { + child.kill('SIGTERM'); + } + }); + + itWithOptions('should be visible when two windows are on screen', { + x: 0, + y: 0, + width: 200, + height: 200 + }, async () => { + await makeOtherWindow({ + x: 200, + y: 0, + width: 200, + height: 200 + }); + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('visible'); + }); + + itWithOptions('should be visible when two windows are on screen that overlap partially', { + x: 50, + y: 50, + width: 150, + height: 150 + }, async () => { + await makeOtherWindow({ + x: 100, + y: 0, + width: 200, + height: 200 + }); + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('visible'); + }); + + itWithOptions('should be hidden when a second window completely occludes the current window', { + x: 50, + y: 50, + width: 50, + height: 50 + }, async function () { + this.timeout(240000); + load(); + const [, state] = await emittedOnce(ipcMain, 'initial-visibility-state'); + expect(state).to.equal('visible'); + makeOtherWindow({ + x: 0, + y: 0, + width: 300, + height: 300 + }); + await emittedOnce(ipcMain, 'visibility-change-hidden'); + }); + }); +}); diff --git a/spec-main/webview-spec.ts b/spec-main/webview-spec.ts new file mode 100644 index 0000000000000..ce5a64ef4754f --- /dev/null +++ b/spec-main/webview-spec.ts @@ -0,0 +1,849 @@ +import * as path from 'path'; +import * as url from 'url'; +import { BrowserWindow, session, ipcMain, app, WebContents } from 'electron/main'; +import { closeAllWindows } from './window-helpers'; +import { emittedOnce, emittedUntil } from './events-helpers'; +import { ifit, delay } from './spec-helpers'; +import { expect } from 'chai'; + +async function loadWebView (w: WebContents, attributes: Record, openDevTools: boolean = false): Promise { + await w.executeJavaScript(` + new Promise((resolve, reject) => { + const webview = new WebView() + for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) { + webview.setAttribute(k, v) + } + document.body.appendChild(webview) + webview.addEventListener('dom-ready', () => { + if (${openDevTools}) { + webview.openDevTools() + } + }) + webview.addEventListener('did-finish-load', () => { + resolve() + }) + }) + `); +} + +describe(' tag', function () { + const fixtures = path.join(__dirname, '..', 'spec', 'fixtures'); + + afterEach(closeAllWindows); + + function hideChildWindows (e: any, wc: WebContents) { + wc.on('new-window', (event, url, frameName, disposition, options) => { + options.show = false; + }); + } + + before(() => { + app.on('web-contents-created', hideChildWindows); + }); + + after(() => { + app.off('web-contents-created', hideChildWindows); + }); + + it('works without script tag in page', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + contextIsolation: false + } + }); + w.loadFile(path.join(fixtures, 'pages', 'webview-no-script.html')); + await emittedOnce(ipcMain, 'pong'); + }); + + it('works with sandbox', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + sandbox: true + } + }); + w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html')); + await emittedOnce(ipcMain, 'pong'); + }); + + it('works with contextIsolation', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + contextIsolation: true + } + }); + w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html')); + await emittedOnce(ipcMain, 'pong'); + }); + + it('works with contextIsolation + sandbox', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + contextIsolation: true, + sandbox: true + } + }); + w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html')); + await emittedOnce(ipcMain, 'pong'); + }); + + it('works with Trusted Types', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true + } + }); + w.loadFile(path.join(fixtures, 'pages', 'webview-trusted-types.html')); + await emittedOnce(ipcMain, 'pong'); + }); + + it('is disabled by default', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(fixtures, 'module', 'preload-webview.js'), + nodeIntegration: true + } + }); + + const webview = emittedOnce(ipcMain, 'webview'); + w.loadFile(path.join(fixtures, 'pages', 'webview-no-script.html')); + const [, type] = await webview; + + expect(type).to.equal('undefined', 'WebView still exists'); + }); + + // FIXME(deepak1556): Ch69 follow up. + xdescribe('document.visibilityState/hidden', () => { + afterEach(() => { + ipcMain.removeAllListeners('pong'); + }); + + it('updates when the window is shown after the ready-to-show event', async () => { + const w = new BrowserWindow({ show: false }); + const readyToShowSignal = emittedOnce(w, 'ready-to-show'); + const pongSignal1 = emittedOnce(ipcMain, 'pong'); + w.loadFile(path.join(fixtures, 'pages', 'webview-visibilitychange.html')); + await pongSignal1; + const pongSignal2 = emittedOnce(ipcMain, 'pong'); + await readyToShowSignal; + w.show(); + + const [, visibilityState, hidden] = await pongSignal2; + expect(visibilityState).to.equal('visible'); + expect(hidden).to.be.false(); + }); + + it('inherits the parent window visibility state and receives visibilitychange events', async () => { + const w = new BrowserWindow({ show: false }); + w.loadFile(path.join(fixtures, 'pages', 'webview-visibilitychange.html')); + const [, visibilityState, hidden] = await emittedOnce(ipcMain, 'pong'); + expect(visibilityState).to.equal('hidden'); + expect(hidden).to.be.true(); + + // We have to start waiting for the event + // before we ask the webContents to resize. + const getResponse = emittedOnce(ipcMain, 'pong'); + w.webContents.emit('-window-visibility-change', 'visible'); + + return getResponse.then(([, visibilityState, hidden]) => { + expect(visibilityState).to.equal('visible'); + expect(hidden).to.be.false(); + }); + }); + }); + + describe('did-attach-webview event', () => { + it('is emitted when a webview has been attached', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + contextIsolation: false + } + }); + const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview'); + const webviewDomReady = emittedOnce(ipcMain, 'webview-dom-ready'); + w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html')); + + const [, webContents] = await didAttachWebview; + const [, id] = await webviewDomReady; + expect(webContents.id).to.equal(id); + }); + }); + + describe('did-attach event', () => { + it('is emitted when a webview has been attached', async () => { + const w = new BrowserWindow({ + webPreferences: { + webviewTag: true + } + }); + await w.loadURL('about:blank'); + const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => { + const webview = new WebView() + webview.setAttribute('src', 'about:blank') + webview.addEventListener('did-attach', (e) => { + resolve('ok') + }) + document.body.appendChild(webview) + })`); + expect(message).to.equal('ok'); + }); + }); + + describe('did-change-theme-color event', () => { + it('emits when theme color changes', async () => { + const w = new BrowserWindow({ + webPreferences: { + webviewTag: true + } + }); + await w.loadURL('about:blank'); + const src = url.format({ + pathname: `${fixtures.replace(/\\/g, '/')}/pages/theme-color.html`, + protocol: 'file', + slashes: true + }); + const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => { + const webview = new WebView() + webview.setAttribute('src', '${src}') + webview.addEventListener('did-change-theme-color', (e) => { + resolve('ok') + }) + document.body.appendChild(webview) + })`); + expect(message).to.equal('ok'); + }); + }); + + // This test is flaky on WOA, so skip it there. + ifit(process.platform !== 'win32' || process.arch !== 'arm64')('loads devtools extensions registered on the parent window', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + contextIsolation: false + } + }); + w.webContents.session.removeExtension('foo'); + + const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo'); + await w.webContents.session.loadExtension(extensionPath); + + w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'webview-devtools.html')); + loadWebView(w.webContents, { + nodeintegration: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${path.join(__dirname, 'fixtures', 'blank.html')}` + }, true); + let childWebContentsId = 0; + app.once('web-contents-created', (e, webContents) => { + childWebContentsId = webContents.id; + webContents.on('devtools-opened', function () { + const showPanelIntervalId = setInterval(function () { + if (!webContents.isDestroyed() && webContents.devToolsWebContents) { + webContents.devToolsWebContents.executeJavaScript('(' + function () { + const { UI } = (window as any); + const tabs = UI.inspectorView.tabbedPane.tabs; + const lastPanelId: any = tabs[tabs.length - 1].id; + UI.inspectorView.showPanel(lastPanelId); + }.toString() + ')()'); + } else { + clearInterval(showPanelIntervalId); + } + }, 100); + }); + }); + + const [, { runtimeId, tabId }] = await emittedOnce(ipcMain, 'answer'); + expect(runtimeId).to.match(/^[a-z]{32}$/); + expect(tabId).to.equal(childWebContentsId); + }); + + describe('zoom behavior', () => { + const zoomScheme = standardScheme; + const webviewSession = session.fromPartition('webview-temp'); + + before(() => { + const protocol = webviewSession.protocol; + protocol.registerStringProtocol(zoomScheme, (request, callback) => { + callback('hello'); + }); + }); + + after(() => { + const protocol = webviewSession.protocol; + protocol.unregisterProtocol(zoomScheme); + }); + + it('inherits the zoomFactor of the parent window', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + zoomFactor: 1.2, + contextIsolation: false + } + }); + const zoomEventPromise = emittedOnce(ipcMain, 'webview-parent-zoom-level'); + w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-factor.html')); + + const [, zoomFactor, zoomLevel] = await zoomEventPromise; + expect(zoomFactor).to.equal(1.2); + expect(zoomLevel).to.equal(1); + }); + + it('maintains zoom level on navigation', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + zoomFactor: 1.2, + contextIsolation: false + } + }); + const promise = new Promise((resolve) => { + ipcMain.on('webview-zoom-level', (event, zoomLevel, zoomFactor, newHost, final) => { + if (!newHost) { + expect(zoomFactor).to.equal(1.44); + expect(zoomLevel).to.equal(2.0); + } else { + expect(zoomFactor).to.equal(1.2); + expect(zoomLevel).to.equal(1); + } + + if (final) { + resolve(); + } + }); + }); + + w.loadFile(path.join(fixtures, 'pages', 'webview-custom-zoom-level.html')); + + await promise; + }); + + it('maintains zoom level when navigating within same page', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + zoomFactor: 1.2, + contextIsolation: false + } + }); + const promise = new Promise((resolve) => { + ipcMain.on('webview-zoom-in-page', (event, zoomLevel, zoomFactor, final) => { + expect(zoomFactor).to.equal(1.44); + expect(zoomLevel).to.equal(2.0); + + if (final) { + resolve(); + } + }); + }); + + w.loadFile(path.join(fixtures, 'pages', 'webview-in-page-navigate.html')); + + await promise; + }); + + it('inherits zoom level for the origin when available', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + zoomFactor: 1.2, + contextIsolation: false + } + }); + w.loadFile(path.join(fixtures, 'pages', 'webview-origin-zoom-level.html')); + + const [, zoomLevel] = await emittedOnce(ipcMain, 'webview-origin-zoom-level'); + expect(zoomLevel).to.equal(2.0); + }); + + it('does not crash when navigating with zoom level inherited from parent', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + zoomFactor: 1.2, + session: webviewSession, + contextIsolation: false + } + }); + const attachPromise = emittedOnce(w.webContents, 'did-attach-webview'); + const readyPromise = emittedOnce(ipcMain, 'dom-ready'); + w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-inherited.html')); + const [, webview] = await attachPromise; + await readyPromise; + expect(webview.getZoomFactor()).to.equal(1.2); + await w.loadURL(`${zoomScheme}://host1`); + }); + + it('does not crash when changing zoom level after webview is destroyed', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + session: webviewSession, + contextIsolation: false + } + }); + const attachPromise = emittedOnce(w.webContents, 'did-attach-webview'); + await w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-inherited.html')); + await attachPromise; + await w.webContents.executeJavaScript('view.remove()'); + w.webContents.setZoomLevel(0.5); + }); + }); + + describe('requestFullscreen from webview', () => { + const loadWebViewWindow = async () => { + const w = new BrowserWindow({ + webPreferences: { + webviewTag: true, + nodeIntegration: true, + contextIsolation: false + } + }); + const attachPromise = emittedOnce(w.webContents, 'did-attach-webview'); + const readyPromise = emittedOnce(ipcMain, 'webview-ready'); + w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html')); + const [, webview] = await attachPromise; + await readyPromise; + return [w, webview]; + }; + + afterEach(async () => { + // The leaving animation is un-observable but can interfere with future tests + // Specifically this is async on macOS but can be on other platforms too + await delay(1000); + + closeAllWindows(); + }); + + it('should make parent frame element fullscreen too', async () => { + const [w, webview] = await loadWebViewWindow(); + expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false(); + + const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange'); + await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true); + await parentFullscreen; + expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true(); + + w.close(); + await emittedOnce(w, 'closed'); + }); + + // FIXME(zcbenz): Fullscreen events do not work on Linux. + ifit(process.platform !== 'linux')('exiting fullscreen should unfullscreen window', async () => { + const [w, webview] = await loadWebViewWindow(); + const enterFullScreen = emittedOnce(w, 'enter-full-screen'); + await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true); + await enterFullScreen; + + const leaveFullScreen = emittedOnce(w, 'leave-full-screen'); + await webview.executeJavaScript('document.exitFullscreen()', true); + await leaveFullScreen; + await delay(0); + expect(w.isFullScreen()).to.be.false(); + + w.close(); + await emittedOnce(w, 'closed'); + }); + + // Sending ESC via sendInputEvent only works on Windows. + ifit(process.platform === 'win32')('pressing ESC should unfullscreen window', async () => { + const [w, webview] = await loadWebViewWindow(); + const enterFullScreen = emittedOnce(w, 'enter-full-screen'); + await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true); + await enterFullScreen; + + const leaveFullScreen = emittedOnce(w, 'leave-full-screen'); + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' }); + await leaveFullScreen; + await delay(0); + expect(w.isFullScreen()).to.be.false(); + + w.close(); + await emittedOnce(w, 'closed'); + }); + + it('pressing ESC should emit the leave-html-full-screen event', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + nodeIntegration: true, + contextIsolation: false + } + }); + + const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview'); + w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html')); + + const [, webContents] = await didAttachWebview; + + const enterFSWindow = emittedOnce(w, 'enter-html-full-screen'); + const enterFSWebview = emittedOnce(webContents, 'enter-html-full-screen'); + await webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true); + await enterFSWindow; + await enterFSWebview; + + const leaveFSWindow = emittedOnce(w, 'leave-html-full-screen'); + const leaveFSWebview = emittedOnce(webContents, 'leave-html-full-screen'); + webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' }); + await leaveFSWindow; + await leaveFSWebview; + + w.close(); + await emittedOnce(w, 'closed'); + }); + }); + + describe('child windows', () => { + let w: BrowserWindow; + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } }); + await w.loadURL('about:blank'); + }); + afterEach(closeAllWindows); + + it('opens window of about:blank with cross-scripting enabled', async () => { + // Don't wait for loading to finish. + loadWebView(w.webContents, { + allowpopups: 'on', + nodeintegration: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${path.join(fixtures, 'api', 'native-window-open-blank.html')}` + }); + + const [, content] = await emittedOnce(ipcMain, 'answer'); + expect(content).to.equal('Hello'); + }); + + it('opens window of same domain with cross-scripting enabled', async () => { + // Don't wait for loading to finish. + loadWebView(w.webContents, { + allowpopups: 'on', + nodeintegration: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${path.join(fixtures, 'api', 'native-window-open-file.html')}` + }); + + const [, content] = await emittedOnce(ipcMain, 'answer'); + expect(content).to.equal('Hello'); + }); + + it('returns null from window.open when allowpopups is not set', async () => { + // Don't wait for loading to finish. + loadWebView(w.webContents, { + nodeintegration: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${path.join(fixtures, 'api', 'native-window-open-no-allowpopups.html')}` + }); + + const [, { windowOpenReturnedNull }] = await emittedOnce(ipcMain, 'answer'); + expect(windowOpenReturnedNull).to.be.true(); + }); + + it('blocks accessing cross-origin frames', async () => { + // Don't wait for loading to finish. + loadWebView(w.webContents, { + allowpopups: 'on', + nodeintegration: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${path.join(fixtures, 'api', 'native-window-open-cross-origin.html')}` + }); + + const [, content] = await emittedOnce(ipcMain, 'answer'); + const expectedContent = + 'Blocked a frame with origin "file://" from accessing a cross-origin frame.'; + + expect(content).to.equal(expectedContent); + }); + + it('emits a new-window event', async () => { + // Don't wait for loading to finish. + const attributes = { + allowpopups: 'on', + nodeintegration: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${fixtures}/pages/window-open.html` + }; + const { url, frameName } = await w.webContents.executeJavaScript(` + new Promise((resolve, reject) => { + const webview = document.createElement('webview') + for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) { + webview.setAttribute(k, v) + } + document.body.appendChild(webview) + webview.addEventListener('new-window', (e) => { + resolve({url: e.url, frameName: e.frameName}) + }) + }) + `); + + expect(url).to.equal('http://host/'); + expect(frameName).to.equal('host'); + }); + + it('emits a browser-window-created event', async () => { + // Don't wait for loading to finish. + loadWebView(w.webContents, { + allowpopups: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${fixtures}/pages/window-open.html` + }); + + await emittedOnce(app, 'browser-window-created'); + }); + + it('emits a web-contents-created event', async () => { + const webContentsCreated = emittedUntil(app, 'web-contents-created', + (event: Electron.Event, contents: Electron.WebContents) => contents.getType() === 'window'); + + loadWebView(w.webContents, { + allowpopups: 'on', + webpreferences: 'contextIsolation=no', + src: `file://${fixtures}/pages/window-open.html` + }); + + await webContentsCreated; + }); + + it('does not crash when creating window with noopener', async () => { + loadWebView(w.webContents, { + allowpopups: 'on', + src: `file://${path.join(fixtures, 'api', 'native-window-open-noopener.html')}` + }); + await emittedOnce(app, 'browser-window-created'); + }); + }); + + describe('webpreferences attribute', () => { + let w: BrowserWindow; + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true } }); + await w.loadURL('about:blank'); + }); + afterEach(closeAllWindows); + + it('can enable context isolation', async () => { + loadWebView(w.webContents, { + allowpopups: 'yes', + preload: `file://${fixtures}/api/isolated-preload.js`, + src: `file://${fixtures}/api/isolated.html`, + webpreferences: 'contextIsolation=yes' + }); + + const [, data] = await emittedOnce(ipcMain, 'isolated-world'); + expect(data).to.deep.equal({ + preloadContext: { + preloadProperty: 'number', + pageProperty: 'undefined', + typeofRequire: 'function', + typeofProcess: 'object', + typeofArrayPush: 'function', + typeofFunctionApply: 'function', + typeofPreloadExecuteJavaScriptProperty: 'undefined' + }, + pageContext: { + preloadProperty: 'undefined', + pageProperty: 'string', + typeofRequire: 'undefined', + typeofProcess: 'undefined', + typeofArrayPush: 'number', + typeofFunctionApply: 'boolean', + typeofPreloadExecuteJavaScriptProperty: 'number', + typeofOpenedWindow: 'object' + } + }); + }); + }); + + describe('permission request handlers', () => { + let w: BrowserWindow; + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } }); + await w.loadURL('about:blank'); + }); + afterEach(closeAllWindows); + + const partition = 'permissionTest'; + + function setUpRequestHandler (webContentsId: number, requestedPermission: string) { + return new Promise((resolve, reject) => { + session.fromPartition(partition).setPermissionRequestHandler(function (webContents, permission, callback) { + if (webContents.id === webContentsId) { + // requestMIDIAccess with sysex requests both midi and midiSysex so + // grant the first midi one and then reject the midiSysex one + if (requestedPermission === 'midiSysex' && permission === 'midi') { + return callback(true); + } + + try { + expect(permission).to.equal(requestedPermission); + } catch (e) { + return reject(e); + } + callback(false); + resolve(); + } + }); + }); + } + afterEach(() => { + session.fromPartition(partition).setPermissionRequestHandler(null); + }); + + // This is disabled because CI machines don't have cameras or microphones, + // so Chrome responds with "NotFoundError" instead of + // "PermissionDeniedError". It should be re-enabled if we find a way to mock + // the presence of a microphone & camera. + xit('emits when using navigator.getUserMedia api', async () => { + const errorFromRenderer = emittedOnce(ipcMain, 'message'); + loadWebView(w.webContents, { + src: `file://${fixtures}/pages/permissions/media.html`, + partition, + nodeintegration: 'on' + }); + const [, webViewContents] = await emittedOnce(app, 'web-contents-created'); + setUpRequestHandler(webViewContents.id, 'media'); + const [, errorName] = await errorFromRenderer; + expect(errorName).to.equal('PermissionDeniedError'); + }); + + it('emits when using navigator.geolocation api', async () => { + const errorFromRenderer = emittedOnce(ipcMain, 'message'); + loadWebView(w.webContents, { + src: `file://${fixtures}/pages/permissions/geolocation.html`, + partition, + nodeintegration: 'on', + webpreferences: 'contextIsolation=no' + }); + const [, webViewContents] = await emittedOnce(app, 'web-contents-created'); + setUpRequestHandler(webViewContents.id, 'geolocation'); + const [, error] = await errorFromRenderer; + expect(error).to.equal('User denied Geolocation'); + }); + + it('emits when using navigator.requestMIDIAccess without sysex api', async () => { + const errorFromRenderer = emittedOnce(ipcMain, 'message'); + loadWebView(w.webContents, { + src: `file://${fixtures}/pages/permissions/midi.html`, + partition, + nodeintegration: 'on', + webpreferences: 'contextIsolation=no' + }); + const [, webViewContents] = await emittedOnce(app, 'web-contents-created'); + setUpRequestHandler(webViewContents.id, 'midi'); + const [, error] = await errorFromRenderer; + expect(error).to.equal('SecurityError'); + }); + + it('emits when using navigator.requestMIDIAccess with sysex api', async () => { + const errorFromRenderer = emittedOnce(ipcMain, 'message'); + loadWebView(w.webContents, { + src: `file://${fixtures}/pages/permissions/midi-sysex.html`, + partition, + nodeintegration: 'on', + webpreferences: 'contextIsolation=no' + }); + const [, webViewContents] = await emittedOnce(app, 'web-contents-created'); + setUpRequestHandler(webViewContents.id, 'midiSysex'); + const [, error] = await errorFromRenderer; + expect(error).to.equal('SecurityError'); + }); + + it('emits when accessing external protocol', async () => { + loadWebView(w.webContents, { + src: 'magnet:test', + partition + }); + const [, webViewContents] = await emittedOnce(app, 'web-contents-created'); + await setUpRequestHandler(webViewContents.id, 'openExternal'); + }); + + it('emits when using Notification.requestPermission', async () => { + const errorFromRenderer = emittedOnce(ipcMain, 'message'); + loadWebView(w.webContents, { + src: `file://${fixtures}/pages/permissions/notification.html`, + partition, + nodeintegration: 'on', + webpreferences: 'contextIsolation=no' + }); + const [, webViewContents] = await emittedOnce(app, 'web-contents-created'); + + await setUpRequestHandler(webViewContents.id, 'notifications'); + + const [, error] = await errorFromRenderer; + expect(error).to.equal('denied'); + }); + }); + + describe('DOM events', () => { + afterEach(closeAllWindows); + it('receives extra properties on DOM events when contextIsolation is enabled', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + contextIsolation: true + } + }); + await w.loadURL('about:blank'); + const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => { + const webview = new WebView() + webview.setAttribute('src', 'data:text/html,') + webview.addEventListener('console-message', (e) => { + resolve(e.message) + }) + document.body.appendChild(webview) + })`); + expect(message).to.equal('hi'); + }); + + it('emits focus event when contextIsolation is enabled', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + webviewTag: true, + contextIsolation: true + } + }); + await w.loadURL('about:blank'); + await w.webContents.executeJavaScript(`new Promise((resolve, reject) => { + const webview = new WebView() + webview.setAttribute('src', 'about:blank') + webview.addEventListener('dom-ready', () => { + webview.focus() + }) + webview.addEventListener('focus', () => { + resolve(); + }) + document.body.appendChild(webview) + })`); + }); + }); +}); diff --git a/spec-main/window-helpers.ts b/spec-main/window-helpers.ts index 96f8847ed29c3..ddc8d9f5b72c1 100644 --- a/spec-main/window-helpers.ts +++ b/spec-main/window-helpers.ts @@ -1,8 +1,8 @@ -import { expect } from 'chai' -import { BrowserWindow } from 'electron' +import { expect } from 'chai'; +import { BrowserWindow } from 'electron/main'; import { emittedOnce } from './events-helpers'; -async function ensureWindowIsClosed(window: BrowserWindow | null) { +async function ensureWindowIsClosed (window: BrowserWindow | null) { if (window && !window.isDestroyed()) { if (window.webContents && !window.webContents.isDestroyed()) { // If a window isn't destroyed already, and it has non-destroyed WebContents, @@ -10,14 +10,14 @@ async function ensureWindowIsClosed(window: BrowserWindow | null) { // children which need to be destroyed first. In that case, we // await the 'closed' event which signals the complete shutdown of the // window. - const isClosed = emittedOnce(window, 'closed') - window.destroy() - await isClosed + const isClosed = emittedOnce(window, 'closed'); + window.destroy(); + await isClosed; } else { // If there's no WebContents or if the WebContents is already destroyed, // then the 'closed' event has already been emitted so there's nothing to // wait for. - window.destroy() + window.destroy(); } } } @@ -26,22 +26,22 @@ export const closeWindow = async ( window: BrowserWindow | null = null, { assertNotWindows } = { assertNotWindows: true } ) => { - await ensureWindowIsClosed(window) + await ensureWindowIsClosed(window); if (assertNotWindows) { - const windows = BrowserWindow.getAllWindows() + const windows = BrowserWindow.getAllWindows(); try { - expect(windows).to.have.lengthOf(0) + expect(windows).to.have.lengthOf(0); } finally { for (const win of windows) { - await ensureWindowIsClosed(win) + await ensureWindowIsClosed(win); } } } -} +}; -export async function closeAllWindows() { +export async function closeAllWindows () { for (const w of BrowserWindow.getAllWindows()) { - await closeWindow(w, {assertNotWindows: false}) + await closeWindow(w, { assertNotWindows: false }); } } diff --git a/spec-main/yarn.lock b/spec-main/yarn.lock new file mode 100644 index 0000000000000..a6789885b19e2 --- /dev/null +++ b/spec-main/yarn.lock @@ -0,0 +1,992 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@electron-ci/echo@file:./fixtures/native-addon/echo": + version "0.0.1" + +"@electron-ci/uv-dlopen@file:./fixtures/native-addon/uv-dlopen": + version "0.0.1" + +"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938" + integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + +"@types/node@*": + version "13.7.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" + integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== + +"@types/sinon@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + +"@types/ws@^7.2.0": + version "7.2.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.1.tgz#b800f2b8aee694e2b581113643e20d79dd3b8556" + integrity sha512-UEmRNbXFGvfs/sLncf01GuVv6U1mZP3Df0iXWx4kUlikJxbFyFADp95mDn1XDTE2mXpzzoHcKlfFcbytLq4vaA== + dependencies: + "@types/node" "*" + +ajv-keywords@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + +ajv@^6.1.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +busboy@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" + integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== + dependencies: + dicer "0.3.0" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chai-as-promised@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" + integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA== + dependencies: + check-error "^1.0.2" + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + +cheerio@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" + integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4= + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash.assignin "^4.0.9" + lodash.bind "^4.1.4" + lodash.defaults "^4.0.1" + lodash.filter "^4.4.0" + lodash.flatten "^4.2.0" + lodash.foreach "^4.3.0" + lodash.map "^4.4.0" + lodash.merge "^4.4.0" + lodash.pick "^4.2.1" + lodash.reduce "^4.4.0" + lodash.reject "^4.4.0" + lodash.some "^4.4.0" + +chroma-js@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-1.4.1.tgz#eb2d9c4d1ff24616be84b35119f4d26f8205f134" + integrity sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ== + +chroma-js@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.1.2.tgz#1075cb9ae25bcb2017c109394168b5cf3aa500ec" + integrity sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ== + dependencies: + cross-env "^6.0.3" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-env@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941" + integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag== + dependencies: + cross-spawn "^7.0.0" + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +cwise-compiler@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5" + integrity sha1-9NZnQQ6FDToxOn0tt7HlBbsDTMU= + dependencies: + uniq "^1.0.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-uri-to-buffer@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz#18ae979a6a0ca994b0625853916d2662bbae0b1a" + integrity sha1-GK6XmmoMqZSwYlhTkW0mYruuCxo= + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +dicer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" + integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== + dependencies: + streamsearch "0.1.2" + +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dirty-chai@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/dirty-chai/-/dirty-chai-2.0.1.tgz#6b2162ef17f7943589da840abc96e75bda01aff3" + integrity sha512-ys79pWKvDMowIDEPC6Fig8d5THiC0DJ2gmTeGzVAoEH18J8OzLud0Jh7I9IWg3NSk8x2UocznUuFmfHCXYZx9w== + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +get-image-colors@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/get-image-colors/-/get-image-colors-4.0.0.tgz#c8fe161c386b5ae6300d953eac6bccc05a56069d" + integrity sha512-qQZ5vyqgJkQp1c8ZRwKGL03oDsyBBUKiwr4GbB2T4F+tHpfQrw1PjKMQai7jcjRdC2wIHl2rV+6ZuHKttpyk7A== + dependencies: + chroma-js "^2.1.0" + get-pixels "^3.3.2" + get-rgba-palette "^2.0.1" + get-svg-colors "^1.5.1" + pify "^5.0.0" + +get-pixels@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/get-pixels/-/get-pixels-3.3.3.tgz#71e2dfd4befb810b5478a61c6354800976ce01c7" + integrity sha512-5kyGBn90i9tSMUVHTqkgCHsoWoR+/lGbl4yC83Gefyr0HLIhgSWEx/2F/3YgsZ7UpYNuM6pDhDK7zebrUJ5nXg== + dependencies: + data-uri-to-buffer "0.0.3" + jpeg-js "^0.4.1" + mime-types "^2.0.1" + ndarray "^1.0.13" + ndarray-pack "^1.1.1" + node-bitmap "0.0.1" + omggif "^1.0.5" + parse-data-uri "^0.2.0" + pngjs "^3.3.3" + request "^2.44.0" + through "^2.3.4" + +get-rgba-palette@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-rgba-palette/-/get-rgba-palette-2.0.1.tgz#5ce70f75c6ef52882f54dd079e5ed68b5a2323ca" + integrity sha1-XOcPdcbvUogvVN0Hnl7Wi1ojI8o= + dependencies: + quantize "^1.0.1" + +get-svg-colors@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/get-svg-colors/-/get-svg-colors-1.5.1.tgz#59f4004f5fb4fc0b0eaaec36dce004b3b10f188b" + integrity sha512-G3gXrkLrlmv2gqZvs05ap/kcGbchhNtUNaoaP6dIefRcrGPqSa17dGp5ap/2yN8Xs2Wi5mWn16Ww+nFuVU8lTw== + dependencies: + cheerio "^0.22.0" + chroma-js "^1.1.1" + is-svg "^3.0.0" + lodash.compact "^3.0.0" + lodash.uniq "^4.5.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +html-comment-regex@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== + +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inherits@^2.0.1, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +iota-array@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087" + integrity sha1-ge9X/l0FgUzVjCSDYyqZwwoOgIc= + +is-buffer@^1.0.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-svg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== + dependencies: + html-comment-regex "^1.1.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jpeg-js@^0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" + integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +just-extend@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" + integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== + +loader-utils@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +lodash.assignin@^4.0.9: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" + integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= + +lodash.bind@^4.1.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= + +lodash.compact@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.compact/-/lodash.compact-3.0.1.tgz#540ce3837745975807471e16b4a2ba21e7256ca5" + integrity sha1-VAzjg3dFl1gHRx4WtKK6IeclbKU= + +lodash.defaults@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.filter@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" + integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= + +lodash.flatten@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.foreach@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.map@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= + +lodash.merge@^4.4.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.pick@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + +lodash.reduce@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" + integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= + +lodash.reject@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" + integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU= + +lodash.some@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" + integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== + +mime-types@^2.0.1, mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + dependencies: + mime-db "1.51.0" + +minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +ndarray-pack@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ndarray-pack/-/ndarray-pack-1.2.1.tgz#8caebeaaa24d5ecf70ff86020637977da8ee585a" + integrity sha1-jK6+qqJNXs9w/4YCBjeXfajuWFo= + dependencies: + cwise-compiler "^1.1.2" + ndarray "^1.0.13" + +ndarray@^1.0.13: + version "1.0.19" + resolved "https://registry.yarnpkg.com/ndarray/-/ndarray-1.0.19.tgz#6785b5f5dfa58b83e31ae5b2a058cfd1ab3f694e" + integrity sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ== + dependencies: + iota-array "^1.0.0" + is-buffer "^1.0.2" + +nise@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913" + integrity sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + +node-bitmap@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091" + integrity sha1-GA6scAPgxwdhjvMTaPYvhLKmkJE= + +node-ensure@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" + integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc= + +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +omggif@^1.0.5: + version "1.0.10" + resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" + integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== + +parse-data-uri@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/parse-data-uri/-/parse-data-uri-0.2.0.tgz#bf04d851dd5c87b0ab238e5d01ace494b604b4c9" + integrity sha1-vwTYUd1ch7CrI45dAazklLYEtMk= + dependencies: + data-uri-to-buffer "0.0.3" + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +pdfjs-dist@^2.2.228: + version "2.2.228" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.2.228.tgz#777b068a0a16c96418433303807c183058b47aaa" + integrity sha512-W5LhYPMS2UKX0ELIa4u+CFCMoox5qQNQElt0bAK2mwz1V8jZL0rvLao+0tBujce84PK6PvWG36Nwr7agCCWFGQ== + dependencies: + node-ensure "^0.0.0" + worker-loader "^2.0.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + +pngjs@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +quantize@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/quantize/-/quantize-1.0.2.tgz#d25ac200a77b6d70f40127ca171a10e33c8546de" + integrity sha1-0lrCAKd7bXD0ASfKFxoQ4zyFRt4= + +readable-stream@^3.1.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +request@^2.44.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +sinon@^9.0.1: + version "9.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" + integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A== + dependencies: + "@sinonjs/commons" "^1.7.2" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.0.3" + diff "^4.0.2" + nise "^4.0.1" + supports-color "^7.1.0" + +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +through@^2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +uniq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw== + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + +ws@^7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== diff --git a/spec/.eslintrc b/spec/.eslintrc index a26475e27dde3..60be8a0dac951 100644 --- a/spec/.eslintrc +++ b/spec/.eslintrc @@ -2,7 +2,6 @@ "env": { "browser": true, "mocha": true, - "jquery": true, "serviceworker": true }, "globals": { diff --git a/spec/BUILD.gn b/spec/BUILD.gn index 45df11a33e432..8385f6b83565b 100644 --- a/spec/BUILD.gn +++ b/spec/BUILD.gn @@ -66,7 +66,5 @@ group("chromium_unittests") { group("chromium_browsertests") { testonly = true - deps = [ - "//content/test:content_browsertests", - ] + deps = [ "//content/test:content_browsertests" ] } diff --git a/spec/api-browser-window-affinity-spec.js b/spec/api-browser-window-affinity-spec.js deleted file mode 100644 index 04a2d5de4b815..0000000000000 --- a/spec/api-browser-window-affinity-spec.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict' - -const { expect } = require('chai') -const path = require('path') - -const { remote } = require('electron') -const { ipcMain, BrowserWindow } = remote -const { closeWindow } = require('./window-helpers') - -describe('BrowserWindow with affinity module', () => { - const fixtures = path.resolve(__dirname, 'fixtures') - const myAffinityName = 'myAffinity' - const myAffinityNameUpper = 'MYAFFINITY' - const anotherAffinityName = 'anotherAffinity' - - async function createWindowWithWebPrefs (webPrefs) { - const w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: webPrefs || {} - }) - await w.loadFile(path.join(fixtures, 'api', 'blank.html')) - return w - } - - function testAffinityProcessIds (name, webPreferences = {}) { - describe(name, () => { - let mAffinityWindow - before(async () => { - mAffinityWindow = await createWindowWithWebPrefs({ affinity: myAffinityName, ...webPreferences }) - }) - - after(async () => { - await closeWindow(mAffinityWindow, { assertSingleWindow: false }) - mAffinityWindow = null - }) - - it('should have a different process id than a default window', async () => { - const w = await createWindowWithWebPrefs({ ...webPreferences }) - const affinityID = mAffinityWindow.webContents.getOSProcessId() - const wcID = w.webContents.getOSProcessId() - - expect(affinityID).to.not.equal(wcID, 'Should have different OS process IDs') - await closeWindow(w, { assertSingleWindow: false }) - }) - - it(`should have a different process id than a window with a different affinity '${anotherAffinityName}'`, async () => { - const w = await createWindowWithWebPrefs({ affinity: anotherAffinityName, ...webPreferences }) - const affinityID = mAffinityWindow.webContents.getOSProcessId() - const wcID = w.webContents.getOSProcessId() - - expect(affinityID).to.not.equal(wcID, 'Should have different OS process IDs') - await closeWindow(w, { assertSingleWindow: false }) - }) - - it(`should have the same OS process id than a window with the same affinity '${myAffinityName}'`, async () => { - const w = await createWindowWithWebPrefs({ affinity: myAffinityName, ...webPreferences }) - const affinityID = mAffinityWindow.webContents.getOSProcessId() - const wcID = w.webContents.getOSProcessId() - - expect(affinityID).to.equal(wcID, 'Should have the same OS process ID') - await closeWindow(w, { assertSingleWindow: false }) - }) - - it(`should have the same OS process id than a window with an equivalent affinity '${myAffinityNameUpper}' (case insensitive)`, async () => { - const w = await createWindowWithWebPrefs({ affinity: myAffinityNameUpper, ...webPreferences }) - const affinityID = mAffinityWindow.webContents.getOSProcessId() - const wcID = w.webContents.getOSProcessId() - - expect(affinityID).to.equal(wcID, 'Should have the same OS process ID') - await closeWindow(w, { assertSingleWindow: false }) - }) - }) - } - - testAffinityProcessIds(`BrowserWindow with an affinity '${myAffinityName}'`) - testAffinityProcessIds(`BrowserWindow with an affinity '${myAffinityName}' and sandbox enabled`, { sandbox: true }) - testAffinityProcessIds(`BrowserWindow with an affinity '${myAffinityName}' and nativeWindowOpen enabled`, { nativeWindowOpen: true }) - - describe(`BrowserWindow with an affinity : nodeIntegration=false`, () => { - const preload = path.join(fixtures, 'module', 'send-later.js') - const affinityWithNodeTrue = 'affinityWithNodeTrue' - const affinityWithNodeFalse = 'affinityWithNodeFalse' - - function testNodeIntegration (present) { - return new Promise((resolve, reject) => { - ipcMain.once('answer', (event, typeofProcess, typeofBuffer) => { - if (present) { - expect(typeofProcess).to.not.equal('undefined') - expect(typeofBuffer).to.not.equal('undefined') - } else { - expect(typeofProcess).to.equal('undefined') - expect(typeofBuffer).to.equal('undefined') - } - resolve() - }) - }) - } - - it('disables node integration when specified to false', async () => { - const [, w] = await Promise.all([ - testNodeIntegration(false), - createWindowWithWebPrefs({ - affinity: affinityWithNodeTrue, - preload, - nodeIntegration: false - }) - ]) - await closeWindow(w, { assertSingleWindow: false }) - }) - it('disables node integration when first window is false', async () => { - const [, w1] = await Promise.all([ - testNodeIntegration(false), - createWindowWithWebPrefs({ - affinity: affinityWithNodeTrue, - preload, - nodeIntegration: false - }) - ]) - const [, w2] = await Promise.all([ - testNodeIntegration(false), - createWindowWithWebPrefs({ - affinity: affinityWithNodeTrue, - preload, - nodeIntegration: true - }) - ]) - await Promise.all([ - closeWindow(w1, { assertSingleWindow: false }), - closeWindow(w2, { assertSingleWindow: false }) - ]) - }) - - it('enables node integration when specified to true', async () => { - const [, w] = await Promise.all([ - testNodeIntegration(true), - createWindowWithWebPrefs({ - affinity: affinityWithNodeFalse, - preload, - nodeIntegration: true - }) - ]) - await closeWindow(w, { assertSingleWindow: false }) - }) - - it('enables node integration when first window is true', async () => { - const [, w1] = await Promise.all([ - testNodeIntegration(true), - createWindowWithWebPrefs({ - affinity: affinityWithNodeFalse, - preload, - nodeIntegration: true - }) - ]) - const [, w2] = await Promise.all([ - testNodeIntegration(true), - createWindowWithWebPrefs({ - affinity: affinityWithNodeFalse, - preload, - nodeIntegration: false - }) - ]) - await Promise.all([ - closeWindow(w1, { assertSingleWindow: false }), - closeWindow(w2, { assertSingleWindow: false }) - ]) - }) - }) -}) diff --git a/spec/api-clipboard-spec.js b/spec/api-clipboard-spec.js index 5267f3f0a0bcc..fb08eb768f6da 100644 --- a/spec/api-clipboard-spec.js +++ b/spec/api-clipboard-spec.js @@ -1,139 +1,144 @@ -const { expect } = require('chai') -const path = require('path') -const { Buffer } = require('buffer') +const { expect } = require('chai'); +const path = require('path'); +const { Buffer } = require('buffer'); +const { ifdescribe, ifit } = require('./spec-helpers'); -const { clipboard, nativeImage } = require('electron') +const { clipboard, nativeImage } = require('electron'); -describe('clipboard module', () => { - const fixtures = path.resolve(__dirname, 'fixtures') +// FIXME(zcbenz): Clipboard tests are failing on WOA. +ifdescribe(process.platform !== 'win32' || process.arch !== 'arm64')('clipboard module', () => { + const fixtures = path.resolve(__dirname, 'fixtures'); describe('clipboard.readImage()', () => { it('returns NativeImage instance', () => { - const p = path.join(fixtures, 'assets', 'logo.png') - const i = nativeImage.createFromPath(p) - clipboard.writeImage(p) - expect(clipboard.readImage().toDataURL()).to.equal(i.toDataURL()) - }) - }) + const p = path.join(fixtures, 'assets', 'logo.png'); + const i = nativeImage.createFromPath(p); + clipboard.writeImage(p); + const readImage = clipboard.readImage(); + expect(readImage.toDataURL()).to.equal(i.toDataURL()); + }); + }); describe('clipboard.readText()', () => { it('returns unicode string correctly', () => { - const text = '千江有水千江月,万里无云万里天' - clipboard.writeText(text) - expect(clipboard.readText()).to.equal(text) - }) - }) + const text = '千江有水千江月,万里无云万里天'; + clipboard.writeText(text); + expect(clipboard.readText()).to.equal(text); + }); + }); describe('clipboard.readHTML()', () => { it('returns markup correctly', () => { - const text = 'Hi' - const markup = process.platform === 'darwin' ? "Hi" : process.platform === 'linux' ? 'Hi' : 'Hi' - clipboard.writeHTML(text) - expect(clipboard.readHTML()).to.equal(markup) - }) - }) + const text = 'Hi'; + const markup = process.platform === 'darwin' ? "Hi" : 'Hi'; + clipboard.writeHTML(text); + expect(clipboard.readHTML()).to.equal(markup); + }); + }); describe('clipboard.readRTF', () => { it('returns rtf text correctly', () => { - const rtf = '{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard\nThis is some {\\b bold} text.\\par\n}' - clipboard.writeRTF(rtf) - expect(clipboard.readRTF()).to.equal(rtf) - }) - }) - - describe('clipboard.readBookmark', () => { - before(function () { - if (process.platform === 'linux') { - this.skip() - } - }) + const rtf = '{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard\nThis is some {\\b bold} text.\\par\n}'; + clipboard.writeRTF(rtf); + expect(clipboard.readRTF()).to.equal(rtf); + }); + }); + ifdescribe(process.platform !== 'linux')('clipboard.readBookmark', () => { it('returns title and url', () => { - clipboard.writeBookmark('a title', 'https://electronjs.org') - expect(clipboard.readBookmark()).to.deep.equal({ - title: 'a title', - url: 'https://electronjs.org' - }) + clipboard.writeBookmark('a title', 'https://electronjs.org'); + + const readBookmark = clipboard.readBookmark(); + if (process.platform !== 'win32') { + expect(readBookmark.title).to.equal('a title'); + } + expect(clipboard.readBookmark().url).to.equal('https://electronjs.org'); - clipboard.writeText('no bookmark') + clipboard.writeText('no bookmark'); expect(clipboard.readBookmark()).to.deep.equal({ title: '', url: '' - }) - }) - }) + }); + }); + }); + + describe('clipboard.read()', () => { + ifit(process.platform !== 'linux')('does not crash when reading various custom clipboard types', () => { + const type = process.platform === 'darwin' ? 'NSFilenamesPboardType' : 'FileNameW'; + + expect(() => { + const result = clipboard.read(type); + }).to.not.throw(); + }); + it('can read data written with writeBuffer', () => { + const testText = 'Testing read'; + const buffer = Buffer.from(testText, 'utf8'); + clipboard.writeBuffer('public/utf8-plain-text', buffer); + expect(clipboard.read('public/utf8-plain-text')).to.equal(testText); + }); + }); describe('clipboard.write()', () => { it('returns data correctly', () => { - const text = 'test' - const rtf = '{\\rtf1\\utf8 text}' - const p = path.join(fixtures, 'assets', 'logo.png') - const i = nativeImage.createFromPath(p) - const markup = process.platform === 'darwin' ? "Hi" : process.platform === 'linux' ? 'Hi' : 'Hi' - const bookmark = { title: 'a title', url: 'test' } + const text = 'test'; + const rtf = '{\\rtf1\\utf8 text}'; + const p = path.join(fixtures, 'assets', 'logo.png'); + const i = nativeImage.createFromPath(p); + const markup = process.platform === 'darwin' ? "Hi" : 'Hi'; + const bookmark = { title: 'a title', url: 'test' }; clipboard.write({ text: 'test', html: 'Hi', rtf: '{\\rtf1\\utf8 text}', bookmark: 'a title', image: p - }) + }); - expect(clipboard.readText()).to.equal(text) - expect(clipboard.readHTML()).to.equal(markup) - expect(clipboard.readRTF()).to.equal(rtf) - expect(clipboard.readImage().toDataURL()).to.equal(i.toDataURL()) + expect(clipboard.readText()).to.equal(text); + expect(clipboard.readHTML()).to.equal(markup); + expect(clipboard.readRTF()).to.equal(rtf); + const readImage = clipboard.readImage(); + expect(readImage.toDataURL()).to.equal(i.toDataURL()); if (process.platform !== 'linux') { - expect(clipboard.readBookmark()).to.deep.equal(bookmark) + if (process.platform !== 'win32') { + expect(clipboard.readBookmark()).to.deep.equal(bookmark); + } else { + expect(clipboard.readBookmark().url).to.equal(bookmark.url); + } } - }) - }) - - describe('clipboard.read/writeFindText(text)', () => { - before(function () { - if (process.platform !== 'darwin') { - this.skip() - } - }) + }); + }); + ifdescribe(process.platform === 'darwin')('clipboard.read/writeFindText(text)', () => { it('reads and write text to the find pasteboard', () => { - clipboard.writeFindText('find this') - expect(clipboard.readFindText()).to.equal('find this') - }) - }) + clipboard.writeFindText('find this'); + expect(clipboard.readFindText()).to.equal('find this'); + }); + }); - describe('clipboard.writeBuffer(format, buffer)', () => { + describe('clipboard.readBuffer(format)', () => { it('writes a Buffer for the specified format', function () { - if (process.platform !== 'darwin') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - const buffer = Buffer.from('writeBuffer', 'utf8') - clipboard.writeBuffer('public.utf8-plain-text', buffer) - expect(clipboard.readText()).to.equal('writeBuffer') - }) + const buffer = Buffer.from('writeBuffer', 'utf8'); + clipboard.writeBuffer('public/utf8-plain-text', buffer); + expect(buffer.equals(clipboard.readBuffer('public/utf8-plain-text'))).to.equal(true); + }); it('throws an error when a non-Buffer is specified', () => { expect(() => { - clipboard.writeBuffer('public.utf8-plain-text', 'hello') - }).to.throw(/buffer must be a node Buffer/) - }) - }) - - describe('clipboard.readBuffer(format)', () => { - before(function () { - if (process.platform !== 'darwin') { - this.skip() + clipboard.writeBuffer('public/utf8-plain-text', 'hello'); + }).to.throw(/buffer must be a node Buffer/); + }); + + ifit(process.platform !== 'win32')('writes a Buffer using a raw format that is used by native apps', function () { + const message = 'Hello from Electron!'; + const buffer = Buffer.from(message); + let rawFormat = 'text/plain'; + if (process.platform === 'darwin') { + rawFormat = 'public.utf8-plain-text'; } - }) - - it('returns a Buffer of the content for the specified format', () => { - const buffer = Buffer.from('this is binary', 'utf8') - clipboard.writeText(buffer.toString()) - expect(buffer.equals(clipboard.readBuffer('public.utf8-plain-text'))).to.equal(true) - }) - }) -}) + clipboard.writeBuffer(rawFormat, buffer); + expect(clipboard.readText()).to.equal(message); + }); + }); +}); diff --git a/spec/api-crash-reporter-spec.js b/spec/api-crash-reporter-spec.js deleted file mode 100644 index f12d25ed2a6b8..0000000000000 --- a/spec/api-crash-reporter-spec.js +++ /dev/null @@ -1,500 +0,0 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const childProcess = require('child_process') -const fs = require('fs') -const http = require('http') -const multiparty = require('multiparty') -const path = require('path') -const temp = require('temp').track() -const url = require('url') -const { closeWindow } = require('./window-helpers') - -const { remote } = require('electron') -const { app, BrowserWindow, crashReporter } = remote - -const { expect } = chai -chai.use(dirtyChai) - -describe('crashReporter module', () => { - if (process.mas || process.env.DISABLE_CRASH_REPORTER_TESTS) return - - // TODO(alexeykuzmin): [Ch66] Fails. Fix it and enable back. - if (process.platform === 'linux') return - - let originalTempDirectory = null - let tempDirectory = null - const specTimeout = 180000 - - before(() => { - tempDirectory = temp.mkdirSync('electronCrashReporterSpec-') - originalTempDirectory = app.getPath('temp') - app.setPath('temp', tempDirectory) - }) - - after(() => { - app.setPath('temp', originalTempDirectory) - }) - - const fixtures = path.resolve(__dirname, 'fixtures') - const generateSpecs = (description, browserWindowOpts) => { - describe(description, () => { - let w = null - let stopServer = null - - beforeEach(() => { - stopServer = null - w = new BrowserWindow(Object.assign({ show: false }, browserWindowOpts)) - }) - - afterEach(() => closeWindow(w).then(() => { w = null })) - - afterEach((done) => { - if (stopServer != null) { - stopServer(done) - } else { - done() - } - }) - - it('should send minidump when renderer crashes', function (done) { - this.timeout(specTimeout) - - stopServer = startServer({ - callback (port) { - w.loadFile(path.join(fixtures, 'api', 'crash.html'), { query: { port } }) - }, - processType: 'renderer', - done: done - }) - }) - - it('should send minidump when node processes crash', function (done) { - this.timeout(specTimeout) - - stopServer = startServer({ - callback (port) { - const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`) - const version = app.getVersion() - const crashPath = path.join(fixtures, 'module', 'crash.js') - - childProcess.fork(crashPath, [port, version, crashesDir], { silent: true }) - }, - processType: 'node', - done: done - }) - }) - - it('should not send minidump if uploadToServer is false', function (done) { - this.timeout(specTimeout) - - let dumpFile - let crashesDir = crashReporter.getCrashesDirectory() - const existingDumpFiles = new Set() - if (process.platform !== 'linux') { - // crashpad puts the dump files in the "completed" subdirectory - if (process.platform === 'darwin') { - crashesDir = path.join(crashesDir, 'completed') - } else { - crashesDir = path.join(crashesDir, 'reports') - } - crashReporter.setUploadToServer(false) - } - const testDone = (uploaded) => { - if (uploaded) return done(new Error('Uploaded crash report')) - if (process.platform !== 'linux') crashReporter.setUploadToServer(true) - expect(fs.existsSync(dumpFile)).to.be.true() - done() - } - - let pollInterval - const pollDumpFile = () => { - fs.readdir(crashesDir, (err, files) => { - if (err) return - const dumps = files.filter((file) => /\.dmp$/.test(file) && !existingDumpFiles.has(file)) - if (!dumps.length) return - - expect(dumps).to.have.lengthOf(1) - dumpFile = path.join(crashesDir, dumps[0]) - clearInterval(pollInterval) - // dump file should not be deleted when not uploading, so we wait - // 1s and assert it still exists in `testDone` - setTimeout(testDone, 1000) - }) - } - - remote.ipcMain.once('list-existing-dumps', (event) => { - fs.readdir(crashesDir, (err, files) => { - if (!err) { - for (const file of files) { - if (/\.dmp$/.test(file)) { - existingDumpFiles.add(file) - } - } - } - event.returnValue = null // allow the renderer to crash - pollInterval = setInterval(pollDumpFile, 100) - }) - }) - - stopServer = startServer({ - callback (port) { - const crashUrl = url.format({ - protocol: 'file', - pathname: path.join(fixtures, 'api', 'crash.html'), - search: `?port=${port}&skipUpload=1` - }) - w.loadURL(crashUrl) - }, - processType: 'renderer', - done: testDone.bind(null, true) - }) - }) - - it('should send minidump with updated extra parameters when node processes crash', function (done) { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - // TODO(alexeykuzmin): Skip the test instead of marking it as passed. - if (process.env.APPVEYOR === 'True') return done() - this.timeout(specTimeout) - stopServer = startServer({ - callback (port) { - const crashesDir = path.join(app.getPath('temp'), `${process.platform === 'win32' ? 'Zombies' : app.getName()} Crashes`) - const version = app.getVersion() - const crashPath = path.join(fixtures, 'module', 'crash.js') - if (process.platform === 'win32') { - const crashServiceProcess = childProcess.spawn(process.execPath, [ - `--reporter-url=http://127.0.0.1:${port}`, - '--application-name=Zombies', - `--crashes-directory=${crashesDir}` - ], { - env: { - ELECTRON_INTERNAL_CRASH_SERVICE: 1 - }, - detached: true - }) - remote.process.crashServicePid = crashServiceProcess.pid - } - childProcess.fork(crashPath, [port, version, crashesDir], { silent: true }) - }, - processType: 'browser', - done: done, - preAssert: fields => { - expect(String(fields.newExtra)).to.equal('newExtra') - expect(String(fields.removeExtra)).to.equal(undefined) - } - }) - }) - - it('should send minidump with updated extra parameters', function (done) { - this.timeout(specTimeout) - - stopServer = startServer({ - callback (port) { - const crashUrl = url.format({ - protocol: 'file', - pathname: path.join(fixtures, 'api', 'crash-restart.html'), - search: `?port=${port}` - }) - w.loadURL(crashUrl) - }, - processType: 'renderer', - done: done - }) - }) - }) - } - - generateSpecs('without sandbox', { - webPreferences: { - nodeIntegration: true - } - }) - generateSpecs('with sandbox', { - webPreferences: { - sandbox: true, - preload: path.join(fixtures, 'module', 'preload-sandbox.js') - } - }) - generateSpecs('with remote module disabled', { - webPreferences: { - nodeIntegration: true, - enableRemoteModule: false - } - }) - - describe('getProductName', () => { - it('returns the product name if one is specified', () => { - const name = crashReporter.getProductName() - const expectedName = 'Electron Test' - expect(name).to.equal(expectedName) - }) - }) - - describe('start(options)', () => { - it('requires that the companyName and submitURL options be specified', () => { - expect(() => { - crashReporter.start({ companyName: 'Missing submitURL' }) - }).to.throw('submitURL is a required option to crashReporter.start') - expect(() => { - crashReporter.start({ submitURL: 'Missing companyName' }) - }).to.throw('companyName is a required option to crashReporter.start') - }) - it('can be called multiple times', () => { - expect(() => { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }) - - crashReporter.start({ - companyName: 'Umbrella Corporation 2', - submitURL: 'http://127.0.0.1/more-crashes' - }) - }).to.not.throw() - }) - }) - - describe('getCrashesDirectory', () => { - it('correctly returns the directory', () => { - const crashesDir = crashReporter.getCrashesDirectory() - const dir = path.join(app.getPath('temp'), 'Electron Test Crashes') - expect(crashesDir).to.equal(dir) - }) - }) - - describe('getUploadedReports', () => { - it('returns an array of reports', () => { - const reports = crashReporter.getUploadedReports() - expect(reports).to.be.an('array') - }) - }) - - // TODO(alexeykuzmin): This suite should explicitly - // generate several crash reports instead of hoping - // that there will be enough of them already. - describe('getLastCrashReport', () => { - it('correctly returns the most recent report', () => { - const reports = crashReporter.getUploadedReports() - expect(reports).to.be.an('array') - expect(reports).to.have.lengthOf.at.least(2, - 'There are not enough reports for this test') - - const lastReport = crashReporter.getLastCrashReport() - expect(lastReport).to.be.an('object').that.includes.a.key('date') - - // Let's find the newest report. - const { report: newestReport } = reports.reduce((acc, cur) => { - const timestamp = new Date(cur.date).getTime() - return (timestamp > acc.timestamp) - ? { report: cur, timestamp: timestamp } - : acc - }, { timestamp: -Infinity }) - expect(newestReport).to.be.an('object') - - expect(lastReport.date.getTime()).to.be.equal( - newestReport.date.getTime(), - 'Last report is not the newest.') - }) - }) - - describe('getUploadToServer()', () => { - it('throws an error when called from the renderer process', () => { - expect(() => require('electron').crashReporter.getUploadToServer()).to.throw() - }) - it('returns true when uploadToServer is set to true', function () { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: true - }) - expect(crashReporter.getUploadToServer()).to.be.true() - }) - it('returns false when uploadToServer is set to false', function () { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: true - }) - crashReporter.setUploadToServer(false) - expect(crashReporter.getUploadToServer()).to.be.false() - }) - }) - - describe('setUploadToServer(uploadToServer)', () => { - it('throws an error when called from the renderer process', () => { - expect(() => require('electron').crashReporter.setUploadToServer('arg')).to.throw() - }) - it('sets uploadToServer false when called with false', function () { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: true - }) - crashReporter.setUploadToServer(false) - expect(crashReporter.getUploadToServer()).to.be.false() - }) - it('sets uploadToServer true when called with true', function () { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: false - }) - crashReporter.setUploadToServer(true) - expect(crashReporter.getUploadToServer()).to.be.true() - }) - }) - - describe('Parameters', () => { - it('returns all of the current parameters', () => { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }) - - const parameters = crashReporter.getParameters() - expect(parameters).to.be.an('object') - }) - it('adds a parameter to current parameters', function () { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }) - - crashReporter.addExtraParameter('hello', 'world') - expect(crashReporter.getParameters()).to.have.a.property('hello') - }) - it('removes a parameter from current parameters', function () { - if (process.platform === 'linux') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }) - - crashReporter.addExtraParameter('hello', 'world') - expect(crashReporter.getParameters()).to.have.a.property('hello') - - crashReporter.removeExtraParameter('hello') - expect(crashReporter.getParameters()).to.not.have.a.property('hello') - }) - }) -}) - -const waitForCrashReport = () => { - return new Promise((resolve, reject) => { - let times = 0 - const checkForReport = () => { - if (crashReporter.getLastCrashReport() != null) { - resolve() - } else if (times >= 10) { - reject(new Error('No crash report available')) - } else { - times++ - setTimeout(checkForReport, 100) - } - } - checkForReport() - }) -} - -const startServer = ({ callback, processType, done, preAssert, postAssert }) => { - let called = false - const server = http.createServer((req, res) => { - const form = new multiparty.Form() - form.parse(req, (error, fields) => { - if (error) throw error - if (called) return - called = true - expect(String(fields.prod)).to.equal('Electron') - expect(String(fields.ver)).to.equal(process.versions.electron) - expect(String(fields.process_type)).to.equal(processType) - expect(String(fields.platform)).to.equal(process.platform) - expect(String(fields.extra1)).to.equal('extra1') - expect(String(fields.extra2)).to.equal('extra2') - expect(fields.extra3).to.be.undefined() - expect(String(fields._productName)).to.equal('Zombies') - expect(String(fields._companyName)).to.equal('Umbrella Corporation') - expect(String(fields._version)).to.equal(app.getVersion()) - if (preAssert) preAssert(fields) - - const reportId = 'abc-123-def-456-abc-789-abc-123-abcd' - res.end(reportId, () => { - waitForCrashReport().then(() => { - if (postAssert) postAssert(reportId) - expect(crashReporter.getLastCrashReport().id).to.equal(reportId) - expect(crashReporter.getUploadedReports()).to.be.an('array').that.is.not.empty() - expect(crashReporter.getUploadedReports()[0].id).to.equal(reportId) - req.socket.destroy() - done() - }, done) - }) - }) - }) - - const activeConnections = new Set() - server.on('connection', (connection) => { - activeConnections.add(connection) - connection.once('close', () => { - activeConnections.delete(connection) - }) - }) - - let { port } = remote.process - server.listen(port, '127.0.0.1', () => { - port = server.address().port - remote.process.port = port - if (process.platform !== 'linux') { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1:' + port - }) - } - callback(port) - }) - - return function stopServer (done) { - for (const connection of activeConnections) { - connection.destroy() - } - server.close(() => { - done() - }) - } -} diff --git a/spec/api-debugger-spec.js b/spec/api-debugger-spec.js deleted file mode 100644 index f6e4cc9d75ccb..0000000000000 --- a/spec/api-debugger-spec.js +++ /dev/null @@ -1,226 +0,0 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const http = require('http') -const path = require('path') -const { emittedOnce } = require('./events-helpers') -const { closeWindow } = require('./window-helpers') -const { BrowserWindow } = require('electron').remote - -const { expect } = chai -chai.use(dirtyChai) - -describe('debugger module', () => { - const fixtures = path.resolve(__dirname, 'fixtures') - let w = null - - beforeEach(() => { - w = new BrowserWindow({ - show: false, - width: 400, - height: 400 - }) - }) - - afterEach(() => closeWindow(w).then(() => { w = null })) - - describe('debugger.attach', () => { - it('succeeds when devtools is already open', done => { - w.webContents.on('did-finish-load', () => { - w.webContents.openDevTools() - try { - w.webContents.debugger.attach() - } catch (err) { - done(`unexpected error : ${err}`) - } - expect(w.webContents.debugger.isAttached()).to.be.true() - done() - }) - w.webContents.loadFile(path.join(fixtures, 'pages', 'a.html')) - }) - - it('fails when protocol version is not supported', done => { - try { - w.webContents.debugger.attach('2.0') - } catch (err) { - expect(w.webContents.debugger.isAttached()).to.be.false() - done() - } - }) - - it('attaches when no protocol version is specified', done => { - try { - w.webContents.debugger.attach() - } catch (err) { - done(`unexpected error : ${err}`) - } - expect(w.webContents.debugger.isAttached()).to.be.true() - done() - }) - }) - - describe('debugger.detach', () => { - it('fires detach event', (done) => { - w.webContents.debugger.on('detach', (e, reason) => { - expect(reason).to.equal('target closed') - expect(w.webContents.debugger.isAttached()).to.be.false() - done() - }) - - try { - w.webContents.debugger.attach() - } catch (err) { - done(`unexpected error : ${err}`) - } - w.webContents.debugger.detach() - }) - - it('doesn\'t disconnect an active devtools session', done => { - w.webContents.loadURL('about:blank') - try { - w.webContents.debugger.attach() - } catch (err) { - return done(`unexpected error : ${err}`) - } - w.webContents.openDevTools() - w.webContents.once('devtools-opened', () => { - w.webContents.debugger.detach() - }) - w.webContents.debugger.on('detach', (e, reason) => { - expect(w.webContents.debugger.isAttached()).to.be.false() - expect(w.devToolsWebContents.isDestroyed()).to.be.false() - done() - }) - }) - }) - - describe('debugger.sendCommand', () => { - let server - - afterEach(() => { - if (server != null) { - server.close() - server = null - } - }) - - it('returns response', async () => { - w.webContents.loadURL('about:blank') - w.webContents.debugger.attach() - - const params = { 'expression': '4+2' } - const res = await w.webContents.debugger.sendCommand('Runtime.evaluate', params) - - expect(res.wasThrown).to.be.undefined() - expect(res.result.value).to.equal(6) - - w.webContents.debugger.detach() - }) - - it('returns response when devtools is opened', async () => { - w.webContents.loadURL('about:blank') - w.webContents.debugger.attach() - - const opened = emittedOnce(w.webContents, 'devtools-opened') - w.webContents.openDevTools() - await opened - - const params = { 'expression': '4+2' } - const res = await w.webContents.debugger.sendCommand('Runtime.evaluate', params) - - expect(res.wasThrown).to.be.undefined() - expect(res.result.value).to.equal(6) - - w.webContents.debugger.detach() - }) - - it('fires message event', done => { - const url = process.platform !== 'win32' - ? `file://${path.join(fixtures, 'pages', 'a.html')}` - : `file:///${path.join(fixtures, 'pages', 'a.html').replace(/\\/g, '/')}` - w.webContents.loadFile(path.join(fixtures, 'pages', 'a.html')) - - try { - w.webContents.debugger.attach() - } catch (err) { - done(`unexpected error : ${err}`) - } - - w.webContents.debugger.on('message', (e, method, params) => { - if (method === 'Console.messageAdded') { - expect(params.message.level).to.equal('log') - expect(params.message.url).to.equal(url) - expect(params.message.text).to.equal('a') - - w.webContents.debugger.detach() - done() - } - }) - w.webContents.debugger.sendCommand('Console.enable') - }) - - it('returns error message when command fails', async () => { - w.webContents.loadURL('about:blank') - w.webContents.debugger.attach() - - const promise = w.webContents.debugger.sendCommand('Test') - await expect(promise).to.be.eventually.rejectedWith(Error, "'Test' wasn't found") - - w.webContents.debugger.detach() - }) - - it('handles valid unicode characters in message', (done) => { - try { - w.webContents.debugger.attach() - } catch (err) { - done(`unexpected error : ${err}`) - } - - w.webContents.debugger.on('message', (event, method, params) => { - if (method === 'Network.loadingFinished') { - w.webContents.debugger.sendCommand('Network.getResponseBody', { - requestId: params.requestId - }).then(data => { - expect(data.body).to.equal('\u0024') - done() - }) - } - }) - - server = http.createServer((req, res) => { - res.setHeader('Content-Type', 'text/plain; charset=utf-8') - res.end('\u0024') - }) - - server.listen(0, '127.0.0.1', () => { - w.webContents.debugger.sendCommand('Network.enable') - w.loadURL(`http://127.0.0.1:${server.address().port}`) - }) - }) - - it('does not crash for invalid unicode characters in message', (done) => { - try { - w.webContents.debugger.attach() - } catch (err) { - done(`unexpected error : ${err}`) - } - - w.webContents.debugger.on('message', (event, method, params) => { - // loadingFinished indicates that page has been loaded and it did not - // crash because of invalid UTF-8 data - if (method === 'Network.loadingFinished') { - done() - } - }) - - server = http.createServer((req, res) => { - res.setHeader('Content-Type', 'text/plain; charset=utf-8') - res.end('\uFFFF') - }) - - server.listen(0, '127.0.0.1', () => { - w.webContents.debugger.sendCommand('Network.enable') - w.loadURL(`http://127.0.0.1:${server.address().port}`) - }) - }) - }) -}) diff --git a/spec/api-deprecate-spec.js b/spec/api-deprecate-spec.js deleted file mode 100644 index 999080f16bffa..0000000000000 --- a/spec/api-deprecate-spec.js +++ /dev/null @@ -1,237 +0,0 @@ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const { deprecate } = require('electron') - -const { expect } = chai -chai.use(dirtyChai) - -describe('deprecate', () => { - beforeEach(() => { - deprecate.setHandler(null) - process.throwDeprecation = true - }) - - it('allows a deprecation handler function to be specified', () => { - const messages = [] - - deprecate.setHandler(message => { - messages.push(message) - }) - - deprecate.log('this is deprecated') - expect(messages).to.deep.equal(['this is deprecated']) - }) - - it('returns a deprecation handler after one is set', () => { - const messages = [] - - deprecate.setHandler(message => { - messages.push(message) - }) - - deprecate.log('this is deprecated') - expect(deprecate.getHandler()).to.be.a('function') - }) - - it('renames a property', () => { - let msg - deprecate.setHandler(m => { msg = m }) - - const oldProp = 'dingyOldName' - const newProp = 'shinyNewName' - - let value = 0 - const o = { [newProp]: value } - expect(o).to.not.have.a.property(oldProp) - expect(o).to.have.a.property(newProp).that.is.a('number') - - deprecate.renameProperty(o, oldProp, newProp) - o[oldProp] = ++value - - expect(msg).to.be.a('string') - expect(msg).to.include(oldProp) - expect(msg).to.include(newProp) - - expect(o).to.have.a.property(newProp).that.is.equal(value) - expect(o).to.have.a.property(oldProp).that.is.equal(value) - }) - - it('doesn\'t deprecate a property not on an object', () => { - const o = {} - - expect(() => { - deprecate.removeProperty(o, 'iDoNotExist') - }).to.throw(/iDoNotExist/) - }) - - it('deprecates a property of an object', () => { - let msg - deprecate.setHandler(m => { msg = m }) - - const prop = 'itMustGo' - const o = { [prop]: 0 } - - deprecate.removeProperty(o, prop) - - const temp = o[prop] - - expect(temp).to.equal(0) - expect(msg).to.be.a('string') - expect(msg).to.include(prop) - }) - - it('warns exactly once when a function is deprecated with no replacement', () => { - let msg - deprecate.setHandler(m => { msg = m }) - - function oldFn () { return 'hello' } - const deprecatedFn = deprecate.removeFunction(oldFn, 'oldFn') - deprecatedFn() - - expect(msg).to.be.a('string') - expect(msg).to.include('oldFn') - }) - - it('warns exactly once when a function is deprecated with a replacement', () => { - let msg - deprecate.setHandler(m => { msg = m }) - - function oldFn () { return 'hello' } - function newFn () { return 'goodbye' } - const deprecatedFn = deprecate.renameFunction(oldFn, newFn) - deprecatedFn() - - expect(msg).to.be.a('string') - expect(msg).to.include('oldFn') - expect(msg).to.include('newFn') - }) - - it('warns only once per item', () => { - const messages = [] - deprecate.setHandler(message => messages.push(message)) - - const key = 'foo' - const val = 'bar' - const o = { [key]: val } - deprecate.removeProperty(o, key) - - for (let i = 0; i < 3; ++i) { - expect(o[key]).to.equal(val) - expect(messages).to.have.length(1) - } - }) - - it('warns if deprecated property is already set', () => { - let msg - deprecate.setHandler(m => { msg = m }) - - const oldProp = 'dingyOldName' - const newProp = 'shinyNewName' - - const o = { [oldProp]: 0 } - deprecate.renameProperty(o, oldProp, newProp) - - expect(msg).to.be.a('string') - expect(msg).to.include(oldProp) - expect(msg).to.include(newProp) - }) - - it('throws an exception if no deprecation handler is specified', () => { - expect(() => { - deprecate.log('this is deprecated') - }).to.throw(/this is deprecated/) - }) - - it('warns when a function is deprecated in favor of a property', () => { - const warnings = [] - deprecate.setHandler(warning => warnings.push(warning)) - - const newProp = 'newProp' - const mod = { - _oldGetterFn () { return 'getter' }, - _oldSetterFn () { return 'setter' } - } - - deprecate.fnToProperty(mod, 'newProp', '_oldGetterFn', '_oldSetterFn') - - mod['oldGetterFn']() - mod['oldSetterFn']() - - expect(warnings).to.have.lengthOf(2) - - expect(warnings[0]).to.include('oldGetterFn') - expect(warnings[0]).to.include(newProp) - - expect(warnings[1]).to.include('oldSetterFn') - expect(warnings[1]).to.include(newProp) - }) - - describe('promisify', () => { - const expected = 'Hello, world!' - let promiseFunc - let warnings - - const enableCallbackWarnings = () => { - warnings = [] - deprecate.setHandler(warning => warnings.push(warning)) - process.enablePromiseAPIs = true - } - - beforeEach(() => { - deprecate.setHandler(null) - process.throwDeprecation = true - - promiseFunc = param => new Promise((resolve, reject) => resolve(param)) - }) - - it('acts as a pass-through for promise-based invocations', async () => { - enableCallbackWarnings() - promiseFunc = deprecate.promisify(promiseFunc) - - const actual = await promiseFunc(expected) - expect(actual).to.equal(expected) - expect(warnings).to.have.lengthOf(0) - }) - - it('only calls back an error if the callback is called with (err, data)', (done) => { - enableCallbackWarnings() - let erringPromiseFunc = () => new Promise((resolve, reject) => { - reject(new Error('fail')) - }) - erringPromiseFunc = deprecate.promisify(erringPromiseFunc) - - erringPromiseFunc((err, data) => { - expect(data).to.be.an('undefined') - expect(err).to.be.an.instanceOf(Error).with.property('message', 'fail') - erringPromiseFunc(data => { - expect(data).to.not.be.an.instanceOf(Error) - expect(data).to.be.an('undefined') - done() - }) - }) - }) - - it('warns exactly once for callback-based invocations', (done) => { - enableCallbackWarnings() - promiseFunc = deprecate.promisify(promiseFunc) - - let callbackCount = 0 - const invocationCount = 3 - const callback = (actual) => { - expect(actual).to.equal(expected) - expect(warnings).to.have.lengthOf(1) - expect(warnings[0]).to.include('promiseFunc') - callbackCount += 1 - if (callbackCount === invocationCount) { - done() - } - } - - for (let i = 0; i < invocationCount; i += 1) { - promiseFunc(expected, callback) - } - }) - }) -}) diff --git a/spec/api-desktop-capturer-spec.js b/spec/api-desktop-capturer-spec.js deleted file mode 100644 index fb9c1d0cef5cf..0000000000000 --- a/spec/api-desktop-capturer-spec.js +++ /dev/null @@ -1,102 +0,0 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const chaiAsPromised = require('chai-as-promised') -const { desktopCapturer, ipcRenderer, remote } = require('electron') -const { screen } = remote -const features = process.electronBinding('features') -const { emittedOnce } = require('./events-helpers') - -const { expect } = chai -chai.use(dirtyChai) -chai.use(chaiAsPromised) - -const isCI = remote.getGlobal('isCi') - -describe('desktopCapturer', () => { - before(function () { - if (!features.isDesktopCapturerEnabled() || process.arch.indexOf('arm') === 0) { - // It's been disabled during build time. - this.skip() - return - } - - if (isCI && process.platform === 'win32') { - this.skip() - } - }) - - it('should return a non-empty array of sources', async () => { - const sources = await desktopCapturer.getSources({ types: ['window', 'screen'] }) - expect(sources).to.be.an('array').that.is.not.empty() - }) - - it('throws an error for invalid options', async () => { - const promise = desktopCapturer.getSources(['window', 'screen']) - expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options') - }) - - it('does not throw an error when called more than once (regression)', async () => { - const sources1 = await desktopCapturer.getSources({ types: ['window', 'screen'] }) - expect(sources1).to.be.an('array').that.is.not.empty() - - const sources2 = await desktopCapturer.getSources({ types: ['window', 'screen'] }) - expect(sources2).to.be.an('array').that.is.not.empty() - }) - - it('responds to subsequent calls of different options', async () => { - const promise1 = desktopCapturer.getSources({ types: ['window'] }) - expect(promise1).to.not.eventually.be.rejected() - - const promise2 = desktopCapturer.getSources({ types: ['screen'] }) - expect(promise2).to.not.eventually.be.rejected() - }) - - it('returns an empty display_id for window sources on Windows and Mac', async () => { - // Linux doesn't return any window sources. - if (process.platform !== 'win32' && process.platform !== 'darwin') return - - const { BrowserWindow } = remote - const w = new BrowserWindow({ width: 200, height: 200 }) - - const sources = await desktopCapturer.getSources({ types: ['window'] }) - w.destroy() - expect(sources).to.be.an('array').that.is.not.empty() - for (const { display_id: displayId } of sources) { - expect(displayId).to.be.a('string').and.be.empty() - } - }) - - it('returns display_ids matching the Screen API on Windows and Mac', async () => { - if (process.platform !== 'win32' && process.platform !== 'darwin') return - - const displays = screen.getAllDisplays() - const sources = await desktopCapturer.getSources({ types: ['screen'] }) - expect(sources).to.be.an('array').of.length(displays.length) - - for (let i = 0; i < sources.length; i++) { - expect(sources[i].display_id).to.equal(displays[i].id.toString()) - } - - it('returns empty sources when blocked', async () => { - ipcRenderer.send('handle-next-desktop-capturer-get-sources') - const sources = await desktopCapturer.getSources({ types: ['screen'] }) - expect(sources).to.be.empty() - }) - }) - - it('disabling thumbnail should return empty images', async () => { - const { BrowserWindow } = remote - const w = new BrowserWindow({ show: false, width: 200, height: 200 }) - const wShown = emittedOnce(w, 'show') - w.show() - await wShown - - const sources = await desktopCapturer.getSources({ types: ['window', 'screen'], thumbnailSize: { width: 0, height: 0 } }) - w.destroy() - expect(sources).to.be.an('array').that.is.not.empty() - for (const { thumbnail: thumbnailImage } of sources) { - expect(thumbnailImage).to.be.a('NativeImage') - expect(thumbnailImage.isEmpty()).to.be.true() - } - }) -}) diff --git a/spec/api-ipc-renderer-spec.js b/spec/api-ipc-renderer-spec.js deleted file mode 100644 index 475a0ac0cb265..0000000000000 --- a/spec/api-ipc-renderer-spec.js +++ /dev/null @@ -1,239 +0,0 @@ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const http = require('http') -const path = require('path') -const { closeWindow } = require('./window-helpers') - -const { expect } = chai -chai.use(dirtyChai) - -const { ipcRenderer, remote } = require('electron') -const { ipcMain, webContents, BrowserWindow } = remote - -describe('ipc renderer module', () => { - const fixtures = path.join(__dirname, 'fixtures') - - let w = null - - afterEach(() => closeWindow(w).then(() => { w = null })) - - describe('ipc.sender.send', () => { - it('should work when sending an object containing id property', done => { - const obj = { - id: 1, - name: 'ly' - } - ipcRenderer.once('message', (event, message) => { - expect(message).to.deep.equal(obj) - done() - }) - ipcRenderer.send('message', obj) - }) - - it('can send instances of Date', done => { - const currentDate = new Date() - ipcRenderer.once('message', (event, value) => { - expect(value).to.equal(currentDate.toISOString()) - done() - }) - ipcRenderer.send('message', currentDate) - }) - - it('can send instances of Buffer', done => { - const buffer = Buffer.from('hello') - ipcRenderer.once('message', (event, message) => { - expect(buffer.equals(message)).to.be.true() - done() - }) - ipcRenderer.send('message', buffer) - }) - - it('can send objects with DOM class prototypes', done => { - ipcRenderer.once('message', (event, value) => { - expect(value.protocol).to.equal('file:') - expect(value.hostname).to.equal('') - done() - }) - ipcRenderer.send('message', document.location) - }) - - it('can send Electron API objects', done => { - const webContents = remote.getCurrentWebContents() - ipcRenderer.once('message', (event, value) => { - expect(value.browserWindowOptions).to.deep.equal(webContents.browserWindowOptions) - done() - }) - ipcRenderer.send('message', webContents) - }) - - it('does not crash on external objects (regression)', done => { - const request = http.request({ port: 5000, hostname: '127.0.0.1', method: 'GET', path: '/' }) - const stream = request.agent.sockets['127.0.0.1:5000:'][0]._handle._externalStream - request.on('error', () => {}) - ipcRenderer.once('message', (event, requestValue, externalStreamValue) => { - expect(requestValue.method).to.equal('GET') - expect(requestValue.path).to.equal('/') - expect(externalStreamValue).to.be.null() - done() - }) - - ipcRenderer.send('message', request, stream) - }) - - it('can send objects that both reference the same object', done => { - const child = { hello: 'world' } - const foo = { name: 'foo', child: child } - const bar = { name: 'bar', child: child } - const array = [foo, bar] - - ipcRenderer.once('message', (event, arrayValue, fooValue, barValue, childValue) => { - expect(arrayValue).to.deep.equal(array) - expect(fooValue).to.deep.equal(foo) - expect(barValue).to.deep.equal(bar) - expect(childValue).to.deep.equal(child) - done() - }) - ipcRenderer.send('message', array, foo, bar, child) - }) - - it('inserts null for cyclic references', done => { - const array = [5] - array.push(array) - - const child = { hello: 'world' } - child.child = child - - ipcRenderer.once('message', (event, arrayValue, childValue) => { - expect(arrayValue[0]).to.equal(5) - expect(arrayValue[1]).to.be.null() - - expect(childValue.hello).to.equal('world') - expect(childValue.child).to.be.null() - - done() - }) - ipcRenderer.send('message', array, child) - }) - }) - - describe('ipc.sendSync', () => { - afterEach(() => { - ipcMain.removeAllListeners('send-sync-message') - }) - - it('can be replied by setting event.returnValue', () => { - const msg = ipcRenderer.sendSync('echo', 'test') - expect(msg).to.equal('test') - }) - }) - - describe('ipcRenderer.sendTo', () => { - let contents = null - - afterEach(() => { - ipcRenderer.removeAllListeners('pong') - contents.destroy() - contents = null - }) - - const generateSpecs = (description, webPreferences) => { - describe(description, () => { - it('sends message to WebContents', done => { - contents = webContents.create({ - preload: path.join(fixtures, 'module', 'preload-ipc-ping-pong.js'), - ...webPreferences - }) - - const payload = 'Hello World!' - - ipcRenderer.once('pong', (event, data) => { - expect(payload).to.equal(data) - done() - }) - - contents.once('did-finish-load', () => { - ipcRenderer.sendTo(contents.id, 'ping', payload) - }) - - contents.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - }) - - it('sends message to WebContents (channel has special chars)', done => { - contents = webContents.create({ - preload: path.join(fixtures, 'module', 'preload-ipc-ping-pong.js'), - ...webPreferences - }) - - const payload = 'Hello World!' - - ipcRenderer.once('pong-æøåü', (event, data) => { - expect(payload).to.equal(data) - done() - }) - - contents.once('did-finish-load', () => { - ipcRenderer.sendTo(contents.id, 'ping-æøåü', payload) - }) - - contents.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - }) - }) - } - - generateSpecs('without sandbox', {}) - generateSpecs('with sandbox', { sandbox: true }) - generateSpecs('with contextIsolation', { contextIsolation: true }) - generateSpecs('with contextIsolation + sandbox', { contextIsolation: true, sandbox: true }) - }) - - describe('remote listeners', () => { - it('detaches listeners subscribed to destroyed renderers, and shows a warning', (done) => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }) - - w.webContents.once('did-finish-load', () => { - w.webContents.once('did-finish-load', () => { - const expectedMessage = [ - 'Attempting to call a function in a renderer window that has been closed or released.', - 'Function provided here: remote-event-handler.html:11:33', - 'Remote event names: remote-handler, other-remote-handler' - ].join('\n') - - const results = ipcRenderer.sendSync('try-emit-web-contents-event', w.webContents.id, 'remote-handler') - - expect(results).to.deep.equal({ - warningMessage: expectedMessage, - listenerCountBefore: 2, - listenerCountAfter: 1 - }) - done() - }) - - w.webContents.reload() - }) - w.loadFile(path.join(fixtures, 'api', 'remote-event-handler.html')) - }) - }) - - describe('ipcRenderer.on', () => { - it('is not used for internals', async () => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }) - await w.loadURL('about:blank') - - const script = `require('electron').ipcRenderer.eventNames()` - const result = await w.webContents.executeJavaScript(script) - expect(result).to.deep.equal([]) - }) - }) -}) diff --git a/spec/api-native-image-spec.js b/spec/api-native-image-spec.js index d7729c4a9bb23..715655f15fd2a 100644 --- a/spec/api-native-image-spec.js +++ b/spec/api-native-image-spec.js @@ -1,18 +1,15 @@ -'use strict' +'use strict'; -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const { nativeImage } = require('electron') -const path = require('path') - -const { expect } = chai -chai.use(dirtyChai) +const { expect } = require('chai'); +const { nativeImage } = require('electron'); +const { ifdescribe, ifit } = require('./spec-helpers'); +const path = require('path'); describe('nativeImage module', () => { const ImageFormat = { PNG: 'png', JPEG: 'jpeg' - } + }; const images = [ { @@ -50,7 +47,7 @@ describe('nativeImage module', () => { height: 3, width: 3 } - ] + ]; /** * @param {?string} filename @@ -58,8 +55,8 @@ describe('nativeImage module', () => { */ const getImagePathFromFilename = (filename) => { return (filename === null) ? null - : path.join(__dirname, 'fixtures', 'assets', filename) - } + : path.join(__dirname, 'fixtures', 'assets', filename); + }; /** * @param {!Object} image @@ -68,12 +65,12 @@ describe('nativeImage module', () => { */ const imageMatchesTheFilters = (image, filters = null) => { if (filters === null) { - return true + return true; } return Object.entries(filters) - .every(([key, value]) => image[key] === value) - } + .every(([key, value]) => image[key] === value); + }; /** * @param {!Object} filters @@ -81,362 +78,342 @@ describe('nativeImage module', () => { */ const getImages = (filters) => { const matchingImages = images - .filter(i => imageMatchesTheFilters(i, filters)) + .filter(i => imageMatchesTheFilters(i, filters)); // Add `.path` property to every image. matchingImages - .forEach(i => { i.path = getImagePathFromFilename(i.filename) }) + .forEach(i => { i.path = getImagePathFromFilename(i.filename); }); - return matchingImages - } + return matchingImages; + }; /** * @param {!Object} filters * @returns {Object} A matching image if any. */ const getImage = (filters) => { - const matchingImages = getImages(filters) + const matchingImages = getImages(filters); - let matchingImage = null + let matchingImage = null; if (matchingImages.length > 0) { - matchingImage = matchingImages[0] + matchingImage = matchingImages[0]; } - return matchingImage - } + return matchingImage; + }; + + ifdescribe(process.platform === 'darwin')('isMacTemplateImage state', () => { + describe('with properties', () => { + it('correctly recognizes a template image', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + expect(image.isMacTemplateImage).to.be.false(); - describe('isMacTemplateImage property', () => { - before(function () { - if (process.platform !== 'darwin') this.skip() - }) + const templateImage = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo_Template.png')); + expect(templateImage.isMacTemplateImage).to.be.true(); + }); - it('returns whether the image is a template image', () => { - const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + it('sets a template image', function () { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + expect(image.isMacTemplateImage).to.be.false(); - expect(image.isMacTemplateImage).to.be.a('boolean') + image.isMacTemplateImage = true; + expect(image.isMacTemplateImage).to.be.true(); + }); + }); - expect(image.isTemplateImage).to.be.a('function') - expect(image.setTemplateImage).to.be.a('function') - }) + describe('with functions', () => { + it('correctly recognizes a template image', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + expect(image.isTemplateImage()).to.be.false(); - it('correctly recognizes a template image', () => { - const templateImage = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo_Template.png')) - expect(templateImage.isMacTemplateImage).to.be.true() - }) + const templateImage = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo_Template.png')); + expect(templateImage.isTemplateImage()).to.be.true(); + }); - it('sets a template image', function () { - const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) - expect(image.isMacTemplateImage).to.be.false() + it('sets a template image', function () { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + expect(image.isTemplateImage()).to.be.false(); - image.isMacTemplateImage = true - expect(image.isMacTemplateImage).to.be.true() - }) - }) + image.setTemplateImage(true); + expect(image.isTemplateImage()).to.be.true(); + }); + }); + }); describe('createEmpty()', () => { it('returns an empty image', () => { - const empty = nativeImage.createEmpty() - expect(empty.isEmpty()).to.be.true() - expect(empty.getAspectRatio()).to.equal(1) - expect(empty.toDataURL()).to.equal('data:image/png;base64,') - expect(empty.toDataURL({ scaleFactor: 2.0 })).to.equal('data:image/png;base64,') - expect(empty.getSize()).to.deep.equal({ width: 0, height: 0 }) - expect(empty.getBitmap()).to.be.empty() - expect(empty.getBitmap({ scaleFactor: 2.0 })).to.be.empty() - expect(empty.toBitmap()).to.be.empty() - expect(empty.toBitmap({ scaleFactor: 2.0 })).to.be.empty() - expect(empty.toJPEG(100)).to.be.empty() - expect(empty.toPNG()).to.be.empty() - expect(empty.toPNG({ scaleFactor: 2.0 })).to.be.empty() + const empty = nativeImage.createEmpty(); + expect(empty.isEmpty()).to.be.true(); + expect(empty.getAspectRatio()).to.equal(1); + expect(empty.toDataURL()).to.equal('data:image/png;base64,'); + expect(empty.toDataURL({ scaleFactor: 2.0 })).to.equal('data:image/png;base64,'); + expect(empty.getSize()).to.deep.equal({ width: 0, height: 0 }); + expect(empty.getBitmap()).to.be.empty(); + expect(empty.getBitmap({ scaleFactor: 2.0 })).to.be.empty(); + expect(empty.toBitmap()).to.be.empty(); + expect(empty.toBitmap({ scaleFactor: 2.0 })).to.be.empty(); + expect(empty.toJPEG(100)).to.be.empty(); + expect(empty.toPNG()).to.be.empty(); + expect(empty.toPNG({ scaleFactor: 2.0 })).to.be.empty(); if (process.platform === 'darwin') { - expect(empty.getNativeHandle()).to.be.empty() + expect(empty.getNativeHandle()).to.be.empty(); } - }) - }) + }); + }); describe('createFromBitmap(buffer, options)', () => { it('returns an empty image when the buffer is empty', () => { - expect(nativeImage.createFromBitmap(Buffer.from([]), { width: 0, height: 0 }).isEmpty()).to.be.true() - }) + expect(nativeImage.createFromBitmap(Buffer.from([]), { width: 0, height: 0 }).isEmpty()).to.be.true(); + }); it('returns an image created from the given buffer', () => { - const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); - const imageB = nativeImage.createFromBitmap(imageA.toBitmap(), imageA.getSize()) - expect(imageB.getSize()).to.deep.equal({ width: 538, height: 190 }) + const imageB = nativeImage.createFromBitmap(imageA.toBitmap(), imageA.getSize()); + expect(imageB.getSize()).to.deep.equal({ width: 538, height: 190 }); - const imageC = nativeImage.createFromBuffer(imageA.toBitmap(), { ...imageA.getSize(), scaleFactor: 2.0 }) - expect(imageC.getSize()).to.deep.equal({ width: 269, height: 95 }) - }) + const imageC = nativeImage.createFromBuffer(imageA.toBitmap(), { ...imageA.getSize(), scaleFactor: 2.0 }); + expect(imageC.getSize()).to.deep.equal({ width: 269, height: 95 }); + }); it('throws on invalid arguments', () => { - expect(() => nativeImage.createFromBitmap(null, {})).to.throw('buffer must be a node Buffer') - expect(() => nativeImage.createFromBitmap([12, 14, 124, 12], {})).to.throw('buffer must be a node Buffer') - expect(() => nativeImage.createFromBitmap(Buffer.from([]), {})).to.throw('width is required') - expect(() => nativeImage.createFromBitmap(Buffer.from([]), { width: 1 })).to.throw('height is required') - expect(() => nativeImage.createFromBitmap(Buffer.from([]), { width: 1, height: 1 })).to.throw('invalid buffer size') - }) - }) + expect(() => nativeImage.createFromBitmap(null, {})).to.throw('buffer must be a node Buffer'); + expect(() => nativeImage.createFromBitmap([12, 14, 124, 12], {})).to.throw('buffer must be a node Buffer'); + expect(() => nativeImage.createFromBitmap(Buffer.from([]), {})).to.throw('width is required'); + expect(() => nativeImage.createFromBitmap(Buffer.from([]), { width: 1 })).to.throw('height is required'); + expect(() => nativeImage.createFromBitmap(Buffer.from([]), { width: 1, height: 1 })).to.throw('invalid buffer size'); + }); + }); describe('createFromBuffer(buffer, options)', () => { it('returns an empty image when the buffer is empty', () => { - expect(nativeImage.createFromBuffer(Buffer.from([])).isEmpty()).to.be.true() - }) + expect(nativeImage.createFromBuffer(Buffer.from([])).isEmpty()).to.be.true(); + }); it('returns an empty image when the buffer is too small', () => { - const image = nativeImage.createFromBuffer(Buffer.from([1, 2, 3, 4]), { width: 100, height: 100 }) - expect(image.isEmpty()).to.be.true() - }) + const image = nativeImage.createFromBuffer(Buffer.from([1, 2, 3, 4]), { width: 100, height: 100 }); + expect(image.isEmpty()).to.be.true(); + }); it('returns an image created from the given buffer', () => { - const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); - const imageB = nativeImage.createFromBuffer(imageA.toPNG()) - expect(imageB.getSize()).to.deep.equal({ width: 538, height: 190 }) - expect(imageA.toBitmap().equals(imageB.toBitmap())).to.be.true() + const imageB = nativeImage.createFromBuffer(imageA.toPNG()); + expect(imageB.getSize()).to.deep.equal({ width: 538, height: 190 }); + expect(imageA.toBitmap().equals(imageB.toBitmap())).to.be.true(); - const imageC = nativeImage.createFromBuffer(imageA.toJPEG(100)) - expect(imageC.getSize()).to.deep.equal({ width: 538, height: 190 }) + const imageC = nativeImage.createFromBuffer(imageA.toJPEG(100)); + expect(imageC.getSize()).to.deep.equal({ width: 538, height: 190 }); const imageD = nativeImage.createFromBuffer(imageA.toBitmap(), - { width: 538, height: 190 }) - expect(imageD.getSize()).to.deep.equal({ width: 538, height: 190 }) + { width: 538, height: 190 }); + expect(imageD.getSize()).to.deep.equal({ width: 538, height: 190 }); const imageE = nativeImage.createFromBuffer(imageA.toBitmap(), - { width: 100, height: 200 }) - expect(imageE.getSize()).to.deep.equal({ width: 100, height: 200 }) + { width: 100, height: 200 }); + expect(imageE.getSize()).to.deep.equal({ width: 100, height: 200 }); - const imageF = nativeImage.createFromBuffer(imageA.toBitmap()) - expect(imageF.isEmpty()).to.be.true() + const imageF = nativeImage.createFromBuffer(imageA.toBitmap()); + expect(imageF.isEmpty()).to.be.true(); const imageG = nativeImage.createFromBuffer(imageA.toPNG(), - { width: 100, height: 200 }) - expect(imageG.getSize()).to.deep.equal({ width: 538, height: 190 }) + { width: 100, height: 200 }); + expect(imageG.getSize()).to.deep.equal({ width: 538, height: 190 }); const imageH = nativeImage.createFromBuffer(imageA.toJPEG(100), - { width: 100, height: 200 }) - expect(imageH.getSize()).to.deep.equal({ width: 538, height: 190 }) + { width: 100, height: 200 }); + expect(imageH.getSize()).to.deep.equal({ width: 538, height: 190 }); const imageI = nativeImage.createFromBuffer(imageA.toBitmap(), - { width: 538, height: 190, scaleFactor: 2.0 }) - expect(imageI.getSize()).to.deep.equal({ width: 269, height: 95 }) - }) + { width: 538, height: 190, scaleFactor: 2.0 }); + expect(imageI.getSize()).to.deep.equal({ width: 269, height: 95 }); + }); it('throws on invalid arguments', () => { - expect(() => nativeImage.createFromBuffer(null)).to.throw('buffer must be a node Buffer') - expect(() => nativeImage.createFromBuffer([12, 14, 124, 12])).to.throw('buffer must be a node Buffer') - }) - }) + expect(() => nativeImage.createFromBuffer(null)).to.throw('buffer must be a node Buffer'); + expect(() => nativeImage.createFromBuffer([12, 14, 124, 12])).to.throw('buffer must be a node Buffer'); + }); + }); describe('createFromDataURL(dataURL)', () => { it('returns an empty image from the empty string', () => { - expect(nativeImage.createFromDataURL('').isEmpty()).to.be.true() - }) + expect(nativeImage.createFromDataURL('').isEmpty()).to.be.true(); + }); it('returns an image created from the given string', () => { - const imagesData = getImages({ hasDataUrl: true }) + const imagesData = getImages({ hasDataUrl: true }); for (const imageData of imagesData) { - const imageFromPath = nativeImage.createFromPath(imageData.path) - const imageFromDataUrl = nativeImage.createFromDataURL(imageData.dataUrl) + const imageFromPath = nativeImage.createFromPath(imageData.path); + const imageFromDataUrl = nativeImage.createFromDataURL(imageData.dataUrl); - expect(imageFromDataUrl.isEmpty()).to.be.false() - expect(imageFromDataUrl.getSize()).to.deep.equal(imageFromPath.getSize()) + expect(imageFromDataUrl.isEmpty()).to.be.false(); + expect(imageFromDataUrl.getSize()).to.deep.equal(imageFromPath.getSize()); expect(imageFromDataUrl.toBitmap()).to.satisfy( - bitmap => imageFromPath.toBitmap().equals(bitmap)) - expect(imageFromDataUrl.toDataURL()).to.equal(imageFromPath.toDataURL()) + bitmap => imageFromPath.toBitmap().equals(bitmap)); + expect(imageFromDataUrl.toDataURL()).to.equal(imageFromPath.toDataURL()); } - }) - }) + }); + }); describe('toDataURL()', () => { it('returns a PNG data URL', () => { - const imagesData = getImages({ hasDataUrl: true }) + const imagesData = getImages({ hasDataUrl: true }); for (const imageData of imagesData) { - const imageFromPath = nativeImage.createFromPath(imageData.path) + const imageFromPath = nativeImage.createFromPath(imageData.path); - const scaleFactors = [1.0, 2.0] + const scaleFactors = [1.0, 2.0]; for (const scaleFactor of scaleFactors) { - expect(imageFromPath.toDataURL({ scaleFactor })).to.equal(imageData.dataUrl) + expect(imageFromPath.toDataURL({ scaleFactor })).to.equal(imageData.dataUrl); } } - }) + }); it('returns a data URL at 1x scale factor by default', () => { - const imageData = getImage({ filename: 'logo.png' }) - const image = nativeImage.createFromPath(imageData.path) + const imageData = getImage({ filename: 'logo.png' }); + const image = nativeImage.createFromPath(imageData.path); const imageOne = nativeImage.createFromBuffer(image.toPNG(), { width: image.getSize().width, height: image.getSize().height, scaleFactor: 2.0 - }) + }); expect(imageOne.getSize()).to.deep.equal( - { width: imageData.width / 2, height: imageData.height / 2 }) + { width: imageData.width / 2, height: imageData.height / 2 }); - const imageTwo = nativeImage.createFromDataURL(imageOne.toDataURL()) + const imageTwo = nativeImage.createFromDataURL(imageOne.toDataURL()); expect(imageTwo.getSize()).to.deep.equal( - { width: imageData.width, height: imageData.height }) + { width: imageData.width, height: imageData.height }); - expect(imageOne.toBitmap().equals(imageTwo.toBitmap())).to.be.true() - }) + expect(imageOne.toBitmap().equals(imageTwo.toBitmap())).to.be.true(); + }); it('supports a scale factor', () => { - const imageData = getImage({ filename: 'logo.png' }) - const image = nativeImage.createFromPath(imageData.path) - const expectedSize = { width: imageData.width, height: imageData.height } + const imageData = getImage({ filename: 'logo.png' }); + const image = nativeImage.createFromPath(imageData.path); + const expectedSize = { width: imageData.width, height: imageData.height }; const imageFromDataUrlOne = nativeImage.createFromDataURL( - image.toDataURL({ scaleFactor: 1.0 })) - expect(imageFromDataUrlOne.getSize()).to.deep.equal(expectedSize) + image.toDataURL({ scaleFactor: 1.0 })); + expect(imageFromDataUrlOne.getSize()).to.deep.equal(expectedSize); const imageFromDataUrlTwo = nativeImage.createFromDataURL( - image.toDataURL({ scaleFactor: 2.0 })) - expect(imageFromDataUrlTwo.getSize()).to.deep.equal(expectedSize) - }) - }) + image.toDataURL({ scaleFactor: 2.0 })); + expect(imageFromDataUrlTwo.getSize()).to.deep.equal(expectedSize); + }); + }); describe('toPNG()', () => { it('returns a buffer at 1x scale factor by default', () => { - const imageData = getImage({ filename: 'logo.png' }) - const imageA = nativeImage.createFromPath(imageData.path) + const imageData = getImage({ filename: 'logo.png' }); + const imageA = nativeImage.createFromPath(imageData.path); const imageB = nativeImage.createFromBuffer(imageA.toPNG(), { width: imageA.getSize().width, height: imageA.getSize().height, scaleFactor: 2.0 - }) + }); expect(imageB.getSize()).to.deep.equal( - { width: imageData.width / 2, height: imageData.height / 2 }) + { width: imageData.width / 2, height: imageData.height / 2 }); - const imageC = nativeImage.createFromBuffer(imageB.toPNG()) + const imageC = nativeImage.createFromBuffer(imageB.toPNG()); expect(imageC.getSize()).to.deep.equal( - { width: imageData.width, height: imageData.height }) + { width: imageData.width, height: imageData.height }); - expect(imageB.toBitmap().equals(imageC.toBitmap())).to.be.true() - }) + expect(imageB.toBitmap().equals(imageC.toBitmap())).to.be.true(); + }); it('supports a scale factor', () => { - const imageData = getImage({ filename: 'logo.png' }) - const image = nativeImage.createFromPath(imageData.path) + const imageData = getImage({ filename: 'logo.png' }); + const image = nativeImage.createFromPath(imageData.path); const imageFromBufferOne = nativeImage.createFromBuffer( - image.toPNG({ scaleFactor: 1.0 })) + image.toPNG({ scaleFactor: 1.0 })); expect(imageFromBufferOne.getSize()).to.deep.equal( - { width: imageData.width, height: imageData.height }) + { width: imageData.width, height: imageData.height }); const imageFromBufferTwo = nativeImage.createFromBuffer( - image.toPNG({ scaleFactor: 2.0 }), { scaleFactor: 2.0 }) + image.toPNG({ scaleFactor: 2.0 }), { scaleFactor: 2.0 }); expect(imageFromBufferTwo.getSize()).to.deep.equal( - { width: imageData.width / 2, height: imageData.height / 2 }) - }) - }) + { width: imageData.width / 2, height: imageData.height / 2 }); + }); + }); describe('createFromPath(path)', () => { it('returns an empty image for invalid paths', () => { - expect(nativeImage.createFromPath('').isEmpty()).to.be.true() - expect(nativeImage.createFromPath('does-not-exist.png').isEmpty()).to.be.true() - expect(nativeImage.createFromPath('does-not-exist.ico').isEmpty()).to.be.true() - expect(nativeImage.createFromPath(__dirname).isEmpty()).to.be.true() - expect(nativeImage.createFromPath(__filename).isEmpty()).to.be.true() - }) + expect(nativeImage.createFromPath('').isEmpty()).to.be.true(); + expect(nativeImage.createFromPath('does-not-exist.png').isEmpty()).to.be.true(); + expect(nativeImage.createFromPath('does-not-exist.ico').isEmpty()).to.be.true(); + expect(nativeImage.createFromPath(__dirname).isEmpty()).to.be.true(); + expect(nativeImage.createFromPath(__filename).isEmpty()).to.be.true(); + }); it('loads images from paths relative to the current working directory', () => { - const imagePath = path.relative('.', path.join(__dirname, 'fixtures', 'assets', 'logo.png')) - const image = nativeImage.createFromPath(imagePath) - expect(image.isEmpty()).to.be.false() - expect(image.getSize()).to.deep.equal({ width: 538, height: 190 }) - }) + const imagePath = path.relative('.', path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + const image = nativeImage.createFromPath(imagePath); + expect(image.isEmpty()).to.be.false(); + expect(image.getSize()).to.deep.equal({ width: 538, height: 190 }); + }); it('loads images from paths with `.` segments', () => { - const imagePath = `${path.join(__dirname, 'fixtures')}${path.sep}.${path.sep}${path.join('assets', 'logo.png')}` - const image = nativeImage.createFromPath(imagePath) - expect(image.isEmpty()).to.be.false() - expect(image.getSize()).to.deep.equal({ width: 538, height: 190 }) - }) + const imagePath = `${path.join(__dirname, 'fixtures')}${path.sep}.${path.sep}${path.join('assets', 'logo.png')}`; + const image = nativeImage.createFromPath(imagePath); + expect(image.isEmpty()).to.be.false(); + expect(image.getSize()).to.deep.equal({ width: 538, height: 190 }); + }); it('loads images from paths with `..` segments', () => { - const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}` - const image = nativeImage.createFromPath(imagePath) - expect(image.isEmpty()).to.be.false() - expect(image.getSize()).to.deep.equal({ width: 538, height: 190 }) - }) - - it('Gets an NSImage pointer on macOS', function () { - if (process.platform !== 'darwin') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } + const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}`; + const image = nativeImage.createFromPath(imagePath); + expect(image.isEmpty()).to.be.false(); + expect(image.getSize()).to.deep.equal({ width: 538, height: 190 }); + }); - const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}` - const image = nativeImage.createFromPath(imagePath) - const nsimage = image.getNativeHandle() + ifit(process.platform === 'darwin')('Gets an NSImage pointer on macOS', function () { + const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}`; + const image = nativeImage.createFromPath(imagePath); + const nsimage = image.getNativeHandle(); - expect(nsimage).to.have.lengthOf(8) + expect(nsimage).to.have.lengthOf(8); // If all bytes are null, that's Bad - const allBytesAreNotNull = nsimage.reduce((acc, x) => acc || (x !== 0), false) - expect(allBytesAreNotNull) - }) - - it('loads images from .ico files on Windows', function () { - if (process.platform !== 'win32') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - const imagePath = path.join(__dirname, 'fixtures', 'assets', 'icon.ico') - const image = nativeImage.createFromPath(imagePath) - expect(image.isEmpty()).to.be.false() - expect(image.getSize()).to.deep.equal({ width: 256, height: 256 }) - }) - }) + const allBytesAreNotNull = nsimage.reduce((acc, x) => acc || (x !== 0), false); + expect(allBytesAreNotNull); + }); + + ifit(process.platform === 'win32')('loads images from .ico files on Windows', function () { + const imagePath = path.join(__dirname, 'fixtures', 'assets', 'icon.ico'); + const image = nativeImage.createFromPath(imagePath); + expect(image.isEmpty()).to.be.false(); + expect(image.getSize()).to.deep.equal({ width: 256, height: 256 }); + }); + }); describe('createFromNamedImage(name)', () => { it('returns empty for invalid options', () => { - const image = nativeImage.createFromNamedImage('totally_not_real') - expect(image.isEmpty()).to.be.true() - }) - - it('returns empty on non-darwin platforms', function () { - if (process.platform === 'darwin') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - const image = nativeImage.createFromNamedImage('NSActionTemplate') - expect(image.isEmpty()).to.be.true() - }) - - it('returns a valid image on darwin', function () { - if (process.platform !== 'darwin') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - const image = nativeImage.createFromNamedImage('NSActionTemplate') - expect(image.isEmpty()).to.be.false() - }) - - it('returns allows an HSL shift for a valid image on darwin', function () { - if (process.platform !== 'darwin') { - // FIXME(alexeykuzmin): Skip the test. - // this.skip() - return - } - - const image = nativeImage.createFromNamedImage('NSActionTemplate', [0.5, 0.2, 0.8]) - expect(image.isEmpty()).to.be.false() - }) - }) + const image = nativeImage.createFromNamedImage('totally_not_real'); + expect(image.isEmpty()).to.be.true(); + }); + + ifit(process.platform !== 'darwin')('returns empty on non-darwin platforms', function () { + const image = nativeImage.createFromNamedImage('NSActionTemplate'); + expect(image.isEmpty()).to.be.true(); + }); + + ifit(process.platform === 'darwin')('returns a valid image on darwin', function () { + const image = nativeImage.createFromNamedImage('NSActionTemplate'); + expect(image.isEmpty()).to.be.false(); + }); + + ifit(process.platform === 'darwin')('returns allows an HSL shift for a valid image on darwin', function () { + const image = nativeImage.createFromNamedImage('NSActionTemplate', [0.5, 0.2, 0.8]); + expect(image.isEmpty()).to.be.false(); + }); + }); describe('resize(options)', () => { it('returns a resized image', () => { - const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); for (const [resizeTo, expectedSize] of new Map([ [{}, { width: 538, height: 190 }], [{ width: 269 }, { width: 269, height: 95 }], @@ -448,167 +425,208 @@ describe('nativeImage module', () => { [{ width: 0, height: 0 }, { width: 0, height: 0 }], [{ width: -1, height: -1 }, { width: 0, height: 0 }] ])) { - const actualSize = image.resize(resizeTo).getSize() - expect(actualSize).to.deep.equal(expectedSize) + const actualSize = image.resize(resizeTo).getSize(); + expect(actualSize).to.deep.equal(expectedSize); } - }) + }); it('returns an empty image when called on an empty image', () => { - expect(nativeImage.createEmpty().resize({ width: 1, height: 1 }).isEmpty()).to.be.true() - expect(nativeImage.createEmpty().resize({ width: 0, height: 0 }).isEmpty()).to.be.true() - }) + expect(nativeImage.createEmpty().resize({ width: 1, height: 1 }).isEmpty()).to.be.true(); + expect(nativeImage.createEmpty().resize({ width: 0, height: 0 }).isEmpty()).to.be.true(); + }); it('supports a quality option', () => { - const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) - const good = image.resize({ width: 100, height: 100, quality: 'good' }) - const better = image.resize({ width: 100, height: 100, quality: 'better' }) - const best = image.resize({ width: 100, height: 100, quality: 'best' }) + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + const good = image.resize({ width: 100, height: 100, quality: 'good' }); + const better = image.resize({ width: 100, height: 100, quality: 'better' }); + const best = image.resize({ width: 100, height: 100, quality: 'best' }); - expect(good.toPNG()).to.have.lengthOf.at.most(better.toPNG().length) - expect(better.toPNG()).to.have.lengthOf.below(best.toPNG().length) - }) - }) + expect(good.toPNG()).to.have.lengthOf.at.most(better.toPNG().length); + expect(better.toPNG()).to.have.lengthOf.below(best.toPNG().length); + }); + }); describe('crop(bounds)', () => { it('returns an empty image when called on an empty image', () => { - expect(nativeImage.createEmpty().crop({ width: 1, height: 2, x: 0, y: 0 }).isEmpty()).to.be.true() - expect(nativeImage.createEmpty().crop({ width: 0, height: 0, x: 0, y: 0 }).isEmpty()).to.be.true() - }) + expect(nativeImage.createEmpty().crop({ width: 1, height: 2, x: 0, y: 0 }).isEmpty()).to.be.true(); + expect(nativeImage.createEmpty().crop({ width: 0, height: 0, x: 0, y: 0 }).isEmpty()).to.be.true(); + }); it('returns an empty image when the bounds are invalid', () => { - const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) - expect(image.crop({ width: 0, height: 0, x: 0, y: 0 }).isEmpty()).to.be.true() - expect(image.crop({ width: -1, height: 10, x: 0, y: 0 }).isEmpty()).to.be.true() - expect(image.crop({ width: 10, height: -35, x: 0, y: 0 }).isEmpty()).to.be.true() - expect(image.crop({ width: 100, height: 100, x: 1000, y: 1000 }).isEmpty()).to.be.true() - }) + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + expect(image.crop({ width: 0, height: 0, x: 0, y: 0 }).isEmpty()).to.be.true(); + expect(image.crop({ width: -1, height: 10, x: 0, y: 0 }).isEmpty()).to.be.true(); + expect(image.crop({ width: 10, height: -35, x: 0, y: 0 }).isEmpty()).to.be.true(); + expect(image.crop({ width: 100, height: 100, x: 1000, y: 1000 }).isEmpty()).to.be.true(); + }); it('returns a cropped image', () => { - const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) - const cropA = image.crop({ width: 25, height: 64, x: 0, y: 0 }) - const cropB = image.crop({ width: 25, height: 64, x: 30, y: 40 }) - expect(cropA.getSize()).to.deep.equal({ width: 25, height: 64 }) - expect(cropB.getSize()).to.deep.equal({ width: 25, height: 64 }) - expect(cropA.toPNG().equals(cropB.toPNG())).to.be.false() - }) - }) + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + const cropA = image.crop({ width: 25, height: 64, x: 0, y: 0 }); + const cropB = image.crop({ width: 25, height: 64, x: 30, y: 40 }); + expect(cropA.getSize()).to.deep.equal({ width: 25, height: 64 }); + expect(cropB.getSize()).to.deep.equal({ width: 25, height: 64 }); + expect(cropA.toPNG().equals(cropB.toPNG())).to.be.false(); + }); + + it('toBitmap() returns a buffer of the right size', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')); + const crop = image.crop({ width: 25, height: 64, x: 0, y: 0 }); + expect(crop.toBitmap().length).to.equal(25 * 64 * 4); + }); + }); describe('getAspectRatio()', () => { it('returns an aspect ratio of an empty image', () => { - expect(nativeImage.createEmpty().getAspectRatio()).to.equal(1.0) - }) + expect(nativeImage.createEmpty().getAspectRatio()).to.equal(1.0); + }); it('returns an aspect ratio of an image', () => { - const imageData = getImage({ filename: 'logo.png' }) + const imageData = getImage({ filename: 'logo.png' }); // imageData.width / imageData.height = 2.831578947368421 - const expectedAspectRatio = 2.8315789699554443 - - const image = nativeImage.createFromPath(imageData.path) - expect(image.getAspectRatio()).to.equal(expectedAspectRatio) - }) - }) + const expectedAspectRatio = 2.8315789699554443; + + const image = nativeImage.createFromPath(imageData.path); + expect(image.getAspectRatio()).to.equal(expectedAspectRatio); + }); + }); + + ifdescribe(process.platform !== 'linux')('createThumbnailFromPath(path, size)', () => { + it('throws when invalid size is passed', async () => { + const badSize = { width: -1, height: -1 }; + + await expect( + nativeImage.createThumbnailFromPath('path', badSize) + ).to.eventually.be.rejectedWith('size must not be empty'); + }); + + it('throws when a bad path is passed', async () => { + const badPath = process.platform === 'win32' ? '\\hey\\hi\\hello' : '/hey/hi/hello'; + const goodSize = { width: 100, height: 100 }; + + await expect( + nativeImage.createThumbnailFromPath(badPath, goodSize) + ).to.eventually.be.rejected(); + }); + + it('returns native image given valid params', async () => { + const goodPath = path.join(__dirname, 'fixtures', 'assets', 'logo.png'); + const goodSize = { width: 100, height: 100 }; + const result = await nativeImage.createThumbnailFromPath(goodPath, goodSize); + expect(result.isEmpty()).to.equal(false); + }); + }); describe('addRepresentation()', () => { it('does not add representation when the buffer is too small', () => { - const image = nativeImage.createEmpty() + const image = nativeImage.createEmpty(); image.addRepresentation({ buffer: Buffer.from([1, 2, 3, 4]), width: 100, height: 100 - }) + }); - expect(image.isEmpty()).to.be.true() - }) + expect(image.isEmpty()).to.be.true(); + }); it('supports adding a buffer representation for a scale factor', () => { - const image = nativeImage.createEmpty() + const image = nativeImage.createEmpty(); - const imageDataOne = getImage({ width: 1, height: 1 }) + const imageDataOne = getImage({ width: 1, height: 1 }); image.addRepresentation({ scaleFactor: 1.0, buffer: nativeImage.createFromPath(imageDataOne.path).toPNG() - }) + }); + + expect(image.getScaleFactors()).to.deep.equal([1]); - const imageDataTwo = getImage({ width: 2, height: 2 }) + const imageDataTwo = getImage({ width: 2, height: 2 }); image.addRepresentation({ scaleFactor: 2.0, buffer: nativeImage.createFromPath(imageDataTwo.path).toPNG() - }) + }); + + expect(image.getScaleFactors()).to.deep.equal([1, 2]); - const imageDataThree = getImage({ width: 3, height: 3 }) + const imageDataThree = getImage({ width: 3, height: 3 }); image.addRepresentation({ scaleFactor: 3.0, buffer: nativeImage.createFromPath(imageDataThree.path).toPNG() - }) + }); + + expect(image.getScaleFactors()).to.deep.equal([1, 2, 3]); image.addRepresentation({ scaleFactor: 4.0, buffer: 'invalid' - }) + }); + + // this one failed, so it shouldn't show up in the scale factors + expect(image.getScaleFactors()).to.deep.equal([1, 2, 3]); - expect(image.isEmpty()).to.be.false() - expect(image.getSize()).to.deep.equal({ width: 1, height: 1 }) + expect(image.isEmpty()).to.be.false(); + expect(image.getSize()).to.deep.equal({ width: 1, height: 1 }); - expect(image.toDataURL({ scaleFactor: 1.0 })).to.equal(imageDataOne.dataUrl) - expect(image.toDataURL({ scaleFactor: 2.0 })).to.equal(imageDataTwo.dataUrl) - expect(image.toDataURL({ scaleFactor: 3.0 })).to.equal(imageDataThree.dataUrl) - expect(image.toDataURL({ scaleFactor: 4.0 })).to.equal(imageDataThree.dataUrl) - }) + expect(image.toDataURL({ scaleFactor: 1.0 })).to.equal(imageDataOne.dataUrl); + expect(image.toDataURL({ scaleFactor: 2.0 })).to.equal(imageDataTwo.dataUrl); + expect(image.toDataURL({ scaleFactor: 3.0 })).to.equal(imageDataThree.dataUrl); + expect(image.toDataURL({ scaleFactor: 4.0 })).to.equal(imageDataThree.dataUrl); + }); it('supports adding a data URL representation for a scale factor', () => { - const image = nativeImage.createEmpty() + const image = nativeImage.createEmpty(); - const imageDataOne = getImage({ width: 1, height: 1 }) + const imageDataOne = getImage({ width: 1, height: 1 }); image.addRepresentation({ scaleFactor: 1.0, dataURL: imageDataOne.dataUrl - }) + }); - const imageDataTwo = getImage({ width: 2, height: 2 }) + const imageDataTwo = getImage({ width: 2, height: 2 }); image.addRepresentation({ scaleFactor: 2.0, dataURL: imageDataTwo.dataUrl - }) + }); - const imageDataThree = getImage({ width: 3, height: 3 }) + const imageDataThree = getImage({ width: 3, height: 3 }); image.addRepresentation({ scaleFactor: 3.0, dataURL: imageDataThree.dataUrl - }) + }); image.addRepresentation({ scaleFactor: 4.0, dataURL: 'invalid' - }) + }); - expect(image.isEmpty()).to.be.false() - expect(image.getSize()).to.deep.equal({ width: 1, height: 1 }) + expect(image.isEmpty()).to.be.false(); + expect(image.getSize()).to.deep.equal({ width: 1, height: 1 }); - expect(image.toDataURL({ scaleFactor: 1.0 })).to.equal(imageDataOne.dataUrl) - expect(image.toDataURL({ scaleFactor: 2.0 })).to.equal(imageDataTwo.dataUrl) - expect(image.toDataURL({ scaleFactor: 3.0 })).to.equal(imageDataThree.dataUrl) - expect(image.toDataURL({ scaleFactor: 4.0 })).to.equal(imageDataThree.dataUrl) - }) + expect(image.toDataURL({ scaleFactor: 1.0 })).to.equal(imageDataOne.dataUrl); + expect(image.toDataURL({ scaleFactor: 2.0 })).to.equal(imageDataTwo.dataUrl); + expect(image.toDataURL({ scaleFactor: 3.0 })).to.equal(imageDataThree.dataUrl); + expect(image.toDataURL({ scaleFactor: 4.0 })).to.equal(imageDataThree.dataUrl); + }); it('supports adding a representation to an existing image', () => { - const imageDataOne = getImage({ width: 1, height: 1 }) - const image = nativeImage.createFromPath(imageDataOne.path) + const imageDataOne = getImage({ width: 1, height: 1 }); + const image = nativeImage.createFromPath(imageDataOne.path); - const imageDataTwo = getImage({ width: 2, height: 2 }) + const imageDataTwo = getImage({ width: 2, height: 2 }); image.addRepresentation({ scaleFactor: 2.0, dataURL: imageDataTwo.dataUrl - }) + }); - const imageDataThree = getImage({ width: 3, height: 3 }) + const imageDataThree = getImage({ width: 3, height: 3 }); image.addRepresentation({ scaleFactor: 2.0, dataURL: imageDataThree.dataUrl - }) + }); - expect(image.toDataURL({ scaleFactor: 1.0 })).to.equal(imageDataOne.dataUrl) - expect(image.toDataURL({ scaleFactor: 2.0 })).to.equal(imageDataTwo.dataUrl) - }) - }) -}) + expect(image.toDataURL({ scaleFactor: 1.0 })).to.equal(imageDataOne.dataUrl); + expect(image.toDataURL({ scaleFactor: 2.0 })).to.equal(imageDataTwo.dataUrl); + }); + }); +}); diff --git a/spec/api-notification-dbus-spec.js b/spec/api-notification-dbus-spec.js deleted file mode 100644 index e93adf4feea85..0000000000000 --- a/spec/api-notification-dbus-spec.js +++ /dev/null @@ -1,125 +0,0 @@ -// For these tests we use a fake DBus daemon to verify libnotify interaction -// with the session bus. This requires python-dbusmock to be installed and -// running at $DBUS_SESSION_BUS_ADDRESS. -// -// script/spec-runner.js spawns dbusmock, which sets DBUS_SESSION_BUS_ADDRESS. -// -// See https://pypi.python.org/pypi/python-dbusmock to read about dbusmock. - -const { expect } = require('chai') -const dbus = require('dbus-native') -const Promise = require('bluebird') - -const { remote } = require('electron') -const { app } = remote - -const skip = process.platform !== 'linux' || - process.arch === 'ia32' || - process.arch.indexOf('arm') === 0 || - !process.env.DBUS_SESSION_BUS_ADDRESS; - -(skip ? describe.skip : describe)('Notification module (dbus)', () => { - let mock, Notification, getCalls, reset - const realAppName = app.name - const realAppVersion = app.getVersion() - const appName = 'api-notification-dbus-spec' - const serviceName = 'org.freedesktop.Notifications' - - before(async () => { - // init app - app.name = appName - app.setDesktopName(`${appName}.desktop`) - // init dbus - const path = '/org/freedesktop/Notifications' - const iface = 'org.freedesktop.DBus.Mock' - const bus = dbus.sessionBus() - console.log(`session bus: ${process.env.DBUS_SESSION_BUS_ADDRESS}`) - const service = bus.getService(serviceName) - const getInterface = Promise.promisify(service.getInterface, { context: service }) - mock = await getInterface(path, iface) - getCalls = Promise.promisify(mock.GetCalls, { context: mock }) - reset = Promise.promisify(mock.Reset, { context: mock }) - }) - - after(async () => { - // cleanup dbus - await reset() - // cleanup app - app.setName(realAppName) - app.setVersion(realAppVersion) - }) - - describe(`Notification module using ${serviceName}`, () => { - function onMethodCalled (done) { - function cb (name) { - console.log(`onMethodCalled: ${name}`) - if (name === 'Notify') { - mock.removeListener('MethodCalled', cb) - console.log('done') - done() - } - } - return cb - } - - function unmarshalDBusNotifyHints (dbusHints) { - const o = {} - for (const hint of dbusHints) { - const key = hint[0] - const value = hint[1][1][0] - o[key] = value - } - return o - } - - function unmarshalDBusNotifyArgs (dbusArgs) { - return { - app_name: dbusArgs[0][1][0], - replaces_id: dbusArgs[1][1][0], - app_icon: dbusArgs[2][1][0], - title: dbusArgs[3][1][0], - body: dbusArgs[4][1][0], - actions: dbusArgs[5][1][0], - hints: unmarshalDBusNotifyHints(dbusArgs[6][1][0]) - } - } - - before(done => { - mock.on('MethodCalled', onMethodCalled(done)) - // lazy load Notification after we listen to MethodCalled mock signal - Notification = require('electron').remote.Notification - const n = new Notification({ - title: 'title', - subtitle: 'subtitle', - body: 'body', - replyPlaceholder: 'replyPlaceholder', - sound: 'sound', - closeButtonText: 'closeButtonText' - }) - n.show() - }) - - it(`should call ${serviceName} to show notifications`, async () => { - const calls = await getCalls() - expect(calls).to.be.an('array').of.lengthOf.at.least(1) - - const lastCall = calls[calls.length - 1] - const methodName = lastCall[1] - expect(methodName).to.equal('Notify') - - const args = unmarshalDBusNotifyArgs(lastCall[2]) - expect(args).to.deep.equal({ - app_name: appName, - replaces_id: 0, - app_icon: '', - title: 'title', - body: 'body', - actions: [], - hints: { - 'append': 'true', - 'desktop-entry': appName - } - }) - }) - }) -}) diff --git a/spec/api-power-monitor-spec.js b/spec/api-power-monitor-spec.js deleted file mode 100644 index 9f5425f8ec631..0000000000000 --- a/spec/api-power-monitor-spec.js +++ /dev/null @@ -1,163 +0,0 @@ -// For these tests we use a fake DBus daemon to verify powerMonitor module -// interaction with the system bus. This requires python-dbusmock installed and -// running (with the DBUS_SYSTEM_BUS_ADDRESS environment variable set). -// script/spec-runner.js will take care of spawning the fake DBus daemon and setting -// DBUS_SYSTEM_BUS_ADDRESS when python-dbusmock is installed. -// -// See https://pypi.python.org/pypi/python-dbusmock for more information about -// python-dbusmock. -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const dbus = require('dbus-native') -const Promise = require('bluebird') - -const { expect } = chai -chai.use(dirtyChai) - -const skip = process.platform !== 'linux' || !process.env.DBUS_SYSTEM_BUS_ADDRESS - -describe('powerMonitor', () => { - let logindMock, dbusMockPowerMonitor, getCalls, emitSignal, reset - - if (!skip) { - before(async () => { - const systemBus = dbus.systemBus() - const loginService = systemBus.getService('org.freedesktop.login1') - const getInterface = Promise.promisify(loginService.getInterface, { context: loginService }) - logindMock = await getInterface('/org/freedesktop/login1', 'org.freedesktop.DBus.Mock') - getCalls = Promise.promisify(logindMock.GetCalls, { context: logindMock }) - emitSignal = Promise.promisify(logindMock.EmitSignal, { context: logindMock }) - reset = Promise.promisify(logindMock.Reset, { context: logindMock }) - }) - - after(async () => { - await reset() - }) - } - - (skip ? describe.skip : describe)('when powerMonitor module is loaded with dbus mock', () => { - function onceMethodCalled (done) { - function cb () { - logindMock.removeListener('MethodCalled', cb) - } - done() - return cb - } - - before(done => { - logindMock.on('MethodCalled', onceMethodCalled(done)) - // lazy load powerMonitor after we listen to MethodCalled mock signal - dbusMockPowerMonitor = require('electron').remote.powerMonitor - }) - - it('should call Inhibit to delay suspend', async () => { - const calls = await getCalls() - expect(calls).to.be.an('array').that.has.lengthOf(1) - expect(calls[0].slice(1)).to.deep.equal([ - 'Inhibit', [ - [[{ type: 's', child: [] }], ['sleep']], - [[{ type: 's', child: [] }], ['electron']], - [[{ type: 's', child: [] }], ['Application cleanup before suspend']], - [[{ type: 's', child: [] }], ['delay']] - ] - ]) - }) - - describe('when PrepareForSleep(true) signal is sent by logind', () => { - it('should emit "suspend" event', (done) => { - dbusMockPowerMonitor.once('suspend', () => done()) - emitSignal('org.freedesktop.login1.Manager', 'PrepareForSleep', - 'b', [['b', true]]) - }) - - describe('when PrepareForSleep(false) signal is sent by logind', () => { - it('should emit "resume" event', done => { - dbusMockPowerMonitor.once('resume', () => done()) - emitSignal('org.freedesktop.login1.Manager', 'PrepareForSleep', - 'b', [['b', false]]) - }) - - it('should have called Inhibit again', async () => { - const calls = await getCalls() - expect(calls).to.be.an('array').that.has.lengthOf(2) - expect(calls[1].slice(1)).to.deep.equal([ - 'Inhibit', [ - [[{ type: 's', child: [] }], ['sleep']], - [[{ type: 's', child: [] }], ['electron']], - [[{ type: 's', child: [] }], ['Application cleanup before suspend']], - [[{ type: 's', child: [] }], ['delay']] - ] - ]) - }) - }) - }) - - describe('when a listener is added to shutdown event', () => { - before(async () => { - const calls = await getCalls() - expect(calls).to.be.an('array').that.has.lengthOf(2) - dbusMockPowerMonitor.once('shutdown', () => { }) - }) - - it('should call Inhibit to delay shutdown', async () => { - const calls = await getCalls() - expect(calls).to.be.an('array').that.has.lengthOf(3) - expect(calls[2].slice(1)).to.deep.equal([ - 'Inhibit', [ - [[{ type: 's', child: [] }], ['shutdown']], - [[{ type: 's', child: [] }], ['electron']], - [[{ type: 's', child: [] }], ['Ensure a clean shutdown']], - [[{ type: 's', child: [] }], ['delay']] - ] - ]) - }) - - describe('when PrepareForShutdown(true) signal is sent by logind', () => { - it('should emit "shutdown" event', done => { - dbusMockPowerMonitor.once('shutdown', () => { done() }) - emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown', - 'b', [['b', true]]) - }) - }) - }) - }) - - describe('when powerMonitor module is loaded', () => { - let powerMonitor - before(() => { - powerMonitor = require('electron').remote.powerMonitor - }) - - describe('powerMonitor.getSystemIdleState', () => { - it('gets current system idle state', () => { - // this function is not mocked out, so we can test the result's - // form and type but not its value. - const idleState = powerMonitor.getSystemIdleState(1) - expect(idleState).to.be.a('string') - const validIdleStates = [ 'active', 'idle', 'locked', 'unknown' ] - expect(validIdleStates).to.include(idleState) - }) - - it('does not accept non positive integer threshold', () => { - expect(() => { - powerMonitor.getSystemIdleState(-1) - }).to.throw(/must be greater than 0/) - - expect(() => { - powerMonitor.getSystemIdleState(NaN) - }).to.throw(/conversion failure/) - - expect(() => { - powerMonitor.getSystemIdleState('a') - }).to.throw(/conversion failure/) - }) - }) - - describe('powerMonitor.getSystemIdleTime', () => { - it('notify current system idle time', () => { - const idleTime = powerMonitor.getSystemIdleTime() - expect(idleTime).to.be.at.least(0) - }) - }) - }) -}) diff --git a/spec/api-process-spec.js b/spec/api-process-spec.js index d49bbe3fff78b..8029107413227 100644 --- a/spec/api-process-spec.js +++ b/spec/api-process-spec.js @@ -1,118 +1,124 @@ -const { remote } = require('electron') -const fs = require('fs') -const path = require('path') +const { ipcRenderer } = require('electron'); +const fs = require('fs'); +const path = require('path'); -const { expect } = require('chai') +const { expect } = require('chai'); describe('process module', () => { describe('process.getCreationTime()', () => { it('returns a creation time', () => { - const creationTime = process.getCreationTime() - expect(creationTime).to.be.a('number').and.be.at.least(0) - }) - }) + const creationTime = process.getCreationTime(); + expect(creationTime).to.be.a('number').and.be.at.least(0); + }); + }); describe('process.getCPUUsage()', () => { it('returns a cpu usage object', () => { - const cpuUsage = process.getCPUUsage() - expect(cpuUsage.percentCPUUsage).to.be.a('number') - expect(cpuUsage.idleWakeupsPerSecond).to.be.a('number') - }) - }) + const cpuUsage = process.getCPUUsage(); + expect(cpuUsage.percentCPUUsage).to.be.a('number'); + expect(cpuUsage.idleWakeupsPerSecond).to.be.a('number'); + }); + }); describe('process.getIOCounters()', () => { before(function () { if (process.platform === 'darwin') { - this.skip() + this.skip(); } - }) + }); it('returns an io counters object', () => { - const ioCounters = process.getIOCounters() - expect(ioCounters.readOperationCount).to.be.a('number') - expect(ioCounters.writeOperationCount).to.be.a('number') - expect(ioCounters.otherOperationCount).to.be.a('number') - expect(ioCounters.readTransferCount).to.be.a('number') - expect(ioCounters.writeTransferCount).to.be.a('number') - expect(ioCounters.otherTransferCount).to.be.a('number') - }) - }) + const ioCounters = process.getIOCounters(); + expect(ioCounters.readOperationCount).to.be.a('number'); + expect(ioCounters.writeOperationCount).to.be.a('number'); + expect(ioCounters.otherOperationCount).to.be.a('number'); + expect(ioCounters.readTransferCount).to.be.a('number'); + expect(ioCounters.writeTransferCount).to.be.a('number'); + expect(ioCounters.otherTransferCount).to.be.a('number'); + }); + }); describe('process.getBlinkMemoryInfo()', () => { it('returns blink memory information object', () => { - const heapStats = process.getBlinkMemoryInfo() - expect(heapStats.allocated).to.be.a('number') - expect(heapStats.total).to.be.a('number') - }) - }) + const heapStats = process.getBlinkMemoryInfo(); + expect(heapStats.allocated).to.be.a('number'); + expect(heapStats.total).to.be.a('number'); + }); + }); describe('process.getProcessMemoryInfo()', async () => { it('resolves promise successfully with valid data', async () => { - const memoryInfo = await process.getProcessMemoryInfo() - expect(memoryInfo).to.be.an('object') + const memoryInfo = await process.getProcessMemoryInfo(); + expect(memoryInfo).to.be.an('object'); if (process.platform === 'linux' || process.platform === 'windows') { - expect(memoryInfo.residentSet).to.be.a('number').greaterThan(0) + expect(memoryInfo.residentSet).to.be.a('number').greaterThan(0); } - expect(memoryInfo.private).to.be.a('number').greaterThan(0) + expect(memoryInfo.private).to.be.a('number').greaterThan(0); // Shared bytes can be zero - expect(memoryInfo.shared).to.be.a('number').greaterThan(-1) - }) - }) + expect(memoryInfo.shared).to.be.a('number').greaterThan(-1); + }); + }); describe('process.getSystemMemoryInfo()', () => { it('returns system memory info object', () => { - const systemMemoryInfo = process.getSystemMemoryInfo() - expect(systemMemoryInfo.free).to.be.a('number') - expect(systemMemoryInfo.total).to.be.a('number') - }) - }) + const systemMemoryInfo = process.getSystemMemoryInfo(); + expect(systemMemoryInfo.free).to.be.a('number'); + expect(systemMemoryInfo.total).to.be.a('number'); + }); + }); describe('process.getSystemVersion()', () => { it('returns a string', () => { - expect(process.getSystemVersion()).to.be.a('string') - }) - }) + expect(process.getSystemVersion()).to.be.a('string'); + }); + }); describe('process.getHeapStatistics()', () => { it('returns heap statistics object', () => { - const heapStats = process.getHeapStatistics() - expect(heapStats.totalHeapSize).to.be.a('number') - expect(heapStats.totalHeapSizeExecutable).to.be.a('number') - expect(heapStats.totalPhysicalSize).to.be.a('number') - expect(heapStats.totalAvailableSize).to.be.a('number') - expect(heapStats.usedHeapSize).to.be.a('number') - expect(heapStats.heapSizeLimit).to.be.a('number') - expect(heapStats.mallocedMemory).to.be.a('number') - expect(heapStats.peakMallocedMemory).to.be.a('number') - expect(heapStats.doesZapGarbage).to.be.a('boolean') - }) - }) + const heapStats = process.getHeapStatistics(); + expect(heapStats.totalHeapSize).to.be.a('number'); + expect(heapStats.totalHeapSizeExecutable).to.be.a('number'); + expect(heapStats.totalPhysicalSize).to.be.a('number'); + expect(heapStats.totalAvailableSize).to.be.a('number'); + expect(heapStats.usedHeapSize).to.be.a('number'); + expect(heapStats.heapSizeLimit).to.be.a('number'); + expect(heapStats.mallocedMemory).to.be.a('number'); + expect(heapStats.peakMallocedMemory).to.be.a('number'); + expect(heapStats.doesZapGarbage).to.be.a('boolean'); + }); + }); describe('process.takeHeapSnapshot()', () => { - it('returns true on success', () => { - const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot') + it('returns true on success', async () => { + const filePath = path.join(await ipcRenderer.invoke('get-temp-dir'), 'test.heapsnapshot'); const cleanup = () => { try { - fs.unlinkSync(filePath) + fs.unlinkSync(filePath); } catch (e) { // ignore error } - } + }; try { - const success = process.takeHeapSnapshot(filePath) - expect(success).to.be.true() - const stats = fs.statSync(filePath) - expect(stats.size).not.to.be.equal(0) + const success = process.takeHeapSnapshot(filePath); + expect(success).to.be.true(); + const stats = fs.statSync(filePath); + expect(stats.size).not.to.be.equal(0); } finally { - cleanup() + cleanup(); } - }) + }); it('returns false on failure', () => { - const success = process.takeHeapSnapshot('') - expect(success).to.be.false() - }) - }) -}) + const success = process.takeHeapSnapshot(''); + expect(success).to.be.false(); + }); + }); + + describe('process.contextId', () => { + it('is a string', () => { + expect(process.contextId).to.be.a('string'); + }); + }); +}); diff --git a/spec/api-remote-spec.js b/spec/api-remote-spec.js deleted file mode 100644 index 4564560d74ea8..0000000000000 --- a/spec/api-remote-spec.js +++ /dev/null @@ -1,511 +0,0 @@ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const path = require('path') -const { closeWindow } = require('./window-helpers') -const { resolveGetters } = require('./expect-helpers') - -const { remote, ipcRenderer } = require('electron') -const { ipcMain, BrowserWindow } = remote -const { expect } = chai - -chai.use(dirtyChai) - -const comparePaths = (path1, path2) => { - if (process.platform === 'win32') { - path1 = path1.toLowerCase() - path2 = path2.toLowerCase() - } - expect(path1).to.equal(path2) -} - -describe('remote module', () => { - const fixtures = path.join(__dirname, 'fixtures') - - describe('remote.require', () => { - it('should returns same object for the same module', () => { - const dialog1 = remote.require('electron') - const dialog2 = remote.require('electron') - expect(dialog1).to.equal(dialog2) - }) - - it('should work when object contains id property', () => { - const a = remote.require(path.join(fixtures, 'module', 'id.js')) - expect(a.id).to.equal(1127) - }) - - it('should work when object has no prototype', () => { - const a = remote.require(path.join(fixtures, 'module', 'no-prototype.js')) - expect(a.foo.constructor.name).to.equal('') - expect(a.foo.bar).to.equal('baz') - expect(a.foo.baz).to.equal(false) - expect(a.bar).to.equal(1234) - expect(a.anonymous.constructor.name).to.equal('') - expect(a.getConstructorName(Object.create(null))).to.equal('') - expect(a.getConstructorName(new (class {})())).to.equal('') - }) - - it('should search module from the user app', () => { - comparePaths(path.normalize(remote.process.mainModule.filename), path.resolve(__dirname, 'static', 'main.js')) - comparePaths(path.normalize(remote.process.mainModule.paths[0]), path.resolve(__dirname, 'static', 'node_modules')) - }) - - it('should work with function properties', () => { - let a = remote.require(path.join(fixtures, 'module', 'export-function-with-properties.js')) - expect(a).to.be.a('function') - expect(a.bar).to.equal('baz') - - a = remote.require(path.join(fixtures, 'module', 'function-with-properties.js')) - expect(a).to.be.an('object') - expect(a.foo()).to.equal('hello') - expect(a.foo.bar).to.equal('baz') - expect(a.foo.nested.prop).to.equal('yes') - expect(a.foo.method1()).to.equal('world') - expect(a.foo.method1.prop1()).to.equal(123) - - expect(a.foo).to.have.a.property('bar') - expect(a.foo).to.have.a.property('nested') - expect(a.foo).to.have.a.property('method1') - - a = remote.require(path.join(fixtures, 'module', 'function-with-missing-properties.js')).setup() - expect(a.bar()).to.equal(true) - expect(a.bar.baz).to.be.undefined() - }) - - it('should work with static class members', () => { - const a = remote.require(path.join(fixtures, 'module', 'remote-static.js')) - expect(a.Foo).to.be.a('function') - expect(a.Foo.foo()).to.equal(3) - expect(a.Foo.bar).to.equal('baz') - - const foo = new a.Foo() - expect(foo.baz()).to.equal(123) - }) - - it('includes the length of functions specified as arguments', () => { - const a = remote.require(path.join(fixtures, 'module', 'function-with-args.js')) - expect(a((a, b, c, d, f) => {})).to.equal(5) - expect(a((a) => {})).to.equal(1) - expect(a((...args) => {})).to.equal(0) - }) - - it('handles circular references in arrays and objects', () => { - const a = remote.require(path.join(fixtures, 'module', 'circular.js')) - - let arrayA = ['foo'] - const arrayB = [arrayA, 'bar'] - arrayA.push(arrayB) - expect(a.returnArgs(arrayA, arrayB)).to.deep.equal([ - ['foo', [null, 'bar']], - [['foo', null], 'bar'] - ]) - - let objectA = { foo: 'bar' } - const objectB = { baz: objectA } - objectA.objectB = objectB - expect(a.returnArgs(objectA, objectB)).to.deep.equal([ - { foo: 'bar', objectB: { baz: null } }, - { baz: { foo: 'bar', objectB: null } } - ]) - - arrayA = [1, 2, 3] - expect(a.returnArgs({ foo: arrayA }, { bar: arrayA })).to.deep.equal([ - { foo: [1, 2, 3] }, - { bar: [1, 2, 3] } - ]) - - objectA = { foo: 'bar' } - expect(a.returnArgs({ foo: objectA }, { bar: objectA })).to.deep.equal([ - { foo: { foo: 'bar' } }, - { bar: { foo: 'bar' } } - ]) - - arrayA = [] - arrayA.push(arrayA) - expect(a.returnArgs(arrayA)).to.deep.equal([ - [null] - ]) - - objectA = {} - objectA.foo = objectA - objectA.bar = 'baz' - expect(a.returnArgs(objectA)).to.deep.equal([ - { foo: null, bar: 'baz' } - ]) - - objectA = {} - objectA.foo = { bar: objectA } - objectA.bar = 'baz' - expect(a.returnArgs(objectA)).to.deep.equal([ - { foo: { bar: null }, bar: 'baz' } - ]) - }) - }) - - describe('remote.createFunctionWithReturnValue', () => { - it('should be called in browser synchronously', () => { - const buf = Buffer.from('test') - const call = remote.require(path.join(fixtures, 'module', 'call.js')) - const result = call.call(remote.createFunctionWithReturnValue(buf)) - expect(result).to.be.an.instanceOf(Buffer) - }) - }) - - describe('remote modules', () => { - it('includes browser process modules as properties', () => { - expect(remote.app.getPath).to.be.a('function') - expect(remote.webContents.getFocusedWebContents).to.be.a('function') - expect(remote.clipboard.readText).to.be.a('function') - }) - - it('returns toString() of original function via toString()', () => { - const { readText } = remote.clipboard - expect(readText.toString().startsWith('function')).to.be.true() - - const { functionWithToStringProperty } = remote.require(path.join(fixtures, 'module', 'to-string-non-function.js')) - expect(functionWithToStringProperty.toString).to.equal('hello') - }) - }) - - describe('remote object in renderer', () => { - it('can change its properties', () => { - const property = remote.require(path.join(fixtures, 'module', 'property.js')) - expect(property).to.have.a.property('property').that.is.equal(1127) - - property.property = null - expect(property).to.have.a.property('property').that.is.null() - property.property = undefined - expect(property).to.have.a.property('property').that.is.undefined() - property.property = 1007 - expect(property).to.have.a.property('property').that.is.equal(1007) - - expect(property.getFunctionProperty()).to.equal('foo-browser') - property.func.property = 'bar' - expect(property.getFunctionProperty()).to.equal('bar-browser') - property.func.property = 'foo' // revert back - - const property2 = remote.require(path.join(fixtures, 'module', 'property.js')) - expect(property2.property).to.equal(1007) - property.property = 1127 - }) - - it('rethrows errors getting/setting properties', () => { - const foo = remote.require(path.join(fixtures, 'module', 'error-properties.js')) - - expect(() => { - // eslint-disable-next-line - foo.bar - }).to.throw('getting error') - - expect(() => { - foo.bar = 'test' - }).to.throw('setting error') - }) - - it('can set a remote property with a remote object', () => { - const foo = remote.require(path.join(fixtures, 'module', 'remote-object-set.js')) - - expect(() => { - foo.bar = remote.getCurrentWindow() - }).to.not.throw() - }) - - it('can construct an object from its member', () => { - const call = remote.require(path.join(fixtures, 'module', 'call.js')) - const obj = new call.constructor() - expect(obj.test).to.equal('test') - }) - - it('can reassign and delete its member functions', () => { - const remoteFunctions = remote.require(path.join(fixtures, 'module', 'function.js')) - expect(remoteFunctions.aFunction()).to.equal(1127) - - remoteFunctions.aFunction = () => { return 1234 } - expect(remoteFunctions.aFunction()).to.equal(1234) - - expect(delete remoteFunctions.aFunction).to.equal(true) - }) - - it('is referenced by its members', () => { - const stringify = remote.getGlobal('JSON').stringify - global.gc() - stringify({}) - }) - }) - - describe('remote value in browser', () => { - const print = path.join(fixtures, 'module', 'print_name.js') - const printName = remote.require(print) - - it('converts NaN to undefined', () => { - expect(printName.getNaN()).to.be.undefined() - expect(printName.echo(NaN)).to.be.undefined() - }) - - it('converts Infinity to undefined', () => { - expect(printName.getInfinity()).to.be.undefined() - expect(printName.echo(Infinity)).to.be.undefined() - }) - - it('keeps its constructor name for objects', () => { - const buf = Buffer.from('test') - expect(printName.print(buf)).to.equal('Buffer') - }) - - it('supports instanceof Date', () => { - const now = new Date() - expect(printName.print(now)).to.equal('Date') - expect(printName.echo(now)).to.deep.equal(now) - }) - - it('supports instanceof Buffer', () => { - const buffer = Buffer.from('test') - expect(buffer.equals(printName.echo(buffer))).to.be.true() - - const objectWithBuffer = { a: 'foo', b: Buffer.from('bar') } - expect(objectWithBuffer.b.equals(printName.echo(objectWithBuffer).b)).to.be.true() - - const arrayWithBuffer = [1, 2, Buffer.from('baz')] - expect(arrayWithBuffer[2].equals(printName.echo(arrayWithBuffer)[2])).to.be.true() - }) - - it('supports instanceof ArrayBuffer', () => { - const buffer = new ArrayBuffer(8) - const view = new DataView(buffer) - - view.setFloat64(0, Math.PI) - expect(printName.echo(buffer)).to.deep.equal(buffer) - expect(printName.print(buffer)).to.equal('ArrayBuffer') - }) - - it('supports instanceof Int8Array', () => { - const values = [1, 2, 3, 4] - expect([...printName.typedArray('Int8Array', values)]).to.deep.equal(values) - - const int8values = new Int8Array(values) - expect(printName.typedArray('Int8Array', int8values)).to.deep.equal(int8values) - expect(printName.print(int8values)).to.equal('Int8Array') - }) - - it('supports instanceof Uint8Array', () => { - const values = [1, 2, 3, 4] - expect([...printName.typedArray('Uint8Array', values)]).to.deep.equal(values) - - const uint8values = new Uint8Array(values) - expect(printName.typedArray('Uint8Array', uint8values)).to.deep.equal(uint8values) - expect(printName.print(uint8values)).to.equal('Uint8Array') - }) - - it('supports instanceof Uint8ClampedArray', () => { - const values = [1, 2, 3, 4] - expect([...printName.typedArray('Uint8ClampedArray', values)]).to.deep.equal(values) - - const uint8values = new Uint8ClampedArray(values) - expect(printName.typedArray('Uint8ClampedArray', uint8values)).to.deep.equal(uint8values) - expect(printName.print(uint8values)).to.equal('Uint8ClampedArray') - }) - - it('supports instanceof Int16Array', () => { - const values = [0x1234, 0x2345, 0x3456, 0x4567] - expect([...printName.typedArray('Int16Array', values)]).to.deep.equal(values) - - const int16values = new Int16Array(values) - expect(printName.typedArray('Int16Array', int16values)).to.deep.equal(int16values) - expect(printName.print(int16values)).to.equal('Int16Array') - }) - - it('supports instanceof Uint16Array', () => { - const values = [0x1234, 0x2345, 0x3456, 0x4567] - expect([...printName.typedArray('Uint16Array', values)]).to.deep.equal(values) - - const uint16values = new Uint16Array(values) - expect(printName.typedArray('Uint16Array', uint16values)).to.deep.equal(uint16values) - expect(printName.print(uint16values)).to.equal('Uint16Array') - }) - - it('supports instanceof Int32Array', () => { - const values = [0x12345678, 0x23456789] - expect([...printName.typedArray('Int32Array', values)]).to.deep.equal(values) - - const int32values = new Int32Array(values) - expect(printName.typedArray('Int32Array', int32values)).to.deep.equal(int32values) - expect(printName.print(int32values)).to.equal('Int32Array') - }) - - it('supports instanceof Uint32Array', () => { - const values = [0x12345678, 0x23456789] - expect([...printName.typedArray('Uint32Array', values)]).to.deep.equal(values) - - const uint32values = new Uint32Array(values) - expect(printName.typedArray('Uint32Array', uint32values)).to.deep.equal(uint32values) - expect(printName.print(uint32values)).to.equal('Uint32Array') - }) - - it('supports instanceof Float32Array', () => { - const values = [0.5, 1.0, 1.5] - expect([...printName.typedArray('Float32Array', values)]).to.deep.equal(values) - - const float32values = new Float32Array() - expect(printName.typedArray('Float32Array', float32values)).to.deep.equal(float32values) - expect(printName.print(float32values)).to.equal('Float32Array') - }) - - it('supports instanceof Float64Array', () => { - const values = [0.5, 1.0, 1.5] - expect([...printName.typedArray('Float64Array', values)]).to.deep.equal(values) - - const float64values = new Float64Array([0.5, 1.0, 1.5]) - expect(printName.typedArray('Float64Array', float64values)).to.deep.equal(float64values) - expect(printName.print(float64values)).to.equal('Float64Array') - }) - }) - - describe('remote promise', () => { - it('can be used as promise in each side', (done) => { - const promise = remote.require(path.join(fixtures, 'module', 'promise.js')) - promise.twicePromise(Promise.resolve(1234)).then((value) => { - expect(value).to.equal(2468) - done() - }) - }) - - it('handles rejections via catch(onRejected)', (done) => { - const promise = remote.require(path.join(fixtures, 'module', 'rejected-promise.js')) - promise.reject(Promise.resolve(1234)).catch((error) => { - expect(error.message).to.equal('rejected') - done() - }) - }) - - it('handles rejections via then(onFulfilled, onRejected)', (done) => { - const promise = remote.require(path.join(fixtures, 'module', 'rejected-promise.js')) - promise.reject(Promise.resolve(1234)).then(() => {}, (error) => { - expect(error.message).to.equal('rejected') - done() - }) - }) - - it('does not emit unhandled rejection events in the main process', (done) => { - remote.process.once('unhandledRejection', function (reason) { - done(reason) - }) - - const promise = remote.require(path.join(fixtures, 'module', 'unhandled-rejection.js')) - promise.reject().then(() => { - done(new Error('Promise was not rejected')) - }).catch((error) => { - expect(error.message).to.equal('rejected') - done() - }) - }) - - it('emits unhandled rejection events in the renderer process', (done) => { - window.addEventListener('unhandledrejection', function handler (event) { - event.preventDefault() - expect(event.reason.message).to.equal('rejected') - window.removeEventListener('unhandledrejection', handler) - done() - }) - - const promise = remote.require(path.join(fixtures, 'module', 'unhandled-rejection.js')) - promise.reject().then(() => { - done(new Error('Promise was not rejected')) - }) - }) - }) - - describe('remote webContents', () => { - it('can return same object with different getters', () => { - const contents1 = remote.getCurrentWindow().webContents - const contents2 = remote.getCurrentWebContents() - expect(contents1).to.equal(contents2) - }) - }) - - describe('remote class', () => { - const cl = remote.require(path.join(fixtures, 'module', 'class.js')) - const base = cl.base - let derived = cl.derived - - it('can get methods', () => { - expect(base.method()).to.equal('method') - }) - - it('can get properties', () => { - expect(base.readonly).to.equal('readonly') - }) - - it('can change properties', () => { - expect(base.value).to.equal('old') - base.value = 'new' - expect(base.value).to.equal('new') - base.value = 'old' - }) - - it('has unenumerable methods', () => { - expect(base).to.not.have.own.property('method') - expect(Object.getPrototypeOf(base)).to.have.own.property('method') - }) - - it('keeps prototype chain in derived class', () => { - expect(derived.method()).to.equal('method') - expect(derived.readonly).to.equal('readonly') - expect(derived).to.not.have.own.property('method') - const proto = Object.getPrototypeOf(derived) - expect(proto).to.not.have.own.property('method') - expect(Object.getPrototypeOf(proto)).to.have.own.property('method') - }) - - it('is referenced by methods in prototype chain', () => { - const method = derived.method - derived = null - global.gc() - expect(method()).to.equal('method') - }) - }) - - describe('remote exception', () => { - const throwFunction = remote.require(path.join(fixtures, 'module', 'exception.js')) - - it('throws errors from the main process', () => { - expect(() => { - throwFunction() - }).to.throw() - }) - - it('throws custom errors from the main process', () => { - const err = new Error('error') - err.cause = new Error('cause') - err.prop = 'error prop' - try { - throwFunction(err) - } catch (error) { - expect(error.from).to.equal('browser') - expect(error.cause).to.deep.equal(...resolveGetters(err)) - } - }) - }) - - describe('remote function in renderer', () => { - let w = null - - afterEach(() => closeWindow(w).then(() => { w = null })) - afterEach(() => { - ipcMain.removeAllListeners('done') - }) - - it('works when created in preload script', (done) => { - ipcMain.once('done', () => w.close()) - const preload = path.join(fixtures, 'module', 'preload-remote-function.js') - w = new BrowserWindow({ - show: false, - webPreferences: { - preload - } - }) - w.once('closed', () => done()) - w.loadURL('about:blank') - }) - }) -}) diff --git a/spec/api-shell-spec.js b/spec/api-shell-spec.js index 346d3f9521bd5..284dd62864d82 100644 --- a/spec/api-shell-spec.js +++ b/spec/api-shell-spec.js @@ -1,19 +1,13 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') +const { expect } = require('chai'); -const fs = require('fs') -const path = require('path') -const os = require('os') -const { shell, remote } = require('electron') -const { BrowserWindow } = remote - -const { closeWindow } = require('./window-helpers') - -const { expect } = chai -chai.use(dirtyChai) +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const http = require('http'); +const { shell } = require('electron'); describe('shell module', () => { - const fixtures = path.resolve(__dirname, 'fixtures') + const fixtures = path.resolve(__dirname, 'fixtures'); const shortcutOptions = { target: 'C:\\target', description: 'description', @@ -21,112 +15,61 @@ describe('shell module', () => { args: 'args', appUserModelId: 'appUserModelId', icon: 'icon', - iconIndex: 1 - } - - describe('shell.openExternal()', () => { - let envVars = {} - let w - - beforeEach(function () { - envVars = { - display: process.env.DISPLAY, - de: process.env.DE, - browser: process.env.BROWSER - } - }) - - afterEach(async () => { - await closeWindow(w) - w = null - // reset env vars to prevent side effects - if (process.platform === 'linux') { - process.env.DE = envVars.de - process.env.BROWSER = envVars.browser - process.env.DISPLAY = envVars.display - } - }) - - it('opens an external link', done => { - const url = 'http://www.example.com' - if (process.platform === 'linux') { - process.env.BROWSER = '/bin/true' - process.env.DE = 'generic' - process.env.DISPLAY = '' - } - - // Ensure an external window is activated via a new window's blur event - w = new BrowserWindow() - let promiseResolved = false - let blurEventEmitted = false - - w.on('blur', () => { - blurEventEmitted = true - if (promiseResolved) { - done() - } - }) - - shell.openExternal(url).then(() => { - promiseResolved = true - if (blurEventEmitted || process.platform === 'linux') { - done() - } - }) - }) - }) + iconIndex: 1, + toastActivatorClsid: '{0E3CFA27-6FEA-410B-824F-A174B6E865E5}' + }; describe('shell.readShortcutLink(shortcutPath)', () => { beforeEach(function () { - if (process.platform !== 'win32') this.skip() - }) + if (process.platform !== 'win32') this.skip(); + }); it('throws when failed', () => { expect(() => { - shell.readShortcutLink('not-exist') - }).to.throw('Failed to read shortcut link') - }) + shell.readShortcutLink('not-exist'); + }).to.throw('Failed to read shortcut link'); + }); it('reads all properties of a shortcut', () => { - const shortcut = shell.readShortcutLink(path.join(fixtures, 'assets', 'shortcut.lnk')) - expect(shortcut).to.deep.equal(shortcutOptions) - }) - }) + const shortcut = shell.readShortcutLink(path.join(fixtures, 'assets', 'shortcut.lnk')); + expect(shortcut).to.deep.equal(shortcutOptions); + }); + }); describe('shell.writeShortcutLink(shortcutPath[, operation], options)', () => { beforeEach(function () { - if (process.platform !== 'win32') this.skip() - }) + if (process.platform !== 'win32') this.skip(); + }); - const tmpShortcut = path.join(os.tmpdir(), `${Date.now()}.lnk`) + const tmpShortcut = path.join(os.tmpdir(), `${Date.now()}.lnk`); afterEach(() => { - fs.unlinkSync(tmpShortcut) - }) + fs.unlinkSync(tmpShortcut); + }); it('writes the shortcut', () => { - expect(shell.writeShortcutLink(tmpShortcut, { target: 'C:\\' })).to.be.true() - expect(fs.existsSync(tmpShortcut)).to.be.true() - }) + expect(shell.writeShortcutLink(tmpShortcut, { target: 'C:\\' })).to.be.true(); + expect(fs.existsSync(tmpShortcut)).to.be.true(); + }); it('correctly sets the fields', () => { - expect(shell.writeShortcutLink(tmpShortcut, shortcutOptions)).to.be.true() - expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(shortcutOptions) - }) + expect(shell.writeShortcutLink(tmpShortcut, shortcutOptions)).to.be.true(); + expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(shortcutOptions); + }); it('updates the shortcut', () => { - expect(shell.writeShortcutLink(tmpShortcut, 'update', shortcutOptions)).to.be.false() - expect(shell.writeShortcutLink(tmpShortcut, 'create', shortcutOptions)).to.be.true() - expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(shortcutOptions) - const change = { target: 'D:\\' } - expect(shell.writeShortcutLink(tmpShortcut, 'update', change)).to.be.true() - expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(Object.assign(shortcutOptions, change)) - }) + expect(shell.writeShortcutLink(tmpShortcut, 'update', shortcutOptions)).to.be.false(); + expect(shell.writeShortcutLink(tmpShortcut, 'create', shortcutOptions)).to.be.true(); + expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(shortcutOptions); + const change = { target: 'D:\\' }; + expect(shell.writeShortcutLink(tmpShortcut, 'update', change)).to.be.true(); + expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(Object.assign(shortcutOptions, change)); + }); it('replaces the shortcut', () => { - expect(shell.writeShortcutLink(tmpShortcut, 'replace', shortcutOptions)).to.be.false() - expect(shell.writeShortcutLink(tmpShortcut, 'create', shortcutOptions)).to.be.true() - expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(shortcutOptions) + expect(shell.writeShortcutLink(tmpShortcut, 'replace', shortcutOptions)).to.be.false(); + expect(shell.writeShortcutLink(tmpShortcut, 'create', shortcutOptions)).to.be.true(); + expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(shortcutOptions); const change = { target: 'D:\\', description: 'description2', @@ -134,10 +77,11 @@ describe('shell module', () => { args: 'args2', appUserModelId: 'appUserModelId2', icon: 'icon2', - iconIndex: 2 - } - expect(shell.writeShortcutLink(tmpShortcut, 'replace', change)).to.be.true() - expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(change) - }) - }) -}) + iconIndex: 2, + toastActivatorClsid: '{C51A3996-CAD9-4934-848B-16285D4A1496}' + }; + expect(shell.writeShortcutLink(tmpShortcut, 'replace', change)).to.be.true(); + expect(shell.readShortcutLink(tmpShortcut)).to.deep.equal(change); + }); + }); +}); diff --git a/spec/api-subframe-spec.js b/spec/api-subframe-spec.js deleted file mode 100644 index bf1b11418c7a2..0000000000000 --- a/spec/api-subframe-spec.js +++ /dev/null @@ -1,267 +0,0 @@ -const { expect } = require('chai') -const { remote } = require('electron') -const path = require('path') -const http = require('http') - -const { emittedNTimes, emittedOnce } = require('./events-helpers') -const { closeWindow } = require('./window-helpers') - -const { app, BrowserWindow, ipcMain } = remote - -describe('renderer nodeIntegrationInSubFrames', () => { - const generateTests = (description, webPreferences) => { - describe(description, () => { - const fixtureSuffix = webPreferences.webviewTag ? '-webview' : '' - let w - - beforeEach(async () => { - await closeWindow(w) - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences - }) - }) - - afterEach(() => { - return closeWindow(w).then(() => { - w = null - }) - }) - - it('should load preload scripts in top level iframes', async () => { - const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2) - w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`)) - const [event1, event2] = await detailsPromise - expect(event1[0].frameId).to.not.equal(event2[0].frameId) - expect(event1[0].frameId).to.equal(event1[2]) - expect(event2[0].frameId).to.equal(event2[2]) - }) - - it('should load preload scripts in nested iframes', async () => { - const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3) - w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`)) - const [event1, event2, event3] = await detailsPromise - expect(event1[0].frameId).to.not.equal(event2[0].frameId) - expect(event1[0].frameId).to.not.equal(event3[0].frameId) - expect(event2[0].frameId).to.not.equal(event3[0].frameId) - expect(event1[0].frameId).to.equal(event1[2]) - expect(event2[0].frameId).to.equal(event2[2]) - expect(event3[0].frameId).to.equal(event3[2]) - }) - - it('should correctly reply to the main frame with using event.reply', async () => { - const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2) - w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`)) - const [event1] = await detailsPromise - const pongPromise = emittedOnce(ipcMain, 'preload-pong') - event1[0].reply('preload-ping') - const details = await pongPromise - expect(details[1]).to.equal(event1[0].frameId) - }) - - it('should correctly reply to the sub-frames with using event.reply', async () => { - const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2) - w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`)) - const [, event2] = await detailsPromise - const pongPromise = emittedOnce(ipcMain, 'preload-pong') - event2[0].reply('preload-ping') - const details = await pongPromise - expect(details[1]).to.equal(event2[0].frameId) - }) - - it('should correctly reply to the nested sub-frames with using event.reply', async () => { - const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3) - w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`)) - const [, , event3] = await detailsPromise - const pongPromise = emittedOnce(ipcMain, 'preload-pong') - event3[0].reply('preload-ping') - const details = await pongPromise - expect(details[1]).to.equal(event3[0].frameId) - }) - - it('should not expose globals in main world', async () => { - const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2) - w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`)) - const details = await detailsPromise - const senders = details.map(event => event[0].sender) - await new Promise(async resolve => { - let resultCount = 0 - senders.forEach(async sender => { - const result = await sender.webContents.executeJavaScript('window.isolatedGlobal') - if (webPreferences.contextIsolation) { - expect(result).to.be.null() - } else { - expect(result).to.equal(true) - } - resultCount++ - if (resultCount === senders.length) resolve() - }) - }) - }) - }) - } - - const generateConfigs = (webPreferences, ...permutations) => { - const configs = [{ webPreferences, names: [] }] - for (let i = 0; i < permutations.length; i++) { - const length = configs.length - for (let j = 0; j < length; j++) { - const newConfig = Object.assign({}, configs[j]) - newConfig.webPreferences = Object.assign({}, - newConfig.webPreferences, permutations[i].webPreferences) - newConfig.names = newConfig.names.slice(0) - newConfig.names.push(permutations[i].name) - configs.push(newConfig) - } - } - - return configs.map(config => { - if (config.names.length > 0) { - config.title = `with ${config.names.join(', ')} on` - } else { - config.title = `without anything special turned on` - } - delete config.names - - return config - }) - } - - generateConfigs( - { - preload: path.resolve(__dirname, 'fixtures/sub-frames/preload.js'), - nodeIntegrationInSubFrames: true - }, - { - name: 'sandbox', - webPreferences: { sandbox: true } - }, - { - name: 'context isolation', - webPreferences: { contextIsolation: true } - }, - { - name: 'webview', - webPreferences: { webviewTag: true, preload: false } - } - ).forEach(config => { - generateTests(config.title, config.webPreferences) - }) - - describe('internal ') - resp = res - // don't end the response yet - }) - await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) - const { port } = s.address() - const p = new Promise(resolve => { - w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { - if (!isMainFrame) { - resolve() - } - }) - }) - const main = w.loadURL(`http://127.0.0.1:${port}`) - await p - resp.end() - await main - s.close() - }) - - it("doesn't resolve when a subframe loads", async () => { - let resp = null - const s = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.write('') - resp = res - // don't end the response yet - }) - await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) - const { port } = s.address() - const p = new Promise(resolve => { - w.webContents.on('did-frame-finish-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { - if (!isMainFrame) { - resolve() - } - }) - }) - const main = w.loadURL(`http://127.0.0.1:${port}`) - await p - resp.destroy() // cause the main request to fail - await expect(main).to.eventually.be.rejected - .and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING - s.close() - }) - }) - - describe('getFocusedWebContents() API', () => { - it('returns the focused web contents', (done) => { - if (isCi) return done() - - const specWebContents = remote.getCurrentWebContents() - expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id) - - specWebContents.once('devtools-opened', () => { - expect(specWebContents.devToolsWebContents.id).to.equal(webContents.getFocusedWebContents().id) - specWebContents.closeDevTools() - }) - - specWebContents.once('devtools-closed', () => { - expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id) - done() - }) - - specWebContents.openDevTools() - }) - - it('does not crash when called on a detached dev tools window', (done) => { - const specWebContents = w.webContents - - specWebContents.once('devtools-opened', () => { - expect(() => { - webContents.getFocusedWebContents() - }).to.not.throw() - specWebContents.closeDevTools() - }) - - specWebContents.once('devtools-closed', () => { - expect(() => { - webContents.getFocusedWebContents() - }).to.not.throw() - done() - }) - - specWebContents.openDevTools({ mode: 'detach' }) - w.inspectElement(100, 100) - }) - }) - - describe('setDevToolsWebContents() API', () => { - it('sets arbitrary webContents as devtools', async () => { - const devtools = new BrowserWindow({ show: false }) - const promise = emittedOnce(devtools.webContents, 'dom-ready') - w.webContents.setDevToolsWebContents(devtools.webContents) - w.webContents.openDevTools() - await promise - expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true() - const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name') - expect(result).to.equal('InspectorFrontendHostImpl') - devtools.destroy() - }) - }) - - describe('isFocused() API', () => { - it('returns false when the window is hidden', () => { - BrowserWindow.getAllWindows().forEach((window) => { - expect(!window.isVisible() && window.webContents.isFocused()).to.be.false() - }) - }) - }) - - describe('isCurrentlyAudible() API', () => { - it('returns whether audio is playing', async () => { - const webContents = remote.getCurrentWebContents() - const context = new window.AudioContext() - // Start in suspended state, because of the - // new web audio api policy. - context.suspend() - const oscillator = context.createOscillator() - oscillator.connect(context.destination) - oscillator.start() - let p = emittedOnce(webContents, '-audio-state-changed') - await context.resume() - await p - expect(webContents.isCurrentlyAudible()).to.be.true() - p = emittedOnce(webContents, '-audio-state-changed') - oscillator.stop() - await p - expect(webContents.isCurrentlyAudible()).to.be.false() - oscillator.disconnect() - context.close() - }) - }) - - describe('getWebPreferences() API', () => { - it('should not crash when called for devTools webContents', (done) => { - w.webContents.openDevTools() - w.webContents.once('devtools-opened', () => { - expect(w.devToolsWebContents.getWebPreferences()).to.be.null() - done() - }) - }) - }) - - describe('openDevTools() API', () => { - it('can show window with activation', async () => { - const focused = emittedOnce(w, 'focus') - w.show() - await focused - expect(w.isFocused()).to.be.true() - const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened') - w.webContents.openDevTools({ mode: 'detach', activate: true }) - await devtoolsOpened - expect(w.isFocused()).to.be.false() - }) - - it('can show window without activation', async () => { - const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened') - w.webContents.openDevTools({ mode: 'detach', activate: false }) - await devtoolsOpened - expect(w.isDevToolsOpened()).to.be.true() - }) - }) - - describe('before-input-event event', () => { - it('can prevent document keyboard events', async () => { - await w.loadFile(path.join(fixtures, 'pages', 'key-events.html')) - const keyDown = new Promise(resolve => { - ipcMain.once('keydown', (event, key) => resolve(key)) - }) - ipcRenderer.sendSync('prevent-next-input-event', 'a', w.webContents.id) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' }) - expect(await keyDown).to.equal('b') - }) - - it('has the correct properties', async () => { - await w.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - const testBeforeInput = async (opts) => { - const modifiers = [] - if (opts.shift) modifiers.push('shift') - if (opts.control) modifiers.push('control') - if (opts.alt) modifiers.push('alt') - if (opts.meta) modifiers.push('meta') - if (opts.isAutoRepeat) modifiers.push('isAutoRepeat') - - const p = emittedOnce(w.webContents, 'before-input-event') - w.webContents.sendInputEvent({ - type: opts.type, - keyCode: opts.keyCode, - modifiers: modifiers - }) - const [, input] = await p - - expect(input.type).to.equal(opts.type) - expect(input.key).to.equal(opts.key) - expect(input.code).to.equal(opts.code) - expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat) - expect(input.shift).to.equal(opts.shift) - expect(input.control).to.equal(opts.control) - expect(input.alt).to.equal(opts.alt) - expect(input.meta).to.equal(opts.meta) - } - await testBeforeInput({ - type: 'keyDown', - key: 'A', - code: 'KeyA', - keyCode: 'a', - shift: true, - control: true, - alt: true, - meta: true, - isAutoRepeat: true - }) - await testBeforeInput({ - type: 'keyUp', - key: '.', - code: 'Period', - keyCode: '.', - shift: false, - control: true, - alt: true, - meta: false, - isAutoRepeat: false - }) - await testBeforeInput({ - type: 'keyUp', - key: '!', - code: 'Digit1', - keyCode: '1', - shift: true, - control: false, - alt: false, - meta: true, - isAutoRepeat: false - }) - await testBeforeInput({ - type: 'keyUp', - key: 'Tab', - code: 'Tab', - keyCode: 'Tab', - shift: false, - control: true, - alt: false, - meta: false, - isAutoRepeat: true - }) - }) - }) - - describe('zoom-changed', () => { - beforeEach(function () { - // On Mac, zooming isn't done with the mouse wheel. - if (process.platform === 'darwin') { - return closeWindow(w).then(() => { - w = null - this.skip() - }) - } - }) - - it('is emitted with the correct zooming info', async () => { - w.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - await emittedOnce(w.webContents, 'did-finish-load') - - const testZoomChanged = async ({ zoomingIn }) => { - const promise = emittedOnce(w.webContents, 'zoom-changed') - - w.webContents.sendInputEvent({ - type: 'mousewheel', - x: 300, - y: 300, - deltaX: 0, - deltaY: zoomingIn ? 1 : -1, - wheelTicksX: 0, - wheelTicksY: zoomingIn ? 1 : -1, - phase: 'began', - modifiers: ['control', 'meta'] - }) - - const [, zoomDirection] = await promise - expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out') - } - - await testZoomChanged({ zoomingIn: true }) - await testZoomChanged({ zoomingIn: false }) - }) - }) - - describe('devtools window', () => { - let testFn = it - if (process.platform === 'darwin' && isCi) { - testFn = it.skip - } - if (process.platform === 'win32' && isCi) { - testFn = it.skip - } - try { - // We have other tests that check if native modules work, if we fail to require - // robotjs let's skip this test to avoid false negatives - require('robotjs') - } catch (err) { - testFn = it.skip - } - - testFn('can receive and handle menu events', async function () { - this.timeout(5000) - w.show() - w.loadFile(path.join(fixtures, 'pages', 'key-events.html')) - // Ensure the devtools are loaded - w.webContents.closeDevTools() - const opened = emittedOnce(w.webContents, 'devtools-opened') - w.webContents.openDevTools() - await opened - await emittedOnce(w.webContents.devToolsWebContents, 'did-finish-load') - w.webContents.devToolsWebContents.focus() - - // Focus an input field - await w.webContents.devToolsWebContents.executeJavaScript( - `const input = document.createElement('input'); - document.body.innerHTML = ''; - document.body.appendChild(input) - input.focus();` - ) - - // Write something to the clipboard - clipboard.writeText('test value') - - // Fake a paste request using robotjs to emulate a REAL keyboard paste event - require('robotjs').keyTap('v', process.platform === 'darwin' ? ['command'] : ['control']) - - const start = Date.now() - let val - - // Check every now and again for the pasted value (paste is async) - while (val !== 'test value' && Date.now() - start <= 1000) { - val = await w.webContents.devToolsWebContents.executeJavaScript( - `document.querySelector('input').value` - ) - await new Promise(resolve => setTimeout(resolve, 10)) - } - - // Once we're done expect the paste to have been successful - expect(val).to.equal('test value', 'value should eventually become the pasted value') - }) - }) - - describe('sendInputEvent(event)', () => { - beforeEach(async () => { - await w.loadFile(path.join(fixtures, 'pages', 'key-events.html')) - }) - - it('can send keydown events', (done) => { - ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('a') - expect(code).to.equal('KeyA') - expect(keyCode).to.equal(65) - expect(shiftKey).to.be.false() - expect(ctrlKey).to.be.false() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }) - }) - - it('can send keydown events with modifiers', (done) => { - ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('Z') - expect(code).to.equal('KeyZ') - expect(keyCode).to.equal(90) - expect(shiftKey).to.be.true() - expect(ctrlKey).to.be.true() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }) - }) - - it('can send keydown events with special keys', (done) => { - ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('Tab') - expect(code).to.equal('Tab') - expect(keyCode).to.equal(9) - expect(shiftKey).to.be.false() - expect(ctrlKey).to.be.false() - expect(altKey).to.be.true() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] }) - }) - - it('can send char events', (done) => { - ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('a') - expect(code).to.equal('KeyA') - expect(keyCode).to.equal(65) - expect(shiftKey).to.be.false() - expect(ctrlKey).to.be.false() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }) - w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' }) - }) - - it('can send char events with modifiers', (done) => { - ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('Z') - expect(code).to.equal('KeyZ') - expect(keyCode).to.equal(90) - expect(shiftKey).to.be.true() - expect(ctrlKey).to.be.true() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' }) - w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }) - }) - }) - - it('supports inserting CSS', async () => { - w.loadURL('about:blank') - await w.webContents.insertCSS('body { background-repeat: round; }') - const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")') - expect(result).to.equal('round') - }) - - it('supports removing inserted CSS', async () => { - w.loadURL('about:blank') - const key = await w.webContents.insertCSS('body { background-repeat: round; }') - await w.webContents.removeInsertedCSS(key) - const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")') - expect(result).to.equal('repeat') - }) - - it('supports inspecting an element in the devtools', (done) => { - w.loadURL('about:blank') - w.webContents.once('devtools-opened', () => { done() }) - w.webContents.inspectElement(10, 10) - }) - - describe('startDrag({file, icon})', () => { - it('throws errors for a missing file or a missing/empty icon', () => { - expect(() => { - w.webContents.startDrag({ icon: path.join(fixtures, 'assets', 'logo.png') }) - }).to.throw(`Must specify either 'file' or 'files' option`) - - expect(() => { - w.webContents.startDrag({ file: __filename }) - }).to.throw(`Must specify 'icon' option`) - - if (process.platform === 'darwin') { - expect(() => { - w.webContents.startDrag({ file: __filename, icon: __filename }) - }).to.throw(`Must specify non-empty 'icon' option`) - } - }) - }) - - describe('focus()', () => { - describe('when the web contents is hidden', () => { - it('does not blur the focused window', (done) => { - ipcMain.once('answer', (event, parentFocused, childFocused) => { - expect(parentFocused).to.be.true() - expect(childFocused).to.be.false() - done() - }) - w.show() - w.loadFile(path.join(fixtures, 'pages', 'focus-web-contents.html')) - }) - }) - }) - - describe('getOSProcessId()', () => { - it('returns a valid procress id', async () => { - expect(w.webContents.getOSProcessId()).to.equal(0) - - await w.loadURL('about:blank') - expect(w.webContents.getOSProcessId()).to.be.above(0) - }) - }) - - describe('zoom api', () => { - const zoomScheme = remote.getGlobal('zoomScheme') - const hostZoomMap = { - host1: 0.3, - host2: 0.7, - host3: 0.2 - } - - before((done) => { - const protocol = session.defaultSession.protocol - protocol.registerStringProtocol(zoomScheme, (request, callback) => { - const response = `` - callback({ data: response, mimeType: 'text/html' }) - }, (error) => done(error)) - }) - - after((done) => { - const protocol = session.defaultSession.protocol - protocol.unregisterProtocol(zoomScheme, (error) => done(error)) - }) - - // TODO(codebytere): remove in Electron v8.0.0 - it('can set the correct zoom level (functions)', async () => { - try { - await w.loadURL('about:blank') - const zoomLevel = w.webContents.getZoomLevel() - expect(zoomLevel).to.eql(0.0) - w.webContents.setZoomLevel(0.5) - const newZoomLevel = w.webContents.getZoomLevel() - expect(newZoomLevel).to.eql(0.5) - } finally { - w.webContents.setZoomLevel(0) - } - }) - - it('can set the correct zoom level', async () => { - try { - await w.loadURL('about:blank') - const zoomLevel = w.webContents.zoomLevel - expect(zoomLevel).to.eql(0.0) - w.webContents.zoomLevel = 0.5 - const newZoomLevel = w.webContents.zoomLevel - expect(newZoomLevel).to.eql(0.5) - } finally { - w.webContents.zoomLevel = 0 - } - }) - - it('can persist zoom level across navigation', (done) => { - let finalNavigation = false - ipcMain.on('set-zoom', (e, host) => { - const zoomLevel = hostZoomMap[host] - if (!finalNavigation) w.webContents.zoomLevel = zoomLevel - console.log() - e.sender.send(`${host}-zoom-set`) - }) - ipcMain.on('host1-zoom-level', (e, zoomLevel) => { - const expectedZoomLevel = hostZoomMap.host1 - expect(zoomLevel).to.equal(expectedZoomLevel) - if (finalNavigation) { - done() - } else { - w.loadURL(`${zoomScheme}://host2`) - } - }) - ipcMain.once('host2-zoom-level', (e, zoomLevel) => { - const expectedZoomLevel = hostZoomMap.host2 - expect(zoomLevel).to.equal(expectedZoomLevel) - finalNavigation = true - w.webContents.goBack() - }) - w.loadURL(`${zoomScheme}://host1`) - }) - - it('can propagate zoom level across same session', (done) => { - const w2 = new BrowserWindow({ - show: false - }) - w2.webContents.on('did-finish-load', () => { - const zoomLevel1 = w.webContents.zoomLevel - expect(zoomLevel1).to.equal(hostZoomMap.host3) - - const zoomLevel2 = w2.webContents.zoomLevel - expect(zoomLevel1).to.equal(zoomLevel2) - w2.setClosable(true) - w2.close() - done() - }) - w.webContents.on('did-finish-load', () => { - w.webContents.zoomLevel = hostZoomMap.host3 - w2.loadURL(`${zoomScheme}://host3`) - }) - w.loadURL(`${zoomScheme}://host3`) - }) - - it('cannot propagate zoom level across different session', (done) => { - const w2 = new BrowserWindow({ - show: false, - webPreferences: { - partition: 'temp' - } - }) - const protocol = w2.webContents.session.protocol - protocol.registerStringProtocol(zoomScheme, (request, callback) => { - callback('hello') - }, (error) => { - if (error) return done(error) - w2.webContents.on('did-finish-load', () => { - const zoomLevel1 = w.webContents.zoomLevel - expect(zoomLevel1).to.equal(hostZoomMap.host3) - - const zoomLevel2 = w2.webContents.zoomLevel - expect(zoomLevel2).to.equal(0) - expect(zoomLevel1).to.not.equal(zoomLevel2) - - protocol.unregisterProtocol(zoomScheme, (error) => { - if (error) return done(error) - w2.setClosable(true) - w2.close() - done() - }) - }) - w.webContents.on('did-finish-load', () => { - w.webContents.zoomLevel = hostZoomMap.host3 - w2.loadURL(`${zoomScheme}://host3`) - }) - w.loadURL(`${zoomScheme}://host3`) - }) - }) - - it('can persist when it contains iframe', (done) => { - const server = http.createServer((req, res) => { - setTimeout(() => { - res.end() - }, 200) - }) - server.listen(0, '127.0.0.1', () => { - const url = 'http://127.0.0.1:' + server.address().port - const content = `` - w.webContents.on('did-frame-finish-load', (e, isMainFrame) => { - if (!isMainFrame) { - const zoomLevel = w.webContents.zoomLevel - expect(zoomLevel).to.equal(2.0) - - w.webContents.zoomLevel = 0 - server.close() - done() - } - }) - w.webContents.on('dom-ready', () => { - w.webContents.zoomLevel = 2.0 - }) - w.loadURL(`data:text/html,${content}`) - }) - }) - - it('cannot propagate when used with webframe', (done) => { - let finalZoomLevel = 0 - const w2 = new BrowserWindow({ - show: false - }) - w2.webContents.on('did-finish-load', () => { - const zoomLevel1 = w.webContents.zoomLevel - expect(zoomLevel1).to.equal(finalZoomLevel) - - const zoomLevel2 = w2.webContents.zoomLevel - expect(zoomLevel2).to.equal(0) - expect(zoomLevel1).to.not.equal(zoomLevel2) - - w2.setClosable(true) - w2.close() - done() - }) - ipcMain.once('temporary-zoom-set', (e, zoomLevel) => { - w2.loadFile(path.join(fixtures, 'pages', 'c.html')) - finalZoomLevel = zoomLevel - }) - w.loadFile(path.join(fixtures, 'pages', 'webframe-zoom.html')) - }) - - it('cannot persist zoom level after navigation with webFrame', (done) => { - let initialNavigation = true - const source = ` - const {ipcRenderer, webFrame} = require('electron') - webFrame.setZoomLevel(0.6) - ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel()) - ` - w.webContents.on('did-finish-load', () => { - if (initialNavigation) { - w.webContents.executeJavaScript(source) - } else { - const zoomLevel = w.webContents.zoomLevel - expect(zoomLevel).to.equal(0) - done() - } - }) - ipcMain.once('zoom-level-set', (e, zoomLevel) => { - expect(zoomLevel).to.equal(0.6) - w.loadFile(path.join(fixtures, 'pages', 'd.html')) - initialNavigation = false - }) - w.loadFile(path.join(fixtures, 'pages', 'c.html')) - }) - }) - - describe('webrtc ip policy api', () => { - it('can set and get webrtc ip policies', () => { - const policies = [ - 'default', - 'default_public_interface_only', - 'default_public_and_private_interfaces', - 'disable_non_proxied_udp' - ] - policies.forEach((policy) => { - w.webContents.setWebRTCIPHandlingPolicy(policy) - expect(w.webContents.getWebRTCIPHandlingPolicy()).to.equal(policy) - }) - }) - }) - - describe('render view deleted events', () => { - let server = null - - before((done) => { - server = http.createServer((req, res) => { - const respond = () => { - if (req.url === '/redirect-cross-site') { - res.setHeader('Location', `${server.cross_site_url}/redirected`) - res.statusCode = 302 - res.end() - } else if (req.url === '/redirected') { - res.end('') - } else { - res.end() - } - } - setTimeout(respond, 0) - }) - server.listen(0, '127.0.0.1', () => { - server.url = `http://127.0.0.1:${server.address().port}` - server.cross_site_url = `http://localhost:${server.address().port}` - done() - }) - }) - - after(() => { - server.close() - server = null - }) - - it('does not emit current-render-view-deleted when speculative RVHs are deleted', (done) => { - let currentRenderViewDeletedEmitted = false - w.webContents.once('destroyed', () => { - expect(currentRenderViewDeletedEmitted).to.be.false('current-render-view-deleted was emitted') - done() - }) - const renderViewDeletedHandler = () => { - currentRenderViewDeletedEmitted = true - } - w.webContents.on('current-render-view-deleted', renderViewDeletedHandler) - w.webContents.on('did-finish-load', (e) => { - w.webContents.removeListener('current-render-view-deleted', renderViewDeletedHandler) - w.close() - }) - w.loadURL(`${server.url}/redirect-cross-site`) - }) - - it('emits current-render-view-deleted if the current RVHs are deleted', (done) => { - let currentRenderViewDeletedEmitted = false - w.webContents.once('destroyed', () => { - expect(currentRenderViewDeletedEmitted).to.be.true('current-render-view-deleted wasn\'t emitted') - done() - }) - w.webContents.on('current-render-view-deleted', () => { - currentRenderViewDeletedEmitted = true - }) - w.webContents.on('did-finish-load', (e) => { - w.close() - }) - w.loadURL(`${server.url}/redirect-cross-site`) - }) - - it('emits render-view-deleted if any RVHs are deleted', (done) => { - let rvhDeletedCount = 0 - w.webContents.once('destroyed', () => { - const expectedRenderViewDeletedEventCount = 3 // 1 speculative upon redirection + 2 upon window close. - expect(rvhDeletedCount).to.equal(expectedRenderViewDeletedEventCount, 'render-view-deleted wasn\'t emitted the expected nr. of times') - done() - }) - w.webContents.on('render-view-deleted', () => { - rvhDeletedCount++ - }) - w.webContents.on('did-finish-load', (e) => { - w.close() - }) - w.loadURL(`${server.url}/redirect-cross-site`) - }) - }) - - describe('setIgnoreMenuShortcuts(ignore)', () => { - it('does not throw', () => { - expect(() => { - w.webContents.setIgnoreMenuShortcuts(true) - w.webContents.setIgnoreMenuShortcuts(false) - }).to.not.throw() - }) - }) - - describe('create()', () => { - it('does not crash on exit', async () => { - const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontents.js') - const electronPath = remote.getGlobal('process').execPath - const appProcess = ChildProcess.spawn(electronPath, [appPath]) - const [code] = await emittedOnce(appProcess, 'close') - expect(code).to.equal(0) - }) - }) - - // Destroying webContents in its event listener is going to crash when - // Electron is built in Debug mode. - xdescribe('destroy()', () => { - let server - - before((done) => { - server = http.createServer((request, response) => { - switch (request.url) { - case '/404': - response.statusCode = '404' - response.end() - break - case '/301': - response.statusCode = '301' - response.setHeader('Location', '/200') - response.end() - break - case '/200': - response.statusCode = '200' - response.end('hello') - break - default: - done('unsupported endpoint') - } - }).listen(0, '127.0.0.1', () => { - server.url = 'http://127.0.0.1:' + server.address().port - done() - }) - }) - - after(() => { - server.close() - server = null - }) - - it('should not crash when invoked synchronously inside navigation observer', (done) => { - const events = [ - { name: 'did-start-loading', url: `${server.url}/200` }, - { name: 'dom-ready', url: `${server.url}/200` }, - { name: 'did-stop-loading', url: `${server.url}/200` }, - { name: 'did-finish-load', url: `${server.url}/200` }, - // FIXME: Multiple Emit calls inside an observer assume that object - // will be alive till end of the observer. Synchronous `destroy` api - // violates this contract and crashes. - // { name: 'did-frame-finish-load', url: `${server.url}/200` }, - { name: 'did-fail-load', url: `${server.url}/404` } - ] - const responseEvent = 'webcontents-destroyed' - - function * genNavigationEvent () { - let eventOptions = null - while ((eventOptions = events.shift()) && events.length) { - eventOptions.responseEvent = responseEvent - ipcRenderer.send('test-webcontents-navigation-observer', eventOptions) - yield 1 - } - } - - const gen = genNavigationEvent() - ipcRenderer.on(responseEvent, () => { - if (!gen.next().value) done() - }) - gen.next() - }) - }) - - describe('did-change-theme-color event', () => { - it('is triggered with correct theme color', (done) => { - let count = 0 - w.webContents.on('did-change-theme-color', (e, color) => { - if (count === 0) { - count += 1 - expect(color).to.equal('#FFEEDD') - w.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - } else if (count === 1) { - expect(color).to.be.null() - done() - } - }) - w.loadFile(path.join(fixtures, 'pages', 'theme-color.html')) - }) - }) - - describe('console-message event', () => { - it('is triggered with correct log message', (done) => { - w.webContents.on('console-message', (e, level, message) => { - // Don't just assert as Chromium might emit other logs that we should ignore. - if (message === 'a') { - done() - } - }) - w.loadFile(path.join(fixtures, 'pages', 'a.html')) - }) - }) - - describe('ipc-message event', () => { - it('emits when the renderer process sends an asynchronous message', async () => { - const webContents = remote.getCurrentWebContents() - const promise = emittedOnce(webContents, 'ipc-message') - - ipcRenderer.send('message', 'Hello World!') - - const [, channel, message] = await promise - expect(channel).to.equal('message') - expect(message).to.equal('Hello World!') - }) - }) - - describe('ipc-message-sync event', () => { - it('emits when the renderer process sends a synchronous message', async () => { - const webContents = remote.getCurrentWebContents() - const promise = emittedOnce(webContents, 'ipc-message-sync') - - ipcRenderer.send('handle-next-ipc-message-sync', 'foobar') - const result = ipcRenderer.sendSync('message', 'Hello World!') - - const [, channel, message] = await promise - expect(channel).to.equal('message') - expect(message).to.equal('Hello World!') - expect(result).to.equal('foobar') - }) - }) - - describe('referrer', () => { - it('propagates referrer information to new target=_blank windows', (done) => { - const server = http.createServer((req, res) => { - if (req.url === '/should_have_referrer') { - expect(req.headers.referer).to.equal(`http://127.0.0.1:${server.address().port}/`) - return done() - } - res.end('link') - }) - server.listen(0, '127.0.0.1', () => { - const url = 'http://127.0.0.1:' + server.address().port + '/' - w.webContents.once('did-finish-load', () => { - w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => { - expect(referrer.url).to.equal(url) - expect(referrer.policy).to.equal('no-referrer-when-downgrade') - }) - w.webContents.executeJavaScript('a.click()') - }) - w.loadURL(url) - }) - }) - - // TODO(jeremy): window.open() in a real browser passes the referrer, but - // our hacked-up window.open() shim doesn't. It should. - xit('propagates referrer information to windows opened with window.open', (done) => { - const server = http.createServer((req, res) => { - if (req.url === '/should_have_referrer') { - expect(req.headers.referer).to.equal(`http://127.0.0.1:${server.address().port}/`) - return done() - } - res.end('') - }) - server.listen(0, '127.0.0.1', () => { - const url = 'http://127.0.0.1:' + server.address().port + '/' - w.webContents.once('did-finish-load', () => { - w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => { - expect(referrer.url).to.equal(url) - expect(referrer.policy).to.equal('no-referrer-when-downgrade') - }) - w.webContents.executeJavaScript('window.open(location.href + "should_have_referrer")') - }) - w.loadURL(url) - }) - }) - }) - - describe('webframe messages in sandboxed contents', () => { - it('responds to executeJavaScript', async () => { - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox: true - } - }) - await w.loadURL('about:blank') - const result = await w.webContents.executeJavaScript('37 + 5') - expect(result).to.equal(42) - }) - }) - - describe('preload-error event', () => { - const generateSpecs = (description, sandbox) => { - describe(description, () => { - it('is triggered when unhandled exception is thrown', async () => { - const preload = path.join(fixtures, 'module', 'preload-error-exception.js') - - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox, - preload - } - }) - - const promise = emittedOnce(w.webContents, 'preload-error') - w.loadURL('about:blank') - - const [, preloadPath, error] = await promise - expect(preloadPath).to.equal(preload) - expect(error.message).to.equal('Hello World!') - }) - - it('is triggered on syntax errors', async () => { - const preload = path.join(fixtures, 'module', 'preload-error-syntax.js') - - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox, - preload - } - }) - - const promise = emittedOnce(w.webContents, 'preload-error') - w.loadURL('about:blank') - - const [, preloadPath, error] = await promise - expect(preloadPath).to.equal(preload) - expect(error.message).to.equal('foobar is not defined') - }) - - it('is triggered when preload script loading fails', async () => { - const preload = path.join(fixtures, 'module', 'preload-invalid.js') - - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox, - preload - } - }) - - const promise = emittedOnce(w.webContents, 'preload-error') - w.loadURL('about:blank') - - const [, preloadPath, error] = await promise - expect(preloadPath).to.equal(preload) - expect(error.message).to.contain('preload-invalid.js') - }) - }) - } - - generateSpecs('without sandbox', false) - generateSpecs('with sandbox', true) - }) - - describe('takeHeapSnapshot()', () => { - it('works with sandboxed renderers', async () => { - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox: true - } - }) - - await w.loadURL('about:blank') - - const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot') - - const cleanup = () => { - try { - fs.unlinkSync(filePath) - } catch (e) { - // ignore error - } - } - - try { - await w.webContents.takeHeapSnapshot(filePath) - const stats = fs.statSync(filePath) - expect(stats.size).not.to.be.equal(0) - } finally { - cleanup() - } - }) - - it('fails with invalid file path', async () => { - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox: true - } - }) - - await w.loadURL('about:blank') - - const promise = w.webContents.takeHeapSnapshot('') - return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed') - }) - }) - - describe('setBackgroundThrottling()', () => { - it('does not crash when allowing', (done) => { - w.webContents.setBackgroundThrottling(true) - done() - }) - - it('does not crash when disallowing', (done) => { - w.destroy() - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: { - backgroundThrottling: true - } - }) - - w.webContents.setBackgroundThrottling(false) - done() - }) - - it('does not crash when called via BrowserWindow', (done) => { - w.setBackgroundThrottling(true) - done() - }) - }) - - describe('getPrinterList()', () => { - before(function () { - if (!features.isPrintingEnabled()) { - return closeWindow(w).then(() => { - w = null - this.skip() - }) - } - }) - - it('can get printer list', async () => { - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox: true - } - }) - await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E') - const printers = w.webContents.getPrinters() - expect(printers).to.be.an('array') - }) - }) - - describe('printToPDF()', () => { - before(function () { - if (!features.isPrintingEnabled()) { - return closeWindow(w).then(() => { - w = null - this.skip() - }) - } - }) - - it('can print to PDF', async () => { - w.destroy() - w = new BrowserWindow({ - show: false, - webPreferences: { - sandbox: true - } - }) - await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E') - const data = await w.webContents.printToPDF({}) - expect(data).to.be.an.instanceof(Buffer).that.is.not.empty() - }) - }) -}) diff --git a/spec/api-web-contents-view-spec.js b/spec/api-web-contents-view-spec.js deleted file mode 100644 index 0afa70eb56713..0000000000000 --- a/spec/api-web-contents-view-spec.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -const chai = require('chai') -const ChildProcess = require('child_process') -const dirtyChai = require('dirty-chai') -const path = require('path') -const { emittedOnce } = require('./events-helpers') -const { closeWindow } = require('./window-helpers') - -const { remote } = require('electron') -const { webContents, TopLevelWindow, WebContentsView } = remote - -const { expect } = chai -chai.use(dirtyChai) - -describe('WebContentsView', () => { - let w = null - afterEach(() => closeWindow(w).then(() => { w = null })) - - it('can be used as content view', () => { - const web = webContents.create({}) - w = new TopLevelWindow({ show: false }) - w.setContentView(new WebContentsView(web)) - }) - - it('prevents adding same WebContents', () => { - const web = webContents.create({}) - w = new TopLevelWindow({ show: false }) - w.setContentView(new WebContentsView(web)) - expect(() => { - w.setContentView(new WebContentsView(web)) - }).to.throw('The WebContents has already been added to a View') - }) - - describe('new WebContentsView()', () => { - it('does not crash on exit', async () => { - const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontentsview.js') - const electronPath = remote.getGlobal('process').execPath - const appProcess = ChildProcess.spawn(electronPath, [appPath]) - const [code] = await emittedOnce(appProcess, 'close') - expect(code).to.equal(0) - }) - }) -}) diff --git a/spec/api-web-frame-spec.js b/spec/api-web-frame-spec.js index e0a6772b0eade..a9a76839530b7 100644 --- a/spec/api-web-frame-spec.js +++ b/spec/api-web-frame-spec.js @@ -1,82 +1,114 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const path = require('path') -const { closeWindow } = require('./window-helpers') -const { remote, webFrame } = require('electron') -const { BrowserWindow, ipcMain } = remote - -const { expect } = chai -chai.use(dirtyChai) - -/* Most of the APIs here don't use standard callbacks */ -/* eslint-disable standard/no-callback-literal */ +const { expect } = require('chai'); +const { webFrame } = require('electron'); describe('webFrame module', function () { - const fixtures = path.resolve(__dirname, 'fixtures') - let w = null - - afterEach(function () { - return closeWindow(w).then(function () { w = null }) - }) - - it('supports setting the visual and layout zoom level limits', function () { - expect(() => { - webFrame.setVisualZoomLevelLimits(1, 50) - webFrame.setLayoutZoomLevelLimits(0, 25) - }).to.not.throw() - }) - - it('calls a spellcheck provider', async () => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }) - await w.loadFile(path.join(fixtures, 'pages', 'webframe-spell-check.html')) - w.focus() - await w.webContents.executeJavaScript('document.querySelector("input").focus()', true) - - const spellCheckerFeedback = - new Promise(resolve => { - ipcMain.on('spec-spell-check', (e, words, callback) => { - if (words.length === 5) { - // The API calls the provider after every completed word. - // The promise is resolved only after this event is received with all words. - resolve([words, callback]) - } - }) - }) - const inputText = `spleling test you're ` - for (const keyCode of inputText) { - w.webContents.sendInputEvent({ type: 'char', keyCode }) - } - const [words, callback] = await spellCheckerFeedback - expect(words.sort()).to.deep.equal(['spleling', 'test', `you're`, 'you', 're'].sort()) - expect(callback).to.be.true() - }) - it('top is self for top frame', () => { - expect(webFrame.top.context).to.equal(webFrame.context) - }) + expect(webFrame.top.context).to.equal(webFrame.context); + }); it('opener is null for top frame', () => { - expect(webFrame.opener).to.be.null() - }) + expect(webFrame.opener).to.be.null(); + }); it('firstChild is null for top frame', () => { - expect(webFrame.firstChild).to.be.null() - }) + expect(webFrame.firstChild).to.be.null(); + }); it('getFrameForSelector() does not crash when not found', () => { - expect(webFrame.getFrameForSelector('unexist-selector')).to.be.null() - }) + expect(webFrame.getFrameForSelector('unexist-selector')).to.be.null(); + }); it('findFrameByName() does not crash when not found', () => { - expect(webFrame.findFrameByName('unexist-name')).to.be.null() - }) + expect(webFrame.findFrameByName('unexist-name')).to.be.null(); + }); it('findFrameByRoutingId() does not crash when not found', () => { - expect(webFrame.findFrameByRoutingId(-1)).to.be.null() - }) -}) + expect(webFrame.findFrameByRoutingId(-1)).to.be.null(); + }); + + describe('executeJavaScript', () => { + let childFrameElement, childFrame; + + before(() => { + childFrameElement = document.createElement('iframe'); + document.body.appendChild(childFrameElement); + childFrame = webFrame.firstChild; + }); + + after(() => { + childFrameElement.remove(); + }); + + it('executeJavaScript() yields results via a promise and a sync callback', async () => { + let callbackResult, callbackError; + + const executeJavaScript = childFrame + .executeJavaScript('1 + 1', (result, error) => { + callbackResult = result; + callbackError = error; + }); + + expect(callbackResult).to.equal(2); + expect(callbackError).to.be.undefined(); + + const promiseResult = await executeJavaScript; + expect(promiseResult).to.equal(2); + }); + + it('executeJavaScriptInIsolatedWorld() yields results via a promise and a sync callback', async () => { + let callbackResult, callbackError; + + const executeJavaScriptInIsolatedWorld = childFrame + .executeJavaScriptInIsolatedWorld(999, [{ code: '1 + 1' }], (result, error) => { + callbackResult = result; + callbackError = error; + }); + + expect(callbackResult).to.equal(2); + expect(callbackError).to.be.undefined(); + + const promiseResult = await executeJavaScriptInIsolatedWorld; + expect(promiseResult).to.equal(2); + }); + + it('executeJavaScript() yields errors via a promise and a sync callback', async () => { + let callbackResult, callbackError; + + const executeJavaScript = childFrame + .executeJavaScript('thisShouldProduceAnError()', (result, error) => { + callbackResult = result; + callbackError = error; + }); + + expect(callbackResult).to.be.undefined(); + expect(callbackError).to.be.an('error'); + + await expect(executeJavaScript).to.eventually.be.rejected('error is expected'); + }); + + // executeJavaScriptInIsolatedWorld is failing to detect exec errors and is neither + // rejecting nor passing the error to the callback. This predates the reintroduction + // of the callback so will not be fixed as part of the callback PR + // if/when this is fixed the test can be uncommented. + // + // it('executeJavaScriptInIsolatedWorld() yields errors via a promise and a sync callback', done => { + // let callbackResult, callbackError + // + // const executeJavaScriptInIsolatedWorld = childFrame + // .executeJavaScriptInIsolatedWorld(999, [{ code: 'thisShouldProduceAnError()' }], (result, error) => { + // callbackResult = result + // callbackError = error + // }); + // + // expect(callbackResult).to.be.undefined() + // expect(callbackError).to.be.an('error') + // + // expect(executeJavaScriptInIsolatedWorld).to.eventually.be.rejected('error is expected'); + // }) + + it('executeJavaScript(InIsolatedWorld) can be used without a callback', async () => { + expect(await webFrame.executeJavaScript('1 + 1')).to.equal(2); + expect(await webFrame.executeJavaScriptInIsolatedWorld(999, [{ code: '1 + 1' }])).to.equal(2); + }); + }); +}); diff --git a/spec/api-web-request-spec.js b/spec/api-web-request-spec.js deleted file mode 100644 index 4dd5cd3cc21b2..0000000000000 --- a/spec/api-web-request-spec.js +++ /dev/null @@ -1,410 +0,0 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') - -const http = require('http') -const qs = require('querystring') -const remote = require('electron').remote -const session = remote.session - -const { expect } = chai -chai.use(dirtyChai) - -/* The whole webRequest API doesn't use standard callbacks */ -/* eslint-disable standard/no-callback-literal */ - -describe('webRequest module', () => { - const ses = session.defaultSession - const server = http.createServer((req, res) => { - if (req.url === '/serverRedirect') { - res.statusCode = 301 - res.setHeader('Location', 'http://' + req.rawHeaders[1]) - res.end() - } else { - res.setHeader('Custom', ['Header']) - let content = req.url - if (req.headers.accept === '*/*;test/header') { - content += 'header/received' - } - res.end(content) - } - }) - let defaultURL = null - - before((done) => { - server.listen(0, '127.0.0.1', () => { - const port = server.address().port - defaultURL = 'http://127.0.0.1:' + port + '/' - done() - }) - }) - - after(() => { - server.close() - }) - - describe('webRequest.onBeforeRequest', () => { - afterEach(() => { - ses.webRequest.onBeforeRequest(null) - }) - - it('can cancel the request', (done) => { - ses.webRequest.onBeforeRequest((details, callback) => { - callback({ - cancel: true - }) - }) - $.ajax({ - url: defaultURL, - success: () => { - done('unexpected success') - }, - error: () => { - done() - } - }) - }) - - it('can filter URLs', (done) => { - const filter = { urls: [defaultURL + 'filter/*'] } - ses.webRequest.onBeforeRequest(filter, (details, callback) => { - callback({ cancel: true }) - }) - $.ajax({ - url: `${defaultURL}nofilter/test`, - success: (data) => { - expect(data).to.equal('/nofilter/test') - $.ajax({ - url: `${defaultURL}filter/test`, - success: () => done('unexpected success'), - error: () => done() - }) - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('receives details object', (done) => { - ses.webRequest.onBeforeRequest((details, callback) => { - expect(details.id).to.be.a('number') - expect(details.timestamp).to.be.a('number') - expect(details.webContentsId).to.be.a('number') - expect(details.url).to.be.a('string').that.is.equal(defaultURL) - expect(details.method).to.be.a('string').that.is.equal('GET') - expect(details.resourceType).to.be.a('string').that.is.equal('xhr') - expect(details.uploadData).to.be.undefined() - callback({}) - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('receives post data in details object', (done) => { - const postData = { - name: 'post test', - type: 'string' - } - ses.webRequest.onBeforeRequest((details, callback) => { - expect(details.url).to.equal(defaultURL) - expect(details.method).to.equal('POST') - expect(details.uploadData).to.have.lengthOf(1) - const data = qs.parse(details.uploadData[0].bytes.toString()) - expect(data).to.deep.equal(postData) - callback({ cancel: true }) - }) - $.ajax({ - url: defaultURL, - type: 'POST', - data: postData, - success: () => {}, - error: () => done() - }) - }) - - it('can redirect the request', (done) => { - ses.webRequest.onBeforeRequest((details, callback) => { - if (details.url === defaultURL) { - callback({ redirectURL: `${defaultURL}redirect` }) - } else { - callback({}) - } - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/redirect') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - }) - - describe('webRequest.onBeforeSendHeaders', () => { - afterEach(() => { - ses.webRequest.onBeforeSendHeaders(null) - }) - - it('receives details object', (done) => { - ses.webRequest.onBeforeSendHeaders((details, callback) => { - expect(details.requestHeaders).to.be.an('object') - expect(details.requestHeaders['Foo.Bar']).to.equal('baz') - callback({}) - }) - $.ajax({ - url: defaultURL, - headers: { 'Foo.Bar': 'baz' }, - success: (data) => { - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('can change the request headers', (done) => { - ses.webRequest.onBeforeSendHeaders((details, callback) => { - const requestHeaders = details.requestHeaders - requestHeaders.Accept = '*/*;test/header' - callback({ requestHeaders: requestHeaders }) - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/header/received') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('resets the whole headers', (done) => { - const requestHeaders = { - Test: 'header' - } - ses.webRequest.onBeforeSendHeaders((details, callback) => { - callback({ requestHeaders: requestHeaders }) - }) - ses.webRequest.onSendHeaders((details) => { - expect(details.requestHeaders).to.deep.equal(requestHeaders) - done() - }) - $.ajax({ - url: defaultURL, - error: (xhr, errorType) => done(errorType) - }) - }) - }) - - describe('webRequest.onSendHeaders', () => { - afterEach(() => { - ses.webRequest.onSendHeaders(null) - }) - - it('receives details object', (done) => { - ses.webRequest.onSendHeaders((details) => { - expect(details.requestHeaders).to.be.an('object') - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - }) - - describe('webRequest.onHeadersReceived', () => { - afterEach(() => { - ses.webRequest.onHeadersReceived(null) - }) - - it('receives details object', (done) => { - ses.webRequest.onHeadersReceived((details, callback) => { - expect(details.statusLine).to.equal('HTTP/1.1 200 OK') - expect(details.statusCode).to.equal(200) - expect(details.responseHeaders['Custom']).to.deep.equal(['Header']) - callback({}) - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('can change the response header', (done) => { - ses.webRequest.onHeadersReceived((details, callback) => { - const responseHeaders = details.responseHeaders - responseHeaders['Custom'] = ['Changed'] - callback({ responseHeaders: responseHeaders }) - }) - $.ajax({ - url: defaultURL, - success: (data, status, xhr) => { - expect(xhr.getResponseHeader('Custom')).to.equal('Changed') - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('does not change header by default', (done) => { - ses.webRequest.onHeadersReceived((details, callback) => { - callback({}) - }) - $.ajax({ - url: defaultURL, - success: (data, status, xhr) => { - expect(xhr.getResponseHeader('Custom')).to.equal('Header') - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('follows server redirect', (done) => { - ses.webRequest.onHeadersReceived((details, callback) => { - const responseHeaders = details.responseHeaders - callback({ responseHeaders: responseHeaders }) - }) - $.ajax({ - url: defaultURL + 'serverRedirect', - success: (data, status, xhr) => { - expect(xhr.getResponseHeader('Custom')).to.equal('Header') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - - it('can change the header status', (done) => { - ses.webRequest.onHeadersReceived((details, callback) => { - const responseHeaders = details.responseHeaders - callback({ - responseHeaders: responseHeaders, - statusLine: 'HTTP/1.1 404 Not Found' - }) - }) - $.ajax({ - url: defaultURL, - success: (data, status, xhr) => {}, - error: (xhr, errorType) => { - expect(xhr.getResponseHeader('Custom')).to.equal('Header') - done() - } - }) - }) - }) - - describe('webRequest.onResponseStarted', () => { - afterEach(() => { - ses.webRequest.onResponseStarted(null) - }) - - it('receives details object', (done) => { - ses.webRequest.onResponseStarted((details) => { - expect(details.fromCache).to.be.a('boolean') - expect(details.statusLine).to.equal('HTTP/1.1 200 OK') - expect(details.statusCode).to.equal(200) - expect(details.responseHeaders['Custom']).to.deep.equal(['Header']) - }) - $.ajax({ - url: defaultURL, - success: (data, status, xhr) => { - expect(xhr.getResponseHeader('Custom')).to.equal('Header') - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - }) - - describe('webRequest.onBeforeRedirect', () => { - afterEach(() => { - ses.webRequest.onBeforeRedirect(null) - ses.webRequest.onBeforeRequest(null) - }) - - it('receives details object', (done) => { - const redirectURL = defaultURL + 'redirect' - ses.webRequest.onBeforeRequest((details, callback) => { - if (details.url === defaultURL) { - callback({ redirectURL: redirectURL }) - } else { - callback({}) - } - }) - ses.webRequest.onBeforeRedirect((details) => { - expect(details.fromCache).to.be.a('boolean') - expect(details.statusLine).to.equal('HTTP/1.1 307 Internal Redirect') - expect(details.statusCode).to.equal(307) - expect(details.redirectURL).to.equal(redirectURL) - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/redirect') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - }) - - describe('webRequest.onCompleted', () => { - afterEach(() => { - ses.webRequest.onCompleted(null) - }) - - it('receives details object', (done) => { - ses.webRequest.onCompleted((details) => { - expect(details.fromCache).to.be.a('boolean') - expect(details.statusLine).to.equal('HTTP/1.1 200 OK') - expect(details.statusCode).to.equal(200) - }) - $.ajax({ - url: defaultURL, - success: (data) => { - expect(data).to.equal('/') - done() - }, - error: (xhr, errorType) => done(errorType) - }) - }) - }) - - describe('webRequest.onErrorOccurred', () => { - afterEach(() => { - ses.webRequest.onErrorOccurred(null) - ses.webRequest.onBeforeRequest(null) - }) - - it('receives details object', (done) => { - ses.webRequest.onBeforeRequest((details, callback) => { - callback({ cancel: true }) - }) - ses.webRequest.onErrorOccurred((details) => { - expect(details.error).to.equal('net::ERR_BLOCKED_BY_CLIENT') - done() - }) - $.ajax({ - url: defaultURL, - success: () => done('unexpected success') - }) - }) - }) -}) diff --git a/spec/asar-spec.js b/spec/asar-spec.js index 10251169b0516..4468bf85e2997 100644 --- a/spec/asar-spec.js +++ b/spec/asar-spec.js @@ -1,1328 +1,1576 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const ChildProcess = require('child_process') -const fs = require('fs') -const path = require('path') -const temp = require('temp').track() -const util = require('util') -const { closeWindow } = require('./window-helpers') - -const nativeImage = require('electron').nativeImage -const remote = require('electron').remote - -const { ipcMain, BrowserWindow } = remote - -const features = process.electronBinding('features') - -const { expect } = chai -chai.use(dirtyChai) +const { expect } = require('chai'); +const ChildProcess = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const temp = require('temp').track(); +const util = require('util'); +const { emittedOnce } = require('./events-helpers'); +const { ifit, ifdescribe } = require('./spec-helpers'); +const nativeImage = require('electron').nativeImage; + +const features = process._linkedBinding('electron_common_features'); async function expectToThrowErrorWithCode (func, code) { - let error + let error; try { - await func() + await func(); } catch (e) { - error = e + error = e; } - expect(error).is.an('Error') - expect(error).to.have.property('code').which.equals(code) + expect(error).is.an('Error'); + expect(error).to.have.property('code').which.equals(code); } describe('asar package', function () { - const fixtures = path.join(__dirname, 'fixtures') + const fixtures = path.join(__dirname, 'fixtures'); + const asarDir = path.join(fixtures, 'test.asar'); describe('node api', function () { it('supports paths specified as a Buffer', function () { - const file = Buffer.from(path.join(fixtures, 'asar', 'a.asar', 'file1')) - expect(fs.existsSync(file)).to.be.true() - }) + const file = Buffer.from(path.join(asarDir, 'a.asar', 'file1')); + expect(fs.existsSync(file)).to.be.true(); + }); describe('fs.readFileSync', function () { it('does not leak fd', function () { - let readCalls = 1 + let readCalls = 1; while (readCalls <= 10000) { - fs.readFileSync(path.join(process.resourcesPath, 'default_app.asar', 'index.js')) - readCalls++ + fs.readFileSync(path.join(process.resourcesPath, 'default_app.asar', 'main.js')); + readCalls++; } - }) + }); it('reads a normal file', function () { - const file1 = path.join(fixtures, 'asar', 'a.asar', 'file1') - expect(fs.readFileSync(file1).toString().trim()).to.equal('file1') - const file2 = path.join(fixtures, 'asar', 'a.asar', 'file2') - expect(fs.readFileSync(file2).toString().trim()).to.equal('file2') - const file3 = path.join(fixtures, 'asar', 'a.asar', 'file3') - expect(fs.readFileSync(file3).toString().trim()).to.equal('file3') - }) + const file1 = path.join(asarDir, 'a.asar', 'file1'); + expect(fs.readFileSync(file1).toString().trim()).to.equal('file1'); + const file2 = path.join(asarDir, 'a.asar', 'file2'); + expect(fs.readFileSync(file2).toString().trim()).to.equal('file2'); + const file3 = path.join(asarDir, 'a.asar', 'file3'); + expect(fs.readFileSync(file3).toString().trim()).to.equal('file3'); + }); it('reads from a empty file', function () { - const file = path.join(fixtures, 'asar', 'empty.asar', 'file1') - const buffer = fs.readFileSync(file) - expect(buffer).to.be.empty() - expect(buffer.toString()).to.equal('') - }) + const file = path.join(asarDir, 'empty.asar', 'file1'); + const buffer = fs.readFileSync(file); + expect(buffer).to.be.empty(); + expect(buffer.toString()).to.equal(''); + }); it('reads a linked file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link1') - expect(fs.readFileSync(p).toString().trim()).to.equal('file1') - }) + const p = path.join(asarDir, 'a.asar', 'link1'); + expect(fs.readFileSync(p).toString().trim()).to.equal('file1'); + }); it('reads a file from linked directory', function () { - const p1 = path.join(fixtures, 'asar', 'a.asar', 'link2', 'file1') - expect(fs.readFileSync(p1).toString().trim()).to.equal('file1') - const p2 = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') - expect(fs.readFileSync(p2).toString().trim()).to.equal('file1') - }) + const p1 = path.join(asarDir, 'a.asar', 'link2', 'file1'); + expect(fs.readFileSync(p1).toString().trim()).to.equal('file1'); + const p2 = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1'); + expect(fs.readFileSync(p2).toString().trim()).to.equal('file1'); + }); it('throws ENOENT error when can not find file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); expect(() => { - fs.readFileSync(p) - }).to.throw(/ENOENT/) - }) + fs.readFileSync(p); + }).to.throw(/ENOENT/); + }); it('passes ENOENT error to callback when can not find file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - let async = false + const p = path.join(asarDir, 'a.asar', 'not-exist'); + let async = false; fs.readFile(p, function (error) { - expect(async).to.be.true() - expect(error).to.match(/ENOENT/) - }) - async = true - }) + expect(async).to.be.true(); + expect(error).to.match(/ENOENT/); + }); + async = true; + }); it('reads a normal file with unpacked files', function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') - expect(fs.readFileSync(p).toString().trim()).to.equal('a') - }) - }) + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); + expect(fs.readFileSync(p).toString().trim()).to.equal('a'); + }); + + it('reads a file in filesystem', function () { + const p = path.resolve(asarDir, 'file'); + expect(fs.readFileSync(p).toString().trim()).to.equal('file'); + }); + }); describe('fs.readFile', function () { it('reads a normal file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); fs.readFile(p, function (err, content) { - expect(err).to.be.null() - expect(String(content).trim()).to.equal('file1') - done() - }) - }) + try { + expect(err).to.be.null(); + expect(String(content).trim()).to.equal('file1'); + done(); + } catch (e) { + done(e); + } + }); + }); it('reads from a empty file', function (done) { - const p = path.join(fixtures, 'asar', 'empty.asar', 'file1') + const p = path.join(asarDir, 'empty.asar', 'file1'); fs.readFile(p, function (err, content) { - expect(err).to.be.null() - expect(String(content)).to.equal('') - done() - }) - }) + try { + expect(err).to.be.null(); + expect(String(content)).to.equal(''); + done(); + } catch (e) { + done(e); + } + }); + }); it('reads from a empty file with encoding', function (done) { - const p = path.join(fixtures, 'asar', 'empty.asar', 'file1') + const p = path.join(asarDir, 'empty.asar', 'file1'); fs.readFile(p, 'utf8', function (err, content) { - expect(err).to.be.null() - expect(content).to.equal('') - done() - }) - }) + try { + expect(err).to.be.null(); + expect(content).to.equal(''); + done(); + } catch (e) { + done(e); + } + }); + }); it('reads a linked file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link1') + const p = path.join(asarDir, 'a.asar', 'link1'); fs.readFile(p, function (err, content) { - expect(err).to.be.null() - expect(String(content).trim()).to.equal('file1') - done() - }) - }) + try { + expect(err).to.be.null(); + expect(String(content).trim()).to.equal('file1'); + done(); + } catch (e) { + done(e); + } + }); + }); it('reads a file from linked directory', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') + const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1'); fs.readFile(p, function (err, content) { - expect(err).to.be.null() - expect(String(content).trim()).to.equal('file1') - done() - }) - }) + try { + expect(err).to.be.null(); + expect(String(content).trim()).to.equal('file1'); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws ENOENT error when can not find file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); fs.readFile(p, function (err) { - expect(err.code).to.equal('ENOENT') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.readFile', function () { it('reads a normal file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - const content = await fs.promises.readFile(p) - expect(String(content).trim()).to.equal('file1') - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + const content = await fs.promises.readFile(p); + expect(String(content).trim()).to.equal('file1'); + }); it('reads from a empty file', async function () { - const p = path.join(fixtures, 'asar', 'empty.asar', 'file1') - const content = await fs.promises.readFile(p) - expect(String(content)).to.equal('') - }) + const p = path.join(asarDir, 'empty.asar', 'file1'); + const content = await fs.promises.readFile(p); + expect(String(content)).to.equal(''); + }); it('reads from a empty file with encoding', async function () { - const p = path.join(fixtures, 'asar', 'empty.asar', 'file1') - const content = await fs.promises.readFile(p, 'utf8') - expect(content).to.equal('') - }) + const p = path.join(asarDir, 'empty.asar', 'file1'); + const content = await fs.promises.readFile(p, 'utf8'); + expect(content).to.equal(''); + }); it('reads a linked file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link1') - const content = await fs.promises.readFile(p) - expect(String(content).trim()).to.equal('file1') - }) + const p = path.join(asarDir, 'a.asar', 'link1'); + const content = await fs.promises.readFile(p); + expect(String(content).trim()).to.equal('file1'); + }); it('reads a file from linked directory', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') - const content = await fs.promises.readFile(p) - expect(String(content).trim()).to.equal('file1') - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1'); + const content = await fs.promises.readFile(p); + expect(String(content).trim()).to.equal('file1'); + }); it('throws ENOENT error when can not find file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - await expectToThrowErrorWithCode(() => fs.promises.readFile(p), 'ENOENT') - }) - }) + const p = path.join(asarDir, 'a.asar', 'not-exist'); + await expectToThrowErrorWithCode(() => fs.promises.readFile(p), 'ENOENT'); + }); + }); describe('fs.copyFile', function () { it('copies a normal file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - const dest = temp.path() + const p = path.join(asarDir, 'a.asar', 'file1'); + const dest = temp.path(); fs.copyFile(p, dest, function (err) { - expect(err).to.be.null() - expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true() - done() - }) - }) + try { + expect(err).to.be.null(); + expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true(); + done(); + } catch (e) { + done(e); + } + }); + }); it('copies a unpacked file', function (done) { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') - const dest = temp.path() + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); + const dest = temp.path(); fs.copyFile(p, dest, function (err) { - expect(err).to.be.null() - expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true() - done() - }) - }) - }) + try { + expect(err).to.be.null(); + expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true(); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.copyFile', function () { it('copies a normal file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - const dest = temp.path() - await fs.promises.copyFile(p, dest) - expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true() - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + const dest = temp.path(); + await fs.promises.copyFile(p, dest); + expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true(); + }); it('copies a unpacked file', async function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') - const dest = temp.path() - await fs.promises.copyFile(p, dest) - expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true() - }) - }) + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); + const dest = temp.path(); + await fs.promises.copyFile(p, dest); + expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true(); + }); + }); describe('fs.copyFileSync', function () { it('copies a normal file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - const dest = temp.path() - fs.copyFileSync(p, dest) - expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true() - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + const dest = temp.path(); + fs.copyFileSync(p, dest); + expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true(); + }); it('copies a unpacked file', function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') - const dest = temp.path() - fs.copyFileSync(p, dest) - expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true() - }) - }) + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); + const dest = temp.path(); + fs.copyFileSync(p, dest); + expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true(); + }); + }); describe('fs.lstatSync', function () { it('handles path with trailing slash correctly', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') - fs.lstatSync(p) - fs.lstatSync(p + '/') - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1'); + fs.lstatSync(p); + fs.lstatSync(p + '/'); + }); it('returns information of root', function () { - const p = path.join(fixtures, 'asar', 'a.asar') - const stats = fs.lstatSync(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar'); + const stats = fs.lstatSync(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + }); it('returns information of root with stats as bigint', function () { - const p = path.join(fixtures, 'asar', 'a.asar') - const stats = fs.lstatSync(p, { bigint: false }) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar'); + const stats = fs.lstatSync(p, { bigint: false }); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + }); it('returns information of a normal file', function () { - const ref2 = ['file1', 'file2', 'file3', path.join('dir1', 'file1'), path.join('link2', 'file1')] + const ref2 = ['file1', 'file2', 'file3', path.join('dir1', 'file1'), path.join('link2', 'file1')]; for (let j = 0, len = ref2.length; j < len; j++) { - const file = ref2[j] - const p = path.join(fixtures, 'asar', 'a.asar', file) - const stats = fs.lstatSync(p) - expect(stats.isFile()).to.be.true() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(6) + const file = ref2[j]; + const p = path.join(asarDir, 'a.asar', file); + const stats = fs.lstatSync(p); + expect(stats.isFile()).to.be.true(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(6); } - }) + }); it('returns information of a normal directory', function () { - const ref2 = ['dir1', 'dir2', 'dir3'] + const ref2 = ['dir1', 'dir2', 'dir3']; for (let j = 0, len = ref2.length; j < len; j++) { - const file = ref2[j] - const p = path.join(fixtures, 'asar', 'a.asar', file) - const stats = fs.lstatSync(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) + const file = ref2[j]; + const p = path.join(asarDir, 'a.asar', file); + const stats = fs.lstatSync(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); } - }) + }); it('returns information of a linked file', function () { - const ref2 = ['link1', path.join('dir1', 'link1'), path.join('link2', 'link2')] + const ref2 = ['link1', path.join('dir1', 'link1'), path.join('link2', 'link2')]; for (let j = 0, len = ref2.length; j < len; j++) { - const file = ref2[j] - const p = path.join(fixtures, 'asar', 'a.asar', file) - const stats = fs.lstatSync(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.true() - expect(stats.size).to.equal(0) + const file = ref2[j]; + const p = path.join(asarDir, 'a.asar', file); + const stats = fs.lstatSync(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.true(); + expect(stats.size).to.equal(0); } - }) + }); it('returns information of a linked directory', function () { - const ref2 = ['link2', path.join('dir1', 'link2'), path.join('link2', 'link2')] + const ref2 = ['link2', path.join('dir1', 'link2'), path.join('link2', 'link2')]; for (let j = 0, len = ref2.length; j < len; j++) { - const file = ref2[j] - const p = path.join(fixtures, 'asar', 'a.asar', file) - const stats = fs.lstatSync(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.true() - expect(stats.size).to.equal(0) + const file = ref2[j]; + const p = path.join(asarDir, 'a.asar', file); + const stats = fs.lstatSync(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.true(); + expect(stats.size).to.equal(0); } - }) + }); it('throws ENOENT error when can not find file', function () { - const ref2 = ['file4', 'file5', path.join('dir1', 'file4')] + const ref2 = ['file4', 'file5', path.join('dir1', 'file4')]; for (let j = 0, len = ref2.length; j < len; j++) { - const file = ref2[j] - const p = path.join(fixtures, 'asar', 'a.asar', file) + const file = ref2[j]; + const p = path.join(asarDir, 'a.asar', file); expect(() => { - fs.lstatSync(p) - }).to.throw(/ENOENT/) + fs.lstatSync(p); + }).to.throw(/ENOENT/); } - }) - }) + }); + }); describe('fs.lstat', function () { it('handles path with trailing slash correctly', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') - fs.lstat(p + '/', done) - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1'); + fs.lstat(p + '/', done); + }); it('returns information of root', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar') + const p = path.join(asarDir, 'a.asar'); fs.lstat(p, function (err, stats) { - expect(err).to.be.null() - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns information of root with stats as bigint', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar') + const p = path.join(asarDir, 'a.asar'); fs.lstat(p, { bigint: false }, function (err, stats) { - expect(err).to.be.null() - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns information of a normal file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'file1') + const p = path.join(asarDir, 'a.asar', 'link2', 'file1'); fs.lstat(p, function (err, stats) { - expect(err).to.be.null() - expect(stats.isFile()).to.be.true() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(6) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(stats.isFile()).to.be.true(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(6); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns information of a normal directory', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') + const p = path.join(asarDir, 'a.asar', 'dir1'); fs.lstat(p, function (err, stats) { - expect(err).to.be.null() - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns information of a linked file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link1') + const p = path.join(asarDir, 'a.asar', 'link2', 'link1'); fs.lstat(p, function (err, stats) { - expect(err).to.be.null() - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.true() - expect(stats.size).to.equal(0) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.true(); + expect(stats.size).to.equal(0); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns information of a linked directory', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') + const p = path.join(asarDir, 'a.asar', 'link2', 'link2'); fs.lstat(p, function (err, stats) { - expect(err).to.be.null() - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.true() - expect(stats.size).to.equal(0) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.true(); + expect(stats.size).to.equal(0); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws ENOENT error when can not find file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file4') + const p = path.join(asarDir, 'a.asar', 'file4'); fs.lstat(p, function (err) { - expect(err.code).to.equal('ENOENT') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.lstat', function () { it('handles path with trailing slash correctly', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') - await fs.promises.lstat(p + '/') - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1'); + await fs.promises.lstat(p + '/'); + }); it('returns information of root', async function () { - const p = path.join(fixtures, 'asar', 'a.asar') - const stats = await fs.promises.lstat(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar'); + const stats = await fs.promises.lstat(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + }); it('returns information of root with stats as bigint', async function () { - const p = path.join(fixtures, 'asar', 'a.asar') - const stats = await fs.promises.lstat(p, { bigint: false }) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar'); + const stats = await fs.promises.lstat(p, { bigint: false }); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + }); it('returns information of a normal file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'file1') - const stats = await fs.promises.lstat(p) - expect(stats.isFile()).to.be.true() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(6) - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'file1'); + const stats = await fs.promises.lstat(p); + expect(stats.isFile()).to.be.true(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(6); + }); it('returns information of a normal directory', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') - const stats = await fs.promises.lstat(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.true() - expect(stats.isSymbolicLink()).to.be.false() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar', 'dir1'); + const stats = await fs.promises.lstat(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.true(); + expect(stats.isSymbolicLink()).to.be.false(); + expect(stats.size).to.equal(0); + }); it('returns information of a linked file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link1') - const stats = await fs.promises.lstat(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.true() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link1'); + const stats = await fs.promises.lstat(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.true(); + expect(stats.size).to.equal(0); + }); it('returns information of a linked directory', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') - const stats = await fs.promises.lstat(p) - expect(stats.isFile()).to.be.false() - expect(stats.isDirectory()).to.be.false() - expect(stats.isSymbolicLink()).to.be.true() - expect(stats.size).to.equal(0) - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2'); + const stats = await fs.promises.lstat(p); + expect(stats.isFile()).to.be.false(); + expect(stats.isDirectory()).to.be.false(); + expect(stats.isSymbolicLink()).to.be.true(); + expect(stats.size).to.equal(0); + }); it('throws ENOENT error when can not find file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file4') - await expectToThrowErrorWithCode(() => fs.promises.lstat(p), 'ENOENT') - }) - }) + const p = path.join(asarDir, 'a.asar', 'file4'); + await expectToThrowErrorWithCode(() => fs.promises.lstat(p), 'ENOENT'); + }); + }); describe('fs.realpathSync', () => { it('returns real path root', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = 'a.asar' - const r = fs.realpathSync(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = 'a.asar'; + const r = fs.realpathSync(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a normal file', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'file1') - const r = fs.realpathSync(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'file1'); + const r = fs.realpathSync(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a normal directory', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'dir1') - const r = fs.realpathSync(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'dir1'); + const r = fs.realpathSync(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a linked file', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link1') - const r = fs.realpathSync(path.join(parent, p)) - expect(r).to.equal(path.join(parent, 'a.asar', 'file1')) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'link2', 'link1'); + const r = fs.realpathSync(path.join(parent, p)); + expect(r).to.equal(path.join(parent, 'a.asar', 'file1')); + }); it('returns real path of a linked directory', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link2') - const r = fs.realpathSync(path.join(parent, p)) - expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'link2', 'link2'); + const r = fs.realpathSync(path.join(parent, p)); + expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')); + }); it('returns real path of an unpacked file', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('unpack.asar', 'a.txt') - const r = fs.realpathSync(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('unpack.asar', 'a.txt'); + const r = fs.realpathSync(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('throws ENOENT error when can not find file', () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'not-exist') + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'not-exist'); expect(() => { - fs.realpathSync(path.join(parent, p)) - }).to.throw(/ENOENT/) - }) - }) + fs.realpathSync(path.join(parent, p)); + }).to.throw(/ENOENT/); + }); + }); describe('fs.realpathSync.native', () => { it('returns real path root', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = 'a.asar' - const r = fs.realpathSync.native(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync.native(asarDir); + const p = 'a.asar'; + const r = fs.realpathSync.native(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a normal file', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'file1') - const r = fs.realpathSync.native(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'file1'); + const r = fs.realpathSync.native(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a normal directory', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'dir1') - const r = fs.realpathSync.native(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'dir1'); + const r = fs.realpathSync.native(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a linked file', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link1') - const r = fs.realpathSync.native(path.join(parent, p)) - expect(r).to.equal(path.join(parent, 'a.asar', 'file1')) - }) + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'link2', 'link1'); + const r = fs.realpathSync.native(path.join(parent, p)); + expect(r).to.equal(path.join(parent, 'a.asar', 'file1')); + }); it('returns real path of a linked directory', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link2') - const r = fs.realpathSync.native(path.join(parent, p)) - expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')) - }) + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'link2', 'link2'); + const r = fs.realpathSync.native(path.join(parent, p)); + expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')); + }); it('returns real path of an unpacked file', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('unpack.asar', 'a.txt') - const r = fs.realpathSync.native(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync.native(asarDir); + const p = path.join('unpack.asar', 'a.txt'); + const r = fs.realpathSync.native(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('throws ENOENT error when can not find file', () => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'not-exist') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'not-exist'); expect(() => { - fs.realpathSync.native(path.join(parent, p)) - }).to.throw(/ENOENT/) - }) - }) + fs.realpathSync.native(path.join(parent, p)); + }).to.throw(/ENOENT/); + }); + }); describe('fs.realpath', () => { it('returns real path root', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = 'a.asar' + const parent = fs.realpathSync(asarDir); + const p = 'a.asar'; fs.realpath(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a normal file', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'file1') + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'file1'); fs.realpath(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a normal directory', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'dir1') + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'dir1'); fs.realpath(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a linked file', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link1') + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'link2', 'link1'); fs.realpath(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, 'a.asar', 'file1')) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, 'a.asar', 'file1')); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a linked directory', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link2') + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'link2', 'link2'); fs.realpath(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of an unpacked file', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('unpack.asar', 'a.txt') + const parent = fs.realpathSync(asarDir); + const p = path.join('unpack.asar', 'a.txt'); fs.realpath(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws ENOENT error when can not find file', done => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'not-exist') + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'not-exist'); fs.realpath(path.join(parent, p), err => { - expect(err.code).to.equal('ENOENT') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.realpath', () => { it('returns real path root', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = 'a.asar' - const r = await fs.promises.realpath(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = 'a.asar'; + const r = await fs.promises.realpath(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a normal file', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'file1') - const r = await fs.promises.realpath(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'file1'); + const r = await fs.promises.realpath(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a normal directory', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'dir1') - const r = await fs.promises.realpath(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'dir1'); + const r = await fs.promises.realpath(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('returns real path of a linked file', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link1') - const r = await fs.promises.realpath(path.join(parent, p)) - expect(r).to.equal(path.join(parent, 'a.asar', 'file1')) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'link2', 'link1'); + const r = await fs.promises.realpath(path.join(parent, p)); + expect(r).to.equal(path.join(parent, 'a.asar', 'file1')); + }); it('returns real path of a linked directory', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link2') - const r = await fs.promises.realpath(path.join(parent, p)) - expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'link2', 'link2'); + const r = await fs.promises.realpath(path.join(parent, p)); + expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')); + }); it('returns real path of an unpacked file', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('unpack.asar', 'a.txt') - const r = await fs.promises.realpath(path.join(parent, p)) - expect(r).to.equal(path.join(parent, p)) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('unpack.asar', 'a.txt'); + const r = await fs.promises.realpath(path.join(parent, p)); + expect(r).to.equal(path.join(parent, p)); + }); it('throws ENOENT error when can not find file', async () => { - const parent = fs.realpathSync(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'not-exist') - await expectToThrowErrorWithCode(() => fs.promises.realpath(path.join(parent, p)), 'ENOENT') - }) - }) + const parent = fs.realpathSync(asarDir); + const p = path.join('a.asar', 'not-exist'); + await expectToThrowErrorWithCode(() => fs.promises.realpath(path.join(parent, p)), 'ENOENT'); + }); + }); describe('fs.realpath.native', () => { it('returns real path root', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = 'a.asar' + const parent = fs.realpathSync.native(asarDir); + const p = 'a.asar'; fs.realpath.native(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a normal file', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'file1') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'file1'); fs.realpath.native(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a normal directory', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'dir1') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'dir1'); fs.realpath.native(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a linked file', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link1') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'link2', 'link1'); fs.realpath.native(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, 'a.asar', 'file1')) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, 'a.asar', 'file1')); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of a linked directory', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'link2', 'link2') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'link2', 'link2'); fs.realpath.native(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, 'a.asar', 'dir1')); + done(); + } catch (e) { + done(e); + } + }); + }); it('returns real path of an unpacked file', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('unpack.asar', 'a.txt') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('unpack.asar', 'a.txt'); fs.realpath.native(path.join(parent, p), (err, r) => { - expect(err).to.be.null() - expect(r).to.equal(path.join(parent, p)) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(r).to.equal(path.join(parent, p)); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws ENOENT error when can not find file', done => { - const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) - const p = path.join('a.asar', 'not-exist') + const parent = fs.realpathSync.native(asarDir); + const p = path.join('a.asar', 'not-exist'); fs.realpath.native(path.join(parent, p), err => { - expect(err.code).to.equal('ENOENT') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.readdirSync', function () { it('reads dirs from root', function () { - const p = path.join(fixtures, 'asar', 'a.asar') - const dirs = fs.readdirSync(p) - expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']) - }) + const p = path.join(asarDir, 'a.asar'); + const dirs = fs.readdirSync(p); + expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + }); it('reads dirs from a normal dir', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') - const dirs = fs.readdirSync(p) - expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']) - }) + const p = path.join(asarDir, 'a.asar', 'dir1'); + const dirs = fs.readdirSync(p); + expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']); + }); + + it('supports withFileTypes', function () { + const p = path.join(asarDir, 'a.asar'); + const dirs = fs.readdirSync(p, { withFileTypes: true }); + for (const dir of dirs) { + expect(dir instanceof fs.Dirent).to.be.true(); + } + const names = dirs.map(a => a.name); + expect(names).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + }); + + it('supports withFileTypes for a deep directory', function () { + const p = path.join(asarDir, 'a.asar', 'dir3'); + const dirs = fs.readdirSync(p, { withFileTypes: true }); + for (const dir of dirs) { + expect(dir instanceof fs.Dirent).to.be.true(); + } + const names = dirs.map(a => a.name); + expect(names).to.deep.equal(['file1', 'file2', 'file3']); + }); it('reads dirs from a linked dir', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') - const dirs = fs.readdirSync(p) - expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']) - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2'); + const dirs = fs.readdirSync(p); + expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']); + }); it('throws ENOENT error when can not find file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); expect(() => { - fs.readdirSync(p) - }).to.throw(/ENOENT/) - }) - }) + fs.readdirSync(p); + }).to.throw(/ENOENT/); + }); + }); describe('fs.readdir', function () { it('reads dirs from root', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar') + const p = path.join(asarDir, 'a.asar'); fs.readdir(p, function (err, dirs) { - expect(err).to.be.null() - expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('supports withFileTypes', function (done) { + const p = path.join(asarDir, 'a.asar'); + + fs.readdir(p, { withFileTypes: true }, (err, dirs) => { + try { + expect(err).to.be.null(); + for (const dir of dirs) { + expect(dir instanceof fs.Dirent).to.be.true(); + } + + const names = dirs.map(a => a.name); + expect(names).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + done(); + } catch (e) { + done(e); + } + }); + }); it('reads dirs from a normal dir', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') + const p = path.join(asarDir, 'a.asar', 'dir1'); fs.readdir(p, function (err, dirs) { - expect(err).to.be.null() - expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']); + done(); + } catch (e) { + done(e); + } + }); + }); + it('reads dirs from a linked dir', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') + const p = path.join(asarDir, 'a.asar', 'link2', 'link2'); fs.readdir(p, function (err, dirs) { - expect(err).to.be.null() - expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']) - done() - }) - }) + try { + expect(err).to.be.null(); + expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws ENOENT error when can not find file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); fs.readdir(p, function (err) { - expect(err.code).to.equal('ENOENT') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.readdir', function () { it('reads dirs from root', async function () { - const p = path.join(fixtures, 'asar', 'a.asar') - const dirs = await fs.promises.readdir(p) - expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']) - }) + const p = path.join(asarDir, 'a.asar'); + const dirs = await fs.promises.readdir(p); + expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + }); + + it('supports withFileTypes', async function () { + const p = path.join(asarDir, 'a.asar'); + const dirs = await fs.promises.readdir(p, { withFileTypes: true }); + for (const dir of dirs) { + expect(dir instanceof fs.Dirent).to.be.true(); + } + const names = dirs.map(a => a.name); + expect(names).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + }); it('reads dirs from a normal dir', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') - const dirs = await fs.promises.readdir(p) - expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']) - }) + const p = path.join(asarDir, 'a.asar', 'dir1'); + const dirs = await fs.promises.readdir(p); + expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']); + }); it('reads dirs from a linked dir', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') - const dirs = await fs.promises.readdir(p) - expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']) - }) + const p = path.join(asarDir, 'a.asar', 'link2', 'link2'); + const dirs = await fs.promises.readdir(p); + expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']); + }); it('throws ENOENT error when can not find file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - await expectToThrowErrorWithCode(() => fs.promises.readdir(p), 'ENOENT') - }) - }) + const p = path.join(asarDir, 'a.asar', 'not-exist'); + await expectToThrowErrorWithCode(() => fs.promises.readdir(p), 'ENOENT'); + }); + }); describe('fs.openSync', function () { it('opens a normal/linked/under-linked-directory file', function () { - const ref2 = ['file1', 'link1', path.join('link2', 'file1')] + const ref2 = ['file1', 'link1', path.join('link2', 'file1')]; for (let j = 0, len = ref2.length; j < len; j++) { - const file = ref2[j] - const p = path.join(fixtures, 'asar', 'a.asar', file) - const fd = fs.openSync(p, 'r') - const buffer = Buffer.alloc(6) - fs.readSync(fd, buffer, 0, 6, 0) - expect(String(buffer).trim()).to.equal('file1') - fs.closeSync(fd) + const file = ref2[j]; + const p = path.join(asarDir, 'a.asar', file); + const fd = fs.openSync(p, 'r'); + const buffer = Buffer.alloc(6); + fs.readSync(fd, buffer, 0, 6, 0); + expect(String(buffer).trim()).to.equal('file1'); + fs.closeSync(fd); } - }) + }); it('throws ENOENT error when can not find file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); expect(() => { - fs.openSync(p) - }).to.throw(/ENOENT/) - }) - }) + fs.openSync(p); + }).to.throw(/ENOENT/); + }); + }); describe('fs.open', function () { it('opens a normal file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); fs.open(p, 'r', function (err, fd) { - expect(err).to.be.null() - const buffer = Buffer.alloc(6) + expect(err).to.be.null(); + const buffer = Buffer.alloc(6); fs.read(fd, buffer, 0, 6, 0, function (err) { - expect(err).to.be.null() - expect(String(buffer).trim()).to.equal('file1') - fs.close(fd, done) - }) - }) - }) + expect(err).to.be.null(); + expect(String(buffer).trim()).to.equal('file1'); + fs.close(fd, done); + }); + }); + }); it('throws ENOENT error when can not find file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); fs.open(p, 'r', function (err) { - expect(err.code).to.equal('ENOENT') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.open', function () { it('opens a normal file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - const fh = await fs.promises.open(p, 'r') - const buffer = Buffer.alloc(6) - await fh.read(buffer, 0, 6, 0) - expect(String(buffer).trim()).to.equal('file1') - await fh.close() - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + const fh = await fs.promises.open(p, 'r'); + const buffer = Buffer.alloc(6); + await fh.read(buffer, 0, 6, 0); + expect(String(buffer).trim()).to.equal('file1'); + await fh.close(); + }); it('throws ENOENT error when can not find file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - await expectToThrowErrorWithCode(() => fs.promises.open(p, 'r'), 'ENOENT') - }) - }) + const p = path.join(asarDir, 'a.asar', 'not-exist'); + await expectToThrowErrorWithCode(() => fs.promises.open(p, 'r'), 'ENOENT'); + }); + }); describe('fs.mkdir', function () { it('throws error when calling inside asar archive', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); fs.mkdir(p, function (err) { - expect(err.code).to.equal('ENOTDIR') - done() - }) - }) - }) + try { + expect(err.code).to.equal('ENOTDIR'); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.mkdir', function () { it('throws error when calling inside asar archive', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - await expectToThrowErrorWithCode(() => fs.promises.mkdir(p), 'ENOTDIR') - }) - }) + const p = path.join(asarDir, 'a.asar', 'not-exist'); + await expectToThrowErrorWithCode(() => fs.promises.mkdir(p), 'ENOTDIR'); + }); + }); describe('fs.mkdirSync', function () { it('throws error when calling inside asar archive', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); expect(() => { - fs.mkdirSync(p) - }).to.throw(/ENOTDIR/) - }) - }) + fs.mkdirSync(p); + }).to.throw(/ENOTDIR/); + }); + }); describe('fs.exists', function () { it('handles an existing file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); // eslint-disable-next-line fs.exists(p, function (exists) { - expect(exists).to.be.true() - done() - }) - }) + try { + expect(exists).to.be.true(); + done(); + } catch (e) { + done(e); + } + }); + }); it('handles a non-existent file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); // eslint-disable-next-line fs.exists(p, function (exists) { - expect(exists).to.be.false() - done() - }) - }) + try { + expect(exists).to.be.false(); + done(); + } catch (e) { + done(e); + } + }); + }); it('promisified version handles an existing file', (done) => { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); // eslint-disable-next-line util.promisify(fs.exists)(p).then(exists => { - expect(exists).to.be.true() - done() - }) - }) + try { + expect(exists).to.be.true(); + done(); + } catch (e) { + done(e); + } + }); + }); it('promisified version handles a non-existent file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); // eslint-disable-next-line util.promisify(fs.exists)(p).then(exists => { - expect(exists).to.be.false() - done() - }) - }) - }) + try { + expect(exists).to.be.false(); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.existsSync', function () { it('handles an existing file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - expect(fs.existsSync(p)).to.be.true() - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + expect(fs.existsSync(p)).to.be.true(); + }); it('handles a non-existent file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - expect(fs.existsSync(p)).to.be.false() - }) - }) + const p = path.join(asarDir, 'a.asar', 'not-exist'); + expect(fs.existsSync(p)).to.be.false(); + }); + }); describe('fs.access', function () { it('accesses a normal file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); fs.access(p, function (err) { - expect(err).to.be.undefined() - done() - }) - }) + try { + expect(err).to.be.undefined(); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws an error when called with write mode', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); fs.access(p, fs.constants.R_OK | fs.constants.W_OK, function (err) { - expect(err.code).to.equal('EACCES') - done() - }) - }) + try { + expect(err.code).to.equal('EACCES'); + done(); + } catch (e) { + done(e); + } + }); + }); it('throws an error when called on non-existent file', function (done) { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); fs.access(p, function (err) { - expect(err.code).to.equal('ENOENT') - done() - }) - }) + try { + expect(err.code).to.equal('ENOENT'); + done(); + } catch (e) { + done(e); + } + }); + }); it('allows write mode for unpacked files', function (done) { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); fs.access(p, fs.constants.R_OK | fs.constants.W_OK, function (err) { - expect(err).to.be.null() - done() - }) - }) - }) + try { + expect(err).to.be.null(); + done(); + } catch (e) { + done(e); + } + }); + }); + }); describe('fs.promises.access', function () { it('accesses a normal file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - await fs.promises.access(p) - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + await fs.promises.access(p); + }); it('throws an error when called with write mode', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') - await expectToThrowErrorWithCode(() => fs.promises.access(p, fs.constants.R_OK | fs.constants.W_OK), 'EACCES') - }) + const p = path.join(asarDir, 'a.asar', 'file1'); + await expectToThrowErrorWithCode(() => fs.promises.access(p, fs.constants.R_OK | fs.constants.W_OK), 'EACCES'); + }); it('throws an error when called on non-existent file', async function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') - await expectToThrowErrorWithCode(() => fs.promises.access(p), 'ENOENT') - }) + const p = path.join(asarDir, 'a.asar', 'not-exist'); + await expectToThrowErrorWithCode(() => fs.promises.access(p), 'ENOENT'); + }); it('allows write mode for unpacked files', async function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') - await fs.promises.access(p, fs.constants.R_OK | fs.constants.W_OK) - }) - }) + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); + await fs.promises.access(p, fs.constants.R_OK | fs.constants.W_OK); + }); + }); describe('fs.accessSync', function () { it('accesses a normal file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); expect(() => { - fs.accessSync(p) - }).to.not.throw() - }) + fs.accessSync(p); + }).to.not.throw(); + }); it('throws an error when called with write mode', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const p = path.join(asarDir, 'a.asar', 'file1'); expect(() => { - fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK) - }).to.throw(/EACCES/) - }) + fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK); + }).to.throw(/EACCES/); + }); it('throws an error when called on non-existent file', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); expect(() => { - fs.accessSync(p) - }).to.throw(/ENOENT/) - }) + fs.accessSync(p); + }).to.throw(/ENOENT/); + }); it('allows write mode for unpacked files', function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); expect(() => { - fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK) - }).to.not.throw() - }) - }) + fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK); + }).to.not.throw(); + }); + }); describe('child_process.fork', function () { before(function () { if (!features.isRunAsNodeEnabled()) { - this.skip() + this.skip(); } - }) + }); it('opens a normal js file', function (done) { - const child = ChildProcess.fork(path.join(fixtures, 'asar', 'a.asar', 'ping.js')) + const child = ChildProcess.fork(path.join(asarDir, 'a.asar', 'ping.js')); child.on('message', function (msg) { - expect(msg).to.equal('message') - done() - }) - child.send('message') - }) + try { + expect(msg).to.equal('message'); + done(); + } catch (e) { + done(e); + } + }); + child.send('message'); + }); it('supports asar in the forked js', function (done) { - const file = path.join(fixtures, 'asar', 'a.asar', 'file1') - const child = ChildProcess.fork(path.join(fixtures, 'module', 'asar.js')) + const file = path.join(asarDir, 'a.asar', 'file1'); + const child = ChildProcess.fork(path.join(fixtures, 'module', 'asar.js')); child.on('message', function (content) { - expect(content).to.equal(fs.readFileSync(file).toString()) - done() - }) - child.send(file) - }) - }) + try { + expect(content).to.equal(fs.readFileSync(file).toString()); + done(); + } catch (e) { + done(e); + } + }); + child.send(file); + }); + }); describe('child_process.exec', function () { - const echo = path.join(fixtures, 'asar', 'echo.asar', 'echo') + const echo = path.join(asarDir, 'echo.asar', 'echo'); it('should not try to extract the command if there is a reference to a file inside an .asar', function (done) { ChildProcess.exec('echo ' + echo + ' foo bar', function (error, stdout) { - expect(error).to.be.null() - expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n') - done() - }) - }) + try { + expect(error).to.be.null(); + expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n'); + done(); + } catch (e) { + done(e); + } + }); + }); it('can be promisified', () => { return util.promisify(ChildProcess.exec)('echo ' + echo + ' foo bar').then(({ stdout }) => { - expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n') - }) - }) - }) + expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n'); + }); + }); + }); describe('child_process.execSync', function () { - const echo = path.join(fixtures, 'asar', 'echo.asar', 'echo') + const echo = path.join(asarDir, 'echo.asar', 'echo'); it('should not try to extract the command if there is a reference to a file inside an .asar', function (done) { - const stdout = ChildProcess.execSync('echo ' + echo + ' foo bar') - expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n') - done() - }) - }) - - describe('child_process.execFile', function () { - const execFile = ChildProcess.execFile - const execFileSync = ChildProcess.execFileSync - const echo = path.join(fixtures, 'asar', 'echo.asar', 'echo') - - before(function () { - if (process.platform !== 'darwin') { - this.skip() + try { + const stdout = ChildProcess.execSync('echo ' + echo + ' foo bar'); + expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n'); + done(); + } catch (e) { + done(e); } - }) + }); + }); + + ifdescribe(process.platform === 'darwin' && process.arch !== 'arm64')('child_process.execFile', function () { + const execFile = ChildProcess.execFile; + const execFileSync = ChildProcess.execFileSync; + const echo = path.join(asarDir, 'echo.asar', 'echo'); it('executes binaries', function (done) { execFile(echo, ['test'], function (error, stdout) { - expect(error).to.be.null() - expect(stdout).to.equal('test\n') - done() - }) - }) + try { + expect(error).to.be.null(); + expect(stdout).to.equal('test\n'); + done(); + } catch (e) { + done(e); + } + }); + }); it('executes binaries without callback', function (done) { - const process = execFile(echo, ['test']) + const process = execFile(echo, ['test']); process.on('close', function (code) { - expect(code).to.equal(0) - done() - }) + try { + expect(code).to.equal(0); + done(); + } catch (e) { + done(e); + } + }); process.on('error', function () { - expect.fail() - done() - }) - }) + done('error'); + }); + }); it('execFileSync executes binaries', function () { - const output = execFileSync(echo, ['test']) - expect(String(output)).to.equal('test\n') - }) + const output = execFileSync(echo, ['test']); + expect(String(output)).to.equal('test\n'); + }); it('can be promisified', () => { return util.promisify(ChildProcess.execFile)(echo, ['test']).then(({ stdout }) => { - expect(stdout).to.equal('test\n') - }) - }) - }) + expect(stdout).to.equal('test\n'); + }); + }); + }); describe('internalModuleReadJSON', function () { - const internalModuleReadJSON = process.binding('fs').internalModuleReadJSON + const { internalModuleReadJSON } = process.binding('fs'); + + it('reads a normal file', function () { + const file1 = path.join(asarDir, 'a.asar', 'file1'); + const [s1, c1] = internalModuleReadJSON(file1); + expect([s1.toString().trim(), c1]).to.eql(['file1', true]); - it('read a normal file', function () { - const file1 = path.join(fixtures, 'asar', 'a.asar', 'file1') - expect(internalModuleReadJSON(file1).toString().trim()).to.equal('file1') - const file2 = path.join(fixtures, 'asar', 'a.asar', 'file2') - expect(internalModuleReadJSON(file2).toString().trim()).to.equal('file2') - const file3 = path.join(fixtures, 'asar', 'a.asar', 'file3') - expect(internalModuleReadJSON(file3).toString().trim()).to.equal('file3') - }) + const file2 = path.join(asarDir, 'a.asar', 'file2'); + const [s2, c2] = internalModuleReadJSON(file2); + expect([s2.toString().trim(), c2]).to.eql(['file2', true]); + + const file3 = path.join(asarDir, 'a.asar', 'file3'); + const [s3, c3] = internalModuleReadJSON(file3); + expect([s3.toString().trim(), c3]).to.eql(['file3', true]); + }); it('reads a normal file with unpacked files', function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') - expect(internalModuleReadJSON(p).toString().trim()).to.equal('a') - }) - }) + const p = path.join(asarDir, 'unpack.asar', 'a.txt'); + const [s, c] = internalModuleReadJSON(p); + expect([s.toString().trim(), c]).to.eql(['a', true]); + }); + }); describe('util.promisify', function () { it('can promisify all fs functions', function () { - const originalFs = require('original-fs') - const { hasOwnProperty } = Object.prototype + const originalFs = require('original-fs'); + const { hasOwnProperty } = Object.prototype; for (const [propertyName, originalValue] of Object.entries(originalFs)) { // Some properties exist but have a value of `undefined` on some platforms. // E.g. `fs.lchmod`, which in only available on MacOS, see // https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_lchmod_path_mode_callback // Also check for `null`s, `hasOwnProperty()` can't handle them. - if (typeof originalValue === 'undefined' || originalValue === null) continue + if (typeof originalValue === 'undefined' || originalValue === null) continue; if (hasOwnProperty.call(originalValue, util.promisify.custom)) { expect(fs).to.have.own.property(propertyName) - .that.has.own.property(util.promisify.custom) + .that.has.own.property(util.promisify.custom); } } - }) - }) + }); + }); describe('process.noAsar', function () { - const errorName = process.platform === 'win32' ? 'ENOENT' : 'ENOTDIR' + const errorName = process.platform === 'win32' ? 'ENOENT' : 'ENOTDIR'; beforeEach(function () { - process.noAsar = true - }) + process.noAsar = true; + }); afterEach(function () { - process.noAsar = false - }) + process.noAsar = false; + }); it('disables asar support in sync API', function () { - const file = path.join(fixtures, 'asar', 'a.asar', 'file1') - const dir = path.join(fixtures, 'asar', 'a.asar', 'dir1') + const file = path.join(asarDir, 'a.asar', 'file1'); + const dir = path.join(asarDir, 'a.asar', 'dir1'); expect(() => { - fs.readFileSync(file) - }).to.throw(new RegExp(errorName)) + fs.readFileSync(file); + }).to.throw(new RegExp(errorName)); expect(() => { - fs.lstatSync(file) - }).to.throw(new RegExp(errorName)) + fs.lstatSync(file); + }).to.throw(new RegExp(errorName)); expect(() => { - fs.realpathSync(file) - }).to.throw(new RegExp(errorName)) + fs.realpathSync(file); + }).to.throw(new RegExp(errorName)); expect(() => { - fs.readdirSync(dir) - }).to.throw(new RegExp(errorName)) - }) + fs.readdirSync(dir); + }).to.throw(new RegExp(errorName)); + }); it('disables asar support in async API', function (done) { - const file = path.join(fixtures, 'asar', 'a.asar', 'file1') - const dir = path.join(fixtures, 'asar', 'a.asar', 'dir1') + const file = path.join(asarDir, 'a.asar', 'file1'); + const dir = path.join(asarDir, 'a.asar', 'dir1'); fs.readFile(file, function (error) { - expect(error.code).to.equal(errorName) + expect(error.code).to.equal(errorName); fs.lstat(file, function (error) { - expect(error.code).to.equal(errorName) + expect(error.code).to.equal(errorName); fs.realpath(file, function (error) { - expect(error.code).to.equal(errorName) + expect(error.code).to.equal(errorName); fs.readdir(dir, function (error) { - expect(error.code).to.equal(errorName) - done() - }) - }) - }) - }) - }) + expect(error.code).to.equal(errorName); + done(); + }); + }); + }); + }); + }); it('disables asar support in promises API', async function () { - const file = path.join(fixtures, 'asar', 'a.asar', 'file1') - const dir = path.join(fixtures, 'asar', 'a.asar', 'dir1') - await expect(fs.promises.readFile(file)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)) - await expect(fs.promises.lstat(file)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)) - await expect(fs.promises.realpath(file)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)) - await expect(fs.promises.readdir(dir)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)) - }) + const file = path.join(asarDir, 'a.asar', 'file1'); + const dir = path.join(asarDir, 'a.asar', 'dir1'); + await expect(fs.promises.readFile(file)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)); + await expect(fs.promises.lstat(file)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)); + await expect(fs.promises.realpath(file)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)); + await expect(fs.promises.readdir(dir)).to.be.eventually.rejectedWith(Error, new RegExp(errorName)); + }); it('treats *.asar as normal file', function () { - const originalFs = require('original-fs') - const asar = path.join(fixtures, 'asar', 'a.asar') - const content1 = fs.readFileSync(asar) - const content2 = originalFs.readFileSync(asar) - expect(content1.compare(content2)).to.equal(0) + const originalFs = require('original-fs'); + const asar = path.join(asarDir, 'a.asar'); + const content1 = fs.readFileSync(asar); + const content2 = originalFs.readFileSync(asar); + expect(content1.compare(content2)).to.equal(0); expect(() => { - fs.readdirSync(asar) - }).to.throw(/ENOTDIR/) - }) + fs.readdirSync(asar); + }).to.throw(/ENOTDIR/); + }); it('is reset to its original value when execSync throws an error', function () { - process.noAsar = false + process.noAsar = false; expect(() => { - ChildProcess.execSync(path.join(__dirname, 'does-not-exist.txt')) - }).to.throw() - expect(process.noAsar).to.be.false() - }) - }) + ChildProcess.execSync(path.join(__dirname, 'does-not-exist.txt')); + }).to.throw(); + expect(process.noAsar).to.be.false(); + }); + }); describe('process.env.ELECTRON_NO_ASAR', function () { before(function () { if (!features.isRunAsNodeEnabled()) { - this.skip() + this.skip(); } - }) + }); it('disables asar support in forked processes', function (done) { const forked = ChildProcess.fork(path.join(__dirname, 'fixtures', 'module', 'no-asar.js'), [], { env: { ELECTRON_NO_ASAR: true } - }) + }); forked.on('message', function (stats) { - expect(stats.isFile).to.be.true() - expect(stats.size).to.equal(778) - done() - }) - }) + try { + expect(stats.isFile).to.be.true(); + expect(stats.size).to.equal(3458); + done(); + } catch (e) { + done(e); + } + }); + }); it('disables asar support in spawned processes', function (done) { const spawned = ChildProcess.spawn(process.execPath, [path.join(__dirname, 'fixtures', 'module', 'no-asar.js')], { @@ -1330,215 +1578,142 @@ describe('asar package', function () { ELECTRON_NO_ASAR: true, ELECTRON_RUN_AS_NODE: true } - }) + }); - let output = '' + let output = ''; spawned.stdout.on('data', function (data) { - output += data - }) + output += data; + }); spawned.stdout.on('close', function () { - const stats = JSON.parse(output) - expect(stats.isFile).to.be.true() - expect(stats.size).to.equal(778) - done() - }) - }) - }) - }) + try { + const stats = JSON.parse(output); + expect(stats.isFile).to.be.true(); + expect(stats.size).to.equal(3458); + done(); + } catch (e) { + done(e); + } + }); + }); + }); + }); describe('asar protocol', function () { - let w = null - - afterEach(function () { - return closeWindow(w).then(function () { w = null }) - }) - - it('can request a file in package', function (done) { - const p = path.resolve(fixtures, 'asar', 'a.asar', 'file1') - $.get('file://' + p, function (data) { - expect(data.trim()).to.equal('file1') - done() - }) - }) - - it('can request a file in package with unpacked files', function (done) { - const p = path.resolve(fixtures, 'asar', 'unpack.asar', 'a.txt') - $.get('file://' + p, function (data) { - expect(data.trim()).to.equal('a') - done() - }) - }) - - it('can request a linked file in package', function (done) { - const p = path.resolve(fixtures, 'asar', 'a.asar', 'link2', 'link1') - $.get('file://' + p, function (data) { - expect(data.trim()).to.equal('file1') - done() - }) - }) - - it('can request a file in filesystem', function (done) { - const p = path.resolve(fixtures, 'asar', 'file') - $.get('file://' + p, function (data) { - expect(data.trim()).to.equal('file') - done() - }) - }) - - it('gets 404 when file is not found', function (done) { - const p = path.resolve(fixtures, 'asar', 'a.asar', 'no-exist') - $.ajax({ - url: 'file://' + p, - error: function (err) { - expect(err.status).to.equal(404) - done() - } - }) - }) - - it('sets __dirname correctly', function (done) { - after(function () { - ipcMain.removeAllListeners('dirname') - }) - - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: { - nodeIntegration: true - } - }) - const p = path.resolve(fixtures, 'asar', 'web.asar', 'index.html') - ipcMain.once('dirname', function (event, dirname) { - expect(dirname).to.equal(path.dirname(p)) - done() - }) - w.loadFile(p) - }) - - it('loads script tag in html', function (done) { - after(function () { - ipcMain.removeAllListeners('ping') - }) - - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: { - nodeIntegration: true - } - }) - const p = path.resolve(fixtures, 'asar', 'script.asar', 'index.html') - w.loadFile(p) - ipcMain.once('ping', function (event, message) { - expect(message).to.equal('pong') - done() - }) - }) - - it('loads video tag in html', function (done) { - this.timeout(60000) - - after(function () { - ipcMain.removeAllListeners('asar-video') - }) - - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: { - nodeIntegration: true - } - }) - const p = path.resolve(fixtures, 'asar', 'video.asar', 'index.html') - w.loadFile(p) - ipcMain.on('asar-video', function (event, message, error) { - if (message === 'ended') { - expect(error).to.be.null() - done() - } else if (message === 'error') { - done(error) - } - }) - }) - }) + it('can request a file in package', async function () { + const p = path.resolve(asarDir, 'a.asar', 'file1'); + const response = await fetch('file://' + p); + const data = await response.text(); + expect(data.trim()).to.equal('file1'); + }); + + it('can request a file in package with unpacked files', async function () { + const p = path.resolve(asarDir, 'unpack.asar', 'a.txt'); + const response = await fetch('file://' + p); + const data = await response.text(); + expect(data.trim()).to.equal('a'); + }); + + it('can request a linked file in package', async function () { + const p = path.resolve(asarDir, 'a.asar', 'link2', 'link1'); + const response = await fetch('file://' + p); + const data = await response.text(); + expect(data.trim()).to.equal('file1'); + }); + + it('can request a file in filesystem', async function () { + const p = path.resolve(asarDir, 'file'); + const response = await fetch('file://' + p); + const data = await response.text(); + expect(data.trim()).to.equal('file'); + }); + + it('gets error when file is not found', async function () { + const p = path.resolve(asarDir, 'a.asar', 'no-exist'); + try { + const response = await fetch('file://' + p); + expect(response.status).to.equal(404); + } catch (error) { + expect(error.message).to.equal('Failed to fetch'); + } + }); + }); describe('original-fs module', function () { - const originalFs = require('original-fs') + const originalFs = require('original-fs'); it('treats .asar as file', function () { - const file = path.join(fixtures, 'asar', 'a.asar') - const stats = originalFs.statSync(file) - expect(stats.isFile()).to.be.true() - }) - - it('is available in forked scripts', function (done) { - if (!features.isRunAsNodeEnabled()) { - this.skip() - done() - } - - const child = ChildProcess.fork(path.join(fixtures, 'module', 'original-fs.js')) - child.on('message', function (msg) { - expect(msg).to.equal('object') - done() - }) - child.send('message') - }) + const file = path.join(asarDir, 'a.asar'); + const stats = originalFs.statSync(file); + expect(stats.isFile()).to.be.true(); + }); + + ifit(features.isRunAsNodeEnabled())('is available in forked scripts', async function () { + const child = ChildProcess.fork(path.join(fixtures, 'module', 'original-fs.js')); + const message = emittedOnce(child, 'message'); + child.send('message'); + const [msg] = await message; + expect(msg).to.equal('object'); + }); it('can be used with streams', () => { - originalFs.createReadStream(path.join(fixtures, 'asar', 'a.asar')) - }) + originalFs.createReadStream(path.join(asarDir, 'a.asar')); + }); + + it('can recursively delete a directory with an asar file in it', () => { + const deleteDir = path.join(asarDir, 'deleteme'); + fs.mkdirSync(deleteDir); + + originalFs.rmdirSync(deleteDir, { recursive: true }); + + expect(fs.existsSync(deleteDir)).to.be.false(); + }); it('has the same APIs as fs', function () { - expect(Object.keys(require('fs'))).to.deep.equal(Object.keys(require('original-fs'))) - expect(Object.keys(require('fs').promises)).to.deep.equal(Object.keys(require('original-fs').promises)) - }) - }) + expect(Object.keys(require('fs'))).to.deep.equal(Object.keys(require('original-fs'))); + expect(Object.keys(require('fs').promises)).to.deep.equal(Object.keys(require('original-fs').promises)); + }); + }); describe('graceful-fs module', function () { - const gfs = require('graceful-fs') + const gfs = require('graceful-fs'); it('recognize asar archvies', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'link1') - expect(gfs.readFileSync(p).toString().trim()).to.equal('file1') - }) + const p = path.join(asarDir, 'a.asar', 'link1'); + expect(gfs.readFileSync(p).toString().trim()).to.equal('file1'); + }); it('does not touch global fs object', function () { - expect(fs.readdir).to.not.equal(gfs.readdir) - }) - }) + expect(fs.readdir).to.not.equal(gfs.readdir); + }); + }); describe('mkdirp module', function () { - const mkdirp = require('mkdirp') + const mkdirp = require('mkdirp'); it('throws error when calling inside asar archive', function () { - const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + const p = path.join(asarDir, 'a.asar', 'not-exist'); expect(() => { - mkdirp.sync(p) - }).to.throw(/ENOTDIR/) - }) - }) + mkdirp.sync(p); + }).to.throw(/ENOTDIR/); + }); + }); describe('native-image', function () { it('reads image from asar archive', function () { - const p = path.join(fixtures, 'asar', 'logo.asar', 'logo.png') - const logo = nativeImage.createFromPath(p) + const p = path.join(asarDir, 'logo.asar', 'logo.png'); + const logo = nativeImage.createFromPath(p); expect(logo.getSize()).to.deep.equal({ width: 55, height: 55 - }) - }) + }); + }); it('reads image from asar archive with unpacked files', function () { - const p = path.join(fixtures, 'asar', 'unpack.asar', 'atom.png') - const logo = nativeImage.createFromPath(p) + const p = path.join(asarDir, 'unpack.asar', 'atom.png'); + const logo = nativeImage.createFromPath(p); expect(logo.getSize()).to.deep.equal({ width: 1024, height: 1024 - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/spec/chrome-api-spec.js b/spec/chrome-api-spec.js deleted file mode 100644 index beacd423151a3..0000000000000 --- a/spec/chrome-api-spec.js +++ /dev/null @@ -1,80 +0,0 @@ -const fs = require('fs') -const path = require('path') - -const { expect } = require('chai') -const { remote } = require('electron') - -const { closeWindow } = require('./window-helpers') -const { emittedOnce } = require('./events-helpers') - -const { BrowserWindow } = remote - -describe('chrome api', () => { - const fixtures = path.resolve(__dirname, 'fixtures') - let w - - before(() => { - BrowserWindow.addExtension(path.join(fixtures, 'extensions/chrome-api')) - }) - - after(() => { - BrowserWindow.removeExtension('chrome-api') - }) - - beforeEach(() => { - w = new BrowserWindow({ - show: false - }) - }) - - afterEach(() => closeWindow(w).then(() => { w = null })) - - it('runtime.getManifest returns extension manifest', async () => { - const actualManifest = (() => { - const data = fs.readFileSync(path.join(fixtures, 'extensions/chrome-api/manifest.json'), 'utf-8') - return JSON.parse(data) - })() - - await w.loadURL('about:blank') - - const promise = emittedOnce(w.webContents, 'console-message') - - const message = { method: 'getManifest' } - w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`) - - const [,, manifestString] = await promise - const manifest = JSON.parse(manifestString) - - expect(manifest.name).to.equal(actualManifest.name) - expect(manifest.content_scripts).to.have.lengthOf(actualManifest.content_scripts.length) - }) - - it('chrome.tabs.sendMessage receives the response', async function () { - await w.loadURL('about:blank') - - const promise = emittedOnce(w.webContents, 'console-message') - - const message = { method: 'sendMessage', args: ['Hello World!'] } - w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`) - - const [,, responseString] = await promise - const response = JSON.parse(responseString) - - expect(response.message).to.equal('Hello World!') - expect(response.tabId).to.equal(w.webContents.id) - }) - - it('chrome.tabs.executeScript receives the response', async function () { - await w.loadURL('about:blank') - - const promise = emittedOnce(w.webContents, 'console-message') - - const message = { method: 'executeScript', args: ['1 + 2'] } - w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`) - - const [,, responseString] = await promise - const response = JSON.parse(responseString) - - expect(response).to.equal(3) - }) -}) diff --git a/spec/chromium-spec.js b/spec/chromium-spec.js index 71b9987c057ba..afcc2b668a756 100644 --- a/spec/chromium-spec.js +++ b/spec/chromium-spec.js @@ -1,111 +1,42 @@ -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const fs = require('fs') -const http = require('http') -const path = require('path') -const ws = require('ws') -const url = require('url') -const ChildProcess = require('child_process') -const { ipcRenderer, remote } = require('electron') -const { emittedOnce } = require('./events-helpers') -const { closeWindow, waitForWebContentsToLoad } = require('./window-helpers') -const { resolveGetters } = require('./expect-helpers') -const { app, BrowserWindow, ipcMain, protocol, session, webContents } = remote -const isCI = remote.getGlobal('isCi') -const features = process.electronBinding('features') - -const { expect } = chai -chai.use(dirtyChai) +const { expect } = require('chai'); +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const ws = require('ws'); +const url = require('url'); +const ChildProcess = require('child_process'); +const { ipcRenderer } = require('electron'); +const { emittedOnce, waitForEvent } = require('./events-helpers'); +const { resolveGetters } = require('./expect-helpers'); +const { ifit, ifdescribe, delay } = require('./spec-helpers'); +const features = process._linkedBinding('electron_common_features'); /* Most of the APIs here don't use standard callbacks */ /* eslint-disable standard/no-callback-literal */ describe('chromium feature', () => { - const fixtures = path.resolve(__dirname, 'fixtures') - let listener = null - let w = null - - afterEach(() => { - if (listener != null) { - window.removeEventListener('message', listener) - } - listener = null - }) - - afterEach(async () => { - await closeWindow(w) - w = null - }) - - describe('command line switches', () => { - describe('--lang switch', () => { - const currentLocale = app.getLocale() - const testLocale = (locale, result, done) => { - const appPath = path.join(__dirname, 'fixtures', 'api', 'locale-check') - const electronPath = remote.getGlobal('process').execPath - let output = '' - const appProcess = ChildProcess.spawn(electronPath, [appPath, `--lang=${locale}`]) - - appProcess.stdout.on('data', (data) => { output += data }) - appProcess.stdout.on('end', () => { - output = output.replace(/(\r\n|\n|\r)/gm, '') - expect(output).to.equal(result) - done() - }) - } + const fixtures = path.resolve(__dirname, 'fixtures'); - it('should set the locale', (done) => testLocale('fr', 'fr', done)) - it('should not set an invalid locale', (done) => testLocale('asdfkl', currentLocale, done)) - }) - - describe('--remote-debugging-port switch', () => { - it('should display the discovery page', (done) => { - const electronPath = remote.getGlobal('process').execPath - let output = '' - const appProcess = ChildProcess.spawn(electronPath, [`--remote-debugging-port=`]) - - appProcess.stderr.on('data', (data) => { - output += data - const m = /DevTools listening on ws:\/\/127.0.0.1:(\d+)\//.exec(output) - if (m) { - appProcess.stderr.removeAllListeners('data') - const port = m[1] - http.get(`http://127.0.0.1:${port}`, (res) => { - res.destroy() - appProcess.kill() - expect(res.statusCode).to.eql(200) - expect(parseInt(res.headers['content-length'])).to.be.greaterThan(0) - done() - }) - } - }) - }) - }) - }) + describe('Badging API', () => { + it('does not crash', () => { + expect(() => { + navigator.setAppBadge(42); + }).to.not.throw(); + expect(() => { + // setAppBadge with no argument should show dot + navigator.setAppBadge(); + }).to.not.throw(); + expect(() => { + navigator.clearAppBadge(); + }).to.not.throw(); + }); + }); describe('heap snapshot', () => { it('does not crash', function () { - process.electronBinding('v8_util').takeHeapSnapshot() - }) - }) - - describe('accessing key names also used as Node.js module names', () => { - it('does not crash', (done) => { - w = new BrowserWindow({ show: false }) - w.webContents.once('did-finish-load', () => { done() }) - w.webContents.once('crashed', () => done(new Error('WebContents crashed.'))) - w.loadFile(path.join(fixtures, 'pages', 'external-string.html')) - }) - }) - - describe('loading jquery', () => { - it('does not crash', (done) => { - w = new BrowserWindow({ show: false }) - w.webContents.once('did-finish-load', () => { done() }) - w.webContents.once('crashed', () => done(new Error('WebContents crashed.'))) - w.loadFile(path.join(fixtures, 'pages', 'jquery.html')) - }) - }) + process._linkedBinding('electron_common_v8_util').takeHeapSnapshot(); + }); + }); describe('navigator.webkitGetUserMedia', () => { it('calls its callbacks', (done) => { @@ -113,259 +44,45 @@ describe('chromium feature', () => { audio: true, video: false }, () => done(), - () => done()) - }) - }) - - describe('navigator.mediaDevices', () => { - if (isCI) return - - afterEach(() => { - remote.getGlobal('permissionChecks').allow() - }) - - it('can return labels of enumerated devices', (done) => { - navigator.mediaDevices.enumerateDevices().then((devices) => { - const labels = devices.map((device) => device.label) - const labelFound = labels.some((label) => !!label) - if (labelFound) { - done() - } else { - done(new Error(`No device labels found: ${JSON.stringify(labels)}`)) - } - }).catch(done) - }) - - it('does not return labels of enumerated devices when permission denied', (done) => { - remote.getGlobal('permissionChecks').reject() - navigator.mediaDevices.enumerateDevices().then((devices) => { - const labels = devices.map((device) => device.label) - const labelFound = labels.some((label) => !!label) - if (labelFound) { - done(new Error(`Device labels were found: ${JSON.stringify(labels)}`)) - } else { - done() - } - }).catch(done) - }) - - it('can return new device id when cookie storage is cleared', (done) => { - const options = { - origin: null, - storages: ['cookies'] - } - const deviceIds = [] - const ses = session.fromPartition('persist:media-device-id') - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - session: ses - } - }) - w.webContents.on('ipc-message', (event, channel, deviceId) => { - if (channel === 'deviceIds') deviceIds.push(deviceId) - if (deviceIds.length === 2) { - expect(deviceIds[0]).to.not.deep.equal(deviceIds[1]) - closeWindow(w).then(() => { - w = null - done() - }).catch((error) => done(error)) - } else { - ses.clearStorageData(options).then(() => { - w.webContents.reload() - }) - } - }) - w.loadFile(path.join(fixtures, 'pages', 'media-id-reset.html')) - }) - }) + () => done()); + }); + }); describe('navigator.language', () => { it('should not be empty', () => { - expect(navigator.language).to.not.equal('') - }) - }) - - describe('navigator.languages', (done) => { - it('should return the system locale only', () => { - const appLocale = app.getLocale() - expect(navigator.languages).to.deep.equal([appLocale]) - }) - }) - - describe('navigator.serviceWorker', () => { - it('should register for file scheme', (done) => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - partition: 'sw-file-scheme-spec' - } - }) - w.webContents.on('ipc-message', (event, channel, message) => { - if (channel === 'reload') { - w.webContents.reload() - } else if (channel === 'error') { - done(message) - } else if (channel === 'response') { - expect(message).to.equal('Hello from serviceWorker!') - session.fromPartition('sw-file-scheme-spec').clearStorageData({ - storages: ['serviceworkers'] - }).then(() => done()) - } - }) - w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))) - w.loadFile(path.join(fixtures, 'pages', 'service-worker', 'index.html')) - }) - - it('should register for intercepted file scheme', (done) => { - const customSession = session.fromPartition('intercept-file') - customSession.protocol.interceptBufferProtocol('file', (request, callback) => { - let file = url.parse(request.url).pathname - if (file[0] === '/' && process.platform === 'win32') file = file.slice(1) - - const content = fs.readFileSync(path.normalize(file)) - const ext = path.extname(file) - let type = 'text/html' - - if (ext === '.js') type = 'application/javascript' - callback({ data: content, mimeType: type }) - }, (error) => { - if (error) done(error) - }) - - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - session: customSession - } - }) - w.webContents.on('ipc-message', (event, channel, message) => { - if (channel === 'reload') { - w.webContents.reload() - } else if (channel === 'error') { - done(`unexpected error : ${message}`) - } else if (channel === 'response') { - expect(message).to.equal('Hello from serviceWorker!') - customSession.clearStorageData({ - storages: ['serviceworkers'] - }).then(() => { - customSession.protocol.uninterceptProtocol('file', error => done(error)) - }) - } - }) - w.webContents.on('crashed', () => done(new Error('WebContents crashed.'))) - w.loadFile(path.join(fixtures, 'pages', 'service-worker', 'index.html')) - }) - }) - - describe('navigator.geolocation', () => { - before(function () { - if (!features.isFakeLocationProviderEnabled()) { - return this.skip() - } - }) - - it('returns position when permission is granted', (done) => { - navigator.geolocation.getCurrentPosition((position) => { - expect(position).to.have.a.property('coords') - expect(position).to.have.a.property('timestamp') - done() - }, (error) => { - done(error) - }) - }) - - it('returns error when permission is denied', (done) => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - partition: 'geolocation-spec' - } - }) - w.webContents.on('ipc-message', (event, channel) => { - if (channel === 'success') { - done() - } else { - done('unexpected response from geolocation api') - } - }) - w.webContents.session.setPermissionRequestHandler((wc, permission, callback) => { - if (permission === 'geolocation') { - callback(false) - } else { - callback(true) - } - }) - w.loadFile(path.join(fixtures, 'pages', 'geolocation', 'index.html')) - }) - }) + expect(navigator.language).to.not.equal(''); + }); + }); + + ifdescribe(features.isFakeLocationProviderEnabled())('navigator.geolocation', () => { + it('returns position when permission is granted', async () => { + const position = await new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject)); + expect(position).to.have.a.property('coords'); + expect(position).to.have.a.property('timestamp'); + }); + }); describe('window.open', () => { - it('returns a BrowserWindowProxy object', () => { - const b = window.open('about:blank', '', 'show=no') - expect(b.closed).to.be.false() - expect(b.constructor.name).to.equal('BrowserWindowProxy') - b.close() - }) - - it('accepts "nodeIntegration" as feature', (done) => { - let b = null - listener = (event) => { - expect(event.data.isProcessGlobalUndefined).to.be.true() - b.close() - done() - } - window.addEventListener('message', listener) - b = window.open(`file://${fixtures}/pages/window-opener-node.html`, '', 'nodeIntegration=no,show=no') - }) - - it('inherit options of parent window', (done) => { - let b = null - listener = (event) => { - const ref1 = remote.getCurrentWindow().getSize() - const width = ref1[0] - const height = ref1[1] - expect(event.data).to.equal(`size: ${width} ${height}`) - b.close() - done() - } - window.addEventListener('message', listener) - b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no') - }) - - for (const show of [true, false]) { - it(`inherits parent visibility over parent {show=${show}} option`, (done) => { - const w = new BrowserWindow({ show }) - - // toggle visibility - if (show) { - w.hide() - } else { - w.show() - } - - w.webContents.once('new-window', (e, url, frameName, disposition, options) => { - expect(options.show).to.equal(w.isVisible()) - w.close() - done() - }) - w.loadFile(path.join(fixtures, 'pages', 'window-open.html')) - }) - } - - it('disables node integration when it is disabled on the parent window', (done) => { - let b = null - listener = (event) => { - expect(event.data.isProcessGlobalUndefined).to.be.true() - b.close() - done() - } - window.addEventListener('message', listener) - + it('accepts "nodeIntegration" as feature', async () => { + const message = waitForEvent(window, 'message'); + const b = window.open(`file://${fixtures}/pages/window-opener-node.html`, '', 'nodeIntegration=no,show=no'); + const event = await message; + b.close(); + expect(event.data.isProcessGlobalUndefined).to.be.true(); + }); + + it('inherit options of parent window', async () => { + const message = waitForEvent(window, 'message'); + const b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no'); + const event = await message; + b.close(); + const width = outerWidth; + const height = outerHeight; + expect(event.data).to.equal(`size: ${width} ${height}`); + }); + + // FIXME(zcbenz): This test is making the spec runner hang on exit on Windows. + ifit(process.platform !== 'win32')('disables node integration when it is disabled on the parent window', async () => { const windowUrl = require('url').format({ pathname: `${fixtures}/pages/window-opener-no-node-integration.html`, protocol: 'file', @@ -373,59 +90,15 @@ describe('chromium feature', () => { p: `${fixtures}/pages/window-opener-node.html` }, slashes: true - }) - b = window.open(windowUrl, '', 'nodeIntegration=no,show=no') - }) - - // TODO(codebytere): re-enable this test - xit('disables node integration when it is disabled on the parent window for chrome devtools URLs', (done) => { - let b = null - app.once('web-contents-created', (event, contents) => { - contents.once('did-finish-load', () => { - contents.executeJavaScript('typeof process').then((typeofProcessGlobal) => { - expect(typeofProcessGlobal).to.equal('undefined') - b.close() - done() - }).catch(done) - }) - }) - b = window.open('devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no') - }) - - it('disables JavaScript when it is disabled on the parent window', (done) => { - let b = null - app.once('web-contents-created', (event, contents) => { - contents.once('did-finish-load', () => { - app.once('browser-window-created', (event, window) => { - const preferences = window.webContents.getLastWebPreferences() - expect(preferences.javascript).to.be.false() - window.destroy() - b.close() - done() - }) - // Click link on page - contents.sendInputEvent({ type: 'mouseDown', clickCount: 1, x: 1, y: 1 }) - contents.sendInputEvent({ type: 'mouseUp', clickCount: 1, x: 1, y: 1 }) - }) - }) - - const windowUrl = require('url').format({ - pathname: `${fixtures}/pages/window-no-javascript.html`, - protocol: 'file', - slashes: true - }) - b = window.open(windowUrl, '', 'javascript=no,show=no') - }) - - it('disables the tag when it is disabled on the parent window', (done) => { - let b = null - listener = (event) => { - expect(event.data.isWebViewGlobalUndefined).to.be.true() - b.close() - done() - } - window.addEventListener('message', listener) - + }); + const message = waitForEvent(window, 'message'); + const b = window.open(windowUrl, '', 'nodeIntegration=no,contextIsolation=no,show=no'); + const event = await message; + b.close(); + expect(event.data.isProcessGlobalUndefined).to.be.true(); + }); + + it('disables the tag when it is disabled on the parent window', async () => { const windowUrl = require('url').format({ pathname: `${fixtures}/pages/window-opener-no-webview-tag.html`, protocol: 'file', @@ -433,344 +106,73 @@ describe('chromium feature', () => { p: `${fixtures}/pages/window-opener-webview.html` }, slashes: true - }) - b = window.open(windowUrl, '', 'webviewTag=no,nodeIntegration=yes,show=no') - }) - - it('does not override child options', (done) => { - let b = null + }); + const message = waitForEvent(window, 'message'); + const b = window.open(windowUrl, '', 'webviewTag=no,contextIsolation=no,nodeIntegration=yes,show=no'); + const event = await message; + b.close(); + expect(event.data.isWebViewGlobalUndefined).to.be.true(); + }); + + it('does not override child options', async () => { const size = { width: 350, height: 450 - } - listener = (event) => { - expect(event.data).to.equal(`size: ${size.width} ${size.height}`) - b.close() - done() - } - window.addEventListener('message', listener) - b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height) - }) - - it('handles cycles when merging the parent options into the child options', (done) => { - w = BrowserWindow.fromId(ipcRenderer.sendSync('create-window-with-options-cycle')) - w.loadFile(path.join(fixtures, 'pages', 'window-open.html')) - w.webContents.once('new-window', (event, url, frameName, disposition, options) => { - expect(options.show).to.be.false() - expect(...resolveGetters(options.foo)).to.deep.equal({ - bar: undefined, - baz: { - hello: { - world: true - } - }, - baz2: { - hello: { - world: true - } - } - }) - done() - }) - }) - - it('defines a window.location getter', (done) => { - let b = null - let targetURL - if (process.platform === 'win32') { - targetURL = `file:///${fixtures.replace(/\\/g, '/')}/pages/base-page.html` - } else { - targetURL = `file://${fixtures}/pages/base-page.html` - } - app.once('browser-window-created', (event, window) => { - window.webContents.once('did-finish-load', () => { - expect(b.location.href).to.equal(targetURL) - b.close() - done() - }) - }) - b = window.open(targetURL) - }) - - it('defines a window.location setter', (done) => { - let b = null - app.once('browser-window-created', (event, { webContents }) => { - webContents.once('did-finish-load', () => { - // When it loads, redirect - b.location = `file://${fixtures}/pages/base-page.html` - webContents.once('did-finish-load', () => { - // After our second redirect, cleanup and callback - b.close() - done() - }) - }) - }) - b = window.open('about:blank') - }) - - it('defines a window.location.href setter', (done) => { - let b = null - app.once('browser-window-created', (event, { webContents }) => { - webContents.once('did-finish-load', () => { - // When it loads, redirect - b.location.href = `file://${fixtures}/pages/base-page.html` - webContents.once('did-finish-load', () => { - // After our second redirect, cleanup and callback - b.close() - done() - }) - }) - }) - b = window.open('about:blank') - }) - - it('open a blank page when no URL is specified', async () => { - const browserWindowCreated = emittedOnce(app, 'browser-window-created') - const w = window.open() - try { - const [, { webContents }] = await browserWindowCreated - await waitForWebContentsToLoad(webContents) - expect(w.location.href).to.equal('about:blank') - } finally { - w.close() - } - }) - - it('open a blank page when an empty URL is specified', async () => { - const browserWindowCreated = emittedOnce(app, 'browser-window-created') - const w = window.open('') - try { - const [, { webContents }] = await browserWindowCreated - await waitForWebContentsToLoad(webContents) - expect(w.location.href).to.equal('about:blank') - } finally { - w.close() - } - }) + }; + const message = waitForEvent(window, 'message'); + const b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height); + const event = await message; + b.close(); + expect(event.data).to.equal(`size: ${size.width} ${size.height}`); + }); it('throws an exception when the arguments cannot be converted to strings', () => { expect(() => { - window.open('', { toString: null }) - }).to.throw('Cannot convert object to primitive value') + window.open('', { toString: null }); + }).to.throw('Cannot convert object to primitive value'); expect(() => { - window.open('', '', { toString: 3 }) - }).to.throw('Cannot convert object to primitive value') - }) - - it('sets the window title to the specified frameName', (done) => { - let b = null - app.once('browser-window-created', (event, createdWindow) => { - expect(createdWindow.getTitle()).to.equal('hello') - b.close() - done() - }) - b = window.open('', 'hello') - }) - - it('does not throw an exception when the frameName is a built-in object property', (done) => { - let b = null - app.once('browser-window-created', (event, createdWindow) => { - expect(createdWindow.getTitle()).to.equal('__proto__') - b.close() - done() - }) - b = window.open('', '__proto__') - }) + window.open('', '', { toString: 3 }); + }).to.throw('Cannot convert object to primitive value'); + }); it('does not throw an exception when the features include webPreferences', () => { - let b = null + let b = null; expect(() => { - b = window.open('', '', 'webPreferences=') - }).to.not.throw() - b.close() - }) - }) + b = window.open('', '', 'webPreferences='); + }).to.not.throw(); + b.close(); + }); + }); describe('window.opener', () => { - const url = `file://${fixtures}/pages/window-opener.html` - it('is null for main window', async () => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }) - const promise = emittedOnce(w.webContents, 'ipc-message') - w.loadFile(path.join(fixtures, 'pages', 'window-opener.html')) - const [, channel, opener] = await promise - expect(channel).to.equal('opener') - expect(opener).to.equal(null) - }) - - it('is not null for window opened by window.open', (done) => { - let b = null - listener = (event) => { - expect(event.data).to.equal('object') - b.close() - done() - } - window.addEventListener('message', listener) - b = window.open(url, '', 'show=no') - }) - }) - - describe('window.opener access from BrowserWindow', () => { - const scheme = 'other' - const url = `${scheme}://${fixtures}/pages/window-opener-location.html` - let w = null - - before((done) => { - protocol.registerFileProtocol(scheme, (request, callback) => { - callback(`${fixtures}/pages/window-opener-location.html`) - }, (error) => done(error)) - }) - - after(() => { - protocol.unregisterProtocol(scheme) - }) - - afterEach(() => { - w.close() - }) - - it('fails when origin of current window does not match opener', (done) => { - listener = (event) => { - expect(event.data).to.equal(null) - done() - } - window.addEventListener('message', listener) - w = window.open(url, '', 'show=no,nodeIntegration=no') - }) - - it('works when origin matches', (done) => { - listener = (event) => { - expect(event.data).to.equal(location.href) - done() - } - window.addEventListener('message', listener) - w = window.open(`file://${fixtures}/pages/window-opener-location.html`, '', 'show=no,nodeIntegration=no') - }) - - it('works when origin does not match opener but has node integration', (done) => { - listener = (event) => { - expect(event.data).to.equal(location.href) - done() - } - window.addEventListener('message', listener) - w = window.open(url, '', 'show=no,nodeIntegration=yes') - }) - }) - - describe('window.opener access from ', () => { - const scheme = 'other' - const srcPath = `${fixtures}/pages/webview-opener-postMessage.html` - const pageURL = `file://${fixtures}/pages/window-opener-location.html` - let webview = null - - before((done) => { - protocol.registerFileProtocol(scheme, (request, callback) => { - callback(srcPath) - }, (error) => done(error)) - }) - - after(() => { - protocol.unregisterProtocol(scheme) - }) - - afterEach(() => { - if (webview != null) webview.remove() - }) - - it('fails when origin of webview src URL does not match opener', (done) => { - webview = new WebView() - webview.addEventListener('console-message', (e) => { - expect(e.message).to.equal('null') - done() - }) - webview.setAttribute('allowpopups', 'on') - webview.src = url.format({ - pathname: srcPath, - protocol: scheme, - query: { - p: pageURL - }, - slashes: true - }) - document.body.appendChild(webview) - }) - - it('works when origin matches', (done) => { - webview = new WebView() - webview.addEventListener('console-message', (e) => { - expect(e.message).to.equal(webview.src) - done() - }) - webview.setAttribute('allowpopups', 'on') - webview.src = url.format({ - pathname: srcPath, - protocol: 'file', - query: { - p: pageURL - }, - slashes: true - }) - document.body.appendChild(webview) - }) - - it('works when origin does not match opener but has node integration', (done) => { - webview = new WebView() - webview.addEventListener('console-message', (e) => { - webview.remove() - expect(e.message).to.equal(webview.src) - done() - }) - webview.setAttribute('allowpopups', 'on') - webview.setAttribute('nodeintegration', 'on') - webview.src = url.format({ - pathname: srcPath, - protocol: scheme, - query: { - p: pageURL - }, - slashes: true - }) - document.body.appendChild(webview) - }) - }) - - describe('window.postMessage', () => { - it('throws an exception when the targetOrigin cannot be converted to a string', () => { - const b = window.open('') - expect(() => { - b.postMessage('test', { toString: null }) - }).to.throw('Cannot convert object to primitive value') - b.close() - }) - }) + it('is not null for window opened by window.open', async () => { + const message = waitForEvent(window, 'message'); + const b = window.open(`file://${fixtures}/pages/window-opener.html`, '', 'show=no'); + const event = await message; + b.close(); + expect(event.data).to.equal('object'); + }); + }); describe('window.opener.postMessage', () => { - it('sets source and origin correctly', (done) => { - let b = null - listener = (event) => { - window.removeEventListener('message', listener) - b.close() - expect(event.source).to.equal(b) - expect(event.origin).to.equal('file://') - done() + it('sets source and origin correctly', async () => { + const message = waitForEvent(window, 'message'); + const b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no'); + const event = await message; + try { + expect(event.source).to.deep.equal(b); + expect(event.origin).to.equal('file://'); + } finally { + b.close(); } - window.addEventListener('message', listener) - b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no') - }) + }); - it('supports windows opened from a ', (done) => { - const webview = new WebView() - webview.addEventListener('console-message', (e) => { - webview.remove() - expect(e.message).to.equal('message') - done() - }) - webview.allowpopups = true + it('supports windows opened from a ', async () => { + const webview = new WebView(); + const consoleMessage = waitForEvent(webview, 'console-message'); + webview.allowpopups = true; + webview.setAttribute('webpreferences', 'contextIsolation=no'); webview.src = url.format({ pathname: `${fixtures}/pages/webview-opener-postMessage.html`, protocol: 'file', @@ -778,666 +180,345 @@ describe('chromium feature', () => { p: `${fixtures}/pages/window-opener-postMessage.html` }, slashes: true - }) - document.body.appendChild(webview) - }) + }); + document.body.appendChild(webview); + const event = await consoleMessage; + webview.remove(); + expect(event.message).to.equal('message'); + }); describe('targetOrigin argument', () => { - let serverURL - let server + let serverURL; + let server; beforeEach((done) => { server = http.createServer((req, res) => { - res.writeHead(200) - const filePath = path.join(fixtures, 'pages', 'window-opener-targetOrigin.html') - res.end(fs.readFileSync(filePath, 'utf8')) - }) + res.writeHead(200); + const filePath = path.join(fixtures, 'pages', 'window-opener-targetOrigin.html'); + res.end(fs.readFileSync(filePath, 'utf8')); + }); server.listen(0, '127.0.0.1', () => { - serverURL = `http://127.0.0.1:${server.address().port}` - done() - }) - }) + serverURL = `http://127.0.0.1:${server.address().port}`; + done(); + }); + }); afterEach(() => { - server.close() - }) - - it('delivers messages that match the origin', (done) => { - let b = null - listener = (event) => { - window.removeEventListener('message', listener) - b.close() - expect(event.data).to.equal('deliver') - done() - } - window.addEventListener('message', listener) - b = window.open(serverURL, '', 'show=no') - }) - }) - }) - - describe('creating a Uint8Array under browser side', () => { - it('does not crash', () => { - const RUint8Array = remote.getGlobal('Uint8Array') - const arr = new RUint8Array() - }) - }) + server.close(); + }); + + it('delivers messages that match the origin', async () => { + const message = waitForEvent(window, 'message'); + const b = window.open(serverURL, '', 'show=no,contextIsolation=no,nodeIntegration=yes'); + const event = await message; + b.close(); + expect(event.data).to.equal('deliver'); + }); + }); + }); describe('webgl', () => { before(function () { - if (isCI && process.platform === 'win32') { - this.skip() + if (process.platform === 'win32') { + this.skip(); } - }) + }); it('can be get as context in canvas', () => { if (process.platform === 'linux') { // FIXME(alexeykuzmin): Skip the test. // this.skip() - return + return; } - const webgl = document.createElement('canvas').getContext('webgl') - expect(webgl).to.not.be.null() - }) - }) + const webgl = document.createElement('canvas').getContext('webgl'); + expect(webgl).to.not.be.null(); + }); + }); describe('web workers', () => { - it('Worker can work', (done) => { - const worker = new Worker('../fixtures/workers/worker.js') - const message = 'ping' - worker.onmessage = (event) => { - expect(event.data).to.equal(message) - worker.terminate() - done() - } - worker.postMessage(message) - }) - - it('Worker has no node integration by default', (done) => { - const worker = new Worker('../fixtures/workers/worker_node.js') - worker.onmessage = (event) => { - expect(event.data).to.equal('undefined undefined undefined undefined') - worker.terminate() - done() - } - }) - - it('Worker has node integration with nodeIntegrationInWorker', (done) => { - const webview = new WebView() - webview.addEventListener('ipc-message', (e) => { - expect(e.channel).to.equal('object function object function') - webview.remove() - done() - }) - webview.src = `file://${fixtures}/pages/worker.html` - webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker') - document.body.appendChild(webview) - }) - - // FIXME: disabled during chromium update due to crash in content::WorkerScriptFetchInitiator::CreateScriptLoaderOnIO - xdescribe('SharedWorker', () => { - it('can work', (done) => { - const worker = new SharedWorker('../fixtures/workers/shared_worker.js') - const message = 'ping' - worker.port.onmessage = (event) => { - expect(event.data).to.equal(message) - done() - } - worker.port.postMessage(message) - }) - - it('has no node integration by default', (done) => { - const worker = new SharedWorker('../fixtures/workers/shared_worker_node.js') - worker.port.onmessage = (event) => { - expect(event.data).to.equal('undefined undefined undefined undefined') - done() - } - }) - - it('has node integration with nodeIntegrationInWorker', (done) => { - const webview = new WebView() + it('Worker can work', async () => { + const worker = new Worker('../fixtures/workers/worker.js'); + const message = 'ping'; + const eventPromise = new Promise((resolve) => { worker.onmessage = resolve; }); + worker.postMessage(message); + const event = await eventPromise; + worker.terminate(); + expect(event.data).to.equal(message); + }); + + it('Worker has no node integration by default', async () => { + const worker = new Worker('../fixtures/workers/worker_node.js'); + const event = await new Promise((resolve) => { worker.onmessage = resolve; }); + worker.terminate(); + expect(event.data).to.equal('undefined undefined undefined undefined'); + }); + + it('Worker has node integration with nodeIntegrationInWorker', async () => { + const webview = new WebView(); + const eventPromise = waitForEvent(webview, 'ipc-message'); + webview.src = `file://${fixtures}/pages/worker.html`; + webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker, contextIsolation=no'); + document.body.appendChild(webview); + const event = await eventPromise; + webview.remove(); + expect(event.channel).to.equal('object function object function'); + }); + + describe('SharedWorker', () => { + it('can work', async () => { + const worker = new SharedWorker('../fixtures/workers/shared_worker.js'); + const message = 'ping'; + const eventPromise = new Promise((resolve) => { worker.port.onmessage = resolve; }); + worker.port.postMessage(message); + const event = await eventPromise; + expect(event.data).to.equal(message); + }); + + it('has no node integration by default', async () => { + const worker = new SharedWorker('../fixtures/workers/shared_worker_node.js'); + const event = await new Promise((resolve) => { worker.port.onmessage = resolve; }); + expect(event.data).to.equal('undefined undefined undefined undefined'); + }); + + // FIXME: disabled during chromium update due to crash in content::WorkerScriptFetchInitiator::CreateScriptLoaderOnIO + xit('has node integration with nodeIntegrationInWorker', async () => { + const webview = new WebView(); webview.addEventListener('console-message', (e) => { - console.log(e) - }) - webview.addEventListener('ipc-message', (e) => { - expect(e.channel).to.equal('object function object function') - webview.remove() - done() - }) - webview.src = `file://${fixtures}/pages/shared_worker.html` - webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker') - document.body.appendChild(webview) - }) - }) - }) + console.log(e); + }); + const eventPromise = waitForEvent(webview, 'ipc-message'); + webview.src = `file://${fixtures}/pages/shared_worker.html`; + webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker'); + document.body.appendChild(webview); + const event = await eventPromise; + webview.remove(); + expect(event.channel).to.equal('object function object function'); + }); + }); + }); describe('iframe', () => { - let iframe = null + let iframe = null; beforeEach(() => { - iframe = document.createElement('iframe') - }) + iframe = document.createElement('iframe'); + }); afterEach(() => { - document.body.removeChild(iframe) - }) + document.body.removeChild(iframe); + }); - it('does not have node integration', (done) => { - iframe.src = `file://${fixtures}/pages/set-global.html` - document.body.appendChild(iframe) - iframe.onload = () => { - expect(iframe.contentWindow.test).to.equal('undefined undefined undefined') - done() - } - }) - }) + it('does not have node integration', async () => { + iframe.src = `file://${fixtures}/pages/set-global.html`; + document.body.appendChild(iframe); + await waitForEvent(iframe, 'load'); + expect(iframe.contentWindow.test).to.equal('undefined undefined undefined'); + }); + }); describe('storage', () => { - describe('DOM storage quota override', () => { + describe('DOM storage quota increase', () => { ['localStorage', 'sessionStorage'].forEach((storageName) => { - it(`allows saving at least 50MiB in ${storageName}`, () => { - const storage = window[storageName] - const testKeyName = '_electronDOMStorageQuotaOverrideTest' - // 25 * 2^20 UTF-16 characters will require 50MiB - const arraySize = 25 * Math.pow(2, 20) - storage[testKeyName] = new Array(arraySize).fill('X').join('') - expect(storage[testKeyName]).to.have.lengthOf(arraySize) - delete storage[testKeyName] - }) - }) - }) - - it('requesting persitent quota works', (done) => { - navigator.webkitPersistentStorage.requestQuota(1024 * 1024, (grantedBytes) => { - expect(grantedBytes).to.equal(1048576) - done() - }) - }) - - describe('custom non standard schemes', () => { - const protocolName = 'storage' - let contents = null - before((done) => { - const handler = (request, callback) => { - const parsedUrl = url.parse(request.url) - let filename - switch (parsedUrl.pathname) { - case '/localStorage' : filename = 'local_storage.html'; break - case '/sessionStorage' : filename = 'session_storage.html'; break - case '/WebSQL' : filename = 'web_sql.html'; break - case '/indexedDB' : filename = 'indexed_db.html'; break - case '/cookie' : filename = 'cookie.html'; break - default : filename = '' + const storage = window[storageName]; + it(`allows saving at least 40MiB in ${storageName}`, async () => { + // Although JavaScript strings use UTF-16, the underlying + // storage provider may encode strings differently, muddling the + // translation between character and byte counts. However, + // a string of 40 * 2^20 characters will require at least 40MiB + // and presumably no more than 80MiB, a size guaranteed to + // to exceed the original 10MiB quota yet stay within the + // new 100MiB quota. + // Note that both the key name and value affect the total size. + const testKeyName = '_electronDOMStorageQuotaIncreasedTest'; + const length = 40 * Math.pow(2, 20) - testKeyName.length; + storage.setItem(testKeyName, 'X'.repeat(length)); + // Wait at least one turn of the event loop to help avoid false positives + // Although not entirely necessary, the previous version of this test case + // failed to detect a real problem (perhaps related to DOM storage data caching) + // wherein calling `getItem` immediately after `setItem` would appear to work + // but then later (e.g. next tick) it would not. + await delay(1); + try { + expect(storage.getItem(testKeyName)).to.have.lengthOf(length); + } finally { + storage.removeItem(testKeyName); } - callback({ path: `${fixtures}/pages/storage/${filename}` }) - } - protocol.registerFileProtocol(protocolName, handler, (error) => done(error)) - }) - - after((done) => { - protocol.unregisterProtocol(protocolName, () => done()) - }) - - beforeEach(() => { - contents = webContents.create({ - nodeIntegration: true - }) - }) - - afterEach(() => { - contents.destroy() - contents = null - }) - - it('cannot access localStorage', (done) => { - ipcMain.once('local-storage-response', (event, error) => { - expect(error).to.equal(`Failed to read the 'localStorage' property from 'Window': Access is denied for this document.`) - done() - }) - contents.loadURL(protocolName + '://host/localStorage') - }) - - it('cannot access sessionStorage', (done) => { - ipcMain.once('session-storage-response', (event, error) => { - expect(error).to.equal(`Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.`) - done() - }) - contents.loadURL(`${protocolName}://host/sessionStorage`) - }) - - it('cannot access WebSQL database', (done) => { - ipcMain.once('web-sql-response', (event, error) => { - expect(error).to.equal(`Failed to execute 'openDatabase' on 'Window': Access to the WebDatabase API is denied in this context.`) - done() - }) - contents.loadURL(`${protocolName}://host/WebSQL`) - }) - - it('cannot access indexedDB', (done) => { - ipcMain.once('indexed-db-response', (event, error) => { - expect(error).to.equal(`Failed to execute 'open' on 'IDBFactory': access to the Indexed Database API is denied in this context.`) - done() - }) - contents.loadURL(`${protocolName}://host/indexedDB`) - }) - - it('cannot access cookie', (done) => { - ipcMain.once('cookie-response', (event, error) => { - expect(error).to.equal(`Failed to set the 'cookie' property on 'Document': Access is denied for this document.`) - done() - }) - contents.loadURL(`${protocolName}://host/cookie`) - }) - }) - - describe('can be accessed', () => { - let server = null - before((done) => { - server = http.createServer((req, res) => { - const respond = () => { - if (req.url === '/redirect-cross-site') { - res.setHeader('Location', `${server.cross_site_url}/redirected`) - res.statusCode = 302 - res.end() - } else if (req.url === '/redirected') { - res.end('') - } else { - res.end() + }); + it(`throws when attempting to use more than 128MiB in ${storageName}`, () => { + expect(() => { + const testKeyName = '_electronDOMStorageQuotaStillEnforcedTest'; + const length = 128 * Math.pow(2, 20) - testKeyName.length; + try { + storage.setItem(testKeyName, 'X'.repeat(length)); + } finally { + storage.removeItem(testKeyName); } - } - setTimeout(respond, 0) - }) - server.listen(0, '127.0.0.1', () => { - server.url = `http://127.0.0.1:${server.address().port}` - server.cross_site_url = `http://localhost:${server.address().port}` - done() - }) - }) - - after(() => { - server.close() - server = null - }) - - const testLocalStorageAfterXSiteRedirect = (testTitle, extraPreferences = {}) => { - it(testTitle, (done) => { - w = new BrowserWindow({ - show: false, - ...extraPreferences - }) - let redirected = false - w.webContents.on('crashed', () => { - expect.fail('renderer crashed / was killed') - }) - w.webContents.on('did-redirect-navigation', (event, url) => { - expect(url).to.equal(`${server.cross_site_url}/redirected`) - redirected = true - }) - w.webContents.on('did-finish-load', () => { - expect(redirected).to.be.true('didnt redirect') - done() - }) - w.loadURL(`${server.url}/redirect-cross-site`) - }) - } - - testLocalStorageAfterXSiteRedirect('after a cross-site redirect') - testLocalStorageAfterXSiteRedirect('after a cross-site redirect in sandbox mode', { sandbox: true }) - }) - }) + }).to.throw(); + }); + }); + }); + + it('requesting persitent quota works', async () => { + const grantedBytes = await new Promise(resolve => { + navigator.webkitPersistentStorage.requestQuota(1024 * 1024, resolve); + }); + expect(grantedBytes).to.equal(1048576); + }); + }); describe('websockets', () => { - let wss = null - let server = null - const WebSocketServer = ws.Server + let wss = null; + let server = null; + const WebSocketServer = ws.Server; afterEach(() => { - wss.close() - server.close() - }) + wss.close(); + server.close(); + }); it('has user agent', (done) => { - server = http.createServer() + server = http.createServer(); server.listen(0, '127.0.0.1', () => { - const port = server.address().port - wss = new WebSocketServer({ server: server }) - wss.on('error', done) + const port = server.address().port; + wss = new WebSocketServer({ server: server }); + wss.on('error', done); wss.on('connection', (ws, upgradeReq) => { if (upgradeReq.headers['user-agent']) { - done() + done(); } else { - done('user agent is empty') + done('user agent is empty'); } - }) - const socket = new WebSocket(`ws://127.0.0.1:${port}`) - }) - }) - }) + }); + const socket = new WebSocket(`ws://127.0.0.1:${port}`); + }); + }); + }); describe('Promise', () => { it('resolves correctly in Node.js calls', (done) => { - document.registerElement('x-element', { - prototype: Object.create(HTMLElement.prototype, { - createdCallback: { - value: () => {} - } - }) - }) + class XElement extends HTMLElement {} + customElements.define('x-element', XElement); setImmediate(() => { - let called = false + let called = false; Promise.resolve().then(() => { - done(called ? void 0 : new Error('wrong sequence')) - }) - document.createElement('x-element') - called = true - }) - }) + done(called ? undefined : new Error('wrong sequence')); + }); + document.createElement('x-element'); + called = true; + }); + }); it('resolves correctly in Electron calls', (done) => { - document.registerElement('y-element', { - prototype: Object.create(HTMLElement.prototype, { - createdCallback: { - value: () => {} - } - }) - }) - remote.getGlobal('setImmediate')(() => { - let called = false + class YElement extends HTMLElement {} + customElements.define('y-element', YElement); + ipcRenderer.invoke('ping').then(() => { + let called = false; Promise.resolve().then(() => { - done(called ? void 0 : new Error('wrong sequence')) - }) - document.createElement('y-element') - called = true - }) - }) - }) + done(called ? undefined : new Error('wrong sequence')); + }); + document.createElement('y-element'); + called = true; + }); + }); + }); describe('fetch', () => { it('does not crash', (done) => { const server = http.createServer((req, res) => { - res.end('test') - server.close() - }) + res.end('test'); + server.close(); + }); server.listen(0, '127.0.0.1', () => { - const port = server.address().port + const port = server.address().port; fetch(`http://127.0.0.1:${port}`).then((res) => res.body.getReader()) .then((reader) => { reader.read().then((r) => { - reader.cancel() - done() - }) - }).catch((e) => done(e)) - }) - }) - }) - - describe('PDF Viewer', () => { - before(function () { - if (!features.isPDFViewerEnabled()) { - return this.skip() - } - }) - - beforeEach(() => { - this.pdfSource = url.format({ - pathname: path.join(fixtures, 'assets', 'cat.pdf').replace(/\\/g, '/'), - protocol: 'file', - slashes: true - }) - - this.pdfSourceWithParams = url.format({ - pathname: path.join(fixtures, 'assets', 'cat.pdf').replace(/\\/g, '/'), - query: { - a: 1, - b: 2 - }, - protocol: 'file', - slashes: true - }) - - this.createBrowserWindow = ({ plugins, preload }) => { - w = new BrowserWindow({ - show: false, - webPreferences: { - preload: path.join(fixtures, 'module', preload), - plugins: plugins - } - }) - } - - this.testPDFIsLoadedInSubFrame = (page, preloadFile, done) => { - const pagePath = url.format({ - pathname: path.join(fixtures, 'pages', page).replace(/\\/g, '/'), - protocol: 'file', - slashes: true - }) - - this.createBrowserWindow({ plugins: true, preload: preloadFile }) - ipcMain.once('pdf-loaded', (event, state) => { - expect(state).to.equal('success') - done() - }) - w.webContents.on('page-title-updated', () => { - const parsedURL = url.parse(w.webContents.getURL(), true) - expect(parsedURL.protocol).to.equal('chrome:') - expect(parsedURL.hostname).to.equal('pdf-viewer') - expect(parsedURL.query.src).to.equal(pagePath) - expect(w.webContents.getTitle()).to.equal('cat.pdf') - }) - w.loadFile(path.join(fixtures, 'pages', page)) - } - }) - - it('opens when loading a pdf resource as top level navigation', (done) => { - this.createBrowserWindow({ plugins: true, preload: 'preload-pdf-loaded.js' }) - ipcMain.once('pdf-loaded', (event, state) => { - expect(state).to.equal('success') - done() - }) - w.webContents.on('page-title-updated', () => { - const parsedURL = url.parse(w.webContents.getURL(), true) - expect(parsedURL.protocol).to.equal('chrome:') - expect(parsedURL.hostname).to.equal('pdf-viewer') - expect(parsedURL.query.src).to.equal(this.pdfSource) - expect(w.webContents.getTitle()).to.equal('cat.pdf') - }) - w.webContents.loadURL(this.pdfSource) - }) - - it('opens a pdf link given params, the query string should be escaped', (done) => { - this.createBrowserWindow({ plugins: true, preload: 'preload-pdf-loaded.js' }) - ipcMain.once('pdf-loaded', (event, state) => { - expect(state).to.equal('success') - done() - }) - w.webContents.on('page-title-updated', () => { - const parsedURL = url.parse(w.webContents.getURL(), true) - expect(parsedURL.protocol).to.equal('chrome:') - expect(parsedURL.hostname).to.equal('pdf-viewer') - expect(parsedURL.query.src).to.equal(this.pdfSourceWithParams) - expect(parsedURL.query.b).to.be.undefined() - expect(parsedURL.search.endsWith('%3Fa%3D1%26b%3D2')).to.be.true() - expect(w.webContents.getTitle()).to.equal('cat.pdf') - }) - w.webContents.loadURL(this.pdfSourceWithParams) - }) - - it('should download a pdf when plugins are disabled', (done) => { - this.createBrowserWindow({ plugins: false, preload: 'preload-pdf-loaded.js' }) - // NOTE(nornagon): this test has been skipped for ages, so there's no way - // to refactor it confidently. The 'set-download-option' ipc was removed - // around May 2019, so if you're working on the pdf viewer and arrive at - // this test and want to know what 'set-download-option' did, look here: - // https://github.com/electron/electron/blob/d87b3ead760ae2d20f2401a8dac4ce548f8cd5f5/spec/static/main.js#L164 - ipcRenderer.sendSync('set-download-option', false, false) - ipcRenderer.once('download-done', (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) => { - expect(state).to.equal('completed') - expect(filename).to.equal('cat.pdf') - expect(mimeType).to.equal('application/pdf') - fs.unlinkSync(path.join(fixtures, 'mock.pdf')) - done() - }) - w.webContents.loadURL(this.pdfSource) - }) - - it('should not open when pdf is requested as sub resource', (done) => { - fetch(this.pdfSource).then((res) => { - expect(res.status).to.equal(200) - expect(document.title).to.not.equal('cat.pdf') - done() - }).catch((e) => done(e)) - }) - - it('opens when loading a pdf resource in a iframe', (done) => { - this.testPDFIsLoadedInSubFrame('pdf-in-iframe.html', 'preload-pdf-loaded-in-subframe.js', done) - }) - - it('opens when loading a pdf resource in a nested iframe', (done) => { - this.testPDFIsLoadedInSubFrame('pdf-in-nested-iframe.html', 'preload-pdf-loaded-in-nested-subframe.js', done) - }) - }) + reader.cancel(); + done(); + }); + }).catch((e) => done(e)); + }); + }); + }); describe('window.alert(message, title)', () => { it('throws an exception when the arguments cannot be converted to strings', () => { expect(() => { - window.alert({ toString: null }) - }).to.throw('Cannot convert object to primitive value') - }) - }) + window.alert({ toString: null }); + }).to.throw('Cannot convert object to primitive value'); + }); + }); describe('window.confirm(message, title)', () => { it('throws an exception when the arguments cannot be converted to strings', () => { expect(() => { - window.confirm({ toString: null }, 'title') - }).to.throw('Cannot convert object to primitive value') - }) - }) + window.confirm({ toString: null }, 'title'); + }).to.throw('Cannot convert object to primitive value'); + }); + }); describe('window.history', () => { describe('window.history.go(offset)', () => { it('throws an exception when the argumnet cannot be converted to a string', () => { expect(() => { - window.history.go({ toString: null }) - }).to.throw('Cannot convert object to primitive value') - }) - }) - - describe('window.history.pushState', () => { - it('should push state after calling history.pushState() from the same url', (done) => { - w = new BrowserWindow({ show: false }) - w.webContents.once('did-finish-load', async () => { - // History should have current page by now. - expect(w.webContents.length()).to.equal(1) - - w.webContents.executeJavaScript('window.history.pushState({}, "")').then(() => { - // Initial page + pushed state - expect(w.webContents.length()).to.equal(2) - done() - }) - }) - w.loadURL('about:blank') - }) - }) - }) - - describe('SpeechSynthesis', () => { + window.history.go({ toString: null }); + }).to.throw('Cannot convert object to primitive value'); + }); + }); + }); + + // TODO(nornagon): this is broken on CI, it triggers: + // [FATAL:speech_synthesis.mojom-shared.h(237)] The outgoing message will + // trigger VALIDATION_ERROR_UNEXPECTED_NULL_POINTER at the receiving side + // (null text in SpeechSynthesisUtterance struct). + describe.skip('SpeechSynthesis', () => { before(function () { - if (isCI || !features.isTtsEnabled()) { - this.skip() + if (!features.isTtsEnabled()) { + this.skip(); } - }) + }); it('should emit lifecycle events', async () => { const sentence = `long sentence which will take at least a few seconds to - utter so that it's possible to pause and resume before the end` - const utter = new SpeechSynthesisUtterance(sentence) + utter so that it's possible to pause and resume before the end`; + const utter = new SpeechSynthesisUtterance(sentence); // Create a dummy utterence so that speech synthesis state // is initialized for later calls. - speechSynthesis.speak(new SpeechSynthesisUtterance()) - speechSynthesis.cancel() - speechSynthesis.speak(utter) + speechSynthesis.speak(new SpeechSynthesisUtterance()); + speechSynthesis.cancel(); + speechSynthesis.speak(utter); // paused state after speak() - expect(speechSynthesis.paused).to.be.false() - await new Promise((resolve) => { utter.onstart = resolve }) + expect(speechSynthesis.paused).to.be.false(); + await new Promise((resolve) => { utter.onstart = resolve; }); // paused state after start event - expect(speechSynthesis.paused).to.be.false() + expect(speechSynthesis.paused).to.be.false(); - speechSynthesis.pause() + speechSynthesis.pause(); // paused state changes async, right before the pause event - expect(speechSynthesis.paused).to.be.false() - await new Promise((resolve) => { utter.onpause = resolve }) - expect(speechSynthesis.paused).to.be.true() + expect(speechSynthesis.paused).to.be.false(); + await new Promise((resolve) => { utter.onpause = resolve; }); + expect(speechSynthesis.paused).to.be.true(); - speechSynthesis.resume() - await new Promise((resolve) => { utter.onresume = resolve }) + speechSynthesis.resume(); + await new Promise((resolve) => { utter.onresume = resolve; }); // paused state after resume event - expect(speechSynthesis.paused).to.be.false() + expect(speechSynthesis.paused).to.be.false(); - await new Promise((resolve) => { utter.onend = resolve }) - }) - }) -}) + await new Promise((resolve) => { utter.onend = resolve; }); + }); + }); +}); describe('console functions', () => { it('should exist', () => { - expect(console.log, 'log').to.be.a('function') - expect(console.error, 'error').to.be.a('function') - expect(console.warn, 'warn').to.be.a('function') - expect(console.info, 'info').to.be.a('function') - expect(console.debug, 'debug').to.be.a('function') - expect(console.trace, 'trace').to.be.a('function') - expect(console.time, 'time').to.be.a('function') - expect(console.timeEnd, 'timeEnd').to.be.a('function') - }) -}) - -describe('font fallback', () => { - async function getRenderedFonts (html) { - const w = new BrowserWindow({ show: false }) - try { - await w.loadURL(`data:text/html,${html}`) - w.webContents.debugger.attach() - const sendCommand = (...args) => w.webContents.debugger.sendCommand(...args) - const { nodeId } = (await sendCommand('DOM.getDocument')).root.children[0] - await sendCommand('CSS.enable') - const { fonts } = await sendCommand('CSS.getPlatformFontsForNode', { nodeId }) - return fonts - } finally { - w.close() - } - } - - it('should use Helvetica for sans-serif on Mac, and Arial on Windows and Linux', async () => { - const html = `test` - const fonts = await getRenderedFonts(html) - expect(fonts).to.be.an('array') - expect(fonts).to.have.length(1) - expect(fonts[0].familyName).to.equal({ - 'win32': 'Arial', - 'darwin': 'Helvetica', - 'linux': 'DejaVu Sans' // I think this depends on the distro? We don't specify a default. - }[process.platform]) - }) - - it('should fall back to Japanese font for sans-serif Japanese script', async function () { - if (process.platform === 'linux') { - return this.skip() - } - const html = ` - - - - - test 智史 - - ` - const fonts = await getRenderedFonts(html) - expect(fonts).to.be.an('array') - expect(fonts).to.have.length(1) - expect(fonts[0].familyName).to.equal({ - 'win32': 'Meiryo', - 'darwin': 'Hiragino Kaku Gothic ProN' - }[process.platform]) - }) -}) + expect(console.log, 'log').to.be.a('function'); + expect(console.error, 'error').to.be.a('function'); + expect(console.warn, 'warn').to.be.a('function'); + expect(console.info, 'info').to.be.a('function'); + expect(console.debug, 'debug').to.be.a('function'); + expect(console.trace, 'trace').to.be.a('function'); + expect(console.time, 'time').to.be.a('function'); + expect(console.timeEnd, 'timeEnd').to.be.a('function'); + }); +}); diff --git a/spec/content-script-spec.js b/spec/content-script-spec.js deleted file mode 100644 index f1951ce94711b..0000000000000 --- a/spec/content-script-spec.js +++ /dev/null @@ -1,150 +0,0 @@ -const { expect } = require('chai') -const { remote } = require('electron') -const path = require('path') - -const { closeWindow } = require('./window-helpers') -const { emittedNTimes } = require('./events-helpers') - -const { BrowserWindow, ipcMain } = remote - -describe('chrome extension content scripts', () => { - const fixtures = path.resolve(__dirname, 'fixtures') - const extensionPath = path.resolve(fixtures, 'extensions') - - const addExtension = (name) => BrowserWindow.addExtension(path.resolve(extensionPath, name)) - const removeAllExtensions = () => { - Object.keys(BrowserWindow.getExtensions()).map(extName => { - BrowserWindow.removeExtension(extName) - }) - } - - let responseIdCounter = 0 - const executeJavaScriptInFrame = (webContents, frameRoutingId, code) => { - return new Promise(resolve => { - const responseId = responseIdCounter++ - ipcMain.once(`executeJavaScriptInFrame_${responseId}`, (event, result) => { - resolve(result) - }) - webContents.send('executeJavaScriptInFrame', frameRoutingId, code, responseId) - }) - } - - const generateTests = (sandboxEnabled, contextIsolationEnabled) => { - describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'} and context isolation ${contextIsolationEnabled ? 'enabled' : 'disabled'}`, () => { - let w - - describe('supports "run_at" option', () => { - beforeEach(async () => { - await closeWindow(w) - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: { - contextIsolation: contextIsolationEnabled, - sandbox: sandboxEnabled - } - }) - }) - - afterEach(() => { - removeAllExtensions() - return closeWindow(w).then(() => { w = null }) - }) - - it('should run content script at document_start', () => { - addExtension('content-script-document-start') - w.webContents.once('dom-ready', async () => { - const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') - expect(result).to.equal('red') - }) - w.loadURL('about:blank') - }) - - it('should run content script at document_idle', async () => { - addExtension('content-script-document-idle') - w.loadURL('about:blank') - const result = await w.webContents.executeJavaScript('document.body.style.backgroundColor') - expect(result).to.equal('red') - }) - - it('should run content script at document_end', () => { - addExtension('content-script-document-end') - w.webContents.once('did-finish-load', async () => { - const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') - expect(result).to.equal('red') - }) - w.loadURL('about:blank') - }) - }) - - describe('supports "all_frames" option', () => { - const contentScript = path.resolve(fixtures, 'extensions/content-script') - - // Computed style values - const COLOR_RED = `rgb(255, 0, 0)` - const COLOR_BLUE = `rgb(0, 0, 255)` - const COLOR_TRANSPARENT = `rgba(0, 0, 0, 0)` - - before(() => { - BrowserWindow.addExtension(contentScript) - }) - - after(() => { - BrowserWindow.removeExtension('content-script-test') - }) - - beforeEach(() => { - w = new BrowserWindow({ - show: false, - webPreferences: { - // enable content script injection in subframes - nodeIntegrationInSubFrames: true, - preload: path.join(contentScript, 'all_frames-preload.js') - } - }) - }) - - afterEach(() => - closeWindow(w).then(() => { - w = null - }) - ) - - it('applies matching rules in subframes', async () => { - const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2) - w.loadFile(path.join(contentScript, 'frame-with-frame.html')) - const frameEvents = await detailsPromise - await Promise.all( - frameEvents.map(async frameEvent => { - const [, isMainFrame, , frameRoutingId] = frameEvent - const result = await executeJavaScriptInFrame( - w.webContents, - frameRoutingId, - `(() => { - const a = document.getElementById('all_frames_enabled') - const b = document.getElementById('all_frames_disabled') - return { - enabledColor: getComputedStyle(a).backgroundColor, - disabledColor: getComputedStyle(b).backgroundColor - } - })()` - ) - expect(result.enabledColor).to.equal(COLOR_RED) - if (isMainFrame) { - expect(result.disabledColor).to.equal(COLOR_BLUE) - } else { - expect(result.disabledColor).to.equal(COLOR_TRANSPARENT) // null color - } - }) - ) - }) - }) - }) - } - - generateTests(false, false) - generateTests(false, true) - generateTests(true, false) - generateTests(true, true) -}) diff --git a/spec/events-helpers.js b/spec/events-helpers.js index 64a4fba447eb3..507f68eb113f1 100644 --- a/spec/events-helpers.js +++ b/spec/events-helpers.js @@ -10,9 +10,9 @@ */ const waitForEvent = (target, eventName) => { return new Promise(resolve => { - target.addEventListener(eventName, resolve, { once: true }) - }) -} + target.addEventListener(eventName, resolve, { once: true }); + }); +}; /** * @param {!EventEmitter} emitter @@ -20,23 +20,23 @@ const waitForEvent = (target, eventName) => { * @return {!Promise} With Event as the first item. */ const emittedOnce = (emitter, eventName) => { - return emittedNTimes(emitter, eventName, 1).then(([result]) => result) -} + return emittedNTimes(emitter, eventName, 1).then(([result]) => result); +}; const emittedNTimes = (emitter, eventName, times) => { - const events = [] + const events = []; return new Promise(resolve => { const handler = (...args) => { - events.push(args) + events.push(args); if (events.length === times) { - emitter.removeListener(eventName, handler) - resolve(events) + emitter.removeListener(eventName, handler); + resolve(events); } - } - emitter.on(eventName, handler) - }) -} + }; + emitter.on(eventName, handler); + }); +}; -exports.emittedOnce = emittedOnce -exports.emittedNTimes = emittedNTimes -exports.waitForEvent = waitForEvent +exports.emittedOnce = emittedOnce; +exports.emittedNTimes = emittedNTimes; +exports.waitForEvent = waitForEvent; diff --git a/spec/expect-helpers.js b/spec/expect-helpers.js index a825f6d2a5c2c..18d72d1ec66b8 100644 --- a/spec/expect-helpers.js +++ b/spec/expect-helpers.js @@ -1,18 +1,18 @@ function resolveSingleObjectGetters (object) { if (object && typeof object === 'object') { - const newObject = {} - for (const key in object) { - newObject[key] = resolveGetters(object[key])[0] + const newObject = {}; + for (const key in object) { // eslint-disable-line guard-for-in + newObject[key] = resolveGetters(object[key])[0]; } - return newObject + return newObject; } - return object + return object; } function resolveGetters (...args) { - return args.map(resolveSingleObjectGetters) + return args.map(resolveSingleObjectGetters); } module.exports = { resolveGetters -} +}; diff --git a/spec/fixtures/api/allocate-memory.html b/spec/fixtures/api/allocate-memory.html deleted file mode 100644 index ce3140ab0cd2e..0000000000000 --- a/spec/fixtures/api/allocate-memory.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/spec/fixtures/api/app-path/lib/index.js b/spec/fixtures/api/app-path/lib/index.js index d1a5732edca51..a4510fd01c7a8 100644 --- a/spec/fixtures/api/app-path/lib/index.js +++ b/spec/fixtures/api/app-path/lib/index.js @@ -1,10 +1,10 @@ -const { app } = require('electron') +const { app } = require('electron'); const payload = { appPath: app.getAppPath() -} +}; -process.stdout.write(JSON.stringify(payload)) -process.stdout.end() +process.stdout.write(JSON.stringify(payload)); +process.stdout.end(); -process.exit() +process.exit(); diff --git a/spec/fixtures/api/app-path/package.json b/spec/fixtures/api/app-path/package.json index 8f9e09dbdabb2..95c402d27efca 100644 --- a/spec/fixtures/api/app-path/package.json +++ b/spec/fixtures/api/app-path/package.json @@ -1,4 +1,4 @@ { - "name": "app-path", + "name": "electron-test-app-path", "main": "lib/index.js" } diff --git a/spec/fixtures/api/beforeunload-false-prevent3.html b/spec/fixtures/api/beforeunload-false-prevent3.html deleted file mode 100644 index 6ed2a7d1aa4e2..0000000000000 --- a/spec/fixtures/api/beforeunload-false-prevent3.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/spec/fixtures/api/beforeunload-false.html b/spec/fixtures/api/beforeunload-false.html deleted file mode 100644 index 4ba1867ce63d2..0000000000000 --- a/spec/fixtures/api/beforeunload-false.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/spec/fixtures/api/close-beforeunload-empty-string.html b/spec/fixtures/api/close-beforeunload-empty-string.html deleted file mode 100644 index 644f4f4530bc8..0000000000000 --- a/spec/fixtures/api/close-beforeunload-empty-string.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/spec/fixtures/api/close-beforeunload-false.html b/spec/fixtures/api/close-beforeunload-false.html deleted file mode 100644 index 5f519ab5598b2..0000000000000 --- a/spec/fixtures/api/close-beforeunload-false.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/spec/fixtures/api/close-beforeunload-undefined.html b/spec/fixtures/api/close-beforeunload-undefined.html deleted file mode 100644 index 910dbc5090fc8..0000000000000 --- a/spec/fixtures/api/close-beforeunload-undefined.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/spec/fixtures/api/command-line/main.js b/spec/fixtures/api/command-line/main.js index 39e62cafbbc87..6b262a022d80c 100644 --- a/spec/fixtures/api/command-line/main.js +++ b/spec/fixtures/api/command-line/main.js @@ -1,15 +1,13 @@ -const { app } = require('electron') +const { app } = require('electron'); -app.on('ready', () => { +app.whenReady().then(() => { const payload = { hasSwitch: app.commandLine.hasSwitch('foobar'), getSwitchValue: app.commandLine.getSwitchValue('foobar') - } + }; - process.stdout.write(JSON.stringify(payload)) - process.stdout.end() + process.stdout.write(JSON.stringify(payload)); + process.stdout.end(); - setImmediate(() => { - app.quit() - }) -}) + app.quit(); +}); diff --git a/spec/fixtures/api/command-line/package.json b/spec/fixtures/api/command-line/package.json index bbe8102015d01..897fa828efb0a 100644 --- a/spec/fixtures/api/command-line/package.json +++ b/spec/fixtures/api/command-line/package.json @@ -1,4 +1,4 @@ { - "name": "command-line", + "name": "electron-test-command-line", "main": "main.js" } diff --git a/spec/fixtures/api/cookie-app/main.js b/spec/fixtures/api/cookie-app/main.js index 3e63186537898..dc10bb8c5dcb4 100644 --- a/spec/fixtures/api/cookie-app/main.js +++ b/spec/fixtures/api/cookie-app/main.js @@ -1,49 +1,43 @@ -const { app, session } = require('electron') +const { app, session } = require('electron'); -app.on('ready', async function () { - const url = 'http://foo.bar' - const persistentSession = session.fromPartition('persist:ence-test') - const name = 'test' - const value = 'true' +app.whenReady().then(async function () { + const url = 'http://foo.bar'; + const persistentSession = session.fromPartition('persist:ence-test'); + const name = 'test'; + const value = 'true'; const set = () => persistentSession.cookies.set({ url, name, value, - expirationDate: Date.now() + 60000 - }) + expirationDate: Date.now() + 60000, + sameSite: 'strict' + }); const get = () => persistentSession.cookies.get({ url - }) + }); - const maybeRemove = async (pred) => new Promise(async (resolve, reject) => { - try { - if (pred()) { - await persistentSession.cookies.remove(url, name) - } - resolve() - } catch (error) { - reject(error) + const maybeRemove = async (pred) => { + if (pred()) { + await persistentSession.cookies.remove(url, name); } - }) + }; try { - await maybeRemove(() => process.env.PHASE === 'one') - const one = await get() - await set() - const two = await get() - await maybeRemove(() => process.env.PHASE === 'two') - const three = await get() + await maybeRemove(() => process.env.PHASE === 'one'); + const one = await get(); + await set(); + const two = await get(); + await maybeRemove(() => process.env.PHASE === 'two'); + const three = await get(); - process.stdout.write(`${one.length}${two.length}${three.length}`) + process.stdout.write(`${one.length}${two.length}${three.length}`); } catch (e) { - process.stdout.write('ERROR') + process.stdout.write(`ERROR : ${e.message}`); } finally { - process.stdout.end() + process.stdout.end(); - setImmediate(() => { - app.quit() - }) + app.quit(); } -}) +}); diff --git a/spec/fixtures/api/crash-restart.html b/spec/fixtures/api/crash-restart.html deleted file mode 100644 index 0ee4ad53503b5..0000000000000 --- a/spec/fixtures/api/crash-restart.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/spec/fixtures/api/crash.html b/spec/fixtures/api/crash.html deleted file mode 100644 index bcce825426c93..0000000000000 --- a/spec/fixtures/api/crash.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/spec/fixtures/api/default-menu/main.js b/spec/fixtures/api/default-menu/main.js index 786d9ab4b0d3f..16e84badc9136 100644 --- a/spec/fixtures/api/default-menu/main.js +++ b/spec/fixtures/api/default-menu/main.js @@ -1,32 +1,32 @@ -const { app, Menu } = require('electron') +const { app, Menu } = require('electron'); function output (value) { - process.stdout.write(JSON.stringify(value)) - process.stdout.end() + process.stdout.write(JSON.stringify(value)); + process.stdout.end(); - app.quit() + app.quit(); } try { - let expectedMenu + let expectedMenu; if (app.commandLine.hasSwitch('custom-menu')) { - expectedMenu = new Menu() - Menu.setApplicationMenu(expectedMenu) + expectedMenu = new Menu(); + Menu.setApplicationMenu(expectedMenu); } else if (app.commandLine.hasSwitch('null-menu')) { - expectedMenu = null - Menu.setApplicationMenu(null) + expectedMenu = null; + Menu.setApplicationMenu(null); } - app.on('ready', () => { + app.whenReady().then(() => { setImmediate(() => { try { - output(Menu.getApplicationMenu() === expectedMenu) + output(Menu.getApplicationMenu() === expectedMenu); } catch (error) { - output(null) + output(null); } - }) - }) + }); + }); } catch (error) { - output(null) + output(null); } diff --git a/spec/fixtures/api/default-menu/package.json b/spec/fixtures/api/default-menu/package.json index 7e253de58f853..614b3ecf3ca60 100644 --- a/spec/fixtures/api/default-menu/package.json +++ b/spec/fixtures/api/default-menu/package.json @@ -1,4 +1,4 @@ { - "name": "default-menu", + "name": "electron-test-default-menu", "main": "main.js" } diff --git a/spec/fixtures/api/electron-main-module/app/index.js b/spec/fixtures/api/electron-main-module/app/index.js index c56845bca42d5..873192591cd3d 100644 --- a/spec/fixtures/api/electron-main-module/app/index.js +++ b/spec/fixtures/api/electron-main-module/app/index.js @@ -1,8 +1,8 @@ try { - require('some-module') + require('some-module'); } catch (err) { - console.error(err) - process.exit(1) + console.error(err); + process.exit(1); } -process.exit(0) +process.exit(0); diff --git a/spec/fixtures/api/electron-module-app/.gitignore b/spec/fixtures/api/electron-module-app/.gitignore new file mode 100644 index 0000000000000..736e8ae58ad87 --- /dev/null +++ b/spec/fixtures/api/electron-module-app/.gitignore @@ -0,0 +1 @@ +!node_modules \ No newline at end of file diff --git a/spec/fixtures/api/electron-module-app/node_modules/foo/index.js b/spec/fixtures/api/electron-module-app/node_modules/foo/index.js index 11d763e517426..41f3ba446f69d 100644 --- a/spec/fixtures/api/electron-module-app/node_modules/foo/index.js +++ b/spec/fixtures/api/electron-module-app/node_modules/foo/index.js @@ -1 +1 @@ -exports.bar = function () {} +exports.bar = function () {}; diff --git a/spec/fixtures/api/exit-closes-all-windows-app/main.js b/spec/fixtures/api/exit-closes-all-windows-app/main.js index 20d5a1dac2a1c..2a427b10ff87f 100644 --- a/spec/fixtures/api/exit-closes-all-windows-app/main.js +++ b/spec/fixtures/api/exit-closes-all-windows-app/main.js @@ -1,19 +1,19 @@ -const { app, BrowserWindow } = require('electron') +const { app, BrowserWindow } = require('electron'); -const windows = [] +const windows = []; function createWindow (id) { - const window = new BrowserWindow({ show: false }) - window.loadURL(`data:,window${id}`) - windows.push(window) + const window = new BrowserWindow({ show: false }); + window.loadURL(`data:,window${id}`); + windows.push(window); } -app.once('ready', () => { +app.whenReady().then(() => { for (let i = 1; i <= 5; i++) { - createWindow(i) + createWindow(i); } setImmediate(function () { - app.exit(123) - }) -}) + app.exit(123); + }); +}); diff --git a/spec/fixtures/api/globals.html b/spec/fixtures/api/globals.html new file mode 100644 index 0000000000000..f1bfc56037c79 --- /dev/null +++ b/spec/fixtures/api/globals.html @@ -0,0 +1,13 @@ + + + + Document + + + + + \ No newline at end of file diff --git a/spec/fixtures/api/gpu-info.js b/spec/fixtures/api/gpu-info.js index 1954f9b4b8a80..3bacd1165b3b0 100644 --- a/spec/fixtures/api/gpu-info.js +++ b/spec/fixtures/api/gpu-info.js @@ -1,17 +1,21 @@ -const { app } = require('electron') +const { app, BrowserWindow } = require('electron'); -app.commandLine.appendSwitch('--disable-software-rasterizer') +app.commandLine.appendSwitch('--disable-software-rasterizer'); -app.on('ready', () => { - const infoType = process.argv.pop() - app.getGPUInfo(infoType).then( - (gpuInfo) => { - console.log(JSON.stringify(gpuInfo)) - app.exit(0) - }, - (error) => { - console.error(error) - app.exit(1) - } - ) -}) +app.whenReady().then(() => { + const infoType = process.argv.pop(); + const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + w.webContents.once('did-finish-load', () => { + app.getGPUInfo(infoType).then( + (gpuInfo) => { + console.log('HERE COMES THE JSON: ' + JSON.stringify(gpuInfo) + ' AND THERE IT WAS'); + setImmediate(() => app.exit(0)); + }, + (error) => { + console.error(error); + setImmediate(() => app.exit(1)); + } + ); + }); + w.loadURL('data:text/html;'); +}); diff --git a/spec/fixtures/api/ipc-main-listeners/main.js b/spec/fixtures/api/ipc-main-listeners/main.js deleted file mode 100644 index d56348136bc1d..0000000000000 --- a/spec/fixtures/api/ipc-main-listeners/main.js +++ /dev/null @@ -1,10 +0,0 @@ -const { app, ipcMain } = require('electron') - -app.on('ready', () => { - process.stdout.write(JSON.stringify(ipcMain.eventNames())) - process.stdout.end() - - setImmediate(() => { - app.quit() - }) -}) diff --git a/spec/fixtures/api/ipc-main-listeners/package.json b/spec/fixtures/api/ipc-main-listeners/package.json deleted file mode 100644 index 7acf9eb5274bc..0000000000000 --- a/spec/fixtures/api/ipc-main-listeners/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "ipc-main-listeners", - "main": "main.js" -} diff --git a/spec/fixtures/api/isolated-fetch-preload.js b/spec/fixtures/api/isolated-fetch-preload.js index 283af99fe16e1..e2c7b75857f77 100644 --- a/spec/fixtures/api/isolated-fetch-preload.js +++ b/spec/fixtures/api/isolated-fetch-preload.js @@ -1,6 +1,6 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); // Ensure fetch works from isolated world origin fetch('https://localhost:1234').catch(err => { - ipcRenderer.send('isolated-fetch-error', err.message) -}) + ipcRenderer.send('isolated-fetch-error', err.message); +}); diff --git a/spec/fixtures/api/isolated-preload.js b/spec/fixtures/api/isolated-preload.js index 20c2181bacbaf..c0ca37505ae6f 100644 --- a/spec/fixtures/api/isolated-preload.js +++ b/spec/fixtures/api/isolated-preload.js @@ -1,8 +1,8 @@ -const { ipcRenderer, webFrame } = require('electron') +const { ipcRenderer, webFrame } = require('electron'); -window.foo = 3 +window.foo = 3; -webFrame.executeJavaScript('window.preloadExecuteJavaScriptProperty = 1234;') +webFrame.executeJavaScript('window.preloadExecuteJavaScriptProperty = 1234;'); window.addEventListener('message', (event) => { ipcRenderer.send('isolated-world', { @@ -16,5 +16,5 @@ window.addEventListener('message', (event) => { typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty }, pageContext: event.data - }) -}) + }); +}); diff --git a/spec/fixtures/api/isolated-process.js b/spec/fixtures/api/isolated-process.js new file mode 100644 index 0000000000000..d5e949ded56fd --- /dev/null +++ b/spec/fixtures/api/isolated-process.js @@ -0,0 +1,3 @@ +const { ipcRenderer } = require('electron'); + +ipcRenderer.send('context-isolation', process.contextIsolated); diff --git a/spec/fixtures/api/leak-exit-browserview.js b/spec/fixtures/api/leak-exit-browserview.js deleted file mode 100644 index 534fea59bbca7..0000000000000 --- a/spec/fixtures/api/leak-exit-browserview.js +++ /dev/null @@ -1,6 +0,0 @@ -const { BrowserView, app } = require('electron') -app.on('ready', function () { - new BrowserView({}) // eslint-disable-line - - process.nextTick(() => app.quit()) -}) diff --git a/spec/fixtures/api/leak-exit-webcontents.js b/spec/fixtures/api/leak-exit-webcontents.js deleted file mode 100644 index 5bf9e205793ce..0000000000000 --- a/spec/fixtures/api/leak-exit-webcontents.js +++ /dev/null @@ -1,6 +0,0 @@ -const { app, webContents } = require('electron') -app.on('ready', function () { - webContents.create({}) - - process.nextTick(() => app.quit()) -}) diff --git a/spec/fixtures/api/leak-exit-webcontentsview.js b/spec/fixtures/api/leak-exit-webcontentsview.js deleted file mode 100644 index 1196cfcca839b..0000000000000 --- a/spec/fixtures/api/leak-exit-webcontentsview.js +++ /dev/null @@ -1,7 +0,0 @@ -const { WebContentsView, app, webContents } = require('electron') -app.on('ready', function () { - const web = webContents.create({}) - new WebContentsView(web) // eslint-disable-line - - process.nextTick(() => app.quit()) -}) diff --git a/spec/fixtures/api/loaded-from-dataurl.js b/spec/fixtures/api/loaded-from-dataurl.js index c4dbdd044bdb2..5653078acceba 100644 --- a/spec/fixtures/api/loaded-from-dataurl.js +++ b/spec/fixtures/api/loaded-from-dataurl.js @@ -1 +1 @@ -require('electron').ipcRenderer.send('answer', 'test') +window.ping = 'pong'; diff --git a/spec/fixtures/api/locale-check/main.js b/spec/fixtures/api/locale-check/main.js index c89e129904245..929a9e0e9519a 100644 --- a/spec/fixtures/api/locale-check/main.js +++ b/spec/fixtures/api/locale-check/main.js @@ -1,10 +1,17 @@ -const { app } = require('electron') +const { app } = require('electron'); -app.on('ready', () => { - process.stdout.write(app.getLocale()) - process.stdout.end() +const locale = process.argv[2].substr(11); +if (locale.length !== 0) { + app.commandLine.appendSwitch('lang', locale); +} - setImmediate(() => { - app.quit() - }) -}) +app.whenReady().then(() => { + if (process.argv[3] === '--print-env') { + process.stdout.write(String(process.env.LC_ALL)); + } else { + process.stdout.write(app.getLocale()); + } + process.stdout.end(); + + app.quit(); +}); diff --git a/spec/fixtures/api/locale-check/package.json b/spec/fixtures/api/locale-check/package.json index a24f536930d25..60e5f4d1cdc47 100644 --- a/spec/fixtures/api/locale-check/package.json +++ b/spec/fixtures/api/locale-check/package.json @@ -1,5 +1,5 @@ { - "name": "locale-check", + "name": "electron-test-locale-check", "main": "main.js" } diff --git a/spec/fixtures/api/mixed-sandbox-app/electron-app-mixed-sandbox-preload.js b/spec/fixtures/api/mixed-sandbox-app/electron-app-mixed-sandbox-preload.js index abe0eeea87edc..fb2d6a30bcf3d 100644 --- a/spec/fixtures/api/mixed-sandbox-app/electron-app-mixed-sandbox-preload.js +++ b/spec/fixtures/api/mixed-sandbox-app/electron-app-mixed-sandbox-preload.js @@ -1 +1 @@ -require('electron').ipcRenderer.send('argv', process.argv) +require('electron').ipcRenderer.send('argv', process.argv); diff --git a/spec/fixtures/api/mixed-sandbox-app/main.js b/spec/fixtures/api/mixed-sandbox-app/main.js index 0479ca1d77040..bb61f2b628c6c 100644 --- a/spec/fixtures/api/mixed-sandbox-app/main.js +++ b/spec/fixtures/api/mixed-sandbox-app/main.js @@ -1,40 +1,40 @@ -const { app, BrowserWindow, ipcMain } = require('electron') -const net = require('net') -const path = require('path') +const { app, BrowserWindow, ipcMain } = require('electron'); +const net = require('net'); +const path = require('path'); process.on('uncaughtException', () => { - app.exit(1) -}) + app.exit(1); +}); if (process.argv.includes('--app-enable-sandbox')) { - app.enableSandbox() + app.enableSandbox(); } -let currentWindowSandboxed = false +let currentWindowSandboxed = false; -app.once('ready', () => { +app.whenReady().then(() => { function testWindow (isSandboxed, callback) { - currentWindowSandboxed = isSandboxed + currentWindowSandboxed = isSandboxed; const currentWindow = new BrowserWindow({ show: false, webPreferences: { preload: path.join(__dirname, 'electron-app-mixed-sandbox-preload.js'), sandbox: isSandboxed } - }) - currentWindow.loadURL('about:blank') + }); + currentWindow.loadURL('about:blank'); currentWindow.webContents.once('devtools-opened', () => { if (isSandboxed) { - argv.sandboxDevtools = true + argv.sandboxDevtools = true; } else { - argv.noSandboxDevtools = true + argv.noSandboxDevtools = true; } if (callback) { - callback() + callback(); } - finish() - }) - currentWindow.webContents.openDevTools() + finish(); + }); + currentWindow.webContents.openDevTools(); } const argv = { @@ -42,36 +42,36 @@ app.once('ready', () => { noSandbox: null, sandboxDevtools: null, noSandboxDevtools: null - } + }; - let connected = false + let connected = false; testWindow(true, () => { - testWindow() - }) + testWindow(); + }); function finish () { if (connected && argv.sandbox != null && argv.noSandbox != null && argv.noSandboxDevtools != null && argv.sandboxDevtools != null) { client.once('end', () => { - app.exit(0) - }) - client.end(JSON.stringify(argv)) + app.exit(0); + }); + client.end(JSON.stringify(argv)); } } - const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-mixed-sandbox' : '/tmp/electron-mixed-sandbox' + const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-mixed-sandbox' : '/tmp/electron-mixed-sandbox'; const client = net.connect(socketPath, () => { - connected = true - finish() - }) + connected = true; + finish(); + }); ipcMain.on('argv', (event, value) => { if (currentWindowSandboxed) { - argv.sandbox = value + argv.sandbox = value; } else { - argv.noSandbox = value + argv.noSandbox = value; } - finish() - }) -}) + finish(); + }); +}); diff --git a/spec/fixtures/api/native-window-open-isolated-preload.js b/spec/fixtures/api/native-window-open-isolated-preload.js index 9491e4efcc465..de5d41564b42f 100644 --- a/spec/fixtures/api/native-window-open-isolated-preload.js +++ b/spec/fixtures/api/native-window-open-isolated-preload.js @@ -1,5 +1,5 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); window.addEventListener('message', (event) => { - ipcRenderer.send('answer', event.data) -}) + ipcRenderer.send('answer', event.data); +}); diff --git a/spec/fixtures/api/native-window-open-noopener.html b/spec/fixtures/api/native-window-open-noopener.html new file mode 100644 index 0000000000000..aa62c81fd12ed --- /dev/null +++ b/spec/fixtures/api/native-window-open-noopener.html @@ -0,0 +1,10 @@ + + +noopener example + + + diff --git a/spec/fixtures/api/new-window-preload.js b/spec/fixtures/api/new-window-preload.js deleted file mode 100644 index 6d9add91f0403..0000000000000 --- a/spec/fixtures/api/new-window-preload.js +++ /dev/null @@ -1,4 +0,0 @@ -const { ipcRenderer, remote } = require('electron') - -ipcRenderer.send('answer', process.argv, remote.getCurrentWindow().webContents.getWebPreferences()) -window.close() diff --git a/spec/fixtures/api/new-window-webview-preload.js b/spec/fixtures/api/new-window-webview-preload.js index ba5d5ded82db1..1336da90a9fa9 100644 --- a/spec/fixtures/api/new-window-webview-preload.js +++ b/spec/fixtures/api/new-window-webview-preload.js @@ -1,3 +1,3 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); -window.ipcRenderer = ipcRenderer +window.ipcRenderer = ipcRenderer; diff --git a/spec/fixtures/api/picture-in-picture.html b/spec/fixtures/api/picture-in-picture.html new file mode 100644 index 0000000000000..36a67070c873d --- /dev/null +++ b/spec/fixtures/api/picture-in-picture.html @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/spec/fixtures/api/quit-app/main.js b/spec/fixtures/api/quit-app/main.js index 9912071b1f365..1df2b675fac13 100644 --- a/spec/fixtures/api/quit-app/main.js +++ b/spec/fixtures/api/quit-app/main.js @@ -1,12 +1,12 @@ -const { app } = require('electron') +const { app } = require('electron'); -app.on('ready', function () { +app.whenReady().then(function () { // This setImmediate call gets the spec passing on Linux setImmediate(function () { - app.exit(123) - }) -}) + app.exit(123); + }); +}); process.on('exit', function (code) { - console.log('Exit event with code: ' + code) -}) + console.log('Exit event with code: ' + code); +}); diff --git a/spec/fixtures/api/relaunch/main.js b/spec/fixtures/api/relaunch/main.js index 69b634f3c2a00..272c41f4421aa 100644 --- a/spec/fixtures/api/relaunch/main.js +++ b/spec/fixtures/api/relaunch/main.js @@ -1,23 +1,22 @@ -const { app } = require('electron') -const net = require('net') +const { app } = require('electron'); +const net = require('net'); -const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch' +const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch'; process.on('uncaughtException', () => { - app.exit(1) -}) + app.exit(1); +}); -app.once('ready', () => { - const lastArg = process.argv[process.argv.length - 1] - const client = net.connect(socketPath) +app.whenReady().then(() => { + const lastArg = process.argv[process.argv.length - 1]; + const client = net.connect(socketPath); client.once('connect', () => { - client.end(String(lastArg === '--second')) - }) + client.end(String(lastArg === '--second')); + }); client.once('end', () => { - app.exit(0) - }) - - if (lastArg !== '--second') { - app.relaunch({ args: process.argv.slice(1).concat('--second') }) - } -}) + if (lastArg !== '--second') { + app.relaunch({ args: process.argv.slice(1).concat('--second') }); + } + app.exit(0); + }); +}); diff --git a/spec/fixtures/api/remote-event-handler.html b/spec/fixtures/api/remote-event-handler.html deleted file mode 100644 index 30c3cfb36ad76..0000000000000 --- a/spec/fixtures/api/remote-event-handler.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - diff --git a/spec/fixtures/api/render-view-deleted.html b/spec/fixtures/api/render-view-deleted.html deleted file mode 100644 index bfc281eb4298c..0000000000000 --- a/spec/fixtures/api/render-view-deleted.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - diff --git a/spec/fixtures/api/service-worker/service-worker.html b/spec/fixtures/api/service-worker/service-worker.html deleted file mode 100644 index 9b05c23b2eaad..0000000000000 --- a/spec/fixtures/api/service-worker/service-worker.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/spec/fixtures/api/service-worker/service-worker.js b/spec/fixtures/api/service-worker/service-worker.js deleted file mode 100644 index 8ae15aa448f8b..0000000000000 --- a/spec/fixtures/api/service-worker/service-worker.js +++ /dev/null @@ -1,5 +0,0 @@ -console.log('Service worker startups.') - -self.addEventListener('install', (event) => { - console.log('Service worker installed.') -}) diff --git a/spec/fixtures/api/shared-worker/shared-worker.html b/spec/fixtures/api/shared-worker/shared-worker.html new file mode 100644 index 0000000000000..3ce51d36f6398 --- /dev/null +++ b/spec/fixtures/api/shared-worker/shared-worker.html @@ -0,0 +1,25 @@ + + + + + diff --git a/spec/fixtures/api/shared-worker/shared-worker1.js b/spec/fixtures/api/shared-worker/shared-worker1.js new file mode 100644 index 0000000000000..f749b244662b9 --- /dev/null +++ b/spec/fixtures/api/shared-worker/shared-worker1.js @@ -0,0 +1,4 @@ +self.onconnect = function (e) { + const port = e.ports[0]; + port.postMessage('ready'); +}; diff --git a/spec/fixtures/api/shared-worker/shared-worker2.js b/spec/fixtures/api/shared-worker/shared-worker2.js new file mode 100644 index 0000000000000..f749b244662b9 --- /dev/null +++ b/spec/fixtures/api/shared-worker/shared-worker2.js @@ -0,0 +1,4 @@ +self.onconnect = function (e) { + const port = e.ports[0]; + port.postMessage('ready'); +}; diff --git a/spec/fixtures/api/singleton-data/main.js b/spec/fixtures/api/singleton-data/main.js new file mode 100644 index 0000000000000..50a623410ea89 --- /dev/null +++ b/spec/fixtures/api/singleton-data/main.js @@ -0,0 +1,37 @@ +const { app } = require('electron'); + +// Send data from the second instance to the first instance. +const sendAdditionalData = app.commandLine.hasSwitch('send-data'); + +app.whenReady().then(() => { + console.log('started'); // ping parent +}); + +let obj = { + level: 1, + testkey: 'testvalue1', + inner: { + level: 2, + testkey: 'testvalue2' + } +}; +if (app.commandLine.hasSwitch('data-content')) { + obj = JSON.parse(app.commandLine.getSwitchValue('data-content')); + if (obj === 'undefined') { + obj = undefined; + } +} + +const gotTheLock = sendAdditionalData + ? app.requestSingleInstanceLock(obj) : app.requestSingleInstanceLock(); + +app.on('second-instance', (event, args, workingDirectory, data) => { + setImmediate(() => { + console.log([JSON.stringify(args), JSON.stringify(data)].join('||')); + app.exit(0); + }); +}); + +if (!gotTheLock) { + app.exit(1); +} diff --git a/spec/fixtures/api/singleton-data/package.json b/spec/fixtures/api/singleton-data/package.json new file mode 100644 index 0000000000000..3c20945331c14 --- /dev/null +++ b/spec/fixtures/api/singleton-data/package.json @@ -0,0 +1,5 @@ +{ + "name": "electron-app-singleton-data", + "main": "main.js" +} + diff --git a/spec/fixtures/api/singleton-userdata/main.js b/spec/fixtures/api/singleton-userdata/main.js new file mode 100644 index 0000000000000..98f6841b4282a --- /dev/null +++ b/spec/fixtures/api/singleton-userdata/main.js @@ -0,0 +1,12 @@ +const { app } = require('electron'); +const fs = require('fs'); +const path = require('path'); + +// non-existent user data folder should not break requestSingleInstanceLock() +// ref: https://github.com/electron/electron/issues/33547 +const userDataFolder = path.join(app.getPath('home'), 'electron-test-singleton-userdata'); +fs.rmSync(userDataFolder, { force: true, recursive: true }); +app.setPath('userData', userDataFolder); + +const gotTheLock = app.requestSingleInstanceLock(); +app.exit(gotTheLock ? 0 : 1); diff --git a/spec/fixtures/api/singleton-userdata/package.json b/spec/fixtures/api/singleton-userdata/package.json new file mode 100644 index 0000000000000..1269c0a67d5dd --- /dev/null +++ b/spec/fixtures/api/singleton-userdata/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-singleton-userdata", + "main": "main.js" +} diff --git a/spec/fixtures/api/singleton/main.js b/spec/fixtures/api/singleton/main.js index 9c1b0a17e4cfb..4bd6433eea215 100644 --- a/spec/fixtures/api/singleton/main.js +++ b/spec/fixtures/api/singleton/main.js @@ -1,18 +1,18 @@ -const { app } = require('electron') +const { app } = require('electron'); -app.once('ready', () => { - console.log('started') // ping parent -}) +app.whenReady().then(() => { + console.log('started'); // ping parent +}); -const gotTheLock = app.requestSingleInstanceLock() +const gotTheLock = app.requestSingleInstanceLock(); -app.on('second-instance', (event, args) => { +app.on('second-instance', (event, args, workingDirectory) => { setImmediate(() => { - console.log(JSON.stringify(args)) - app.exit(0) - }) -}) + console.log(JSON.stringify(args)); + app.exit(0); + }); +}); if (!gotTheLock) { - app.exit(1) + app.exit(1); } diff --git a/spec/fixtures/api/site-instance-overrides/main.js b/spec/fixtures/api/site-instance-overrides/main.js deleted file mode 100644 index 20d10b8ff54c9..0000000000000 --- a/spec/fixtures/api/site-instance-overrides/main.js +++ /dev/null @@ -1,33 +0,0 @@ -const { app, BrowserWindow, ipcMain } = require('electron') -const path = require('path') - -process.on('uncaughtException', (e) => { - console.error(e) - process.exit(1) -}) - -app.allowRendererProcessReuse = JSON.parse(process.argv[2]) - -const pids = [] -let win - -ipcMain.on('pid', (event, pid) => { - pids.push(pid) - if (pids.length === 2) { - console.log(JSON.stringify(pids)) - if (win) win.close() - app.quit() - } else { - if (win) win.reload() - } -}) - -app.whenReady().then(() => { - win = new BrowserWindow({ - show: false, - webPreferences: { - preload: path.resolve(__dirname, 'preload.js') - } - }) - win.loadFile('index.html') -}) diff --git a/spec/fixtures/api/site-instance-overrides/package.json b/spec/fixtures/api/site-instance-overrides/package.json deleted file mode 100644 index 511d3a3c573e9..0000000000000 --- a/spec/fixtures/api/site-instance-overrides/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "site-instance-overrides", - "main": "main.js" -} diff --git a/spec/fixtures/api/site-instance-overrides/preload.js b/spec/fixtures/api/site-instance-overrides/preload.js deleted file mode 100644 index 721369deb3449..0000000000000 --- a/spec/fixtures/api/site-instance-overrides/preload.js +++ /dev/null @@ -1,3 +0,0 @@ -const { ipcRenderer } = require('electron') - -ipcRenderer.send('pid', process.pid) diff --git a/spec/fixtures/api/window-all-closed/main.js b/spec/fixtures/api/window-all-closed/main.js index 67450d93e751c..12f3133d26f01 100644 --- a/spec/fixtures/api/window-all-closed/main.js +++ b/spec/fixtures/api/window-all-closed/main.js @@ -1,20 +1,24 @@ -const { app, BrowserWindow } = require('electron') +const { app, BrowserWindow } = require('electron'); -let handled = false +let handled = false; if (app.commandLine.hasSwitch('handle-event')) { app.on('window-all-closed', () => { - handled = true - app.quit() - }) + handled = true; + app.quit(); + }); } app.on('quit', () => { - process.stdout.write(JSON.stringify(handled)) - process.stdout.end() -}) + process.stdout.write(JSON.stringify(handled)); + process.stdout.end(); +}); -app.on('ready', () => { - const win = new BrowserWindow() - win.close() -}) +app.whenReady().then(() => { + const win = new BrowserWindow({ + webPreferences: { + contextIsolation: true + } + }); + win.close(); +}); diff --git a/spec/fixtures/api/window-all-closed/package.json b/spec/fixtures/api/window-all-closed/package.json index 6486c5bc6e98a..bd54ca2fe4023 100644 --- a/spec/fixtures/api/window-all-closed/package.json +++ b/spec/fixtures/api/window-all-closed/package.json @@ -1,4 +1,4 @@ { - "name": "window-all-closed", + "name": "electron-test-window-all-closed", "main": "main.js" } diff --git a/spec/fixtures/api/window-open-preload.js b/spec/fixtures/api/window-open-preload.js deleted file mode 100644 index ced0c05b1badd..0000000000000 --- a/spec/fixtures/api/window-open-preload.js +++ /dev/null @@ -1,9 +0,0 @@ -const { ipcRenderer } = require('electron') - -setImmediate(function () { - if (window.location.toString() === 'bar://page') { - const windowOpenerIsNull = window.opener == null - ipcRenderer.send('answer', process.argv, typeof global.process, windowOpenerIsNull) - window.close() - } -}) diff --git a/spec/fixtures/asar/a.asar b/spec/fixtures/asar/a.asar deleted file mode 100644 index 0b74f5639cd8a..0000000000000 Binary files a/spec/fixtures/asar/a.asar and /dev/null differ diff --git a/spec/fixtures/asar/empty.asar b/spec/fixtures/asar/empty.asar deleted file mode 100644 index 10cddfb67654e..0000000000000 Binary files a/spec/fixtures/asar/empty.asar and /dev/null differ diff --git a/spec/fixtures/asar/script.asar b/spec/fixtures/asar/script.asar deleted file mode 100755 index 7239786ec90ea..0000000000000 Binary files a/spec/fixtures/asar/script.asar and /dev/null differ diff --git a/spec/fixtures/asar/unpack.asar b/spec/fixtures/asar/unpack.asar deleted file mode 100644 index 8c1231c1b230a..0000000000000 Binary files a/spec/fixtures/asar/unpack.asar and /dev/null differ diff --git a/spec/fixtures/asar/web.asar b/spec/fixtures/asar/web.asar deleted file mode 100644 index 1e9db65b8128e..0000000000000 Binary files a/spec/fixtures/asar/web.asar and /dev/null differ diff --git a/spec/fixtures/assets/shortcut.lnk b/spec/fixtures/assets/shortcut.lnk index 5f325ca733ea3..8f04d3bfef2f7 100755 Binary files a/spec/fixtures/assets/shortcut.lnk and b/spec/fixtures/assets/shortcut.lnk differ diff --git a/spec/fixtures/auto-update/check/index.js b/spec/fixtures/auto-update/check/index.js deleted file mode 100644 index 1f82a3acbac60..0000000000000 --- a/spec/fixtures/auto-update/check/index.js +++ /dev/null @@ -1,23 +0,0 @@ -process.on('uncaughtException', (err) => { - console.error(err) - process.exit(1) -}) - -const { autoUpdater } = require('electron') - -autoUpdater.on('error', (err) => { - console.error(err) - process.exit(1) -}) - -const feedUrl = process.argv[1] - -autoUpdater.setFeedURL({ - url: feedUrl -}) - -autoUpdater.checkForUpdates() - -autoUpdater.on('update-not-available', () => { - process.exit(0) -}) diff --git a/spec/fixtures/auto-update/initial/index.js b/spec/fixtures/auto-update/initial/index.js deleted file mode 100644 index ef1332ebbac9f..0000000000000 --- a/spec/fixtures/auto-update/initial/index.js +++ /dev/null @@ -1,18 +0,0 @@ -process.on('uncaughtException', (err) => { - console.error(err) - process.exit(1) -}) - -const { autoUpdater } = require('electron') - -const feedUrl = process.argv[1] - -console.log('Setting Feed URL') - -autoUpdater.setFeedURL({ - url: feedUrl -}) - -console.log('Feed URL Set:', feedUrl) - -process.exit(0) diff --git a/spec/fixtures/auto-update/update/index.js b/spec/fixtures/auto-update/update/index.js deleted file mode 100644 index aec688cbb96d1..0000000000000 --- a/spec/fixtures/auto-update/update/index.js +++ /dev/null @@ -1,42 +0,0 @@ -const fs = require('fs') -const path = require('path') - -process.on('uncaughtException', (err) => { - console.error(err) - process.exit(1) -}) - -const { app, autoUpdater } = require('electron') - -autoUpdater.on('error', (err) => { - console.error(err) - process.exit(1) -}) - -const urlPath = path.resolve(__dirname, '../../../../url.txt') -let feedUrl = process.argv[1] -if (!feedUrl || !feedUrl.startsWith('http')) { - feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}` -} else { - fs.writeFileSync(urlPath, `${feedUrl}/updated`) -} - -autoUpdater.setFeedURL({ - url: feedUrl -}) - -autoUpdater.checkForUpdates() - -autoUpdater.on('update-available', () => { - console.log('Update Available') -}) - -autoUpdater.on('update-downloaded', () => { - console.log('Update Downloaded') - autoUpdater.quitAndInstall() -}) - -autoUpdater.on('update-not-available', () => { - console.error('No update available') - process.exit(1) -}) diff --git a/spec/fixtures/auto-update/update/package.json b/spec/fixtures/auto-update/update/package.json deleted file mode 100644 index 5edc5dc51ce71..0000000000000 --- a/spec/fixtures/auto-update/update/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "initial-app", - "version": "1.0.0", - "main": "./index.js" -} \ No newline at end of file diff --git a/spec/fixtures/devtools-extensions/foo/foo.html b/spec/fixtures/devtools-extensions/foo/foo.html deleted file mode 100644 index a326639c38deb..0000000000000 --- a/spec/fixtures/devtools-extensions/foo/foo.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - foo - - - diff --git a/spec/fixtures/devtools-extensions/foo/index.html b/spec/fixtures/devtools-extensions/foo/index.html deleted file mode 100644 index 6642121c2410f..0000000000000 --- a/spec/fixtures/devtools-extensions/foo/index.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - a custom devtools extension - - diff --git a/spec/fixtures/devtools-extensions/foo/manifest.json b/spec/fixtures/devtools-extensions/foo/manifest.json deleted file mode 100644 index b88ab65a1ea8f..0000000000000 --- a/spec/fixtures/devtools-extensions/foo/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "foo", - "version": "1.0", - "devtools_page": "foo.html", - "default_locale": "en" -} diff --git a/spec/fixtures/extensions/chrome-api/background.js b/spec/fixtures/extensions/chrome-api/background.js deleted file mode 100644 index c86ff51ddb9dd..0000000000000 --- a/spec/fixtures/extensions/chrome-api/background.js +++ /dev/null @@ -1,20 +0,0 @@ -/* global chrome */ - -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - const { method, args = [] } = message - const tabId = sender.tab.id - - switch (method) { - case 'sendMessage': { - const [message] = args - chrome.tabs.sendMessage(tabId, { message, tabId }, undefined, sendResponse) - break - } - - case 'executeScript': { - const [code] = args - chrome.tabs.executeScript(tabId, { code }, ([result]) => sendResponse(result)) - break - } - } -}) diff --git a/spec/fixtures/extensions/chrome-api/main.js b/spec/fixtures/extensions/chrome-api/main.js deleted file mode 100644 index 2784e82c37fd0..0000000000000 --- a/spec/fixtures/extensions/chrome-api/main.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global chrome */ - -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - sendResponse(message) -}) - -const testMap = { - getManifest () { - const manifest = chrome.runtime.getManifest() - console.log(JSON.stringify(manifest)) - }, - sendMessage (message) { - chrome.runtime.sendMessage({ method: 'sendMessage', args: [message] }, response => { - console.log(JSON.stringify(response)) - }) - }, - executeScript (code) { - chrome.runtime.sendMessage({ method: 'executeScript', args: [code] }, response => { - console.log(JSON.stringify(response)) - }) - } -} - -const dispatchTest = (event) => { - const { method, args = [] } = JSON.parse(event.data) - testMap[method](...args) -} -window.addEventListener('message', dispatchTest, false) diff --git a/spec/fixtures/extensions/chrome-api/manifest.json b/spec/fixtures/extensions/chrome-api/manifest.json deleted file mode 100644 index fd4d88b910968..0000000000000 --- a/spec/fixtures/extensions/chrome-api/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "chrome-api", - "version": "1.0", - "content_scripts": [ - { - "matches": [""], - "js": ["main.js"], - "run_at": "document_start" - } - ], - "background": { - "scripts": ["background.js"], - "persistent": false - }, - "manifest_version": 2 -} diff --git a/spec/fixtures/extensions/content-script-document-end/end.js b/spec/fixtures/extensions/content-script-document-end/end.js deleted file mode 100644 index 787b050adc8cf..0000000000000 --- a/spec/fixtures/extensions/content-script-document-end/end.js +++ /dev/null @@ -1 +0,0 @@ -document.documentElement.style.backgroundColor = 'red' diff --git a/spec/fixtures/extensions/content-script-document-idle/idle.js b/spec/fixtures/extensions/content-script-document-idle/idle.js deleted file mode 100644 index b45176a31eaf8..0000000000000 --- a/spec/fixtures/extensions/content-script-document-idle/idle.js +++ /dev/null @@ -1 +0,0 @@ -document.body.style.backgroundColor = 'red' diff --git a/spec/fixtures/extensions/content-script-document-start/start.js b/spec/fixtures/extensions/content-script-document-start/start.js deleted file mode 100644 index 787b050adc8cf..0000000000000 --- a/spec/fixtures/extensions/content-script-document-start/start.js +++ /dev/null @@ -1 +0,0 @@ -document.documentElement.style.backgroundColor = 'red' diff --git a/spec/fixtures/extensions/content-script/all_frames-preload.js b/spec/fixtures/extensions/content-script/all_frames-preload.js deleted file mode 100644 index 54133d143c9fc..0000000000000 --- a/spec/fixtures/extensions/content-script/all_frames-preload.js +++ /dev/null @@ -1,14 +0,0 @@ -const { ipcRenderer, webFrame } = require('electron') - -if (process.isMainFrame) { - // https://github.com/electron/electron/issues/17252 - ipcRenderer.on('executeJavaScriptInFrame', (event, frameRoutingId, code, responseId) => { - const frame = webFrame.findFrameByRoutingId(frameRoutingId) - if (!frame) { - throw new Error(`Can't find frame for routing ID ${frameRoutingId}`) - } - frame.executeJavaScript(code, false).then(result => { - event.sender.send(`executeJavaScriptInFrame_${responseId}`, result) - }) - }) -} diff --git a/spec/fixtures/module/access-blink-apis.js b/spec/fixtures/module/access-blink-apis.js index 19f2d51cc8712..625451dac3b52 100644 --- a/spec/fixtures/module/access-blink-apis.js +++ b/spec/fixtures/module/access-blink-apis.js @@ -1,17 +1,17 @@ -window.delayed = true +window.delayed = true; global.getGlobalNames = () => { return Object.getOwnPropertyNames(global) .filter(key => typeof global[key] === 'function') .filter(key => key !== 'WebView') - .sort() -} + .sort(); +}; -const atPreload = global.getGlobalNames() +const atPreload = global.getGlobalNames(); window.addEventListener('load', () => { window.test = { atPreload, atLoad: global.getGlobalNames() - } -}) + }; +}); diff --git a/spec/fixtures/module/answer.js b/spec/fixtures/module/answer.js deleted file mode 100644 index 8a3690555096a..0000000000000 --- a/spec/fixtures/module/answer.js +++ /dev/null @@ -1,4 +0,0 @@ -const { ipcRenderer } = require('electron') -window.answer = function (answer) { - ipcRenderer.send('answer', answer) -} diff --git a/spec/fixtures/module/asar.js b/spec/fixtures/module/asar.js index 624daaeaf7a28..6a973ad386045 100644 --- a/spec/fixtures/module/asar.js +++ b/spec/fixtures/module/asar.js @@ -1,4 +1,4 @@ -const fs = require('fs') +const fs = require('fs'); process.on('message', function (file) { - process.send(fs.readFileSync(file).toString()) -}) + process.send(fs.readFileSync(file).toString()); +}); diff --git a/spec/fixtures/module/call.js b/spec/fixtures/module/call.js deleted file mode 100644 index d09d677199b19..0000000000000 --- a/spec/fixtures/module/call.js +++ /dev/null @@ -1,7 +0,0 @@ -exports.call = function (func) { - return func() -} - -exports.constructor = function () { - this.test = 'test' -} diff --git a/spec/fixtures/module/check-arguments.js b/spec/fixtures/module/check-arguments.js index 96f788e5b1a44..8a5ef8dde197d 100644 --- a/spec/fixtures/module/check-arguments.js +++ b/spec/fixtures/module/check-arguments.js @@ -1,4 +1,4 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); window.onload = function () { - ipcRenderer.send('answer', process.argv) -} + ipcRenderer.send('answer', process.argv); +}; diff --git a/spec/fixtures/module/circular.js b/spec/fixtures/module/circular.js deleted file mode 100644 index e8629c424acd9..0000000000000 --- a/spec/fixtures/module/circular.js +++ /dev/null @@ -1,3 +0,0 @@ -exports.returnArgs = function (...args) { - return args -} diff --git a/spec/fixtures/module/class.js b/spec/fixtures/module/class.js deleted file mode 100644 index 9b971e52335b6..0000000000000 --- a/spec/fixtures/module/class.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict' - -let value = 'old' - -class BaseClass { - method () { - return 'method' - } - - get readonly () { - return 'readonly' - } - - get value () { - return value - } - - set value (val) { - value = val - } -} - -class DerivedClass extends BaseClass { -} - -module.exports = { - base: new BaseClass(), - derived: new DerivedClass() -} diff --git a/spec/fixtures/module/crash.js b/spec/fixtures/module/crash.js deleted file mode 100644 index 2e15f1c6b19bb..0000000000000 --- a/spec/fixtures/module/crash.js +++ /dev/null @@ -1,19 +0,0 @@ -process.crashReporter.start({ - productName: 'Zombies', - companyName: 'Umbrella Corporation', - crashesDirectory: process.argv[4], - submitURL: `http://127.0.0.1:${process.argv[2]}`, - extra: { - extra1: 'extra1', - extra2: 'extra2', - _version: process.argv[3] - } -}) - -if (process.platform !== 'linux') { - process.crashReporter.addExtraParameter('newExtra', 'newExtra') - process.crashReporter.addExtraParameter('removeExtra', 'removeExtra') - process.crashReporter.removeExtraParameter('removeExtra') -} - -process.nextTick(() => process.crash()) diff --git a/spec/fixtures/module/create_socket.js b/spec/fixtures/module/create_socket.js index 5e1f49255d504..d5eca125a541e 100644 --- a/spec/fixtures/module/create_socket.js +++ b/spec/fixtures/module/create_socket.js @@ -1,4 +1,4 @@ -const net = require('net') -const server = net.createServer(function () {}) -server.listen(process.argv[2]) -process.exit(0) +const net = require('net'); +const server = net.createServer(function () {}); +server.listen(process.argv[2]); +process.exit(0); diff --git a/spec/fixtures/module/declare-buffer.js b/spec/fixtures/module/declare-buffer.js deleted file mode 100644 index 9a054a24b5c08..0000000000000 --- a/spec/fixtures/module/declare-buffer.js +++ /dev/null @@ -1,2 +0,0 @@ -const Buffer = 'declared Buffer' -module.exports = Buffer diff --git a/spec/fixtures/module/declare-global.js b/spec/fixtures/module/declare-global.js deleted file mode 100644 index bec8dc534f29e..0000000000000 --- a/spec/fixtures/module/declare-global.js +++ /dev/null @@ -1,2 +0,0 @@ -const global = 'declared global' -module.exports = global diff --git a/spec/fixtures/module/declare-process.js b/spec/fixtures/module/declare-process.js deleted file mode 100644 index 257278a4d0b64..0000000000000 --- a/spec/fixtures/module/declare-process.js +++ /dev/null @@ -1,2 +0,0 @@ -const process = 'declared process' -module.exports = process diff --git a/spec/fixtures/module/delay-exit.js b/spec/fixtures/module/delay-exit.js index fa7895449b32f..d990675c20493 100644 --- a/spec/fixtures/module/delay-exit.js +++ b/spec/fixtures/module/delay-exit.js @@ -1,3 +1,6 @@ -const { app } = require('electron') +const { app } = require('electron'); -process.on('message', () => app.quit()) +process.on('message', () => { + console.log('Notified to quit'); + app.quit(); +}); diff --git a/spec/fixtures/module/delete-buffer.js b/spec/fixtures/module/delete-buffer.js deleted file mode 100644 index b90af7d6181ed..0000000000000 --- a/spec/fixtures/module/delete-buffer.js +++ /dev/null @@ -1,11 +0,0 @@ -const path = require('path') -const { remote } = require('electron') -const { Buffer } = window - -delete window.Buffer -delete global.Buffer - -// Test that remote.js doesn't use Buffer global -remote.require(path.join(__dirname, 'print_name.js')).echo(Buffer.from('bar')) - -window.test = Buffer.from('buffer') diff --git a/spec/fixtures/module/echo-renamed.js b/spec/fixtures/module/echo-renamed.js deleted file mode 100644 index 80718356038d9..0000000000000 --- a/spec/fixtures/module/echo-renamed.js +++ /dev/null @@ -1,7 +0,0 @@ -let echo -try { - echo = require('echo') -} catch (e) { - process.exit(1) -} -process.exit(echo(0)) diff --git a/spec/fixtures/module/echo.js b/spec/fixtures/module/echo.js deleted file mode 100644 index 55283b9b392e1..0000000000000 --- a/spec/fixtures/module/echo.js +++ /dev/null @@ -1,6 +0,0 @@ -process.on('uncaughtException', function (err) { - process.send(err.message) -}) - -const echo = require('echo') -process.send(echo('ok')) diff --git a/spec/fixtures/module/empty.js b/spec/fixtures/module/empty.js index 3f0e6f4f0707d..7dafbcc94f339 100644 --- a/spec/fixtures/module/empty.js +++ b/spec/fixtures/module/empty.js @@ -1,5 +1,5 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); window.addEventListener('message', (event) => { - ipcRenderer.send('leak-result', event.data) -}) + ipcRenderer.send('leak-result', event.data); +}); diff --git a/spec/fixtures/module/error-properties.js b/spec/fixtures/module/error-properties.js deleted file mode 100644 index c3a1e3b3a7f6a..0000000000000 --- a/spec/fixtures/module/error-properties.js +++ /dev/null @@ -1,11 +0,0 @@ -class Foo { - set bar (value) { - throw new Error('setting error') - } - - get bar () { - throw new Error('getting error') - } -} - -module.exports = new Foo() diff --git a/spec/fixtures/module/exception.js b/spec/fixtures/module/exception.js deleted file mode 100644 index 1465833ff8da4..0000000000000 --- a/spec/fixtures/module/exception.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (error) { - throw error -} diff --git a/spec/fixtures/module/export-function-with-properties.js b/spec/fixtures/module/export-function-with-properties.js deleted file mode 100644 index 9df7d79deb246..0000000000000 --- a/spec/fixtures/module/export-function-with-properties.js +++ /dev/null @@ -1,4 +0,0 @@ -function foo () {} -foo.bar = 'baz' - -module.exports = foo diff --git a/spec/fixtures/module/fail.js b/spec/fixtures/module/fail.js new file mode 100644 index 0000000000000..6cee2e1e79a14 --- /dev/null +++ b/spec/fixtures/module/fail.js @@ -0,0 +1 @@ +process.exit(1); diff --git a/spec/fixtures/module/fork_ping.js b/spec/fixtures/module/fork_ping.js index a2c8782600ec1..e9b28bde1d61c 100644 --- a/spec/fixtures/module/fork_ping.js +++ b/spec/fixtures/module/fork_ping.js @@ -1,16 +1,16 @@ -const path = require('path') +const path = require('path'); process.on('uncaughtException', function (error) { - process.send(error.stack) -}) + process.send(error.stack); +}); -const child = require('child_process').fork(path.join(__dirname, '/ping.js')) +const child = require('child_process').fork(path.join(__dirname, '/ping.js')); process.on('message', function (msg) { - child.send(msg) -}) + child.send(msg); +}); child.on('message', function (msg) { - process.send(msg) -}) + process.send(msg); +}); child.on('exit', function (code) { - process.exit(code) -}) + process.exit(code); +}); diff --git a/spec/fixtures/module/function-with-args.js b/spec/fixtures/module/function-with-args.js deleted file mode 100644 index ed636e5988a2d..0000000000000 --- a/spec/fixtures/module/function-with-args.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (cb) { - return cb.length -} diff --git a/spec/fixtures/module/function-with-missing-properties.js b/spec/fixtures/module/function-with-missing-properties.js deleted file mode 100644 index d247485a17b69..0000000000000 --- a/spec/fixtures/module/function-with-missing-properties.js +++ /dev/null @@ -1,13 +0,0 @@ -exports.setup = function () { - const foo = {} - - foo.bar = function () { - return delete foo.bar.baz && delete foo.bar - } - - foo.bar.baz = function () { - return 3 - } - - return foo -} diff --git a/spec/fixtures/module/function-with-properties.js b/spec/fixtures/module/function-with-properties.js deleted file mode 100644 index 3ae617133e7e9..0000000000000 --- a/spec/fixtures/module/function-with-properties.js +++ /dev/null @@ -1,17 +0,0 @@ -function foo () { - return 'hello' -} -foo.bar = 'baz' -foo.nested = { - prop: 'yes' -} -foo.method1 = function () { - return 'world' -} -foo.method1.prop1 = function () { - return 123 -} - -module.exports = { - foo: foo -} diff --git a/spec/fixtures/module/function.js b/spec/fixtures/module/function.js deleted file mode 100644 index 8a2bb6c421ec8..0000000000000 --- a/spec/fixtures/module/function.js +++ /dev/null @@ -1 +0,0 @@ -exports.aFunction = function () { return 1127 } diff --git a/spec/fixtures/module/get-global-preload.js b/spec/fixtures/module/get-global-preload.js index c1010dd472b2b..1f02aa132bc56 100644 --- a/spec/fixtures/module/get-global-preload.js +++ b/spec/fixtures/module/get-global-preload.js @@ -1 +1 @@ -require('electron').ipcRenderer.send('vars', window.preload1, window.preload2, window.preload3) +require('electron').ipcRenderer.send('vars', window.preload1, window.preload2, window.preload3); diff --git a/spec/fixtures/module/hello-child.js b/spec/fixtures/module/hello-child.js index 09ac18900e162..a40ab2165751b 100644 --- a/spec/fixtures/module/hello-child.js +++ b/spec/fixtures/module/hello-child.js @@ -1,6 +1,6 @@ class Hello { say () { - return 'hi child window' + return 'hi child window'; } } -module.exports = Hello +module.exports = Hello; diff --git a/spec/fixtures/module/hello.js b/spec/fixtures/module/hello.js index 9debd60618ecc..49d2cb827ef29 100644 --- a/spec/fixtures/module/hello.js +++ b/spec/fixtures/module/hello.js @@ -1,6 +1,6 @@ class Hello { say () { - return 'hi' + return 'hi'; } } -module.exports = Hello +module.exports = Hello; diff --git a/spec/fixtures/module/id.js b/spec/fixtures/module/id.js deleted file mode 100644 index 2faec9d383216..0000000000000 --- a/spec/fixtures/module/id.js +++ /dev/null @@ -1 +0,0 @@ -exports.id = 1127 diff --git a/spec/fixtures/module/inspector-binding.js b/spec/fixtures/module/inspector-binding.js index 64505429508bd..64a5986e8c75a 100644 --- a/spec/fixtures/module/inspector-binding.js +++ b/spec/fixtures/module/inspector-binding.js @@ -1,76 +1,76 @@ -const inspector = require('inspector') -const path = require('path') -const { pathToFileURL } = require('url') +const inspector = require('inspector'); +const path = require('path'); +const { pathToFileURL } = require('url'); // This test case will set a breakpoint 4 lines below function debuggedFunction () { - let i - let accum = 0 + let i; + let accum = 0; for (i = 0; i < 5; i++) { - accum += i + accum += i; } - return accum + return accum; } -let scopeCallback = null +let scopeCallback = null; function checkScope (session, scopeId) { session.post('Runtime.getProperties', { - 'objectId': scopeId, - 'ownProperties': false, - 'accessorPropertiesOnly': false, - 'generatePreview': true - }, scopeCallback) + objectId: scopeId, + ownProperties: false, + accessorPropertiesOnly: false, + generatePreview: true + }, scopeCallback); } function debuggerPausedCallback (session, notification) { - const params = notification['params'] - const callFrame = params['callFrames'][0] - const scopeId = callFrame['scopeChain'][0]['object']['objectId'] - checkScope(session, scopeId) + const params = notification.params; + const callFrame = params.callFrames[0]; + const scopeId = callFrame.scopeChain[0].object.objectId; + checkScope(session, scopeId); } function testSampleDebugSession () { - let cur = 0 - const failures = [] + let cur = 0; + const failures = []; const expects = { i: [0, 1, 2, 3, 4], accum: [0, 0, 1, 3, 6] - } + }; scopeCallback = function (error, result) { - if (error) failures.push(error) - const i = cur++ - let v, actual, expected - for (v of result['result']) { - actual = v['value']['value'] - expected = expects[v['name']][i] + if (error) failures.push(error); + const i = cur++; + let v, actual, expected; + for (v of result.result) { + actual = v.value.value; + expected = expects[v.name][i]; if (actual !== expected) { - failures.push(`Iteration ${i} variable: ${v['name']} ` + - `expected: ${expected} actual: ${actual}`) + failures.push(`Iteration ${i} variable: ${v.name} ` + + `expected: ${expected} actual: ${actual}`); } } - } - const session = new inspector.Session() - session.connect() + }; + const session = new inspector.Session(); + session.connect(); session.on('Debugger.paused', - (notification) => debuggerPausedCallback(session, notification)) - let cbAsSecondArgCalled = false - session.post('Debugger.enable', () => { cbAsSecondArgCalled = true }) + (notification) => debuggerPausedCallback(session, notification)); + let cbAsSecondArgCalled = false; + session.post('Debugger.enable', () => { cbAsSecondArgCalled = true; }); session.post('Debugger.setBreakpointByUrl', { - 'lineNumber': 9, - 'url': pathToFileURL(path.resolve(__dirname, __filename)).toString(), - 'columnNumber': 0, - 'condition': '' - }) + lineNumber: 9, + url: pathToFileURL(path.resolve(__dirname, __filename)).toString(), + columnNumber: 0, + condition: '' + }); - debuggedFunction() - scopeCallback = null - session.disconnect() + debuggedFunction(); + scopeCallback = null; + session.disconnect(); process.send({ - 'cmd': 'assert', - 'debuggerEnabled': cbAsSecondArgCalled, - 'success': (cur === 5) && (failures.length === 0) - }) + cmd: 'assert', + debuggerEnabled: cbAsSecondArgCalled, + success: (cur === 5) && (failures.length === 0) + }); } -testSampleDebugSession() +testSampleDebugSession(); diff --git a/spec/fixtures/module/isolated-ping.js b/spec/fixtures/module/isolated-ping.js index 7088d346c8ef1..90392e46fe4de 100644 --- a/spec/fixtures/module/isolated-ping.js +++ b/spec/fixtures/module/isolated-ping.js @@ -1,2 +1,2 @@ -const { ipcRenderer } = require('electron') -ipcRenderer.send('pong') +const { ipcRenderer } = require('electron'); +ipcRenderer.send('pong'); diff --git a/spec/fixtures/module/locale-compare.js b/spec/fixtures/module/locale-compare.js index 4027540044347..48c4ebbdc8a47 100644 --- a/spec/fixtures/module/locale-compare.js +++ b/spec/fixtures/module/locale-compare.js @@ -3,5 +3,5 @@ process.on('message', function () { 'a'.localeCompare('a'), 'ä'.localeCompare('z', 'de'), 'ä'.localeCompare('a', 'sv', { sensitivity: 'base' }) - ]) -}) + ]); +}); diff --git a/spec/fixtures/module/no-asar.js b/spec/fixtures/module/no-asar.js index e574e3c6511b8..8835a22c42d1e 100644 --- a/spec/fixtures/module/no-asar.js +++ b/spec/fixtures/module/no-asar.js @@ -1,15 +1,15 @@ -const fs = require('fs') -const path = require('path') +const fs = require('fs'); +const path = require('path'); -const stats = fs.statSync(path.join(__dirname, '..', 'asar', 'a.asar')) +const stats = fs.statSync(path.join(__dirname, '..', 'test.asar', 'a.asar')); const details = { isFile: stats.isFile(), size: stats.size -} +}; if (process.send != null) { - process.send(details) + process.send(details); } else { - console.log(JSON.stringify(details)) + console.log(JSON.stringify(details)); } diff --git a/spec/fixtures/module/no-prototype.js b/spec/fixtures/module/no-prototype.js deleted file mode 100644 index 000eb183fffb7..0000000000000 --- a/spec/fixtures/module/no-prototype.js +++ /dev/null @@ -1,11 +0,0 @@ -const foo = Object.create(null) -foo.bar = 'baz' -foo.baz = false -module.exports = { - foo: foo, - bar: 1234, - anonymous: new (class {})(), - getConstructorName: function (value) { - return value.constructor.name - } -} diff --git a/spec/fixtures/module/node-promise-timer.js b/spec/fixtures/module/node-promise-timer.js new file mode 100644 index 0000000000000..b21c24f5dfc36 --- /dev/null +++ b/spec/fixtures/module/node-promise-timer.js @@ -0,0 +1,23 @@ +const waitMs = (msec) => new Promise((resolve) => setTimeout(resolve, msec)); + +const intervalMsec = 100; +const numIterations = 2; +let curIteration = 0; +let promise; + +for (let i = 0; i < numIterations; i++) { + promise = (promise || waitMs(intervalMsec)).then(() => { + ++curIteration; + return waitMs(intervalMsec); + }); +} + +// https://github.com/electron/electron/issues/21515 was about electron +// exiting before promises finished. This test sets the pending exitCode +// to failure, then resets it to success only if all promises finish. +process.exitCode = 1; +promise.then(() => { + if (curIteration === numIterations) { + process.exitCode = 0; + } +}); diff --git a/spec/fixtures/module/noop.js b/spec/fixtures/module/noop.js index 23d7e9991574b..dcbbff6c93458 100644 --- a/spec/fixtures/module/noop.js +++ b/spec/fixtures/module/noop.js @@ -1 +1 @@ -process.exit(0) +process.exit(0); diff --git a/spec/fixtures/module/original-fs.js b/spec/fixtures/module/original-fs.js index 341dcb2e0de80..7a527c6335853 100644 --- a/spec/fixtures/module/original-fs.js +++ b/spec/fixtures/module/original-fs.js @@ -1,3 +1,3 @@ process.on('message', function () { - process.send(typeof require('original-fs')) -}) + process.send(typeof require('original-fs')); +}); diff --git a/spec/fixtures/module/ping.js b/spec/fixtures/module/ping.js index 90b3d1fb20a18..7479ac7419fa6 100644 --- a/spec/fixtures/module/ping.js +++ b/spec/fixtures/module/ping.js @@ -1,4 +1,4 @@ process.on('message', function (msg) { - process.send(msg) - process.exit(0) -}) + process.send(msg); + process.exit(0); +}); diff --git a/spec/fixtures/module/preload-context.js b/spec/fixtures/module/preload-context.js index 3d3f8bc9755cc..4dbc3a9a58d3b 100644 --- a/spec/fixtures/module/preload-context.js +++ b/spec/fixtures/module/preload-context.js @@ -5,6 +5,6 @@ const types = { electron: typeof electron, window: typeof window, localVar: typeof window.test -} +}; -console.log(JSON.stringify(types)) +console.log(JSON.stringify(types)); diff --git a/spec/fixtures/module/preload-disable-remote.js b/spec/fixtures/module/preload-disable-remote.js deleted file mode 100644 index 008acec7b5d60..0000000000000 --- a/spec/fixtures/module/preload-disable-remote.js +++ /dev/null @@ -1,8 +0,0 @@ -setImmediate(function () { - try { - const { remote } = require('electron') - console.log(JSON.stringify(typeof remote)) - } catch (e) { - console.log(e.message) - } -}) diff --git a/spec/fixtures/module/preload-electron.js b/spec/fixtures/module/preload-electron.js new file mode 100644 index 0000000000000..9b3482f5df220 --- /dev/null +++ b/spec/fixtures/module/preload-electron.js @@ -0,0 +1 @@ +window.electron = require('electron'); diff --git a/spec/fixtures/module/preload-error-exception.js b/spec/fixtures/module/preload-error-exception.js index 710907d35a33a..ec3582c249628 100644 --- a/spec/fixtures/module/preload-error-exception.js +++ b/spec/fixtures/module/preload-error-exception.js @@ -1 +1 @@ -throw new Error('Hello World!') +throw new Error('Hello World!'); diff --git a/spec/fixtures/module/preload-ipc-ping-pong.js b/spec/fixtures/module/preload-ipc-ping-pong.js index 6ea0b32fdad95..41d4c75382230 100644 --- a/spec/fixtures/module/preload-ipc-ping-pong.js +++ b/spec/fixtures/module/preload-ipc-ping-pong.js @@ -1,9 +1,9 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); ipcRenderer.on('ping', function (event, payload) { - ipcRenderer.sendTo(event.senderId, 'pong', payload) -}) + ipcRenderer.sendTo(event.senderId, 'pong', payload); +}); ipcRenderer.on('ping-æøåü', function (event, payload) { - ipcRenderer.sendTo(event.senderId, 'pong-æøåü', payload) -}) + ipcRenderer.sendTo(event.senderId, 'pong-æøåü', payload); +}); diff --git a/spec/fixtures/module/preload-ipc.js b/spec/fixtures/module/preload-ipc.js index 3f97d1a56b3bb..390fa920dfa09 100644 --- a/spec/fixtures/module/preload-ipc.js +++ b/spec/fixtures/module/preload-ipc.js @@ -1,4 +1,4 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); ipcRenderer.on('ping', function (event, message) { - ipcRenderer.sendToHost('pong', message) -}) + ipcRenderer.sendToHost('pong', message); +}); diff --git a/spec/fixtures/module/preload-node-off-wrapper.js b/spec/fixtures/module/preload-node-off-wrapper.js index dbe1330adcf3c..614f0db1c68bd 100644 --- a/spec/fixtures/module/preload-node-off-wrapper.js +++ b/spec/fixtures/module/preload-node-off-wrapper.js @@ -1,3 +1,3 @@ setImmediate(function () { - require('./preload-required-module') -}) + require('./preload-required-module'); +}); diff --git a/spec/fixtures/module/preload-node-off.js b/spec/fixtures/module/preload-node-off.js index 65db2a7e4dc05..0b21b2ae32d5d 100644 --- a/spec/fixtures/module/preload-node-off.js +++ b/spec/fixtures/module/preload-node-off.js @@ -5,9 +5,9 @@ setImmediate(function () { setImmediate: typeof setImmediate, global: typeof global, Buffer: typeof Buffer - } - console.log(JSON.stringify(types)) + }; + console.log(JSON.stringify(types)); } catch (e) { - console.log(e.message) + console.log(e.message); } -}) +}); diff --git a/spec/fixtures/module/preload-pdf-loaded-in-nested-subframe.js b/spec/fixtures/module/preload-pdf-loaded-in-nested-subframe.js index 3e631a7ffb64c..e72a4a4058db4 100644 --- a/spec/fixtures/module/preload-pdf-loaded-in-nested-subframe.js +++ b/spec/fixtures/module/preload-pdf-loaded-in-nested-subframe.js @@ -1,15 +1,15 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); document.addEventListener('DOMContentLoaded', (event) => { - const outerFrame = document.querySelector('#outer-frame') + const outerFrame = document.querySelector('#outer-frame'); if (outerFrame) { outerFrame.onload = function () { - const pdframe = outerFrame.contentWindow.document.getElementById('pdf-frame') + const pdframe = outerFrame.contentWindow.document.getElementById('pdf-frame'); if (pdframe) { pdframe.contentWindow.addEventListener('pdf-loaded', (event) => { - ipcRenderer.send('pdf-loaded', event.detail) - }) + ipcRenderer.send('pdf-loaded', event.detail); + }); } - } + }; } -}) +}); diff --git a/spec/fixtures/module/preload-pdf-loaded-in-subframe.js b/spec/fixtures/module/preload-pdf-loaded-in-subframe.js index 40ba24a4d958c..dd7a7aaa42d6d 100644 --- a/spec/fixtures/module/preload-pdf-loaded-in-subframe.js +++ b/spec/fixtures/module/preload-pdf-loaded-in-subframe.js @@ -1,10 +1,10 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); document.addEventListener('DOMContentLoaded', (event) => { - const subframe = document.querySelector('#pdf-frame') + const subframe = document.querySelector('#pdf-frame'); if (subframe) { subframe.contentWindow.addEventListener('pdf-loaded', (event) => { - ipcRenderer.send('pdf-loaded', event.detail) - }) + ipcRenderer.send('pdf-loaded', event.detail); + }); } -}) +}); diff --git a/spec/fixtures/module/preload-pdf-loaded.js b/spec/fixtures/module/preload-pdf-loaded.js index 9393898b50846..aa5c8fb4ffac6 100644 --- a/spec/fixtures/module/preload-pdf-loaded.js +++ b/spec/fixtures/module/preload-pdf-loaded.js @@ -1,5 +1,5 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); window.addEventListener('pdf-loaded', function (event) { - ipcRenderer.send('pdf-loaded', event.detail) -}) + ipcRenderer.send('pdf-loaded', event.detail); +}); diff --git a/spec/fixtures/module/preload-remote-function.js b/spec/fixtures/module/preload-remote-function.js deleted file mode 100644 index e9d5c3311c3ef..0000000000000 --- a/spec/fixtures/module/preload-remote-function.js +++ /dev/null @@ -1,5 +0,0 @@ -const { remote, ipcRenderer } = require('electron') -remote.getCurrentWindow().rendererFunc = () => { - ipcRenderer.send('done') -} -remote.getCurrentWindow().rendererFunc() diff --git a/spec/fixtures/module/preload-remote.js b/spec/fixtures/module/preload-remote.js deleted file mode 100644 index a9014c726ff6c..0000000000000 --- a/spec/fixtures/module/preload-remote.js +++ /dev/null @@ -1,5 +0,0 @@ -const { ipcRenderer, remote } = require('electron') - -window.onload = function () { - ipcRenderer.send('remote', typeof remote) -} diff --git a/spec/fixtures/module/preload-required-module.js b/spec/fixtures/module/preload-required-module.js index 75b2bf4add880..23ff1780021cd 100644 --- a/spec/fixtures/module/preload-required-module.js +++ b/spec/fixtures/module/preload-required-module.js @@ -5,8 +5,8 @@ try { global: typeof global, Buffer: typeof Buffer, 'global.Buffer': typeof global.Buffer - } - console.log(JSON.stringify(types)) + }; + console.log(JSON.stringify(types)); } catch (e) { - console.log(e.message) + console.log(e.message); } diff --git a/spec/fixtures/module/preload-sandbox.js b/spec/fixtures/module/preload-sandbox.js deleted file mode 100644 index e1fb9fbd7df85..0000000000000 --- a/spec/fixtures/module/preload-sandbox.js +++ /dev/null @@ -1,61 +0,0 @@ -(function () { - const { setImmediate } = require('timers') - const { ipcRenderer } = require('electron') - window.ipcRenderer = ipcRenderer - window.setImmediate = setImmediate - window.require = require - - function invoke (code) { - try { - return code() - } catch { - return null - } - } - - process.once('loaded', () => { - ipcRenderer.send('process-loaded') - }) - - if (location.protocol === 'file:') { - window.test = 'preload' - window.process = process - if (process.env.sandboxmain) { - window.test = { - osSandbox: !process.argv.includes('--no-sandbox'), - hasCrash: typeof process.crash === 'function', - hasHang: typeof process.hang === 'function', - creationTime: invoke(() => process.getCreationTime()), - heapStatistics: invoke(() => process.getHeapStatistics()), - blinkMemoryInfo: invoke(() => process.getBlinkMemoryInfo()), - processMemoryInfo: invoke(() => process.getProcessMemoryInfo()), - systemMemoryInfo: invoke(() => process.getSystemMemoryInfo()), - systemVersion: invoke(() => process.getSystemVersion()), - cpuUsage: invoke(() => process.getCPUUsage()), - ioCounters: invoke(() => process.getIOCounters()), - env: process.env, - execPath: process.execPath, - pid: process.pid, - arch: process.arch, - platform: process.platform, - sandboxed: process.sandboxed, - type: process.type, - version: process.version, - versions: process.versions - } - } - } else if (location.href !== 'about:blank') { - addEventListener('DOMContentLoaded', () => { - ipcRenderer.on('touch-the-opener', () => { - let errorMessage = null - try { - const openerDoc = opener.document // eslint-disable-line no-unused-vars - } catch (error) { - errorMessage = error.message - } - ipcRenderer.send('answer', errorMessage) - }) - ipcRenderer.send('child-loaded', window.opener == null, document.body.innerHTML, location.href) - }) - } -})() diff --git "a/spec/fixtures/module/preload-sandbox\303\246\303\270 \303\245\303\274.js" "b/spec/fixtures/module/preload-sandbox\303\246\303\270 \303\245\303\274.js" index 3799c8e69c7af..596e9ca4c12ea 100644 --- "a/spec/fixtures/module/preload-sandbox\303\246\303\270 \303\245\303\274.js" +++ "b/spec/fixtures/module/preload-sandbox\303\246\303\270 \303\245\303\274.js" @@ -1,6 +1,6 @@ (function () { - window.require = require + window.require = require; if (location.protocol === 'file:') { - window.test = 'preload' + window.test = 'preload'; } -})() +})(); diff --git a/spec/fixtures/module/preload-set-global.js b/spec/fixtures/module/preload-set-global.js index 6737b06982dc8..ff1dff5e793a6 100644 --- a/spec/fixtures/module/preload-set-global.js +++ b/spec/fixtures/module/preload-set-global.js @@ -1 +1 @@ -window.foo = 'bar' +window.foo = 'bar'; diff --git a/spec/fixtures/module/preload-webview.js b/spec/fixtures/module/preload-webview.js index b1386a95280c9..1789551556b57 100644 --- a/spec/fixtures/module/preload-webview.js +++ b/spec/fixtures/module/preload-webview.js @@ -1,5 +1,5 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); window.onload = function () { - ipcRenderer.send('webview', typeof WebView) -} + ipcRenderer.send('webview', typeof WebView); +}; diff --git a/spec/fixtures/module/preload.js b/spec/fixtures/module/preload.js index 6b77bde032940..aa7bba4cae8f0 100644 --- a/spec/fixtures/module/preload.js +++ b/spec/fixtures/module/preload.js @@ -3,5 +3,5 @@ const types = { module: typeof module, process: typeof process, Buffer: typeof Buffer -} -console.log(JSON.stringify(types)) +}; +console.log(JSON.stringify(types)); diff --git a/spec/fixtures/module/print_name.js b/spec/fixtures/module/print_name.js deleted file mode 100644 index 8583af00f4abc..0000000000000 --- a/spec/fixtures/module/print_name.js +++ /dev/null @@ -1,36 +0,0 @@ -exports.print = function (obj) { - return obj.constructor.name -} - -exports.echo = function (obj) { - return obj -} - -const typedArrays = { - Int8Array, - Uint8Array, - Uint8ClampedArray, - Int16Array, - Uint16Array, - Int32Array, - Uint32Array, - Float32Array, - Float64Array -} - -exports.typedArray = function (type, values) { - const constructor = typedArrays[type] - const array = new constructor(values.length) - for (let i = 0; i < values.length; ++i) { - array[i] = values[i] - } - return array -} - -exports.getNaN = function () { - return NaN -} - -exports.getInfinity = function () { - return Infinity -} diff --git a/spec/fixtures/module/process-stdout.js b/spec/fixtures/module/process-stdout.js index 953750a247f99..f45b5d60408d3 100644 --- a/spec/fixtures/module/process-stdout.js +++ b/spec/fixtures/module/process-stdout.js @@ -1 +1 @@ -process.stdout.write('pipes stdio') +process.stdout.write('pipes stdio'); diff --git a/spec/fixtures/module/process_args.js b/spec/fixtures/module/process_args.js index 56e3906c55345..88191450f1cb3 100644 --- a/spec/fixtures/module/process_args.js +++ b/spec/fixtures/module/process_args.js @@ -1,4 +1,4 @@ process.on('message', function () { - process.send(process.argv) - process.exit(0) -}) + process.send(process.argv); + process.exit(0); +}); diff --git a/spec/fixtures/module/promise.js b/spec/fixtures/module/promise.js deleted file mode 100644 index d34058cc80336..0000000000000 --- a/spec/fixtures/module/promise.js +++ /dev/null @@ -1,5 +0,0 @@ -exports.twicePromise = function (promise) { - return promise.then(function (value) { - return value * 2 - }) -} diff --git a/spec/fixtures/module/property.js b/spec/fixtures/module/property.js deleted file mode 100644 index e570ca9351a90..0000000000000 --- a/spec/fixtures/module/property.js +++ /dev/null @@ -1,11 +0,0 @@ -exports.property = 1127 - -function func () { - -} -func.property = 'foo' -exports.func = func - -exports.getFunctionProperty = () => { - return `${func.property}-${process.type}` -} diff --git a/spec/fixtures/module/rejected-promise.js b/spec/fixtures/module/rejected-promise.js deleted file mode 100644 index 93dd9accc0308..0000000000000 --- a/spec/fixtures/module/rejected-promise.js +++ /dev/null @@ -1,5 +0,0 @@ -exports.reject = function (promise) { - return promise.then(function () { - throw Error('rejected') - }) -} diff --git a/spec/fixtures/module/remote-object-set.js b/spec/fixtures/module/remote-object-set.js deleted file mode 100644 index 74c574722037d..0000000000000 --- a/spec/fixtures/module/remote-object-set.js +++ /dev/null @@ -1,11 +0,0 @@ -const { BrowserWindow } = require('electron') - -class Foo { - set bar (value) { - if (!(value instanceof BrowserWindow)) { - throw new Error('setting error') - } - } -} - -module.exports = new Foo() diff --git a/spec/fixtures/module/remote-static.js b/spec/fixtures/module/remote-static.js deleted file mode 100644 index ed35b1d4476a8..0000000000000 --- a/spec/fixtures/module/remote-static.js +++ /dev/null @@ -1,15 +0,0 @@ -class Foo { - static foo () { - return 3 - } - - baz () { - return 123 - } -} - -Foo.bar = 'baz' - -module.exports = { - Foo: Foo -} diff --git a/spec/fixtures/module/run-as-node.js b/spec/fixtures/module/run-as-node.js index 20812f0463eb5..fc15cb54ef778 100644 --- a/spec/fixtures/module/run-as-node.js +++ b/spec/fixtures/module/run-as-node.js @@ -1,5 +1,5 @@ console.log(JSON.stringify({ - processLog: typeof process.log, + stdoutType: process.stdout._type, processType: typeof process.type, window: typeof window -})) +})); diff --git a/spec/fixtures/module/send-later.js b/spec/fixtures/module/send-later.js index 34250aedce3fb..5b4a22097275c 100644 --- a/spec/fixtures/module/send-later.js +++ b/spec/fixtures/module/send-later.js @@ -1,4 +1,4 @@ -const { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron'); window.onload = function () { - ipcRenderer.send('answer', typeof window.process, typeof window.Buffer) -} + ipcRenderer.send('answer', typeof window.process, typeof window.Buffer); +}; diff --git a/spec/fixtures/module/set-global-preload-1.js b/spec/fixtures/module/set-global-preload-1.js index 22dfdf918506e..92e8741de1771 100644 --- a/spec/fixtures/module/set-global-preload-1.js +++ b/spec/fixtures/module/set-global-preload-1.js @@ -1 +1 @@ -window.preload1 = 'preload-1' +window.preload1 = 'preload-1'; diff --git a/spec/fixtures/module/set-global-preload-2.js b/spec/fixtures/module/set-global-preload-2.js index 7542009f7b09a..af100270d070c 100644 --- a/spec/fixtures/module/set-global-preload-2.js +++ b/spec/fixtures/module/set-global-preload-2.js @@ -1 +1 @@ -window.preload2 = window.preload1 + '-2' +window.preload2 = window.preload1 + '-2'; diff --git a/spec/fixtures/module/set-global-preload-3.js b/spec/fixtures/module/set-global-preload-3.js index 9cfef949277ed..8491be46727cf 100644 --- a/spec/fixtures/module/set-global-preload-3.js +++ b/spec/fixtures/module/set-global-preload-3.js @@ -1 +1 @@ -window.preload3 = window.preload2 + '-3' +window.preload3 = window.preload2 + '-3'; diff --git a/spec/fixtures/module/set-global.js b/spec/fixtures/module/set-global.js index 5ad98817e0bd5..c63ed6cf4486f 100644 --- a/spec/fixtures/module/set-global.js +++ b/spec/fixtures/module/set-global.js @@ -1 +1 @@ -if (!window.test) window.test = 'preload' +if (!window.test) window.test = 'preload'; diff --git a/spec/fixtures/module/set-immediate.js b/spec/fixtures/module/set-immediate.js index 69563fd0a832f..263801c3b9bc4 100644 --- a/spec/fixtures/module/set-immediate.js +++ b/spec/fixtures/module/set-immediate.js @@ -1,11 +1,11 @@ process.on('uncaughtException', function (error) { - process.send(error.message) - process.exit(1) -}) + process.send(error.message); + process.exit(1); +}); process.on('message', function () { setImmediate(function () { - process.send('ok') - process.exit(0) - }) -}) + process.send('ok'); + process.exit(0); + }); +}); diff --git a/spec/fixtures/module/to-string-non-function.js b/spec/fixtures/module/to-string-non-function.js deleted file mode 100644 index a898fc4892386..0000000000000 --- a/spec/fixtures/module/to-string-non-function.js +++ /dev/null @@ -1,4 +0,0 @@ -function hello () { -} -hello.toString = 'hello' -module.exports = { functionWithToStringProperty: hello } diff --git a/spec/fixtures/module/unhandled-rejection.js b/spec/fixtures/module/unhandled-rejection.js deleted file mode 100644 index 6cb870ec88625..0000000000000 --- a/spec/fixtures/module/unhandled-rejection.js +++ /dev/null @@ -1,3 +0,0 @@ -exports.reject = function () { - return Promise.reject(new Error('rejected')) -} diff --git a/spec/fixtures/native-addon/echo/lib/echo.js b/spec/fixtures/native-addon/echo/lib/echo.js deleted file mode 100644 index 8c2c673b4b043..0000000000000 --- a/spec/fixtures/native-addon/echo/lib/echo.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../build/Release/echo.node').Print diff --git a/spec/fixtures/native-addon/echo/package.json b/spec/fixtures/native-addon/echo/package.json deleted file mode 100644 index 650586e326125..0000000000000 --- a/spec/fixtures/native-addon/echo/package.json +++ /dev/null @@ -1,6 +0,0 @@ - -{ - "main": "./lib/echo.js", - "name": "echo", - "version": "0.0.1" -} diff --git a/spec/fixtures/no-proprietary-codecs.js b/spec/fixtures/no-proprietary-codecs.js index d29342d0ea2ca..c3453a829cfac 100644 --- a/spec/fixtures/no-proprietary-codecs.js +++ b/spec/fixtures/no-proprietary-codecs.js @@ -4,47 +4,48 @@ // proprietary codecs to ensure Electron uses it instead of the version // that does include proprietary codecs. -const { app, BrowserWindow, ipcMain } = require('electron') -const path = require('path') +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); -const MEDIA_ERR_SRC_NOT_SUPPORTED = 4 -const FIVE_MINUTES = 5 * 60 * 1000 +const MEDIA_ERR_SRC_NOT_SUPPORTED = 4; +const FIVE_MINUTES = 5 * 60 * 1000; -let window +let window; -app.once('ready', () => { +app.whenReady().then(() => { window = new BrowserWindow({ show: false, webPreferences: { - nodeIntegration: true + nodeIntegration: true, + contextIsolation: false } - }) + }); window.webContents.on('crashed', (event, killed) => { - console.log(`WebContents crashed (killed=${killed})`) - app.exit(1) - }) + console.log(`WebContents crashed (killed=${killed})`); + app.exit(1); + }); - window.loadFile(path.resolve(__dirname, 'asar', 'video.asar', 'index.html')) + window.loadFile(path.resolve(__dirname, 'test.asar', 'video.asar', 'index.html')); ipcMain.on('asar-video', (event, message, error) => { if (message === 'ended') { - console.log('Video played, proprietary codecs are included') - app.exit(1) - return + console.log('Video played, proprietary codecs are included'); + app.exit(1); + return; } if (message === 'error' && error === MEDIA_ERR_SRC_NOT_SUPPORTED) { - app.exit(0) - return + app.exit(0); + return; } - console.log(`Unexpected response from page: ${message} ${error}`) - app.exit(1) - }) + console.log(`Unexpected response from page: ${message} ${error}`); + app.exit(1); + }); setTimeout(() => { - console.log('No IPC message after 5 minutes') - app.exit(1) - }, FIVE_MINUTES) -}) + console.log('No IPC message after 5 minutes'); + app.exit(1); + }, FIVE_MINUTES); +}); diff --git a/spec/fixtures/pages/a.html b/spec/fixtures/pages/a.html index 675d81e97b724..060841461d40e 100644 --- a/spec/fixtures/pages/a.html +++ b/spec/fixtures/pages/a.html @@ -3,6 +3,7 @@ +
Hello World
diff --git a/spec/fixtures/pages/cache-storage.html b/spec/fixtures/pages/cache-storage.html index 0b6717201e50e..e91cae61546e3 100644 --- a/spec/fixtures/pages/cache-storage.html +++ b/spec/fixtures/pages/cache-storage.html @@ -1,5 +1,5 @@ diff --git a/spec/fixtures/pages/focus-web-contents.html b/spec/fixtures/pages/focus-web-contents.html deleted file mode 100644 index 83675bc1d3e01..0000000000000 --- a/spec/fixtures/pages/focus-web-contents.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - diff --git a/spec/fixtures/pages/form-with-data.html b/spec/fixtures/pages/form-with-data.html new file mode 100644 index 0000000000000..711f6196d525f --- /dev/null +++ b/spec/fixtures/pages/form-with-data.html @@ -0,0 +1,8 @@ + + +
+ + +
+ + diff --git a/spec/fixtures/pages/fullscreen-ipif.html b/spec/fixtures/pages/fullscreen-ipif.html new file mode 100644 index 0000000000000..dfca5396e1900 --- /dev/null +++ b/spec/fixtures/pages/fullscreen-ipif.html @@ -0,0 +1,15 @@ + + + + diff --git a/spec/fixtures/pages/fullscreen-oopif.html b/spec/fixtures/pages/fullscreen-oopif.html new file mode 100644 index 0000000000000..db9a24d77ff6c --- /dev/null +++ b/spec/fixtures/pages/fullscreen-oopif.html @@ -0,0 +1,19 @@ + + diff --git a/spec/fixtures/pages/insecure-resources.html b/spec/fixtures/pages/insecure-resources.html index 97f91b769eaed..1059113253834 100644 --- a/spec/fixtures/pages/insecure-resources.html +++ b/spec/fixtures/pages/insecure-resources.html @@ -1,6 +1,6 @@ - + diff --git a/spec/fixtures/pages/jquery.html b/spec/fixtures/pages/jquery.html deleted file mode 100644 index 45c33a63cf803..0000000000000 --- a/spec/fixtures/pages/jquery.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/spec/fixtures/pages/native-module.html b/spec/fixtures/pages/native-module.html index 6d6bb821b302b..922a09aeeddfe 100644 --- a/spec/fixtures/pages/native-module.html +++ b/spec/fixtures/pages/native-module.html @@ -2,7 +2,7 @@ diff --git a/spec/fixtures/pages/pdf-in-iframe.html b/spec/fixtures/pages/pdf-in-iframe.html deleted file mode 100644 index 5b656df4c778f..0000000000000 --- a/spec/fixtures/pages/pdf-in-iframe.html +++ /dev/null @@ -1,6 +0,0 @@ - - -