mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7394591138 | ||
|
|
d37b4f5d9f | ||
|
|
6f1d53ae8f | ||
|
|
fb150b2f17 | ||
|
|
c219f2c990 | ||
|
|
3fa5280fde | ||
|
|
45ad6b3525 | ||
|
|
26e20c7402 | ||
|
|
ca1522385c | ||
|
|
3d743a6ef7 | ||
|
|
aafa96f929 | ||
|
|
898e77a9ee | ||
|
|
e1bb3e7165 | ||
|
|
dbc7cbd000 | ||
|
|
821b738db0 | ||
|
|
969741f9f8 | ||
|
|
476a864388 | ||
|
|
65c5528d13 | ||
|
|
81333d7c79 | ||
|
|
fd56128f46 | ||
|
|
75d8a239a0 | ||
|
|
e03cb79aa5 | ||
|
|
78896775d9 | ||
|
|
40eb41656a | ||
|
|
5a69e80cac | ||
|
|
90decd4eaf | ||
|
|
ba551d265c | ||
|
|
24784ed024 | ||
|
|
f49f6b1a29 | ||
|
|
c63e0d8b96 | ||
|
|
33a81b40c2 | ||
|
|
eb49ed962d | ||
|
|
7e36ac67ce | ||
|
|
cbae32aac6 | ||
|
|
880b1e08e7 | ||
|
|
aedea576da | ||
|
|
707541d9b2 | ||
|
|
3dcb641a99 | ||
|
|
878a763344 | ||
|
|
6a8d187105 | ||
|
|
29622930a0 |
19
.github/actions/build-electron/action.yml
vendored
19
.github/actions/build-electron/action.yml
vendored
@@ -125,6 +125,9 @@ runs:
|
||||
fi
|
||||
sed $SEDOPTION '/.*builtins-pgo/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--turbo-profiling-input/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--reorder-builtins/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--warn-about-builtin-profile-data/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--abort-on-bad-builtin-profile-data/d' out/Default/mksnapshot_args
|
||||
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
cd out/Default
|
||||
@@ -202,7 +205,17 @@ runs:
|
||||
if: ${{ inputs.is-release == 'true' }}
|
||||
run: |
|
||||
cd src
|
||||
gn gen out/ffmpeg --args="import(\"//electron/build/args/ffmpeg.gn\") use_remoteexec=true use_siso=true $GN_EXTRA_ARGS"
|
||||
# Reuse the hermetic mac_sdk_path that `e build` wrote for out/Default so
|
||||
# out/ffmpeg builds against the same SDK instead of the runner's system Xcode.
|
||||
# The path has to live under root_build_dir, so copy the symlink tree and
|
||||
# rewrite Default -> ffmpeg.
|
||||
MAC_SDK_ARG=""
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
mkdir -p out/ffmpeg
|
||||
cp -a out/Default/xcode_links out/ffmpeg/
|
||||
MAC_SDK_ARG=$(sed -n 's|^\(mac_sdk_path = "//out/\)Default/|\1ffmpeg/|p' out/Default/args.gn)
|
||||
fi
|
||||
gn gen out/ffmpeg --args="import(\"//electron/build/args/ffmpeg.gn\") use_remoteexec=true use_siso=true $MAC_SDK_ARG $GN_EXTRA_ARGS"
|
||||
e build --target electron:electron_ffmpeg_zip -C ../../out/ffmpeg
|
||||
- name: Remove Clang problem matcher
|
||||
shell: bash
|
||||
@@ -271,12 +284,12 @@ runs:
|
||||
run: ./src/electron/script/actions/move-artifacts.sh
|
||||
- name: Upload Generated Artifacts ${{ inputs.step-suffix }}
|
||||
if: always() && !cancelled()
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
|
||||
with:
|
||||
name: generated_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./generated_artifacts_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
- name: Upload Src Artifacts ${{ inputs.step-suffix }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
|
||||
with:
|
||||
name: src_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./src_artifacts_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
|
||||
30
.github/actions/checkout/action.yml
vendored
30
.github/actions/checkout/action.yml
vendored
@@ -28,7 +28,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
node src/electron/script/generate-deps-hash.js
|
||||
DEPSHASH="v1-src-cache-$(cat src/electron/.depshash)"
|
||||
DEPSHASH="v2-src-cache-$(cat src/electron/.depshash)"
|
||||
echo "DEPSHASH=$DEPSHASH" >> $GITHUB_ENV
|
||||
echo "CACHE_FILE=$DEPSHASH.tar" >> $GITHUB_ENV
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
@@ -43,7 +43,7 @@ runs:
|
||||
curl --unix-socket /var/run/sas/sas.sock --fail "http://foo/$CACHE_FILE?platform=${{ inputs.target-platform }}&getAccountName=true" > sas-token
|
||||
- name: Save SAS Key
|
||||
if: ${{ inputs.generate-sas-token == 'true' }}
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: sas-token
|
||||
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
@@ -109,7 +109,7 @@ runs:
|
||||
echo "target_os=['$TARGET_OS']" >> ./.gclient
|
||||
fi
|
||||
|
||||
ELECTRON_USE_THREE_WAY_MERGE_FOR_PATCHES=1 e d gclient sync --with_branch_heads --with_tags -vv
|
||||
ELECTRON_DEPOT_TOOLS_WIN_TOOLCHAIN=0 DEPOT_TOOLS_WIN_TOOLCHAIN=0 ELECTRON_USE_THREE_WAY_MERGE_FOR_PATCHES=1 e d gclient sync --with_branch_heads --with_tags
|
||||
if [[ "${{ inputs.is-release }}" != "true" ]]; then
|
||||
# Re-export all the patches to check if there were changes.
|
||||
python3 src/electron/script/export_all_patches.py src/electron/patches/config.json
|
||||
@@ -187,21 +187,35 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Uncompressed src size: $(du -sh src | cut -f1 -d' ')"
|
||||
tar -cf $CACHE_FILE src
|
||||
# Named .tar but zstd-compressed; the sas-sidecar's filename allowlist
|
||||
# only permits .tar/.tgz so we keep the extension and decode on restore.
|
||||
tar -cf - src | zstd -T0 --long=30 -f -o $CACHE_FILE
|
||||
echo "Compressed src to $(du -sh $CACHE_FILE | cut -f1 -d' ')"
|
||||
cp ./$CACHE_FILE $CACHE_DRIVE/
|
||||
- name: Persist Src Cache
|
||||
if: ${{ steps.check-cache.outputs.cache_exists == 'false' && inputs.use-cache == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
final_cache_path=$CACHE_DRIVE/$CACHE_FILE
|
||||
# Upload to a run-unique temp name first so concurrent readers never
|
||||
# observe a partially-written file, and an interrupted copy can't leave
|
||||
# a truncated file at the final path. Orphaned temp files get swept by
|
||||
# the clean-orphaned-cache-uploads workflow.
|
||||
tmp_cache_path=$final_cache_path.upload-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}
|
||||
echo "Uploading to temp path: $tmp_cache_path"
|
||||
cp ./$CACHE_FILE $tmp_cache_path
|
||||
|
||||
echo "Using cache key: $DEPSHASH"
|
||||
echo "Checking path: $final_cache_path"
|
||||
if [ -f "$final_cache_path" ]; then
|
||||
echo "Cache already persisted at $final_cache_path by a concurrent run; discarding ours"
|
||||
rm -f $tmp_cache_path
|
||||
else
|
||||
mv -f $tmp_cache_path $final_cache_path
|
||||
echo "Cache key persisted in $final_cache_path"
|
||||
fi
|
||||
|
||||
if [ ! -f "$final_cache_path" ]; then
|
||||
echo "Cache key not found"
|
||||
exit 1
|
||||
else
|
||||
echo "Cache key persisted in $final_cache_path"
|
||||
fi
|
||||
- name: Wait for active SSH sessions
|
||||
shell: bash
|
||||
|
||||
38
.github/actions/cipd-install/action.yml
vendored
38
.github/actions/cipd-install/action.yml
vendored
@@ -22,30 +22,50 @@ runs:
|
||||
steps:
|
||||
- name: Delete wrong ${{ inputs.dependency }}
|
||||
shell: bash
|
||||
env:
|
||||
CIPD_ROOT_PREFIX: ${{ inputs.cipd-root-prefix-path }}
|
||||
INSTALLATION_DIR: ${{ inputs.installation-dir }}
|
||||
run : |
|
||||
rm -rf ${{ inputs.cipd-root-prefix-path }}${{ inputs.installation-dir }}
|
||||
rm -rf "${CIPD_ROOT_PREFIX}${INSTALLATION_DIR}"
|
||||
- name: Create ensure file for ${{ inputs.dependency }}
|
||||
if: ${{ inputs.dependency-version == '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE: ${{ inputs.package }}
|
||||
DEPS_FILE: ${{ inputs.deps-file }}
|
||||
INSTALLATION_DIR: ${{ inputs.installation-dir }}
|
||||
DEPENDENCY: ${{ inputs.dependency }}
|
||||
run: |
|
||||
echo '${{ inputs.package }}' `e d gclient getdep --deps-file=${{ inputs.deps-file }} -r '${{ inputs.installation-dir }}:${{ inputs.package }}'` > ${{ inputs.dependency }}_ensure_file
|
||||
cat ${{ inputs.dependency }}_ensure_file
|
||||
echo "$PACKAGE" $(e d gclient getdep --deps-file="$DEPS_FILE" -r "${INSTALLATION_DIR}:${PACKAGE}") > "${DEPENDENCY}_ensure_file"
|
||||
cat "${DEPENDENCY}_ensure_file"
|
||||
|
||||
- name: Create ensure file for ${{ inputs.dependency }} from dependency-version
|
||||
if: ${{ inputs.dependency-version != '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE: ${{ inputs.package }}
|
||||
DEPENDENCY_VERSION: ${{ inputs.dependency-version }}
|
||||
DEPENDENCY: ${{ inputs.dependency }}
|
||||
run: |
|
||||
echo '${{ inputs.package }} ${{ inputs.dependency-version }}' > ${{ inputs.dependency }}_ensure_file
|
||||
cat ${{ inputs.dependency }}_ensure_file
|
||||
echo "$PACKAGE $DEPENDENCY_VERSION" > "${DEPENDENCY}_ensure_file"
|
||||
cat "${DEPENDENCY}_ensure_file"
|
||||
- name: CIPD installation of ${{ inputs.dependency }} (macOS)
|
||||
if: ${{ inputs.target-platform != 'win' }}
|
||||
shell: bash
|
||||
env:
|
||||
CIPD_ROOT_PREFIX: ${{ inputs.cipd-root-prefix-path }}
|
||||
INSTALLATION_DIR: ${{ inputs.installation-dir }}
|
||||
DEPENDENCY: ${{ inputs.dependency }}
|
||||
run: |
|
||||
echo "ensuring ${{ inputs.dependency }}"
|
||||
e d cipd ensure --root ${{ inputs.cipd-root-prefix-path }}${{ inputs.installation-dir }} -ensure-file ${{ inputs.dependency }}_ensure_file
|
||||
echo "ensuring $DEPENDENCY"
|
||||
e d cipd ensure --root "${CIPD_ROOT_PREFIX}${INSTALLATION_DIR}" -ensure-file "${DEPENDENCY}_ensure_file"
|
||||
- name: CIPD installation of ${{ inputs.dependency }} (Windows)
|
||||
if: ${{ inputs.target-platform == 'win' }}
|
||||
shell: powershell
|
||||
env:
|
||||
CIPD_ROOT_PREFIX: ${{ inputs.cipd-root-prefix-path }}
|
||||
INSTALLATION_DIR: ${{ inputs.installation-dir }}
|
||||
DEPENDENCY: ${{ inputs.dependency }}
|
||||
run: |
|
||||
echo "ensuring ${{ inputs.dependency }} on Windows"
|
||||
e d cipd ensure --root ${{ inputs.cipd-root-prefix-path }}${{ inputs.installation-dir }} -ensure-file ${{ inputs.dependency }}_ensure_file
|
||||
echo "ensuring $env:DEPENDENCY on Windows"
|
||||
e d cipd ensure --root "$env:CIPD_ROOT_PREFIX$env:INSTALLATION_DIR" -ensure-file "$($env:DEPENDENCY)_ensure_file"
|
||||
|
||||
1
.github/actions/fix-sync/action.yml
vendored
1
.github/actions/fix-sync/action.yml
vendored
@@ -27,6 +27,7 @@ runs:
|
||||
python3 src/tools/clang/scripts/update.py
|
||||
# Refs https://chromium-review.googlesource.com/c/chromium/src/+/6667681
|
||||
python3 src/tools/clang/scripts/update.py --package objdump
|
||||
python3 src/tools/clang/scripts/update.py --package clang-tidy
|
||||
- name: Fix esbuild
|
||||
if: ${{ inputs.target-platform != 'linux' }}
|
||||
uses: ./src/electron/.github/actions/cipd-install
|
||||
|
||||
@@ -7,7 +7,7 @@ runs:
|
||||
shell: bash
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(node src/electron/script/yarn.js config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
2
.github/actions/restore-cache-aks/action.yml
vendored
2
.github/actions/restore-cache-aks/action.yml
vendored
@@ -31,7 +31,7 @@ runs:
|
||||
fi
|
||||
|
||||
mkdir temp-cache
|
||||
tar -xf $cache_path -C temp-cache
|
||||
zstd -d --long=30 -c $cache_path | tar -xf - -C temp-cache
|
||||
echo "Unzipped cache is $(du -sh temp-cache/src | cut -f1)"
|
||||
|
||||
if [ -d "temp-cache/src" ]; then
|
||||
|
||||
33
.github/actions/restore-cache-azcopy/action.yml
vendored
33
.github/actions/restore-cache-azcopy/action.yml
vendored
@@ -8,14 +8,14 @@ runs:
|
||||
steps:
|
||||
- name: Obtain SAS Key
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: sas-token
|
||||
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-1
|
||||
enableCrossOsArchive: true
|
||||
- name: Obtain SAS Key
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: sas-token
|
||||
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
@@ -24,7 +24,7 @@ runs:
|
||||
# The cache will always exist here as a result of the checkout job
|
||||
# Either it was uploaded to Azure in the checkout job for this commit
|
||||
# or it was uploaded in the checkout job for a previous commit.
|
||||
uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -61,9 +61,9 @@ runs:
|
||||
echo "Cache is empty - exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
mkdir temp-cache
|
||||
tar -xf $DEPSHASH.tar -C temp-cache
|
||||
zstd -d --long=30 -c $DEPSHASH.tar | tar -xf - -C temp-cache
|
||||
echo "Unzipped cache is $(du -sh temp-cache/src | cut -f1)"
|
||||
|
||||
if [ -d "temp-cache/src" ]; then
|
||||
@@ -85,23 +85,21 @@ runs:
|
||||
|
||||
- name: Unzip and Ensure Src Cache (Windows)
|
||||
if: ${{ inputs.target-platform == 'win' }}
|
||||
shell: powershell
|
||||
shell: bash
|
||||
run: |
|
||||
$src_cache = "$env:DEPSHASH.tar"
|
||||
$cache_size = $(Get-Item $src_cache).length
|
||||
Write-Host "Downloaded cache is $cache_size"
|
||||
if ($cache_size -eq 0) {
|
||||
Write-Host "Cache is empty - exiting"
|
||||
echo "Downloaded cache is $(du -sh $DEPSHASH.tar | cut -f1)"
|
||||
if [ `du $DEPSHASH.tar | cut -f1` = "0" ]; then
|
||||
echo "Cache is empty - exiting"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
$TEMP_DIR=New-Item -ItemType Directory -Path temp-cache
|
||||
$TEMP_DIR_PATH = $TEMP_DIR.FullName
|
||||
C:\ProgramData\Chocolatey\bin\7z.exe -y -snld20 x $src_cache -o"$TEMP_DIR_PATH"
|
||||
mkdir temp-cache
|
||||
zstd -d --long=30 -c $DEPSHASH.tar | tar -xf - -C temp-cache
|
||||
rm -f $DEPSHASH.tar
|
||||
|
||||
- name: Move Src Cache (Windows)
|
||||
if: ${{ inputs.target-platform == 'win' }}
|
||||
uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -112,9 +110,6 @@ runs:
|
||||
Write-Host "Relocating Cache"
|
||||
Remove-Item -Recurse -Force src
|
||||
Move-Item temp-cache\src src
|
||||
|
||||
Write-Host "Deleting zip file"
|
||||
Remove-Item -Force $src_cache
|
||||
}
|
||||
if (-Not (Test-Path "src\third_party\blink")) {
|
||||
Write-Host "Cache was not correctly restored - exiting"
|
||||
|
||||
@@ -7,7 +7,7 @@ runs:
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -z "${{ env.CHROMIUM_GIT_COOKIE }}" ]]; then
|
||||
if [[ -z "$CHROMIUM_GIT_COOKIE" ]]; then
|
||||
echo "CHROMIUM_GIT_COOKIE is not set - cannot authenticate."
|
||||
exit 0
|
||||
fi
|
||||
@@ -18,9 +18,7 @@ runs:
|
||||
|
||||
git config --global http.cookiefile ~/.gitcookies
|
||||
|
||||
tr , \\t <<\__END__ >>~/.gitcookies
|
||||
${{ env.CHROMIUM_GIT_COOKIE }}
|
||||
__END__
|
||||
echo "$CHROMIUM_GIT_COOKIE" | tr , \\t >>~/.gitcookies
|
||||
eval 'set -o history' 2>/dev/null || unsetopt HIST_IGNORE_SPACE 2>/dev/null
|
||||
|
||||
RESPONSE=$(curl -s -b ~/.gitcookies https://chromium-review.googlesource.com/a/accounts/self)
|
||||
@@ -42,7 +40,7 @@ runs:
|
||||
)
|
||||
|
||||
git config --global http.cookiefile "%USERPROFILE%\.gitcookies"
|
||||
powershell -noprofile -nologo -command Write-Output "${{ env.CHROMIUM_GIT_COOKIE_WINDOWS_STRING }}" >>"%USERPROFILE%\.gitcookies"
|
||||
powershell -noprofile -nologo -command Write-Output $env:CHROMIUM_GIT_COOKIE_WINDOWS_STRING >>"%USERPROFILE%\.gitcookies"
|
||||
|
||||
curl -s -b "%USERPROFILE%\.gitcookies" https://chromium-review.googlesource.com/a/accounts/self > response.txt
|
||||
|
||||
|
||||
20
.github/workflows/archaeologist-dig.yml
vendored
20
.github/workflows/archaeologist-dig.yml
vendored
@@ -21,17 +21,21 @@ jobs:
|
||||
with:
|
||||
node-version: 24.12.x
|
||||
- name: Setting Up Dig Site
|
||||
env:
|
||||
CLONE_URL: ${{ github.event.pull_request.head.repo.clone_url }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
run: |
|
||||
echo "remote: ${{ github.event.pull_request.head.repo.clone_url }}"
|
||||
echo "sha ${{ github.event.pull_request.head.sha }}"
|
||||
echo "base ref ${{ github.event.pull_request.base.ref }}"
|
||||
git clone https://github.com/electron/electron.git electron
|
||||
echo "remote: $CLONE_URL"
|
||||
echo "sha $HEAD_SHA"
|
||||
echo "base ref $BASE_REF"
|
||||
git clone https://github.com/electron/electron.git electron
|
||||
cd electron
|
||||
mkdir -p artifacts
|
||||
git remote add fork ${{ github.event.pull_request.head.repo.clone_url }} && git fetch fork
|
||||
git checkout ${{ github.event.pull_request.head.sha }}
|
||||
git merge-base origin/${{ github.event.pull_request.base.ref }} HEAD > .dig-old
|
||||
echo ${{ github.event.pull_request.head.sha }} > .dig-new
|
||||
git remote add fork "$CLONE_URL" && git fetch fork
|
||||
git checkout "$HEAD_SHA"
|
||||
git merge-base "origin/$BASE_REF" HEAD > .dig-old
|
||||
echo "$HEAD_SHA" > .dig-new
|
||||
cp .dig-old artifacts
|
||||
|
||||
- name: Generating Types for SHA in .dig-new
|
||||
|
||||
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
@@ -431,3 +431,30 @@ jobs:
|
||||
- name: GitHub Actions Jobs Done
|
||||
run: |
|
||||
echo "All GitHub Actions Jobs are done"
|
||||
|
||||
check-signed-commits:
|
||||
name: Check signed commits in green PR
|
||||
needs: gha-done
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
|
||||
with:
|
||||
comment: |
|
||||
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
|
||||
for all incoming PRs. To get your PR merged, please sign those commits
|
||||
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
|
||||
(`git push --force-with-lease`)
|
||||
|
||||
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
|
||||
|
||||
- name: Remove needs-signed-commits label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh pr edit $PR_URL --remove-label needs-signed-commits
|
||||
|
||||
32
.github/workflows/clean-orphaned-cache-uploads.yml
vendored
Normal file
32
.github/workflows/clean-orphaned-cache-uploads.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Clean Orphaned Cache Uploads
|
||||
|
||||
# Description:
|
||||
# Sweeps orphaned in-flight upload temp files left on the src-cache volumes
|
||||
# by checkout/action.yml when its cp-to-share step dies before the rename.
|
||||
# A successful upload finishes in minutes, so anything older than 4h is dead.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
clean-orphaned-uploads:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
container:
|
||||
image: ghcr.io/electron/build:bc2f48b2415a670de18d13605b1cf0eb5fdbaae1
|
||||
options: --user root
|
||||
volumes:
|
||||
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
|
||||
- /mnt/win-cache:/mnt/win-cache
|
||||
steps:
|
||||
- name: Remove Orphaned Upload Temp Files
|
||||
shell: bash
|
||||
run: |
|
||||
find /mnt/cross-instance-cache -maxdepth 1 -type f -name '*.tar.upload-*' -mmin +240 -print -delete
|
||||
find /mnt/win-cache -maxdepth 1 -type f -name '*.tar.upload-*' -mmin +240 -print -delete
|
||||
3
.github/workflows/issue-labeled.yml
vendored
3
.github/workflows/issue-labeled.yml
vendored
@@ -61,9 +61,10 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: electron/electron
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
COMMENT_COUNT=$(gh issue view ${{ github.event.issue.number }} --comments --json comments | jq '[ .comments[] | select(.author.login == "electron-issue-triage" or .authorAssociation == "OWNER" or .authorAssociation == "MEMBER") | select(.body | startswith("<!-- blocked/need-repro -->")) ] | length')
|
||||
COMMENT_COUNT=$(gh issue view "$ISSUE_NUMBER" --comments --json comments | jq '[ .comments[] | select(.author.login == "electron-issue-triage" or .authorAssociation == "OWNER" or .authorAssociation == "MEMBER") | select(.body | startswith("<!-- blocked/need-repro -->")) ] | length')
|
||||
if [[ $COMMENT_COUNT -eq 0 ]]; then
|
||||
echo "SHOULD_COMMENT=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
4
.github/workflows/issue-unlabeled.yml
vendored
4
.github/workflows/issue-unlabeled.yml
vendored
@@ -16,9 +16,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check for any blocked labels
|
||||
id: check-for-blocked-labels
|
||||
env:
|
||||
LABELS_JSON: ${{ toJSON(github.event.issue.labels.*.name) }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
BLOCKED_LABEL_COUNT=$(echo '${{ toJSON(github.event.issue.labels.*.name) }}' | jq '[ .[] | select(startswith("blocked/")) ] | length')
|
||||
BLOCKED_LABEL_COUNT=$(echo "$LABELS_JSON" | jq '[ .[] | select(startswith("blocked/")) ] | length')
|
||||
if [[ $BLOCKED_LABEL_COUNT -eq 0 ]]; then
|
||||
echo "NOT_BLOCKED=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
@@ -10,6 +10,10 @@ on:
|
||||
- '.yarn/**'
|
||||
- '.yarnrc.yml'
|
||||
|
||||
# SECURITY: This workflow uses pull_request_target and has access to secrets.
|
||||
# Do NOT checkout or run code from the PR head. All code execution must use
|
||||
# the base branch only. Adding a ref to PR head would expose secrets to
|
||||
# untrusted code.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
@@ -45,5 +49,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
run: |
|
||||
printf "<!-- disallowed-non-maintainer-change -->\n\nHello @${{ github.event.pull_request.user.login }}! It looks like this pull request touches one of our dependency or CI files, and per [our contribution policy](https://github.com/electron/electron/blob/main/CONTRIBUTING.md#dependencies-upgrades-policy) we do not accept these types of changes in PRs." | gh pr review $PR_URL -r --body-file=-
|
||||
printf "<!-- disallowed-non-maintainer-change -->\n\nHello @${PR_AUTHOR}! It looks like this pull request touches one of our dependency or CI files, and per [our contribution policy](https://github.com/electron/electron/blob/main/CONTRIBUTING.md#dependencies-upgrades-policy) we do not accept these types of changes in PRs." | gh pr review $PR_URL -r --body-file=-
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Generate DEPS Hash
|
||||
run: |
|
||||
node src/electron/script/generate-deps-hash.js
|
||||
DEPSHASH=v1-src-cache-$(cat src/electron/.depshash)
|
||||
DEPSHASH=v2-src-cache-$(cat src/electron/.depshash)
|
||||
echo "DEPSHASH=$DEPSHASH" >> $GITHUB_ENV
|
||||
echo "CACHE_PATH=$DEPSHASH.tar" >> $GITHUB_ENV
|
||||
- name: Restore src cache via AKS
|
||||
|
||||
12
.github/workflows/pipeline-electron-lint.yml
vendored
12
.github/workflows/pipeline-electron-lint.yml
vendored
@@ -46,7 +46,11 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
chromium_revision="$(grep -A1 chromium_version src/electron/DEPS | tr -d '\n' | cut -d\' -f4)"
|
||||
gn_version="$(curl -sL -b ~/.gitcookies "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/DEPS?format=TEXT" | base64 -d | grep gn_version | head -n1 | cut -d\' -f4)"
|
||||
if [[ ! "$chromium_revision" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
||||
echo "::error::Invalid chromium_revision: $chromium_revision"
|
||||
exit 1
|
||||
fi
|
||||
gn_version="$(curl -sL "https://raw.githubusercontent.com/chromium/chromium/refs/tags/${chromium_revision}/DEPS" | grep gn_version | head -n1 | cut -d\' -f4)"
|
||||
|
||||
cipd ensure -ensure-file - -root . <<-CIPD
|
||||
\$ServiceURL https://chrome-infra-packages.appspot.com/
|
||||
@@ -60,9 +64,13 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
chromium_revision="$(grep -A1 chromium_version src/electron/DEPS | tr -d '\n' | cut -d\' -f4)"
|
||||
if [[ ! "$chromium_revision" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
||||
echo "::error::Invalid chromium_revision: $chromium_revision"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p src/buildtools
|
||||
curl -sL -b ~/.gitcookies "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/buildtools/DEPS?format=TEXT" | base64 -d > src/buildtools/DEPS
|
||||
curl -sL "https://raw.githubusercontent.com/chromium/chromium/refs/tags/${chromium_revision}/buildtools/DEPS" > src/buildtools/DEPS
|
||||
|
||||
gclient sync --spec="solutions=[{'name':'src/buildtools','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':True},'managed':False}]"
|
||||
- name: Add problem matchers
|
||||
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
- name: Generate DEPS Hash
|
||||
run: |
|
||||
node src/electron/script/generate-deps-hash.js
|
||||
DEPSHASH=v1-src-cache-$(cat src/electron/.depshash)
|
||||
DEPSHASH=v2-src-cache-$(cat src/electron/.depshash)
|
||||
echo "DEPSHASH=$DEPSHASH" >> $GITHUB_ENV
|
||||
echo "CACHE_PATH=$DEPSHASH.tar" >> $GITHUB_ENV
|
||||
- name: Restore src cache via AZCopy
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
- name: Generate DEPS Hash
|
||||
run: |
|
||||
node src/electron/script/generate-deps-hash.js
|
||||
DEPSHASH=v1-src-cache-$(cat src/electron/.depshash)
|
||||
DEPSHASH=v2-src-cache-$(cat src/electron/.depshash)
|
||||
echo "DEPSHASH=$DEPSHASH" >> $GITHUB_ENV
|
||||
echo "CACHE_PATH=$DEPSHASH.tar" >> $GITHUB_ENV
|
||||
- name: Restore src cache via AZCopy
|
||||
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
- name: Generate DEPS Hash
|
||||
run: |
|
||||
node src/electron/script/generate-deps-hash.js
|
||||
DEPSHASH=v1-src-cache-$(cat src/electron/.depshash)
|
||||
DEPSHASH=v2-src-cache-$(cat src/electron/.depshash)
|
||||
echo "DEPSHASH=$DEPSHASH" >> $GITHUB_ENV
|
||||
echo "CACHE_PATH=$DEPSHASH.tar" >> $GITHUB_ENV
|
||||
- name: Restore src cache via AZCopy
|
||||
|
||||
@@ -43,6 +43,8 @@ env:
|
||||
ELECTRON_OUT_DIR: Default
|
||||
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
|
||||
ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }}
|
||||
# @sentry/cli is only needed by release upload-symbols.py; skip the ~17MB CDN download on test jobs
|
||||
SENTRYCLI_SKIP_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -289,7 +291,7 @@ jobs:
|
||||
if: always() && !cancelled()
|
||||
- name: Upload Test Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
|
||||
with:
|
||||
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}
|
||||
path: src/electron/spec/artifacts
|
||||
|
||||
@@ -36,6 +36,8 @@ env:
|
||||
CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
|
||||
ELECTRON_OUT_DIR: Default
|
||||
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
|
||||
# @sentry/cli is only needed by release upload-symbols.py; skip the ~17MB CDN download on test jobs
|
||||
SENTRYCLI_SKIP_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
node-tests:
|
||||
|
||||
4
.github/workflows/pull-request-labeled.yml
vendored
4
.github/workflows/pull-request-labeled.yml
vendored
@@ -4,6 +4,10 @@ on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
# SECURITY: This workflow uses pull_request_target and has access to secrets.
|
||||
# Do NOT checkout or run code from the PR head. All code execution must use
|
||||
# the base branch only. Adding a ref to PR head would expose secrets to
|
||||
# untrusted code.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
|
||||
39
.github/workflows/pull-request-opened-synchronized.yml
vendored
Normal file
39
.github/workflows/pull-request-opened-synchronized.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Pull Request Opened/Synchronized
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# SECURITY: This workflow uses pull_request_target and has access to secrets.
|
||||
# Do NOT checkout or run code from the PR head. All code execution must use
|
||||
# the base branch only. Adding a ref to PR head would expose secrets to
|
||||
# untrusted code.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-signed-commits:
|
||||
name: Check signed commits in PR
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
|
||||
with:
|
||||
comment: |
|
||||
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
|
||||
for all incoming PRs. To get your PR merged, please sign those commits
|
||||
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
|
||||
(`git push --force-with-lease`)
|
||||
|
||||
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
|
||||
|
||||
- name: Add needs-signed-commits label
|
||||
if: ${{ failure() }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh pr edit $PR_URL --add-label needs-signed-commits
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ spec/.hash
|
||||
|
||||
# Generated native addon files
|
||||
/spec/fixtures/native-addon/echo/build/
|
||||
/spec/fixtures/native-addon/dialog-helper/build/
|
||||
|
||||
# If someone runs tsc this is where stuff will end up
|
||||
ts-gen
|
||||
|
||||
2
DEPS
2
DEPS
@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
|
||||
|
||||
vars = {
|
||||
'chromium_version':
|
||||
'146.0.7680.153',
|
||||
'146.0.7680.179',
|
||||
'node_version':
|
||||
'v24.14.0',
|
||||
'nan_version':
|
||||
|
||||
@@ -51,9 +51,6 @@ is_cfi = false
|
||||
use_qt5 = false
|
||||
use_qt6 = false
|
||||
|
||||
# Disables the builtins PGO for V8
|
||||
v8_builtins_profiling_log_file = ""
|
||||
|
||||
# https://chromium.googlesource.com/chromium/src/+/main/docs/dangling_ptr.md
|
||||
# TODO(vertedinde): hunt down dangling pointers on Linux
|
||||
enable_dangling_raw_ptr_checks = false
|
||||
|
||||
@@ -44,8 +44,8 @@ See [`Menu`](menu.md) for examples.
|
||||
menu items.
|
||||
* `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
|
||||
* `sharingItem` [SharingItem](structures/sharing-item.md) (optional) _macOS_ - The item to share when the `role` is `shareMenu`.
|
||||
* `submenu` ([MenuItemConstructorOptions](#new-menuitemoptions)[] | [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`.
|
||||
@@ -89,7 +89,7 @@ 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` [BaseWindow](browser-window.md)
|
||||
* `focusedWindow` [BaseWindow](base-window.md)
|
||||
* `focusedWebContents` [WebContents](web-contents.md)
|
||||
|
||||
#### `menuItem.submenu`
|
||||
@@ -110,11 +110,11 @@ A `string` (optional) indicating the item's role, if set. Can be `undo`, `redo`,
|
||||
|
||||
#### `menuItem.accelerator`
|
||||
|
||||
An `Accelerator | null` indicating the item's accelerator, if set.
|
||||
An [`Accelerator | null`](../tutorial/keyboard-shortcuts.md#accelerators) 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.
|
||||
An [`Accelerator | null`](../tutorial/keyboard-shortcuts.md#accelerators) 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`.
|
||||
@@ -170,7 +170,7 @@ This property can be dynamically changed.
|
||||
|
||||
#### `menuItem.sharingItem` _macOS_
|
||||
|
||||
A `SharingItem` indicating the item to share when the `role` is `shareMenu`.
|
||||
A [`SharingItem`](structures/sharing-item.md) indicating the item to share when the `role` is `shareMenu`.
|
||||
|
||||
This property can be dynamically changed.
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ for more information on macOS' native actions.
|
||||
|
||||
#### `Menu.buildFromTemplate(template)`
|
||||
|
||||
- `template` (MenuItemConstructorOptions | [MenuItem](menu-item.md))[]
|
||||
- `template` ([MenuItemConstructorOptions](menu-item.md#new-menuitemoptions) | [MenuItem](menu-item.md))[]
|
||||
|
||||
Returns [`Menu`](menu.md)
|
||||
|
||||
@@ -162,7 +162,7 @@ Emitted when a popup is closed either manually or with `menu.closePopup()`.
|
||||
|
||||
#### `menu.items`
|
||||
|
||||
A `MenuItem[]` array containing the menu's items.
|
||||
A [`MenuItem[]`](menu-item.md) array containing the menu's items.
|
||||
|
||||
Each `Menu` consists of multiple [`MenuItem`](menu-item.md) instances and each `MenuItem`
|
||||
can nest a `Menu` into its `submenu` property.
|
||||
|
||||
@@ -84,3 +84,7 @@ Currently, Windows high contrast is the only system setting that triggers forced
|
||||
### `nativeTheme.prefersReducedTransparency` _Readonly_
|
||||
|
||||
A `boolean` that indicates whether the user has chosen via system accessibility settings to reduce transparency at the OS level.
|
||||
|
||||
### `nativeTheme.shouldDifferentiateWithoutColor` _macOS_ _Readonly_
|
||||
|
||||
A `boolean` that indicates whether the user prefers UI that differentiates items using something other than color alone (e.g. shapes or labels). This maps to [NSWorkspace.accessibilityDisplayShouldDifferentiateWithoutColor](https://developer.apple.com/documentation/appkit/nsworkspace/accessibilitydisplayshoulddifferentiatewithoutcolor).
|
||||
|
||||
@@ -42,11 +42,15 @@ Returns `boolean` - Whether or not desktop notifications are supported on the cu
|
||||
* `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'.
|
||||
* `urgency` string (optional) _Linux_ _Windows_ - 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.
|
||||
* `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.
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, `urgency` type 'critical' sorts the notification higher in Action Center (above default priority notifications), but does not prevent auto-dismissal. To prevent auto-dismissal, you should also set
|
||||
> `timeoutType` to 'never'.
|
||||
|
||||
### Instance Events
|
||||
|
||||
Objects created with `new Notification` emit the following events:
|
||||
|
||||
@@ -56,6 +56,15 @@ app.whenReady().then(() => {
|
||||
})
|
||||
```
|
||||
|
||||
## Protocol names
|
||||
|
||||
[RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) defines what a valid
|
||||
protocol name is:
|
||||
|
||||
> Scheme names consist of a sequence of characters beginning with a letter and followed
|
||||
> by any combination of letters, digits, plus ("+"), period ("."), or hyphen ("-").
|
||||
> Although schemes are case-insensitive, the canonical form is lowercase […].
|
||||
|
||||
## Methods
|
||||
|
||||
The `protocol` module has the following methods:
|
||||
|
||||
@@ -11,3 +11,5 @@
|
||||
* `stream` boolean (optional) - Default false.
|
||||
* `codeCache` boolean (optional) - Enable V8 code cache for the scheme, only
|
||||
works when `standard` is also set to true. Default false.
|
||||
* `allowExtensions` boolean (optional) - Allow Chrome extensions to be used
|
||||
on pages served over this protocol. Default false.
|
||||
|
||||
@@ -79,7 +79,7 @@ $ ../../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`).
|
||||
Note that `git-import-patches` will mark the commit that was `HEAD` when it was run as `refs/patches/upstream-head` (and a checkout-specific `refs/patches/upstream-head-<hash>` so that gclient worktrees sharing a `.git/refs` directory don't clobber each other). 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
|
||||
|
||||
|
||||
@@ -17,11 +17,6 @@ export type WindowOpenArgs = {
|
||||
features: string,
|
||||
}
|
||||
|
||||
const frameNamesToWindow = new Map<string, WebContents>();
|
||||
const registerFrameNameToGuestWindow = (name: string, webContents: WebContents) => frameNamesToWindow.set(name, webContents);
|
||||
const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
|
||||
const getGuestWebContentsByFrameName = (name: string) => frameNamesToWindow.get(name);
|
||||
|
||||
/**
|
||||
* `openGuestWindow` is called to create and setup event handling for the new
|
||||
* window.
|
||||
@@ -47,20 +42,6 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
...overrideBrowserWindowOptions
|
||||
};
|
||||
|
||||
// To spec, subsequent window.open calls with the same frame name (`target` in
|
||||
// spec parlance) will reuse the previous window.
|
||||
// https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
|
||||
const existingWebContents = getGuestWebContentsByFrameName(frameName);
|
||||
if (existingWebContents) {
|
||||
if (existingWebContents.isDestroyed()) {
|
||||
// FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
|
||||
unregisterFrameName(frameName);
|
||||
} else {
|
||||
existingWebContents.loadURL(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (createWindow) {
|
||||
const webContents = createWindow({
|
||||
webContents: guest,
|
||||
@@ -72,7 +53,7 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
throw new Error('Invalid webContents. Created window should be connected to webContents passed with options object.');
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, guest, outlivesOpener });
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -96,7 +77,7 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
});
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest: window.webContents, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, guest: window.webContents, outlivesOpener });
|
||||
|
||||
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
|
||||
}
|
||||
@@ -107,10 +88,9 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
* too is the guest destroyed; this is Electron convention and isn't based in
|
||||
* browser behavior.
|
||||
*/
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, outlivesOpener }: {
|
||||
embedder: WebContents,
|
||||
guest: WebContents,
|
||||
frameName: string,
|
||||
outlivesOpener: boolean
|
||||
}) {
|
||||
const closedByEmbedder = function () {
|
||||
@@ -128,13 +108,6 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outl
|
||||
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
|
||||
}
|
||||
guest.once('destroyed', closedByUser);
|
||||
|
||||
if (frameName) {
|
||||
registerFrameNameToGuestWindow(frameName, guest);
|
||||
guest.once('destroyed', function () {
|
||||
unregisterFrameName(frameName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Security options that child windows will always inherit from parent windows
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@electron/typescript-definitions": "^9.1.5",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
"@primer/octicons": "^10.0.0",
|
||||
"@sentry/cli": "1.72.0",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node": "^24.9.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
@@ -155,6 +156,9 @@
|
||||
"spec/fixtures/native-addon/*"
|
||||
],
|
||||
"dependenciesMeta": {
|
||||
"@sentry/cli": {
|
||||
"built": true
|
||||
},
|
||||
"abstract-socket": {
|
||||
"built": true
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ build_disable_thin_lto_mac.patch
|
||||
feat_corner_smoothing_css_rule_and_blink_painting.patch
|
||||
build_add_public_config_simdutf_config.patch
|
||||
fix_multiple_scopedpumpmessagesinprivatemodes_instances.patch
|
||||
revert_code_health_clean_up_stale_macwebcontentsocclusion.patch
|
||||
fix_handle_embedder_windows_shown_after_webcontentsviewcocoa_attach.patch
|
||||
feat_add_signals_when_embedder_cleanup_callbacks_run_for.patch
|
||||
feat_separate_content_settings_callback_for_sync_and_async_clipboard.patch
|
||||
fix_win32_synchronous_spellcheck.patch
|
||||
@@ -147,3 +147,6 @@ fix_update_dbus_signal_signature_for_xdg_globalshortcuts_portal.patch
|
||||
fix_set_correct_app_id_on_linux.patch
|
||||
fix_pass_trigger_for_global_shortcuts_on_wayland.patch
|
||||
feat_plumb_node_integration_in_worker_through_workersettings.patch
|
||||
fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch
|
||||
extensions_return_early_from_urlpattern_isvalidscheme.patch
|
||||
feat_allow_enabling_extensions_on_custom_protocols.patch
|
||||
|
||||
@@ -43,7 +43,7 @@ index 21d5ab99800c0830cc31ec4ebb24e3f05cd904d8..3f8f514519d6e4a0abe3690f5df35de8
|
||||
// When the enterprise policy is not set, use finch/feature flag choice.
|
||||
return base::FeatureList::IsEnabled(chrome_pdf::features::kPdfXfaSupport);
|
||||
diff --git a/chrome/browser/pdf/pdf_extension_util.cc b/chrome/browser/pdf/pdf_extension_util.cc
|
||||
index 83bc44f0c1928b9023efa54bfb57bed69d77484a..9c79f96931a0b2a05d98191ea8eb31a3a01818fc 100644
|
||||
index 24ca200ac662028d45180b21c3d79f2a4b96636e..b35025f7a06cae964858452c8f9e96655e34c47a 100644
|
||||
--- a/chrome/browser/pdf/pdf_extension_util.cc
|
||||
+++ b/chrome/browser/pdf/pdf_extension_util.cc
|
||||
@@ -259,10 +259,13 @@ bool IsPrintingEnabled(content::BrowserContext* context) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Niklas Wenzel <dev@nikwen.de>
|
||||
Date: Tue, 31 Mar 2026 00:11:27 +0200
|
||||
Subject: [Extensions] Return early from URLPattern::IsValidScheme()
|
||||
|
||||
|scheme| will match at most one entry in |kValidSchemes|. No need to
|
||||
iterate through the remaining ones.
|
||||
|
||||
Change-Id: I1f37383faccaddc775faabb797aea2851d93382f
|
||||
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7639311
|
||||
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
|
||||
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
|
||||
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
|
||||
Cr-Commit-Position: refs/heads/main@{#1594934}
|
||||
|
||||
diff --git a/extensions/common/url_pattern.cc b/extensions/common/url_pattern.cc
|
||||
index 4054af728030306c5473f9a47e580595596768a0..d4328ca22fdeefd3dca88bfe959dfb849705b109 100644
|
||||
--- a/extensions/common/url_pattern.cc
|
||||
+++ b/extensions/common/url_pattern.cc
|
||||
@@ -396,8 +396,8 @@ bool URLPattern::IsValidScheme(std::string_view scheme) const {
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < std::size(kValidSchemes); ++i) {
|
||||
- if (scheme == kValidSchemes[i] && (valid_schemes_ & kValidSchemeMasks[i])) {
|
||||
- return true;
|
||||
+ if (scheme == kValidSchemes[i]) {
|
||||
+ return valid_schemes_ & kValidSchemeMasks[i];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Niklas Wenzel <dev@nikwen.de>
|
||||
Date: Wed, 25 Feb 2026 16:24:03 +0100
|
||||
Subject: feat: allow enabling extensions on custom protocols
|
||||
|
||||
This allows us to use Chrome extensions on custom protocols.
|
||||
|
||||
The patch can't really be upstreamed, unfortunately, because there are
|
||||
other URLPattern functions that we don't patch that Chrome needs.
|
||||
|
||||
Patching those properly would require replacing the bitmask logic in
|
||||
URLPattern with a more flexible solution. This would be a larger effort
|
||||
and Chromium might reject it for performance reasons.
|
||||
|
||||
See: https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/url_pattern.h;l=53-74;drc=50dbcddad2f8e36ddfcec21d4551f389df425c37
|
||||
|
||||
This patch makes it work in the context of Electron.
|
||||
|
||||
diff --git a/extensions/browser/api/content_settings/content_settings_helpers.cc b/extensions/browser/api/content_settings/content_settings_helpers.cc
|
||||
index 34fa528a82f03891c89b3bb95bc9d2a135ee5f36..f88041554b828215a32dbb4aadcc73df40e6d8c2 100644
|
||||
--- a/extensions/browser/api/content_settings/content_settings_helpers.cc
|
||||
+++ b/extensions/browser/api/content_settings/content_settings_helpers.cc
|
||||
@@ -37,7 +37,7 @@ ContentSettingsPattern ParseExtensionPattern(const std::string& pattern_str,
|
||||
std::string* error) {
|
||||
const int kAllowedSchemes =
|
||||
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS |
|
||||
- URLPattern::SCHEME_FILE;
|
||||
+ URLPattern::SCHEME_FILE | URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS;
|
||||
URLPattern url_pattern(kAllowedSchemes);
|
||||
URLPattern::ParseResult result = url_pattern.Parse(pattern_str);
|
||||
if (result != URLPattern::ParseResult::kSuccess) {
|
||||
diff --git a/extensions/browser/api/web_request/extension_web_request_event_router.h b/extensions/browser/api/web_request/extension_web_request_event_router.h
|
||||
index 3de9285a548c1812783e90e76417b060dea612af..fad75693a26a139695f822e8b7567b0d38bb53cc 100644
|
||||
--- a/extensions/browser/api/web_request/extension_web_request_event_router.h
|
||||
+++ b/extensions/browser/api/web_request/extension_web_request_event_router.h
|
||||
@@ -52,7 +52,8 @@ inline constexpr int kWebRequestFilterValidSchemes =
|
||||
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS |
|
||||
URLPattern::SCHEME_FTP | URLPattern::SCHEME_FILE |
|
||||
URLPattern::SCHEME_EXTENSION | URLPattern::SCHEME_WS |
|
||||
- URLPattern::SCHEME_WSS | URLPattern::SCHEME_UUID_IN_PACKAGE;
|
||||
+ URLPattern::SCHEME_WSS | URLPattern::SCHEME_UUID_IN_PACKAGE |
|
||||
+ URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS;
|
||||
|
||||
class WebRequestEventRouter : public KeyedService {
|
||||
public:
|
||||
diff --git a/extensions/common/extension.cc b/extensions/common/extension.cc
|
||||
index 25f6860b1db1fecba457c06ecff9263efa4a6a8a..bc7e9834d2ad3f7abd8a58ee49f6a7f447915754 100644
|
||||
--- a/extensions/common/extension.cc
|
||||
+++ b/extensions/common/extension.cc
|
||||
@@ -219,7 +219,8 @@ const int Extension::kValidHostPermissionSchemes =
|
||||
URLPattern::SCHEME_CHROMEUI | URLPattern::SCHEME_HTTP |
|
||||
URLPattern::SCHEME_HTTPS | URLPattern::SCHEME_FILE |
|
||||
URLPattern::SCHEME_FTP | URLPattern::SCHEME_WS | URLPattern::SCHEME_WSS |
|
||||
- URLPattern::SCHEME_UUID_IN_PACKAGE;
|
||||
+ URLPattern::SCHEME_UUID_IN_PACKAGE |
|
||||
+ URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS;
|
||||
|
||||
//
|
||||
// Extension
|
||||
diff --git a/extensions/common/url_pattern.cc b/extensions/common/url_pattern.cc
|
||||
index d4328ca22fdeefd3dca88bfe959dfb849705b109..ba24e788d4a2e467d24f6369e2d93ea3b4a0c9d7 100644
|
||||
--- a/extensions/common/url_pattern.cc
|
||||
+++ b/extensions/common/url_pattern.cc
|
||||
@@ -140,6 +140,11 @@ bool URLPattern::IsValidSchemeForExtensions(std::string_view scheme) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+ for (auto& extension_scheme : url::GetExtensionSchemes()) {
|
||||
+ if (scheme == extension_scheme) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -401,6 +406,14 @@ bool URLPattern::IsValidScheme(std::string_view scheme) const {
|
||||
}
|
||||
}
|
||||
|
||||
+ if (valid_schemes_ & URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS) {
|
||||
+ for (auto& extension_scheme : url::GetExtensionSchemes()) {
|
||||
+ if (scheme == extension_scheme) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
return false;
|
||||
}
|
||||
|
||||
diff --git a/extensions/common/url_pattern.h b/extensions/common/url_pattern.h
|
||||
index 4d09251b0160644d86682ad3db7c41b50f360e6f..8a626e14eff2d58d8218a7b0df820c6c0522b00f 100644
|
||||
--- a/extensions/common/url_pattern.h
|
||||
+++ b/extensions/common/url_pattern.h
|
||||
@@ -64,6 +64,9 @@ class URLPattern {
|
||||
SCHEME_DATA = 1 << 9,
|
||||
SCHEME_UUID_IN_PACKAGE = 1 << 10,
|
||||
|
||||
+ // Represents the schemes returned by url::GetExtensionSchemes().
|
||||
+ SCHEME_ELECTRON_CUSTOM_PROTOCOLS = 1 << 11,
|
||||
+
|
||||
// IMPORTANT!
|
||||
// SCHEME_ALL will match every scheme, including chrome://, chrome-
|
||||
// extension://, about:, etc. Because this has lots of security
|
||||
diff --git a/extensions/common/user_script.cc b/extensions/common/user_script.cc
|
||||
index f680ef4d31d580a285abe51387e3df043d4458f1..afde40d56d7874aa04ea2b1d881e5cab79fd7661 100644
|
||||
--- a/extensions/common/user_script.cc
|
||||
+++ b/extensions/common/user_script.cc
|
||||
@@ -69,7 +69,8 @@ enum {
|
||||
kValidUserScriptSchemes = URLPattern::SCHEME_CHROMEUI |
|
||||
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS |
|
||||
URLPattern::SCHEME_FILE | URLPattern::SCHEME_FTP |
|
||||
- URLPattern::SCHEME_UUID_IN_PACKAGE
|
||||
+ URLPattern::SCHEME_UUID_IN_PACKAGE |
|
||||
+ URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS
|
||||
};
|
||||
|
||||
// static
|
||||
diff --git a/url/url_util.cc b/url/url_util.cc
|
||||
index 21cfd314fd52727e4b07de880bd80a0bec7407d5..cb90ea396122d8f5dc27e6d9ffc36cc0abedb97d 100644
|
||||
--- a/url/url_util.cc
|
||||
+++ b/url/url_util.cc
|
||||
@@ -134,6 +134,9 @@ struct SchemeRegistry {
|
||||
// Embedder schemes that have V8 code cache enabled in js and wasm scripts.
|
||||
std::vector<std::string> code_cache_schemes = {};
|
||||
|
||||
+ // Embedder schemes on which Chrome extensions can be used.
|
||||
+ std::vector<std::string> extension_schemes = {};
|
||||
+
|
||||
// Schemes with a predefined default custom handler.
|
||||
std::vector<SchemeWithHandler> predefined_handler_schemes;
|
||||
|
||||
@@ -678,6 +681,15 @@ const std::vector<std::string>& GetCodeCacheSchemes() {
|
||||
return GetSchemeRegistry().code_cache_schemes;
|
||||
}
|
||||
|
||||
+void AddExtensionScheme(std::string_view new_scheme) {
|
||||
+ DoAddScheme(new_scheme,
|
||||
+ &GetSchemeRegistryWithoutLocking()->extension_schemes);
|
||||
+}
|
||||
+
|
||||
+const std::vector<std::string>& GetExtensionSchemes() {
|
||||
+ return GetSchemeRegistry().extension_schemes;
|
||||
+}
|
||||
+
|
||||
void AddPredefinedHandlerScheme(std::string_view new_scheme,
|
||||
std::string_view handler) {
|
||||
DoAddSchemeWithHandler(
|
||||
diff --git a/url/url_util.h b/url/url_util.h
|
||||
index f965c1dbd47781748d3091209d140a128ca7192f..b6d42ae86e4cf981ae00e96a0f296ada50c90d29 100644
|
||||
--- a/url/url_util.h
|
||||
+++ b/url/url_util.h
|
||||
@@ -124,6 +124,11 @@ COMPONENT_EXPORT(URL) const std::vector<std::string>& GetEmptyDocumentSchemes();
|
||||
COMPONENT_EXPORT(URL) void AddCodeCacheScheme(std::string_view new_scheme);
|
||||
COMPONENT_EXPORT(URL) const std::vector<std::string>& GetCodeCacheSchemes();
|
||||
|
||||
+// Adds an application-defined scheme to the list of schemes on which Chrome
|
||||
+// extensions can be used.
|
||||
+COMPONENT_EXPORT(URL) void AddExtensionScheme(std::string_view new_scheme);
|
||||
+COMPONENT_EXPORT(URL) const std::vector<std::string>& GetExtensionSchemes();
|
||||
+
|
||||
// Adds a scheme with a predefined default handler.
|
||||
//
|
||||
// This pair of strings must be normalized protocol handler parameters as
|
||||
@@ -313,7 +313,7 @@ index 18f283e625101318ee14b50e6e765dfd1c9a1a44..44a3a55974c9e4b9e715574075f25661
|
||||
|
||||
auto DrawAsSinglePath = [&]() {
|
||||
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
|
||||
index 70a7e2a5203d3cdddbad7eecca28d65945522fed..35751435ebe8205a5c9d73bed0422ccbe61ab8b4 100644
|
||||
index 640a50a1af53f0771da02de73d70a94c973aa624..f981c8dc7492872f296e01cd64692859671be5d5 100644
|
||||
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
|
||||
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
|
||||
@@ -214,6 +214,10 @@
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Keeley Hammond <khammond@slack-corp.com>
|
||||
Date: Thu, 19 Mar 2026 00:34:37 -0700
|
||||
Subject: fix: fire MENU_POPUP_START for dynamically created ARIA menus
|
||||
|
||||
When an ARIA menu element is dynamically created (e.g. via appendChild)
|
||||
rather than being shown by toggling visibility, the AXMenuOpened event
|
||||
was not fired. The OnIgnoredChanged path handles the visibility toggle
|
||||
case, but OnAtomicUpdateFinished did not fire MENU_POPUP_START for
|
||||
newly created menu nodes.
|
||||
|
||||
Previous attempts to fix this (crbug.com/1254875) were reverted because
|
||||
they fired the event too eagerly in OnNodeCreated (before the tree was
|
||||
fully formed) and without filtering, causing regressions with screen
|
||||
readers on pages that misused role="menu".
|
||||
|
||||
This fix addresses both issues:
|
||||
1. Fires MENU_POPUP_START in OnAtomicUpdateFinished (after the tree
|
||||
update is complete) rather than in OnNodeCreated.
|
||||
2. Only fires if the menu has at least one menuitem child, filtering
|
||||
out false positives from misused role="menu" elements.
|
||||
|
||||
MENU_POPUP_END for deleted menus is already handled by
|
||||
AXTreeManager::OnNodeWillBeDeleted, which fires the event directly
|
||||
on the menu node before destruction.
|
||||
|
||||
The change is behind the DynamicMenuPopupEvents feature flag, disabled
|
||||
by default, to allow stabilization before enabling by default. Enable
|
||||
with --enable-features=DynamicMenuPopupEvents.
|
||||
|
||||
This patch can be removed when a CL containing the fix is accepted
|
||||
into Chromium.
|
||||
|
||||
Bug: 40794596
|
||||
|
||||
diff --git a/ui/accessibility/ax_event_generator.cc b/ui/accessibility/ax_event_generator.cc
|
||||
index 5e0d7a48b4a039db67b5cc6b7e86103739702b40..517fb5e9904f3907de177e172c76328910bb7333 100644
|
||||
--- a/ui/accessibility/ax_event_generator.cc
|
||||
+++ b/ui/accessibility/ax_event_generator.cc
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "ui/accessibility/ax_event_generator.h"
|
||||
|
||||
+#include "base/feature_list.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "ui/accessibility/ax_enums.mojom.h"
|
||||
#include "ui/accessibility/ax_event.h"
|
||||
@@ -12,6 +13,12 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
+// Feature flag for firing MENU_POPUP_START for dynamically created ARIA menus.
|
||||
+// Disabled by default to allow stabilization before enabling globally.
|
||||
+BASE_FEATURE(kDynamicMenuPopupEvents,
|
||||
+ "DynamicMenuPopupEvents",
|
||||
+ base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
+
|
||||
namespace {
|
||||
|
||||
bool HasEvent(const std::set<AXEventGenerator::EventParams>& node_events,
|
||||
@@ -914,12 +921,31 @@ void AXEventGenerator::OnAtomicUpdateFinished(
|
||||
/*new_value*/ true);
|
||||
}
|
||||
|
||||
- if (IsAlert(change.node->GetRole()))
|
||||
+ if (IsAlert(change.node->GetRole())) {
|
||||
AddEvent(change.node, Event::ALERT);
|
||||
- else if (change.node->data().IsActiveLiveRegionRoot())
|
||||
+ } else if (change.node->data().IsActiveLiveRegionRoot()) {
|
||||
AddEvent(change.node, Event::LIVE_REGION_CREATED);
|
||||
- else if (change.node->data().IsContainedInActiveLiveRegion())
|
||||
+ } else if (change.node->data().IsContainedInActiveLiveRegion()) {
|
||||
FireLiveRegionEvents(change.node, /* is_removal */ false);
|
||||
+ }
|
||||
+
|
||||
+ // Fire MENU_POPUP_START when a menu is dynamically created (e.g. via
|
||||
+ // appendChild). The OnIgnoredChanged path handles menus that already exist
|
||||
+ // in the DOM and are shown/hidden. This handles the case where the menu
|
||||
+ // element itself is created on the fly.
|
||||
+ // Only fire if the menu has at least one menuitem child, to avoid false
|
||||
+ // positives from elements that misuse role="menu".
|
||||
+ if (base::FeatureList::IsEnabled(kDynamicMenuPopupEvents) &&
|
||||
+ change.node->GetRole() == ax::mojom::Role::kMenu &&
|
||||
+ !change.node->IsInvisibleOrIgnored()) {
|
||||
+ for (auto iter = change.node->UnignoredChildrenBegin();
|
||||
+ iter != change.node->UnignoredChildrenEnd(); ++iter) {
|
||||
+ if (IsMenuItem(iter->GetRole())) {
|
||||
+ AddEvent(change.node, Event::MENU_POPUP_START);
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
FireActiveDescendantEvents();
|
||||
@@ -0,0 +1,81 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <sattard@anthropic.com>
|
||||
Date: Mon, 30 Mar 2026 03:05:40 -0700
|
||||
Subject: fix: handle embedder windows shown after WebContentsViewCocoa attach
|
||||
|
||||
The occlusion checker assumes windows are shown before or at the same
|
||||
time as a WebContentsViewCocoa is attached. Embedders like Electron
|
||||
support creating a window hidden, attaching web contents, and showing
|
||||
later. This breaks three assumptions:
|
||||
|
||||
1. updateWebContentsVisibility only checks -[NSWindow isOccluded], which
|
||||
defaults to NO for never-shown windows, so viewDidMoveToWindow
|
||||
incorrectly reports kVisible for hidden windows.
|
||||
|
||||
2. windowChangedOcclusionState: only responds to checker-originated
|
||||
notifications, but setOccluded: early-returns when isOccluded doesn't
|
||||
change. A hidden window's isOccluded is NO and stays NO after show(),
|
||||
so no checker notification fires on show and the view never updates
|
||||
to kVisible.
|
||||
|
||||
3. performOcclusionStateUpdates iterates orderedWindows and marks
|
||||
not-yet-shown windows as occluded (their occlusionState lacks the
|
||||
Visible bit), which stops painting before first show.
|
||||
|
||||
Fix by also checking occlusionState in updateWebContentsVisibility,
|
||||
responding to macOS-originated notifications in
|
||||
windowChangedOcclusionState:, and skipping non-visible windows in
|
||||
performOcclusionStateUpdates.
|
||||
|
||||
This patch can be removed if the changes are upstreamed to Chromium.
|
||||
|
||||
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
|
||||
index a5570988c3721d9f6bd05c402a7658d3af6f2c2c..54aaffde30c14a27068f89b6de6123abd6ea0660 100644
|
||||
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
|
||||
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
|
||||
@@ -400,9 +400,11 @@ - (void)performOcclusionStateUpdates {
|
||||
for (NSWindow* window in windowsFromFrontToBack) {
|
||||
// The fullscreen transition causes spurious occlusion notifications.
|
||||
// See https://crbug.com/1081229 . Also, ignore windows that don't have
|
||||
- // web contentses.
|
||||
+ // web contentses, and windows that aren't visible (embedders like
|
||||
+ // Electron may create windows hidden with web contents already attached;
|
||||
+ // marking these as occluded would stop painting before first show).
|
||||
if (window == _windowReceivingFullscreenTransitionNotifications ||
|
||||
- ![window containsWebContentsViewCocoa])
|
||||
+ ![window isVisible] || ![window containsWebContentsViewCocoa])
|
||||
continue;
|
||||
|
||||
[window setOccluded:[self isWindowOccluded:window
|
||||
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
|
||||
index 1ef2c9052262eccdbc40030746a858b7f30ac469..34708d45274f95b5f35cdefad98ad4a1c3c28e1c 100644
|
||||
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
|
||||
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
|
||||
@@ -477,7 +477,8 @@ - (void)updateWebContentsVisibility {
|
||||
Visibility visibility = Visibility::kVisible;
|
||||
if ([self isHiddenOrHasHiddenAncestor] || ![self window])
|
||||
visibility = Visibility::kHidden;
|
||||
- else if ([[self window] isOccluded])
|
||||
+ else if ([[self window] isOccluded] ||
|
||||
+ !([[self window] occlusionState] & NSWindowOcclusionStateVisible))
|
||||
visibility = Visibility::kOccluded;
|
||||
|
||||
[self updateWebContentsVisibility:visibility];
|
||||
@@ -521,11 +522,12 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
|
||||
}
|
||||
|
||||
- (void)windowChangedOcclusionState:(NSNotification*)aNotification {
|
||||
- // Only respond to occlusion notifications sent by the occlusion checker.
|
||||
- NSDictionary* userInfo = [aNotification userInfo];
|
||||
- NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className];
|
||||
- if (userInfo[occlusionCheckerKey] != nil)
|
||||
- [self updateWebContentsVisibility];
|
||||
+ // Respond to occlusion notifications from both macOS and the occlusion
|
||||
+ // checker. Embedders (e.g. Electron) may attach a WebContentsViewCocoa to
|
||||
+ // a window that has not yet been shown; macOS will notify us when the
|
||||
+ // window's occlusion state changes, but the occlusion checker will not
|
||||
+ // because -[NSWindow isOccluded] remains NO before and after show.
|
||||
+ [self updateWebContentsVisibility];
|
||||
}
|
||||
|
||||
- (void)viewDidMoveToWindow {
|
||||
@@ -68,7 +68,7 @@ index f91857eb0b6ad385721b8224100de26dfdd7dd8d..45e8766fcb8d46d8edc3bf8d21d3f826
|
||||
: PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3;
|
||||
}
|
||||
diff --git a/chrome/browser/printing/print_view_manager_base.cc b/chrome/browser/printing/print_view_manager_base.cc
|
||||
index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a705b5d994 100644
|
||||
index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f0c49c1e8 100644
|
||||
--- a/chrome/browser/printing/print_view_manager_base.cc
|
||||
+++ b/chrome/browser/printing/print_view_manager_base.cc
|
||||
@@ -80,6 +80,20 @@ namespace printing {
|
||||
@@ -326,14 +326,23 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
ReleasePrinterQuery();
|
||||
}
|
||||
|
||||
@@ -851,15 +886,24 @@ void PrintViewManagerBase::RemoveTestObserver(TestObserver& observer) {
|
||||
@@ -851,15 +886,33 @@ void PrintViewManagerBase::RemoveTestObserver(TestObserver& observer) {
|
||||
test_observers_.RemoveObserver(&observer);
|
||||
}
|
||||
|
||||
+void PrintViewManagerBase::ShowInvalidPrinterSettingsError() {
|
||||
+ if (!callback_.is_null()) {
|
||||
+ printing_status_ = PrintStatus::kInvalid;
|
||||
+ TerminatePrintJob(true);
|
||||
+ if (print_job_) {
|
||||
+ TerminatePrintJob(true);
|
||||
+ } else {
|
||||
+ // No print job was created, so TerminatePrintJob would bail out
|
||||
+ // without ever calling ReleasePrintJob (where the callback is
|
||||
+ // invoked). Fire the callback directly to avoid leaking it until
|
||||
+ // WebContents destruction.
|
||||
+ std::move(callback_).Run(false,
|
||||
+ PrintReasonFromPrintStatus(printing_status_));
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
@@ -351,7 +360,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
}
|
||||
|
||||
void PrintViewManagerBase::RenderFrameDeleted(
|
||||
@@ -901,13 +945,14 @@ void PrintViewManagerBase::SystemDialogCancelled() {
|
||||
@@ -901,13 +954,14 @@ void PrintViewManagerBase::SystemDialogCancelled() {
|
||||
// System dialog was cancelled. Clean up the print job and notify the
|
||||
// BackgroundPrintingManager.
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
@@ -367,7 +376,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
}
|
||||
|
||||
void PrintViewManagerBase::OnDocDone(int job_id, PrintedDocument* document) {
|
||||
@@ -921,18 +966,26 @@ void PrintViewManagerBase::OnJobDone() {
|
||||
@@ -921,18 +975,26 @@ void PrintViewManagerBase::OnJobDone() {
|
||||
// Printing is done, we don't need it anymore.
|
||||
// print_job_->is_job_pending() may still be true, depending on the order
|
||||
// of object registration.
|
||||
@@ -396,7 +405,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
TerminatePrintJob(true);
|
||||
}
|
||||
|
||||
@@ -942,7 +995,7 @@ bool PrintViewManagerBase::RenderAllMissingPagesNow() {
|
||||
@@ -942,7 +1004,7 @@ bool PrintViewManagerBase::RenderAllMissingPagesNow() {
|
||||
|
||||
// Is the document already complete?
|
||||
if (print_job_->document() && print_job_->document()->IsComplete()) {
|
||||
@@ -405,7 +414,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -995,7 +1048,10 @@ bool PrintViewManagerBase::SetupNewPrintJob(
|
||||
@@ -995,7 +1057,10 @@ bool PrintViewManagerBase::SetupNewPrintJob(
|
||||
|
||||
// Disconnect the current `print_job_`.
|
||||
auto weak_this = weak_ptr_factory_.GetWeakPtr();
|
||||
@@ -417,7 +426,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
if (!weak_this)
|
||||
return false;
|
||||
|
||||
@@ -1015,7 +1071,7 @@ bool PrintViewManagerBase::SetupNewPrintJob(
|
||||
@@ -1015,7 +1080,7 @@ bool PrintViewManagerBase::SetupNewPrintJob(
|
||||
#endif
|
||||
print_job_->AddObserver(*this);
|
||||
|
||||
@@ -426,7 +435,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1073,7 +1129,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
|
||||
@@ -1073,7 +1138,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
|
||||
// Ensure that any residual registration of printing client is released.
|
||||
// This might be necessary in some abnormal cases, such as the associated
|
||||
// render process having terminated.
|
||||
@@ -435,7 +444,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
if (!analyzing_content_) {
|
||||
UnregisterSystemPrintClient();
|
||||
}
|
||||
@@ -1083,6 +1139,11 @@ void PrintViewManagerBase::ReleasePrintJob() {
|
||||
@@ -1083,6 +1148,11 @@ void PrintViewManagerBase::ReleasePrintJob() {
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -447,7 +456,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
if (!print_job_)
|
||||
return;
|
||||
|
||||
@@ -1090,7 +1151,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
|
||||
@@ -1090,7 +1160,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
|
||||
// printing_rfh_ should only ever point to a RenderFrameHost with a live
|
||||
// RenderFrame.
|
||||
DCHECK(rfh->IsRenderFrameLive());
|
||||
@@ -456,7 +465,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
}
|
||||
|
||||
print_job_->RemoveObserver(*this);
|
||||
@@ -1132,7 +1193,7 @@ bool PrintViewManagerBase::RunInnerMessageLoop() {
|
||||
@@ -1132,7 +1202,7 @@ bool PrintViewManagerBase::RunInnerMessageLoop() {
|
||||
}
|
||||
|
||||
bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
|
||||
@@ -465,7 +474,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
return true;
|
||||
|
||||
if (!cookie) {
|
||||
@@ -1155,7 +1216,7 @@ bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
|
||||
@@ -1155,7 +1225,7 @@ bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -474,7 +483,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
// Don't start printing if enterprise checks are being performed to check if
|
||||
// printing is allowed, or if content analysis is going to take place right
|
||||
// before starting `print_job_`.
|
||||
@@ -1286,6 +1347,8 @@ void PrintViewManagerBase::CompleteScriptedPrint(
|
||||
@@ -1286,6 +1356,8 @@ void PrintViewManagerBase::CompleteScriptedPrint(
|
||||
auto callback_wrapper = base::BindOnce(
|
||||
&PrintViewManagerBase::ScriptedPrintReply, weak_ptr_factory_.GetWeakPtr(),
|
||||
std::move(callback), render_process_host->GetDeprecatedID());
|
||||
@@ -483,7 +492,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
|
||||
std::unique_ptr<PrinterQuery> printer_query =
|
||||
queue()->PopPrinterQuery(params->cookie);
|
||||
if (!printer_query)
|
||||
@@ -1296,10 +1359,10 @@ void PrintViewManagerBase::CompleteScriptedPrint(
|
||||
@@ -1296,10 +1368,10 @@ void PrintViewManagerBase::CompleteScriptedPrint(
|
||||
params->expected_pages_count, params->has_selection, params->margin_type,
|
||||
params->is_scripted, !render_process_host->IsPdf(),
|
||||
base::BindOnce(&OnDidScriptedPrint, queue_, std::move(printer_query),
|
||||
@@ -666,7 +675,7 @@ index ac2f719be566020d9f41364560c12e6d6d0fe3d8..16d758a6936f66148a196761cfb875f6
|
||||
PrintingFailed(int32 cookie, PrintFailureReason reason);
|
||||
|
||||
diff --git a/components/printing/renderer/print_render_frame_helper.cc b/components/printing/renderer/print_render_frame_helper.cc
|
||||
index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..1320f3b10b07b2cee90f39f406604176c7575796 100644
|
||||
index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..dd83b6cfb6e3f916e60f50402014cd931a4d8850 100644
|
||||
--- a/components/printing/renderer/print_render_frame_helper.cc
|
||||
+++ b/components/printing/renderer/print_render_frame_helper.cc
|
||||
@@ -54,6 +54,7 @@
|
||||
@@ -790,7 +799,7 @@ index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..1320f3b10b07b2cee90f39f406604176
|
||||
// Check if `this` is still valid.
|
||||
if (!self)
|
||||
return;
|
||||
@@ -2394,29 +2415,43 @@ void PrintRenderFrameHelper::IPCProcessed() {
|
||||
@@ -2394,29 +2415,47 @@ void PrintRenderFrameHelper::IPCProcessed() {
|
||||
}
|
||||
|
||||
bool PrintRenderFrameHelper::InitPrintSettings(blink::WebLocalFrame* frame,
|
||||
@@ -826,8 +835,12 @@ index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..1320f3b10b07b2cee90f39f406604176
|
||||
- : mojom::PrintScalingOption::kSourceSize;
|
||||
- RecordDebugEvent(settings.params->printed_doc_type ==
|
||||
+ bool silent = new_settings.FindBool("silent").value_or(false);
|
||||
+ if (silent) {
|
||||
+ settings->params->print_scaling_option = mojom::PrintScalingOption::kFitToPrintableArea;
|
||||
+ int margins_type = new_settings.FindInt(kSettingMarginsType)
|
||||
+ .value_or(static_cast<int>(mojom::MarginType::kDefaultMargins));
|
||||
+ if (silent &&
|
||||
+ margins_type == static_cast<int>(mojom::MarginType::kDefaultMargins)) {
|
||||
+ settings->params->print_scaling_option =
|
||||
+ mojom::PrintScalingOption::kFitToPrintableArea;
|
||||
+ } else {
|
||||
+ settings->params->print_scaling_option =
|
||||
+ center_on_paper ? mojom::PrintScalingOption::kCenterShrinkToFitPaper
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: David Sanders <dsanders11@ucsbalum.com>
|
||||
Date: Wed, 8 Jan 2025 23:53:27 -0800
|
||||
Subject: Revert "Code Health: Clean up stale MacWebContentsOcclusion"
|
||||
|
||||
Chrome has removed this WebContentsOcclusion feature flag upstream,
|
||||
which is now causing our visibility tests to break. This patch
|
||||
restores the legacy occlusion behavior to ensure the roll can continue
|
||||
while we debug the issue.
|
||||
|
||||
This patch can be removed when the root cause because the visibility
|
||||
specs failing on MacOS only is debugged and fixed. It should be removed
|
||||
before Electron 35's stable date.
|
||||
|
||||
Refs: https://chromium-review.googlesource.com/c/chromium/src/+/6078344
|
||||
|
||||
This partially (leaves the removal of the feature flag) reverts
|
||||
ef865130abd5539e7bce12308659b19980368f12.
|
||||
|
||||
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
|
||||
index 04c7635cc093d9d676869383670a8f2199f14ac6..52d76e804e47ab0b56016d26262d6d67cbc00875 100644
|
||||
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
|
||||
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "base/metrics/field_trial_params.h"
|
||||
#import "content/app_shim_remote_cocoa/web_contents_view_cocoa.h"
|
||||
|
||||
+extern CONTENT_EXPORT const base::FeatureParam<bool>
|
||||
+ kEnhancedWindowOcclusionDetection;
|
||||
extern CONTENT_EXPORT const base::FeatureParam<bool>
|
||||
kDisplaySleepAndAppHideDetection;
|
||||
|
||||
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
|
||||
index a5570988c3721d9f6bd05c402a7658d3af6f2c2c..0a2dba6aa2d48bc39d2a55c8b4d6606744c10ca7 100644
|
||||
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
|
||||
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
|
||||
@@ -14,9 +14,16 @@
|
||||
#include "base/mac/mac_util.h"
|
||||
#include "base/metrics/field_trial_params.h"
|
||||
#include "base/no_destructor.h"
|
||||
+#include "content/common/features.h"
|
||||
#include "content/public/browser/content_browser_client.h"
|
||||
#include "content/public/common/content_client.h"
|
||||
|
||||
+using features::kMacWebContentsOcclusion;
|
||||
+
|
||||
+// Experiment features.
|
||||
+const base::FeatureParam<bool> kEnhancedWindowOcclusionDetection{
|
||||
+ &kMacWebContentsOcclusion, "EnhancedWindowOcclusionDetection", false};
|
||||
+
|
||||
namespace {
|
||||
|
||||
NSString* const kWindowDidChangePositionInWindowList =
|
||||
@@ -125,7 +132,8 @@ - (void)dealloc {
|
||||
|
||||
- (BOOL)isManualOcclusionDetectionEnabled {
|
||||
return [WebContentsOcclusionCheckerMac
|
||||
- manualOcclusionDetectionSupportedForCurrentMacOSVersion];
|
||||
+ manualOcclusionDetectionSupportedForCurrentMacOSVersion] &&
|
||||
+ kEnhancedWindowOcclusionDetection.Get();
|
||||
}
|
||||
|
||||
// Alternative implementation of orderWindow:relativeTo:. Replaces
|
||||
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
|
||||
index 1ef2c9052262eccdbc40030746a858b7f30ac469..c7101b0d71826b05f61bfe0e74429d922769e792 100644
|
||||
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
|
||||
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "content/app_shim_remote_cocoa/web_drag_source_mac.h"
|
||||
#import "content/browser/web_contents/web_contents_view_mac.h"
|
||||
#import "content/browser/web_contents/web_drag_dest_mac.h"
|
||||
+#include "content/common/features.h"
|
||||
#include "content/public/browser/content_browser_client.h"
|
||||
#include "content/public/common/content_client.h"
|
||||
#include "ui/base/clipboard/clipboard_constants.h"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include "ui/resources/grit/ui_resources.h"
|
||||
|
||||
using content::DropData;
|
||||
+using features::kMacWebContentsOcclusion;
|
||||
using remote_cocoa::mojom::DraggingInfo;
|
||||
using remote_cocoa::mojom::SelectionDirection;
|
||||
|
||||
@@ -122,12 +124,15 @@ @implementation WebContentsViewCocoa {
|
||||
WebDragSource* __strong _dragSource;
|
||||
NSDragOperation _dragOperation;
|
||||
|
||||
+ BOOL _inFullScreenTransition;
|
||||
BOOL _willSetWebContentsOccludedAfterDelay;
|
||||
}
|
||||
|
||||
+ (void)initialize {
|
||||
- // Create the WebContentsOcclusionCheckerMac shared instance.
|
||||
- [WebContentsOcclusionCheckerMac sharedInstance];
|
||||
+ if (base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
|
||||
+ // Create the WebContentsOcclusionCheckerMac shared instance.
|
||||
+ [WebContentsOcclusionCheckerMac sharedInstance];
|
||||
+ }
|
||||
}
|
||||
|
||||
- (instancetype)initWithViewsHostableView:(ui::ViewsHostableView*)v {
|
||||
@@ -438,6 +443,7 @@ - (void)updateWebContentsVisibility:
|
||||
(remote_cocoa::mojom::Visibility)visibility {
|
||||
using remote_cocoa::mojom::Visibility;
|
||||
|
||||
+ DCHECK(base::FeatureList::IsEnabled(kMacWebContentsOcclusion));
|
||||
if (!_host)
|
||||
return;
|
||||
|
||||
@@ -483,6 +489,20 @@ - (void)updateWebContentsVisibility {
|
||||
[self updateWebContentsVisibility:visibility];
|
||||
}
|
||||
|
||||
+- (void)legacyUpdateWebContentsVisibility {
|
||||
+ using remote_cocoa::mojom::Visibility;
|
||||
+ if (!_host || _inFullScreenTransition)
|
||||
+ return;
|
||||
+ Visibility visibility = Visibility::kVisible;
|
||||
+ if ([self isHiddenOrHasHiddenAncestor] || ![self window])
|
||||
+ visibility = Visibility::kHidden;
|
||||
+ else if ([[self window] occlusionState] & NSWindowOcclusionStateVisible)
|
||||
+ visibility = Visibility::kVisible;
|
||||
+ else
|
||||
+ visibility = Visibility::kOccluded;
|
||||
+ _host->OnWindowVisibilityChanged(visibility);
|
||||
+}
|
||||
+
|
||||
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
|
||||
// Subviews do not participate in auto layout unless the the size this view
|
||||
// changes. This allows RenderWidgetHostViewMac::SetBounds(..) to select a
|
||||
@@ -505,11 +525,39 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
|
||||
|
||||
NSWindow* oldWindow = [self window];
|
||||
|
||||
+ if (base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
|
||||
+ if (oldWindow) {
|
||||
+ [notificationCenter
|
||||
+ removeObserver:self
|
||||
+ name:NSWindowDidChangeOcclusionStateNotification
|
||||
+ object:oldWindow];
|
||||
+ }
|
||||
+
|
||||
+ if (newWindow) {
|
||||
+ [notificationCenter
|
||||
+ addObserver:self
|
||||
+ selector:@selector(windowChangedOcclusionState:)
|
||||
+ name:NSWindowDidChangeOcclusionStateNotification
|
||||
+ object:newWindow];
|
||||
+ }
|
||||
+
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ _inFullScreenTransition = NO;
|
||||
if (oldWindow) {
|
||||
- [notificationCenter
|
||||
- removeObserver:self
|
||||
- name:NSWindowDidChangeOcclusionStateNotification
|
||||
- object:oldWindow];
|
||||
+ NSArray* notificationsToRemove = @[
|
||||
+ NSWindowDidChangeOcclusionStateNotification,
|
||||
+ NSWindowWillEnterFullScreenNotification,
|
||||
+ NSWindowDidEnterFullScreenNotification,
|
||||
+ NSWindowWillExitFullScreenNotification,
|
||||
+ NSWindowDidExitFullScreenNotification
|
||||
+ ];
|
||||
+ for (NSString* notificationName in notificationsToRemove) {
|
||||
+ [notificationCenter removeObserver:self
|
||||
+ name:notificationName
|
||||
+ object:oldWindow];
|
||||
+ }
|
||||
}
|
||||
|
||||
if (newWindow) {
|
||||
@@ -517,26 +565,66 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
|
||||
selector:@selector(windowChangedOcclusionState:)
|
||||
name:NSWindowDidChangeOcclusionStateNotification
|
||||
object:newWindow];
|
||||
+ // The fullscreen transition causes spurious occlusion notifications.
|
||||
+ // See https://crbug.com/1081229
|
||||
+ [notificationCenter addObserver:self
|
||||
+ selector:@selector(fullscreenTransitionStarted:)
|
||||
+ name:NSWindowWillEnterFullScreenNotification
|
||||
+ object:newWindow];
|
||||
+ [notificationCenter addObserver:self
|
||||
+ selector:@selector(fullscreenTransitionComplete:)
|
||||
+ name:NSWindowDidEnterFullScreenNotification
|
||||
+ object:newWindow];
|
||||
+ [notificationCenter addObserver:self
|
||||
+ selector:@selector(fullscreenTransitionStarted:)
|
||||
+ name:NSWindowWillExitFullScreenNotification
|
||||
+ object:newWindow];
|
||||
+ [notificationCenter addObserver:self
|
||||
+ selector:@selector(fullscreenTransitionComplete:)
|
||||
+ name:NSWindowDidExitFullScreenNotification
|
||||
+ object:newWindow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)windowChangedOcclusionState:(NSNotification*)aNotification {
|
||||
- // Only respond to occlusion notifications sent by the occlusion checker.
|
||||
- NSDictionary* userInfo = [aNotification userInfo];
|
||||
- NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className];
|
||||
- if (userInfo[occlusionCheckerKey] != nil)
|
||||
- [self updateWebContentsVisibility];
|
||||
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
|
||||
+ [self legacyUpdateWebContentsVisibility];
|
||||
+ return;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+- (void)fullscreenTransitionStarted:(NSNotification*)notification {
|
||||
+ _inFullScreenTransition = YES;
|
||||
+}
|
||||
+
|
||||
+- (void)fullscreenTransitionComplete:(NSNotification*)notification {
|
||||
+ _inFullScreenTransition = NO;
|
||||
}
|
||||
|
||||
- (void)viewDidMoveToWindow {
|
||||
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
|
||||
+ [self legacyUpdateWebContentsVisibility];
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
[self updateWebContentsVisibility];
|
||||
}
|
||||
|
||||
- (void)viewDidHide {
|
||||
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
|
||||
+ [self legacyUpdateWebContentsVisibility];
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
[self updateWebContentsVisibility];
|
||||
}
|
||||
|
||||
- (void)viewDidUnhide {
|
||||
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
|
||||
+ [self legacyUpdateWebContentsVisibility];
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
[self updateWebContentsVisibility];
|
||||
}
|
||||
|
||||
diff --git a/content/common/features.cc b/content/common/features.cc
|
||||
index ee2fa2cd950a6c5cddc5904ee7a4656a18b9d73d..10a1e1e14777b61f6c42266f6f085bd47b759efd 100644
|
||||
--- a/content/common/features.cc
|
||||
+++ b/content/common/features.cc
|
||||
@@ -364,6 +364,14 @@ BASE_FEATURE(kInterestGroupUpdateIfOlderThan, base::FEATURE_ENABLED_BY_DEFAULT);
|
||||
BASE_FEATURE(kIOSurfaceCapturer, base::FEATURE_ENABLED_BY_DEFAULT);
|
||||
#endif
|
||||
|
||||
+// Feature that controls whether WebContentsOcclusionChecker should handle
|
||||
+// occlusion notifications.
|
||||
+#if BUILDFLAG(IS_MAC)
|
||||
+BASE_FEATURE(kMacWebContentsOcclusion,
|
||||
+ "MacWebContentsOcclusion",
|
||||
+ base::FEATURE_ENABLED_BY_DEFAULT);
|
||||
+#endif
|
||||
+
|
||||
// When enabled, child process will not terminate itself when IPC is reset.
|
||||
BASE_FEATURE(kKeepChildProcessAfterIPCReset, base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
|
||||
diff --git a/content/common/features.h b/content/common/features.h
|
||||
index 24443780a7196b40096f44826232f77eaab68ffa..9164f2cf39542525ef2c30f572c7d0b557473f5d 100644
|
||||
--- a/content/common/features.h
|
||||
+++ b/content/common/features.h
|
||||
@@ -140,6 +140,9 @@ CONTENT_EXPORT BASE_DECLARE_FEATURE(kInterestGroupUpdateIfOlderThan);
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
CONTENT_EXPORT BASE_DECLARE_FEATURE(kIOSurfaceCapturer);
|
||||
#endif
|
||||
+#if BUILDFLAG(IS_MAC)
|
||||
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kMacWebContentsOcclusion);
|
||||
+#endif
|
||||
CONTENT_EXPORT BASE_DECLARE_FEATURE(kKeepChildProcessAfterIPCReset);
|
||||
|
||||
CONTENT_EXPORT BASE_DECLARE_FEATURE(kLocalNetworkAccessForWorkers);
|
||||
@@ -10,10 +10,10 @@ on Windows. We should refactor our code so that this patch isn't
|
||||
necessary.
|
||||
|
||||
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
|
||||
index b50c4004adfa883dfd670611f45856454517e877..a2086481f5120b36400588dfb2b941457e42ae67 100644
|
||||
index d17637a54208450504d071a3f10c20668cfbe76d..f3ffc975d794f356d9a83837fd977e758b726501 100644
|
||||
--- a/testing/variations/fieldtrial_testing_config.json
|
||||
+++ b/testing/variations/fieldtrial_testing_config.json
|
||||
@@ -27080,6 +27080,21 @@
|
||||
@@ -27095,6 +27095,21 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
chore_expose_ui_to_allow_electron_to_set_dock_side.patch
|
||||
fix_prefer_browser_runtime_over_node_in_hostruntime_detection.patch
|
||||
feat_allow_enabling_extension_panels_on_custom_protocols.patch
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Niklas Wenzel <dev@nikwen.de>
|
||||
Date: Wed, 25 Feb 2026 16:23:07 +0100
|
||||
Subject: feat: allow enabling extension panels on custom protocols
|
||||
|
||||
This allows us to show Chrome extension panels on pages served over
|
||||
custom protocols.
|
||||
|
||||
diff --git a/front_end/core/root/Runtime.ts b/front_end/core/root/Runtime.ts
|
||||
index d2c063a85e0fcd1e658be6709dd55dbcda5601a6..0dd32e1468296484976034d47ebb8b8632fa6835 100644
|
||||
--- a/front_end/core/root/Runtime.ts
|
||||
+++ b/front_end/core/root/Runtime.ts
|
||||
@@ -637,6 +637,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{
|
||||
* or guest mode, rather than a "normal" profile.
|
||||
*/
|
||||
isOffTheRecord: boolean,
|
||||
+ devToolsExtensionSchemes: readonly string[],
|
||||
devToolsEnableOriginBoundCookies: HostConfigEnableOriginBoundCookies,
|
||||
devToolsAnimationStylesInStylesTab: HostConfigAnimationStylesInStylesTab,
|
||||
thirdPartyCookieControls: HostConfigThirdPartyCookieControls,
|
||||
diff --git a/front_end/panels/common/ExtensionServer.ts b/front_end/panels/common/ExtensionServer.ts
|
||||
index 0a5ec620b135b128013d6ddbb5299f9a5813f122..0f8c04bb5c02c9f1ee3af785a60a2450c47fff58 100644
|
||||
--- a/front_end/panels/common/ExtensionServer.ts
|
||||
+++ b/front_end/panels/common/ExtensionServer.ts
|
||||
@@ -12,6 +12,7 @@ import * as Host from '../../core/host/host.js';
|
||||
import * as i18n from '../../core/i18n/i18n.js';
|
||||
import * as Platform from '../../core/platform/platform.js';
|
||||
import * as _ProtocolClient from '../../core/protocol_client/protocol_client.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
+import * as Root from '../../core/root/root.js';
|
||||
import * as SDK from '../../core/sdk/sdk.js';
|
||||
import type * as Protocol from '../../generated/protocol.js';
|
||||
import * as Bindings from '../../models/bindings/bindings.js';
|
||||
@@ -1607,7 +1608,8 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
return false;
|
||||
}
|
||||
|
||||
- if (!kPermittedSchemes.includes(parsedURL.protocol)) {
|
||||
+ if (!kPermittedSchemes.includes(parsedURL.protocol) &&
|
||||
+ !Root.Runtime.hostConfig.devToolsExtensionSchemes?.includes(parsedURL.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
chore_allow_customizing_microtask_policy_per_context.patch
|
||||
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Attard <sattard@anthropic.com>
|
||||
Date: Sun, 22 Mar 2026 10:51:26 +0000
|
||||
Subject: build: warn instead of abort on builtin PGO profile mismatch
|
||||
|
||||
Electron sets v8_enable_javascript_promise_hooks = true to support
|
||||
Node.js async_hooks (see node/src/env.cc SetPromiseHooks usage:
|
||||
https://github.com/nodejs/node/blob/abff716eaccd0c4f4949d1315cb057a45979649d/src/env.cc#L223-L236).
|
||||
This flag adds conditional branches to builtins-microtask-queue-gen.cc
|
||||
and promise-misc.tq, changing the control-flow graph hash of several
|
||||
Promise/async builtins. This invalidates V8's pre-generated PGO profile
|
||||
for those builtins (built with Chrome defaults where the flag is off).
|
||||
|
||||
Rather than disabling builtins PGO entirely, warn and skip mismatched
|
||||
builtins so all other builtins still benefit from PGO.
|
||||
|
||||
diff --git a/BUILD.gn b/BUILD.gn
|
||||
index 15de2179a0e5ce50d5c659a9d15a920c50124c3e..9fb3a69450bdcab42c2571e8b1f57c4f3c283d9a 100644
|
||||
--- a/BUILD.gn
|
||||
+++ b/BUILD.gn
|
||||
@@ -2764,9 +2764,11 @@ template("run_mksnapshot") {
|
||||
"--turbo-profiling-input",
|
||||
rebase_path(v8_builtins_profiling_log_file, root_build_dir),
|
||||
|
||||
- # Replace this with --warn-about-builtin-profile-data to see the full
|
||||
- # list of builtins with incompatible profiles.
|
||||
- "--abort-on-bad-builtin-profile-data",
|
||||
+ # Electron: Use warn instead of abort so that builtins whose control
|
||||
+ # flow is changed by Electron's build flags (e.g. RunMicrotasks via
|
||||
+ # v8_enable_javascript_promise_hooks) are skipped rather than failing
|
||||
+ # the build. All other builtins still receive PGO.
|
||||
+ "--warn-about-builtin-profile-data",
|
||||
]
|
||||
|
||||
if (!v8_enable_builtins_profiling && v8_enable_builtins_reordering) {
|
||||
@@ -6,6 +6,7 @@ Everything here should be project agnostic: it shouldn't rely on project's
|
||||
structure, or make assumptions about the passed arguments or calls' outcomes.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import posixpath
|
||||
@@ -18,7 +19,14 @@ sys.path.append(SCRIPT_DIR)
|
||||
|
||||
from patches import PATCH_FILENAME_PREFIX, is_patch_location_line
|
||||
|
||||
UPSTREAM_HEAD='refs/patches/upstream-head'
|
||||
# In gclient-new-workdir worktrees, .git/refs is symlinked back to the source
|
||||
# checkout, so a single fixed ref name would be shared (and clobbered) across
|
||||
# worktrees. Derive a per-checkout suffix from this script's absolute path so
|
||||
# each worktree records its own upstream head in the shared refs directory.
|
||||
_LEGACY_UPSTREAM_HEAD = 'refs/patches/upstream-head'
|
||||
UPSTREAM_HEAD = (
|
||||
_LEGACY_UPSTREAM_HEAD + '-' + hashlib.md5(SCRIPT_DIR.encode()).hexdigest()[:8]
|
||||
)
|
||||
|
||||
def is_repo_root(path):
|
||||
path_exists = os.path.exists(path)
|
||||
@@ -83,6 +91,8 @@ def import_patches(repo, ref=UPSTREAM_HEAD, **kwargs):
|
||||
"""same as am(), but we save the upstream HEAD so we can refer to it when we
|
||||
later export patches"""
|
||||
update_ref(repo=repo, ref=ref, newvalue='HEAD')
|
||||
if ref != _LEGACY_UPSTREAM_HEAD:
|
||||
update_ref(repo=repo, ref=_LEGACY_UPSTREAM_HEAD, newvalue='HEAD')
|
||||
am(repo=repo, **kwargs)
|
||||
|
||||
|
||||
@@ -102,19 +112,21 @@ def get_commit_count(repo, commit_range):
|
||||
|
||||
def guess_base_commit(repo, ref):
|
||||
"""Guess which commit the patches might be based on"""
|
||||
try:
|
||||
upstream_head = get_commit_for_ref(repo, ref)
|
||||
num_commits = get_commit_count(repo, upstream_head + '..')
|
||||
return [upstream_head, num_commits]
|
||||
except subprocess.CalledProcessError:
|
||||
args = [
|
||||
'git',
|
||||
'-C',
|
||||
repo,
|
||||
'describe',
|
||||
'--tags',
|
||||
]
|
||||
return subprocess.check_output(args).decode('utf-8').rsplit('-', 2)[0:2]
|
||||
for candidate in (ref, _LEGACY_UPSTREAM_HEAD):
|
||||
try:
|
||||
upstream_head = get_commit_for_ref(repo, candidate)
|
||||
num_commits = get_commit_count(repo, upstream_head + '..')
|
||||
return [upstream_head, num_commits]
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
args = [
|
||||
'git',
|
||||
'-C',
|
||||
repo,
|
||||
'describe',
|
||||
'--tags',
|
||||
]
|
||||
return subprocess.check_output(args).decode('utf-8').rsplit('-', 2)[0:2]
|
||||
|
||||
|
||||
def format_patch(repo, since):
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def npx(*npx_args):
|
||||
npx_env = os.environ.copy()
|
||||
npx_env['npm_config_yes'] = 'true'
|
||||
call_args = [__get_executable_name()] + list(npx_args)
|
||||
subprocess.check_call(call_args, env=npx_env)
|
||||
|
||||
|
||||
def __get_executable_name():
|
||||
executable = 'npx'
|
||||
if sys.platform == 'win32':
|
||||
executable += '.cmd'
|
||||
return executable
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
npx(*sys.argv[1:])
|
||||
@@ -6,7 +6,7 @@ const path = require('node:path');
|
||||
|
||||
const BASE = path.resolve(__dirname, '../..');
|
||||
const NAN_DIR = path.resolve(BASE, 'third_party', 'nan');
|
||||
const NPX_CMD = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
const NODE_GYP_BIN = path.join(NAN_DIR, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
|
||||
|
||||
const utils = require('./lib/utils');
|
||||
const { YARN_SCRIPT_PATH } = require('./yarn');
|
||||
@@ -19,14 +19,6 @@ const args = minimist(process.argv.slice(2), {
|
||||
string: ['only']
|
||||
});
|
||||
|
||||
const getNodeGypVersion = () => {
|
||||
const nanPackageJSONPath = path.join(NAN_DIR, 'package.json');
|
||||
const nanPackageJSON = JSON.parse(fs.readFileSync(nanPackageJSONPath, 'utf8'));
|
||||
const { devDependencies } = nanPackageJSON;
|
||||
const nodeGypVersion = devDependencies['node-gyp'];
|
||||
return nodeGypVersion || 'latest';
|
||||
};
|
||||
|
||||
async function main () {
|
||||
const outDir = utils.getOutDir({ shouldLog: true });
|
||||
const nodeDir = path.resolve(BASE, 'out', outDir, 'gen', 'node_headers');
|
||||
@@ -34,8 +26,7 @@ async function main () {
|
||||
npm_config_msvs_version: '2022',
|
||||
...process.env,
|
||||
npm_config_nodedir: nodeDir,
|
||||
npm_config_arch: process.env.NPM_CONFIG_ARCH,
|
||||
npm_config_yes: 'true'
|
||||
npm_config_arch: process.env.NPM_CONFIG_ARCH
|
||||
};
|
||||
|
||||
const clangDir = path.resolve(BASE, 'third_party', 'llvm-build', 'Release+Asserts', 'bin');
|
||||
@@ -105,30 +96,26 @@ async function main () {
|
||||
env.LDFLAGS = ldflags;
|
||||
}
|
||||
|
||||
const nodeGypVersion = getNodeGypVersion();
|
||||
const { status: buildStatus, signal } = cp.spawnSync(NPX_CMD, [`node-gyp@${nodeGypVersion}`, 'rebuild', '--verbose', '--directory', 'test', '-j', 'max'], {
|
||||
const { status: installStatus, signal: installSignal } = cp.spawnSync(process.execPath, [YARN_SCRIPT_PATH, 'install'], {
|
||||
env,
|
||||
cwd: NAN_DIR,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32'
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (installStatus !== 0 || installSignal != null) {
|
||||
console.error('Failed to install nan node_modules');
|
||||
return process.exit(installStatus !== 0 ? installStatus : installSignal);
|
||||
}
|
||||
|
||||
const { status: buildStatus, signal } = cp.spawnSync(process.execPath, [NODE_GYP_BIN, 'rebuild', '--verbose', '--directory', 'test', '-j', 'max'], {
|
||||
env,
|
||||
cwd: NAN_DIR,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (buildStatus !== 0 || signal != null) {
|
||||
console.error('Failed to build nan test modules');
|
||||
return process.exit(buildStatus !== 0 ? buildStatus : signal);
|
||||
}
|
||||
|
||||
const { status: installStatus, signal: installSignal } = cp.spawnSync(process.execPath, [YARN_SCRIPT_PATH, 'install'], {
|
||||
env,
|
||||
cwd: NAN_DIR,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
|
||||
if (installStatus !== 0 || installSignal != null) {
|
||||
console.error('Failed to install nan node_modules');
|
||||
return process.exit(installStatus !== 0 ? installStatus : installSignal);
|
||||
}
|
||||
|
||||
const onlyTests = args.only?.split(',');
|
||||
|
||||
const DISABLED_TESTS = new Set([
|
||||
|
||||
@@ -212,10 +212,15 @@ new Promise<string>((resolve, reject) => {
|
||||
});
|
||||
})
|
||||
.then((tarballPath) => {
|
||||
// TODO: Remove NPX
|
||||
const existingVersionJSON = childProcess.execSync(`npx npm@7 view ${rootPackageJson.name}@${currentElectronVersion} --json`).toString('utf-8');
|
||||
// It's possible this is a re-run and we already have published the package, if not we just publish like normal
|
||||
if (!existingVersionJSON) {
|
||||
let versionAlreadyPublished = false;
|
||||
try {
|
||||
childProcess.execSync(`npm view ${rootPackageJson.name}@${currentElectronVersion} --json`, { stdio: 'pipe' });
|
||||
versionAlreadyPublished = true;
|
||||
} catch (e: any) {
|
||||
if (!e.stdout?.toString().includes('E404')) throw e;
|
||||
}
|
||||
if (!versionAlreadyPublished) {
|
||||
childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag} --otp=${process.env.ELECTRON_NPM_OTP}`);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,9 +31,9 @@ PDB_LIST = [
|
||||
|
||||
PDB_LIST += glob.glob(os.path.join(RELEASE_DIR, '*.dll.pdb'))
|
||||
|
||||
NPX_CMD = "npx"
|
||||
SENTRY_CLI = os.path.join(ELECTRON_DIR, 'node_modules', '.bin', 'sentry-cli')
|
||||
if sys.platform == "win32":
|
||||
NPX_CMD += ".cmd"
|
||||
SENTRY_CLI += ".cmd"
|
||||
|
||||
|
||||
def main():
|
||||
@@ -48,11 +48,8 @@ def main():
|
||||
|
||||
for symbol_file in files:
|
||||
print("Generating Sentry src bundle for: " + symbol_file)
|
||||
npx_env = os.environ.copy()
|
||||
npx_env['npm_config_yes'] = 'true'
|
||||
subprocess.check_output([
|
||||
NPX_CMD, '@sentry/cli@1.62.0', 'difutil', 'bundle-sources',
|
||||
symbol_file], env=npx_env)
|
||||
SENTRY_CLI, 'difutil', 'bundle-sources', symbol_file])
|
||||
|
||||
files += glob.glob(SYMBOLS_DIR + '/*/*/*.src.zip')
|
||||
|
||||
|
||||
@@ -151,7 +151,10 @@ void OnTraceBufferUsageAvailable(
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise,
|
||||
float percent_full,
|
||||
size_t approximate_count) {
|
||||
auto dict = gin_helper::Dictionary::CreateEmpty(promise.isolate());
|
||||
v8::Isolate* isolate = promise.isolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
|
||||
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
|
||||
dict.Set("percentage", percent_full);
|
||||
dict.Set("value", approximate_count);
|
||||
|
||||
|
||||
@@ -147,7 +147,12 @@ gin::ObjectTemplateBuilder NativeTheme::GetObjectTemplateBuilder(
|
||||
&NativeTheme::ShouldUseInvertedColorScheme)
|
||||
.SetProperty("inForcedColorsMode", &NativeTheme::InForcedColorsMode)
|
||||
.SetProperty("prefersReducedTransparency",
|
||||
&NativeTheme::GetPrefersReducedTransparency);
|
||||
&NativeTheme::GetPrefersReducedTransparency)
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
.SetProperty("shouldDifferentiateWithoutColor",
|
||||
&NativeTheme::ShouldDifferentiateWithoutColor)
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
const char* NativeTheme::GetTypeName() {
|
||||
|
||||
@@ -56,6 +56,9 @@ class NativeTheme final : public gin_helper::DeprecatedWrappable<NativeTheme>,
|
||||
bool ShouldUseInvertedColorScheme();
|
||||
bool InForcedColorsMode();
|
||||
bool GetPrefersReducedTransparency();
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
bool ShouldDifferentiateWithoutColor();
|
||||
#endif
|
||||
|
||||
// ui::NativeThemeObserver:
|
||||
void OnNativeThemeUpdated(ui::NativeTheme* theme) override;
|
||||
|
||||
@@ -26,4 +26,9 @@ void NativeTheme::UpdateMacOSAppearanceForOverrideValue(
|
||||
[[NSApplication sharedApplication] setAppearance:new_appearance];
|
||||
}
|
||||
|
||||
bool NativeTheme::ShouldDifferentiateWithoutColor() {
|
||||
return [[NSWorkspace sharedWorkspace]
|
||||
accessibilityDisplayShouldDifferentiateWithoutColor];
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -38,6 +38,7 @@ struct SchemeOptions {
|
||||
bool corsEnabled = false;
|
||||
bool stream = false;
|
||||
bool codeCache = false;
|
||||
bool allowExtensions = false;
|
||||
};
|
||||
|
||||
struct CustomScheme {
|
||||
@@ -70,6 +71,7 @@ struct Converter<CustomScheme> {
|
||||
opt.Get("corsEnabled", &(out->options.corsEnabled));
|
||||
opt.Get("stream", &(out->options.stream));
|
||||
opt.Get("codeCache", &(out->options.codeCache));
|
||||
opt.Get("allowExtensions", &(out->options.allowExtensions));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -124,7 +126,7 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
|
||||
}
|
||||
|
||||
std::vector<std::string> secure_schemes, cspbypassing_schemes, fetch_schemes,
|
||||
service_worker_schemes, cors_schemes;
|
||||
service_worker_schemes, cors_schemes, extension_schemes;
|
||||
for (const auto& custom_scheme : custom_schemes) {
|
||||
// Register scheme to privileged list (https, wss, data, chrome-extension)
|
||||
if (custom_scheme.options.standard) {
|
||||
@@ -160,6 +162,10 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
|
||||
GetCodeCacheSchemes().push_back(custom_scheme.scheme);
|
||||
url::AddCodeCacheScheme(custom_scheme.scheme.c_str());
|
||||
}
|
||||
if (custom_scheme.options.allowExtensions) {
|
||||
extension_schemes.push_back(custom_scheme.scheme);
|
||||
url::AddExtensionScheme(custom_scheme.scheme.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const auto AppendSchemesToCmdLine = [](const std::string_view switch_name,
|
||||
@@ -179,6 +185,8 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
|
||||
AppendSchemesToCmdLine(electron::switches::kFetchSchemes, fetch_schemes);
|
||||
AppendSchemesToCmdLine(electron::switches::kServiceWorkerSchemes,
|
||||
service_worker_schemes);
|
||||
AppendSchemesToCmdLine(electron::switches::kExtensionSchemes,
|
||||
extension_schemes);
|
||||
AppendSchemesToCmdLine(electron::switches::kStandardSchemes,
|
||||
GetStandardSchemes());
|
||||
AppendSchemesToCmdLine(electron::switches::kStreamingSchemes,
|
||||
|
||||
@@ -1739,7 +1739,8 @@ bool WebContents::CheckMediaAccessPermission(
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
return permission_helper->CheckMediaAccessPermission(security_origin, type);
|
||||
return permission_helper->CheckMediaAccessPermission(render_frame_host,
|
||||
security_origin, type);
|
||||
}
|
||||
|
||||
void WebContents::RequestMediaAccessPermission(
|
||||
|
||||
@@ -555,7 +555,7 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
|
||||
if (process_type == ::switches::kUtilityProcess ||
|
||||
process_type == ::switches::kRendererProcess) {
|
||||
// Copy following switches to child process.
|
||||
static constexpr std::array<const char*, 10U> kCommonSwitchNames = {
|
||||
static constexpr std::array<const char*, 11U> kCommonSwitchNames = {
|
||||
switches::kStandardSchemes.c_str(),
|
||||
switches::kEnableSandbox.c_str(),
|
||||
switches::kSecureSchemes.c_str(),
|
||||
@@ -565,7 +565,8 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
|
||||
switches::kServiceWorkerSchemes.c_str(),
|
||||
switches::kStreamingSchemes.c_str(),
|
||||
switches::kNoStdioInit.c_str(),
|
||||
switches::kCodeCacheSchemes.c_str()};
|
||||
switches::kCodeCacheSchemes.c_str(),
|
||||
switches::kExtensionSchemes.c_str()};
|
||||
command_line->CopySwitchesFrom(*base::CommandLine::ForCurrentProcess(),
|
||||
kCommonSwitchNames);
|
||||
if (process_type == ::switches::kUtilityProcess ||
|
||||
|
||||
@@ -127,6 +127,10 @@
|
||||
#include "shell/common/plugin_info.h"
|
||||
#endif // BUILDFLAG(ENABLE_PLUGINS)
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
#include "components/printing/common/print_dialog_linux_factory.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
@@ -415,6 +419,10 @@ void ElectronBrowserMainParts::ToolkitInitialized() {
|
||||
|
||||
ui::LinuxUi::SetInstance(linux_ui);
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
print_dialog_factory_ = std::make_unique<printing::PrintDialogLinuxFactory>();
|
||||
#endif
|
||||
|
||||
// Cursor theme changes are tracked by LinuxUI (via a CursorThemeManager
|
||||
// implementation). Start observing them once it's initialized.
|
||||
ui::CursorFactory::GetInstance()->ObserveThemeChanges();
|
||||
|
||||
@@ -14,8 +14,13 @@
|
||||
#include "content/public/browser/browser_main_parts.h"
|
||||
#include "electron/buildflags/buildflags.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "printing/buildflags/buildflags.h"
|
||||
#include "services/device/public/mojom/geolocation_control.mojom.h"
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
#include "printing/printing_context_linux.h"
|
||||
#endif
|
||||
|
||||
class BrowserProcessImpl;
|
||||
class IconManager;
|
||||
|
||||
@@ -179,6 +184,11 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
|
||||
std::unique_ptr<display::ScopedNativeScreen> screen_;
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
std::unique_ptr<printing::PrintingContextLinux::PrintDialogFactory>
|
||||
print_dialog_factory_;
|
||||
#endif
|
||||
|
||||
static ElectronBrowserMainParts* self_;
|
||||
};
|
||||
|
||||
|
||||
@@ -87,13 +87,6 @@ void InitializeFeatureList() {
|
||||
std::string(",") + sandbox::policy::features::kNetworkServiceSandbox.name;
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
disable_features +=
|
||||
// MacWebContentsOcclusion is causing some odd visibility
|
||||
// issues with multiple web contents
|
||||
std::string(",") + features::kMacWebContentsOcclusion.name;
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(ENABLE_PDF_VIEWER)
|
||||
// Enable window.showSaveFilePicker api for saving pdf files.
|
||||
// Refs https://issues.chromium.org/issues/373852607
|
||||
|
||||
@@ -697,7 +697,11 @@ void FileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess(
|
||||
content::GlobalRenderFrameHostId frame_id,
|
||||
base::OnceCallback<void(SensitiveEntryResult)> callback) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
callback_map_.try_emplace(path_info.path, std::move(callback));
|
||||
|
||||
auto [it, inserted] = callback_map_.try_emplace(path_info.path);
|
||||
it->second.push_back(std::move(callback));
|
||||
if (!inserted)
|
||||
return;
|
||||
|
||||
auto after_blocklist_check_callback = base::BindOnce(
|
||||
&FileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist,
|
||||
@@ -769,8 +773,11 @@ void FileSystemAccessPermissionContext::PerformAfterWriteChecks(
|
||||
void FileSystemAccessPermissionContext::RunRestrictedPathCallback(
|
||||
const base::FilePath& file_path,
|
||||
SensitiveEntryResult result) {
|
||||
if (auto val = callback_map_.extract(file_path))
|
||||
std::move(val.mapped()).Run(result);
|
||||
if (auto val = callback_map_.extract(file_path)) {
|
||||
for (auto& callback : val.mapped()) {
|
||||
std::move(callback).Run(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemAccessPermissionContext::OnRestrictedPathResult(
|
||||
|
||||
@@ -196,7 +196,8 @@ class FileSystemAccessPermissionContext
|
||||
|
||||
std::map<url::Origin, base::DictValue> id_pathinfo_map_;
|
||||
|
||||
std::map<base::FilePath, base::OnceCallback<void(SensitiveEntryResult)>>
|
||||
std::map<base::FilePath,
|
||||
std::vector<base::OnceCallback<void(SensitiveEntryResult)>>>
|
||||
callback_map_;
|
||||
|
||||
std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
|
||||
|
||||
@@ -136,24 +136,10 @@ NativeWindow::~NativeWindow() {
|
||||
|
||||
void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
|
||||
// Setup window from options.
|
||||
if (int x, y; options.Get(options::kX, &x) && options.Get(options::kY, &y)) {
|
||||
SetPosition(gfx::Point{x, y});
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// FIXME(felixrieseberg): Dirty, dirty workaround for
|
||||
// https://github.com/electron/electron/issues/10862
|
||||
// Somehow, we need to call `SetBounds` twice to get
|
||||
// usable results. The root cause is still unknown.
|
||||
SetPosition(gfx::Point{x, y});
|
||||
#endif
|
||||
} else if (bool center; options.Get(options::kCenter, ¢er) && center) {
|
||||
Center();
|
||||
}
|
||||
|
||||
const bool use_content_size =
|
||||
options.ValueOrDefault(options::kUseContentSize, false);
|
||||
|
||||
// On Linux and Window we may already have maximum size defined.
|
||||
// On Linux and Windows we may already have minimum and maximum size defined.
|
||||
extensions::SizeConstraints size_constraints(
|
||||
use_content_size ? GetContentSizeConstraints() : GetSizeConstraints());
|
||||
|
||||
@@ -180,10 +166,32 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
|
||||
size_constraints.set_maximum_size(gfx::Size(max_width, max_height));
|
||||
|
||||
if (use_content_size) {
|
||||
gfx::Size clamped = size_constraints.ClampSize(GetContentSize());
|
||||
if (clamped != GetContentSize()) {
|
||||
SetContentSize(clamped);
|
||||
}
|
||||
SetContentSizeConstraints(size_constraints);
|
||||
} else {
|
||||
gfx::Size clamped = size_constraints.ClampSize(GetSize());
|
||||
if (clamped != GetSize()) {
|
||||
SetSize(clamped);
|
||||
}
|
||||
SetSizeConstraints(size_constraints);
|
||||
}
|
||||
|
||||
if (int x, y; options.Get(options::kX, &x) && options.Get(options::kY, &y)) {
|
||||
SetPosition(gfx::Point{x, y});
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// FIXME(felixrieseberg): Dirty, dirty workaround for
|
||||
// https://github.com/electron/electron/issues/10862
|
||||
// Somehow, we need to call `SetBounds` twice to get
|
||||
// usable results. The root cause is still unknown.
|
||||
SetPosition(gfx::Point{x, y});
|
||||
#endif
|
||||
} else if (bool center; options.Get(options::kCenter, ¢er) && center) {
|
||||
Center();
|
||||
}
|
||||
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
|
||||
if (bool val; options.Get(options::kClosable, &val))
|
||||
SetClosable(val);
|
||||
|
||||
@@ -45,7 +45,7 @@ struct NotificationOptions {
|
||||
std::u16string timeout_type;
|
||||
std::u16string reply_placeholder;
|
||||
std::u16string sound;
|
||||
std::u16string urgency; // Linux
|
||||
std::u16string urgency; // Linux/Windows
|
||||
std::vector<NotificationAction> actions;
|
||||
std::u16string close_button_text;
|
||||
std::u16string toast_xml;
|
||||
|
||||
@@ -72,7 +72,8 @@ std::wstring NotificationPresenterWin::SaveIconToFilesystem(
|
||||
|
||||
std::string filename;
|
||||
if (origin.is_valid()) {
|
||||
filename = base::SHA1HashString(origin.spec()) + ".png";
|
||||
const auto hash = base::SHA1HashString(origin.spec());
|
||||
filename = base::HexEncode(hash) + ".png";
|
||||
} else {
|
||||
const int64_t now_usec = base::Time::Now().since_origin().InMicroseconds();
|
||||
filename = base::NumberToString(now_usec) + ".png";
|
||||
|
||||
@@ -96,6 +96,21 @@ std::wstring GetExecutablePath() {
|
||||
return std::wstring(path, len);
|
||||
}
|
||||
|
||||
// Installers sometimes put the running app in a versioned subfolder and ship a
|
||||
// stub with the same filename one directory up. Point the Start Menu shortcut
|
||||
// at the stub when it exists so toast activation and updates keep a stable
|
||||
// launch path.
|
||||
std::wstring GetShortcutTargetPath(const std::wstring& exe_path) {
|
||||
if (exe_path.empty())
|
||||
return L"";
|
||||
base::FilePath exe_fp(exe_path);
|
||||
base::FilePath stub_candidate =
|
||||
exe_fp.DirName().DirName().Append(exe_fp.BaseName());
|
||||
if (base::PathExists(stub_candidate))
|
||||
return stub_candidate.value();
|
||||
return exe_path;
|
||||
}
|
||||
|
||||
void EnsureCLSIDRegistry() {
|
||||
std::wstring exe = GetExecutablePath();
|
||||
if (exe.empty())
|
||||
@@ -116,7 +131,10 @@ void EnsureCLSIDRegistry() {
|
||||
server_key.WriteValue(nullptr, exe.c_str());
|
||||
}
|
||||
|
||||
bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
|
||||
bool ExistingShortcutValid(const base::FilePath& lnk_path,
|
||||
PCWSTR aumid,
|
||||
const std::wstring& expected_target_path,
|
||||
const std::wstring& expected_working_dir) {
|
||||
if (!base::PathExists(lnk_path))
|
||||
return false;
|
||||
Microsoft::WRL::ComPtr<IShellLink> existing;
|
||||
@@ -128,6 +146,31 @@ bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
|
||||
FAILED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// After an auto-update the .lnk may still have the correct AUMID/CLSID but
|
||||
// point at an old install path; treat that as invalid so we rewrite it.
|
||||
wchar_t target_path[MAX_PATH];
|
||||
if (FAILED(existing->GetPath(target_path, MAX_PATH, nullptr, SLGP_RAWPATH)))
|
||||
return false;
|
||||
if (base::FilePath::CompareIgnoreCase(
|
||||
base::FilePath(expected_target_path).value(),
|
||||
base::FilePath(target_path).value()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t work_dir[MAX_PATH];
|
||||
work_dir[0] = L'\0';
|
||||
if (FAILED(existing->GetWorkingDirectory(work_dir, MAX_PATH)))
|
||||
return false;
|
||||
base::FilePath expected_cwd =
|
||||
base::FilePath(expected_working_dir).NormalizePathSeparators();
|
||||
base::FilePath actual_cwd =
|
||||
base::FilePath(work_dir).NormalizePathSeparators();
|
||||
if (base::FilePath::CompareIgnoreCase(expected_cwd.value(),
|
||||
actual_cwd.value()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IPropertyStore> store;
|
||||
if (FAILED(existing.As(&store)))
|
||||
return false;
|
||||
@@ -157,6 +200,7 @@ void EnsureShortcut() {
|
||||
std::wstring exe = GetExecutablePath();
|
||||
if (exe.empty())
|
||||
return;
|
||||
std::wstring shortcut_target = GetShortcutTargetPath(exe);
|
||||
|
||||
PWSTR programs_path = nullptr;
|
||||
if (FAILED(
|
||||
@@ -195,18 +239,20 @@ void EnsureShortcut() {
|
||||
}
|
||||
}
|
||||
|
||||
if (ExistingShortcutValid(lnk_path, aumid))
|
||||
const std::wstring expected_working_dir =
|
||||
base::FilePath(exe).DirName().value();
|
||||
if (ExistingShortcutValid(lnk_path, aumid, shortcut_target,
|
||||
expected_working_dir))
|
||||
return;
|
||||
|
||||
Microsoft::WRL::ComPtr<IShellLink> shell_link;
|
||||
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&shell_link))))
|
||||
return;
|
||||
shell_link->SetPath(exe.c_str());
|
||||
shell_link->SetPath(shortcut_target.c_str());
|
||||
shell_link->SetArguments(L"");
|
||||
shell_link->SetDescription(product_name.c_str());
|
||||
shell_link->SetWorkingDirectory(
|
||||
base::FilePath(exe).DirName().value().c_str());
|
||||
shell_link->SetWorkingDirectory(expected_working_dir.c_str());
|
||||
|
||||
Microsoft::WRL::ComPtr<IPropertyStore> prop_store;
|
||||
if (SUCCEEDED(shell_link.As(&prop_store))) {
|
||||
|
||||
@@ -280,8 +280,9 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
|
||||
// Continue to create the toast notification
|
||||
ComPtr<ABI::Windows::UI::Notifications::IToastNotification>
|
||||
toast_notification;
|
||||
if (!CreateToastNotification(toast_xml, notification_id, weak_notification,
|
||||
ui_task_runner, &toast_notification)) {
|
||||
if (!CreateToastNotification(toast_xml, options, notification_id,
|
||||
weak_notification, ui_task_runner,
|
||||
&toast_notification)) {
|
||||
return; // Error already posted to UI thread
|
||||
}
|
||||
|
||||
@@ -349,6 +350,7 @@ bool WindowsToastNotification::CreateToastXmlDocument(
|
||||
// returns the created notification via out parameter.
|
||||
bool WindowsToastNotification::CreateToastNotification(
|
||||
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> toast_xml,
|
||||
const NotificationOptions& options,
|
||||
const std::string& notification_id,
|
||||
base::WeakPtr<Notification> weak_notification,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
|
||||
@@ -416,6 +418,27 @@ bool WindowsToastNotification::CreateToastNotification(
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<winui::Notifications::IToastNotification4> toast4;
|
||||
hr = (*toast_notification)->QueryInterface(IID_PPV_ARGS(&toast4));
|
||||
if (SUCCEEDED(hr)) {
|
||||
winui::Notifications::ToastNotificationPriority priority =
|
||||
winui::Notifications::ToastNotificationPriority::
|
||||
ToastNotificationPriority_Default;
|
||||
if (options.urgency == u"critical") {
|
||||
priority = winui::Notifications::ToastNotificationPriority::
|
||||
ToastNotificationPriority_High;
|
||||
}
|
||||
|
||||
hr = toast4->put_Priority(priority);
|
||||
if (FAILED(hr)) {
|
||||
std::string err = base::StrCat({"WinAPI: Setting priority failed, ERROR ",
|
||||
FailureResultToString(hr)});
|
||||
DebugLog(err);
|
||||
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ class WindowsToastNotification : public Notification {
|
||||
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner);
|
||||
static bool CreateToastNotification(
|
||||
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> toast_xml,
|
||||
const NotificationOptions& options,
|
||||
const std::string& notification_id,
|
||||
base::WeakPtr<Notification> weak_notification,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
|
||||
|
||||
@@ -59,6 +59,8 @@ gfx::Size GetDefaultPrinterDPI(const std::u16string& device_name) {
|
||||
GtkPrintSettings* print_settings = gtk_print_settings_new();
|
||||
int dpi = gtk_print_settings_get_resolution(print_settings);
|
||||
g_object_unref(print_settings);
|
||||
if (dpi <= 0)
|
||||
dpi = printing::kDefaultPdfDpi;
|
||||
return {dpi, dpi};
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ bool ElectronSerialDelegate::CanRequestPortPermission(
|
||||
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
return permission_helper->CheckSerialAccessPermission(
|
||||
frame->GetLastCommittedOrigin());
|
||||
return permission_helper->CheckSerialAccessPermission(frame);
|
||||
}
|
||||
|
||||
bool ElectronSerialDelegate::HasPortPermission(
|
||||
|
||||
@@ -478,7 +478,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
|
||||
if (![represented
|
||||
isKindOfClass:[WeakPtrToElectronMenuModelAsNSObject class]]) {
|
||||
NSLog(@"representedObject is not a WeakPtrToElectronMenuModelAsNSObject");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,14 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
|
||||
#pragma mark - NSWindowDelegate
|
||||
|
||||
- (void)windowDidChangeOcclusionState:(NSNotification*)notification {
|
||||
// Chromium's WebContentsOcclusionCheckerMac posts synthetic occlusion
|
||||
// notifications tagged with its class name in userInfo. These reflect the
|
||||
// checker's manual frame-intersection heuristic, not an actual macOS
|
||||
// occlusion state change, so the real occlusionState hasn't changed and
|
||||
// emitting show/hide in response would be spurious.
|
||||
if (notification.userInfo[@"WebContentsOcclusionCheckerMac"] != nil)
|
||||
return;
|
||||
|
||||
// notification.object is the window that changed its state.
|
||||
// It's safe to use self.window instead if you don't assign one delegate to
|
||||
// many windows
|
||||
|
||||
@@ -239,7 +239,9 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
|
||||
if (ui::OzonePlatform::GetInstance()->IsWindowCompositingSupported()) {
|
||||
// Set the opaque region.
|
||||
std::vector<gfx::Rect> opaque_region;
|
||||
if (IsShowingFrame(window_state)) {
|
||||
if (native_window_view_->IsTranslucent()) {
|
||||
// Leave opaque_region empty.
|
||||
} else if (IsShowingFrame(window_state)) {
|
||||
// The opaque region is a list of rectangles that contain only fully
|
||||
// opaque pixels of the window. We need to convert the clipping
|
||||
// rounded-rect into this format.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "base/base64.h"
|
||||
#include "base/containers/fixed_flat_set.h"
|
||||
#include "base/containers/span.h"
|
||||
#include "base/dcheck_is_on.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
@@ -58,6 +59,7 @@
|
||||
#include "third_party/blink/public/common/page/page_zoom.h"
|
||||
#include "ui/display/display.h"
|
||||
#include "ui/display/screen.h"
|
||||
#include "url/url_util.h"
|
||||
#include "v8/include/v8.h"
|
||||
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
@@ -159,6 +161,13 @@ void OnOpenItemComplete(const base::FilePath& path, const std::string& result) {
|
||||
constexpr base::TimeDelta kInitialBackoffDelay = base::Milliseconds(250);
|
||||
constexpr base::TimeDelta kMaxBackoffDelay = base::Seconds(10);
|
||||
|
||||
constexpr auto kValidDockStates = base::MakeFixedFlatSet<std::string_view>(
|
||||
{"bottom", "left", "right", "undocked"});
|
||||
|
||||
bool IsValidDockState(const std::string& state) {
|
||||
return kValidDockStates.contains(state);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class InspectableWebContents::NetworkResourceLoader
|
||||
@@ -393,7 +402,7 @@ void InspectableWebContents::SetDockState(const std::string& state) {
|
||||
can_dock_ = false;
|
||||
} else {
|
||||
can_dock_ = true;
|
||||
dock_state_ = state;
|
||||
dock_state_ = IsValidDockState(state) ? state : "right";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +567,13 @@ void InspectableWebContents::LoadCompleted() {
|
||||
pref_service_->GetDict(kDevToolsPreferences);
|
||||
const std::string* current_dock_state =
|
||||
prefs.FindString("currentDockState");
|
||||
base::RemoveChars(*current_dock_state, "\"", &dock_state_);
|
||||
if (current_dock_state) {
|
||||
std::string sanitized;
|
||||
base::RemoveChars(*current_dock_state, "\"", &sanitized);
|
||||
dock_state_ = IsValidDockState(sanitized) ? sanitized : "right";
|
||||
} else {
|
||||
dock_state_ = "right";
|
||||
}
|
||||
}
|
||||
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
|
||||
auto* api_web_contents = api::WebContents::From(GetWebContents());
|
||||
@@ -869,6 +884,13 @@ void InspectableWebContents::GetSyncInformation(DispatchCallback callback) {
|
||||
|
||||
void InspectableWebContents::GetHostConfig(DispatchCallback callback) {
|
||||
base::DictValue response_dict;
|
||||
|
||||
base::ListValue extension_schemes;
|
||||
for (const std::string& scheme : url::GetExtensionSchemes())
|
||||
extension_schemes.Append(scheme + ":");
|
||||
response_dict.Set("devToolsExtensionSchemes",
|
||||
base::Value(std::move(extension_schemes)));
|
||||
|
||||
base::Value response = base::Value(std::move(response_dict));
|
||||
std::move(callback).Run(&response);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
|
||||
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
|
||||
gfx::Rect client_bounds = bounds();
|
||||
if (!frame_->IsFullscreen()) {
|
||||
client_bounds.Inset(RestoredFrameBorderInsets());
|
||||
client_bounds.Inset(linux_frame_layout_->FrameBorderInsets(false));
|
||||
client_bounds.Inset(
|
||||
gfx::Insets::TLBR(GetTitlebarBounds().height(), 0, 0, 0));
|
||||
}
|
||||
@@ -236,6 +236,21 @@ void ClientFrameViewLinux::Layout(PassKey) {
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
|
||||
if (frame_->IsFullscreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame_->IsMaximized()) {
|
||||
// Some GTK themes (Breeze) still render shadow/border assets when
|
||||
// maximized, and we don't need a border when maximized anyway. Chromium
|
||||
// switches on this too: OpaqueBrowserFrameView::PaintMaximizedFrameBorder.
|
||||
PaintMaximizedFrameBorder(canvas);
|
||||
} else {
|
||||
PaintRestoredFrameBorder(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::PaintRestoredFrameBorder(gfx::Canvas* canvas) {
|
||||
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
|
||||
frame_provider->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
|
||||
@@ -243,6 +258,18 @@ void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
|
||||
}
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::PaintMaximizedFrameBorder(gfx::Canvas* canvas) {
|
||||
ui::NativeTheme::FrameTopAreaExtraParams frame_top_area;
|
||||
frame_top_area.use_custom_frame = true;
|
||||
frame_top_area.is_active = ShouldPaintAsActive();
|
||||
frame_top_area.default_background_color = SK_ColorTRANSPARENT;
|
||||
ui::NativeTheme::ExtraParams params(frame_top_area);
|
||||
GetNativeTheme()->Paint(
|
||||
canvas->sk_canvas(), GetColorProvider(), ui::NativeTheme::kFrameTopArea,
|
||||
ui::NativeTheme::kNormal,
|
||||
gfx::Rect(0, 0, width(), GetTitlebarBounds().bottom()), params);
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::PaintAsActiveChanged() {
|
||||
UpdateThemeValues();
|
||||
}
|
||||
@@ -251,23 +278,15 @@ void ClientFrameViewLinux::UpdateThemeValues() {
|
||||
gtk::GtkCssContext window_context =
|
||||
gtk::AppendCssNodeToStyleContext({}, "window.background.csd");
|
||||
gtk::GtkCssContext headerbar_context = gtk::AppendCssNodeToStyleContext(
|
||||
{}, "headerbar.default-decoration.titlebar");
|
||||
window_context, "headerbar.default-decoration.titlebar");
|
||||
gtk::GtkCssContext title_context =
|
||||
gtk::AppendCssNodeToStyleContext(headerbar_context, "label.title");
|
||||
gtk::GtkCssContext button_context = gtk::AppendCssNodeToStyleContext(
|
||||
headerbar_context, "button.image-button");
|
||||
|
||||
gtk_style_context_set_parent(headerbar_context, window_context);
|
||||
gtk_style_context_set_parent(title_context, headerbar_context);
|
||||
gtk_style_context_set_parent(button_context, headerbar_context);
|
||||
|
||||
// ShouldPaintAsActive asks the widget, so assume active if the widget is not
|
||||
// set yet.
|
||||
if (GetWidget() != nullptr && !ShouldPaintAsActive()) {
|
||||
gtk_style_context_set_state(window_context, GTK_STATE_FLAG_BACKDROP);
|
||||
gtk_style_context_set_state(headerbar_context, GTK_STATE_FLAG_BACKDROP);
|
||||
gtk_style_context_set_state(title_context, GTK_STATE_FLAG_BACKDROP);
|
||||
gtk_style_context_set_state(button_context, GTK_STATE_FLAG_BACKDROP);
|
||||
}
|
||||
|
||||
theme_values_.window_border_radius =
|
||||
@@ -281,10 +300,6 @@ void ClientFrameViewLinux::UpdateThemeValues() {
|
||||
theme_values_.title_color = gtk::GtkStyleContextGetColor(title_context);
|
||||
theme_values_.title_padding = gtk::GtkStyleContextGetPadding(title_context);
|
||||
|
||||
gtk::GtkStyleContextGet(button_context, "min-height",
|
||||
&theme_values_.button_min_size, nullptr);
|
||||
theme_values_.button_padding = gtk::GtkStyleContextGetPadding(button_context);
|
||||
|
||||
title_->SetEnabledColor(theme_values_.title_color);
|
||||
|
||||
InvalidateLayout();
|
||||
@@ -299,8 +314,9 @@ ClientFrameViewLinux::GetButtonTypeToSkip() const {
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::UpdateButtonImages() {
|
||||
nav_button_provider_->RedrawImages(theme_values_.button_min_size,
|
||||
frame_->IsMaximized(),
|
||||
int top_area_height = theme_values_.titlebar_min_height +
|
||||
theme_values_.titlebar_padding.height();
|
||||
nav_button_provider_->RedrawImages(top_area_height, frame_->IsMaximized(),
|
||||
ShouldPaintAsActive());
|
||||
|
||||
ui::NavButtonProvider::FrameButtonDisplayType skip_type =
|
||||
@@ -368,7 +384,14 @@ void ClientFrameViewLinux::LayoutButtonsOnSide(
|
||||
|
||||
button->button->SetVisible(true);
|
||||
|
||||
int button_width = theme_values_.button_min_size;
|
||||
// CSS min-size/height/width is not enough to determine the actual size of
|
||||
// the buttons, so we sample the rendered image. See Chromium's
|
||||
// BrowserFrameViewLinuxNative::MaybeUpdateCachedFrameButtonImages.
|
||||
int button_width =
|
||||
nav_button_provider_
|
||||
->GetImage(button->type,
|
||||
ui::NavButtonProvider::ButtonState::kNormal)
|
||||
.width();
|
||||
int next_button_offset =
|
||||
button_width + nav_button_provider_->GetInterNavButtonSpacing();
|
||||
|
||||
@@ -404,7 +427,7 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const {
|
||||
std::max(font_height, theme_values_.titlebar_min_height) +
|
||||
GetTitlebarContentInsets().height();
|
||||
|
||||
gfx::Insets decoration_insets = RestoredFrameBorderInsets();
|
||||
gfx::Insets decoration_insets = linux_frame_layout_->FrameBorderInsets(false);
|
||||
|
||||
// We add the inset height here, so the .Inset() that follows won't reduce it
|
||||
// to be too small.
|
||||
|
||||
@@ -91,12 +91,11 @@ class ClientFrameViewLinux : public FramelessView,
|
||||
|
||||
SkColor title_color;
|
||||
gfx::Insets title_padding;
|
||||
|
||||
int button_min_size;
|
||||
gfx::Insets button_padding;
|
||||
};
|
||||
|
||||
void PaintAsActiveChanged();
|
||||
void PaintRestoredFrameBorder(gfx::Canvas* canvas);
|
||||
void PaintMaximizedFrameBorder(gfx::Canvas* canvas);
|
||||
|
||||
void UpdateThemeValues();
|
||||
|
||||
|
||||
@@ -83,6 +83,12 @@ gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
|
||||
return gfx::Insets();
|
||||
}
|
||||
|
||||
gfx::Insets LinuxFrameLayout::FrameBorderInsets(bool restored) const {
|
||||
return !restored && (window_->IsMaximized() || window_->IsFullscreen())
|
||||
? gfx::Insets()
|
||||
: RestoredFrameBorderInsets();
|
||||
}
|
||||
|
||||
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(kResizeInsideBoundsSize);
|
||||
}
|
||||
@@ -106,7 +112,7 @@ void LinuxFrameLayout::set_tiled(bool tiled) {
|
||||
|
||||
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
|
||||
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
|
||||
bounds.Inset(RestoredFrameBorderInsets());
|
||||
bounds.Inset(FrameBorderInsets(false));
|
||||
return bounds;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ class LinuxFrameLayout {
|
||||
CSDStyle csd_style);
|
||||
|
||||
// Insets from the transparent widget border to the opaque part of the window.
|
||||
// Returns empty insets when maximized or fullscreen unless |restored| is
|
||||
// true. Matches Chromium's OpaqueBrowserFrameViewLayout::FrameBorderInsets.
|
||||
gfx::Insets FrameBorderInsets(bool restored) const;
|
||||
virtual gfx::Insets RestoredFrameBorderInsets() const;
|
||||
// Insets for parts of the surface that should be counted for user input.
|
||||
virtual gfx::Insets GetInputInsets() const;
|
||||
|
||||
@@ -203,8 +203,14 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
|
||||
if (frame()->IsFullscreen())
|
||||
return;
|
||||
|
||||
if (window()->IsWindowControlsOverlayEnabled())
|
||||
UpdateFrameCaptionButtons();
|
||||
|
||||
if (window()->IsTranslucent())
|
||||
return;
|
||||
|
||||
const bool active = ShouldPaintAsActive();
|
||||
const gfx::Insets border = RestoredFrameBorderInsets();
|
||||
const gfx::Insets border = FrameBorderInsets(false);
|
||||
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
|
||||
gfx::RectF bounds_dip(GetLocalBounds());
|
||||
if (showing_shadow) {
|
||||
@@ -228,11 +234,6 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
|
||||
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
|
||||
showing_shadow, active, border, shadow_values,
|
||||
linux_frame_layout_->tiled());
|
||||
|
||||
if (!window()->IsWindowControlsOverlayEnabled())
|
||||
return;
|
||||
|
||||
UpdateFrameCaptionButtons();
|
||||
}
|
||||
|
||||
void OpaqueFrameView::PaintAsActiveChanged() {
|
||||
@@ -341,9 +342,7 @@ views::Button* OpaqueFrameView::CreateButton(
|
||||
}
|
||||
|
||||
gfx::Insets OpaqueFrameView::FrameBorderInsets(bool restored) const {
|
||||
return !restored && IsFrameCondensed()
|
||||
? gfx::Insets()
|
||||
: linux_frame_layout_->RestoredFrameBorderInsets();
|
||||
return linux_frame_layout_->FrameBorderInsets(restored);
|
||||
}
|
||||
|
||||
int OpaqueFrameView::FrameTopBorderThickness(bool restored) const {
|
||||
|
||||
@@ -228,14 +228,14 @@ void WebContentsPermissionHelper::RequestPermission(
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckPermission(
|
||||
content::RenderFrameHost* requesting_frame,
|
||||
blink::PermissionType permission,
|
||||
base::DictValue details) const {
|
||||
auto* rfh = web_contents_->GetPrimaryMainFrame();
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
|
||||
auto origin = web_contents_->GetLastCommittedURL();
|
||||
return permission_manager->CheckPermissionWithDetails(permission, rfh, origin,
|
||||
std::move(details));
|
||||
auto origin = requesting_frame->GetLastCommittedOrigin().GetURL();
|
||||
return permission_manager->CheckPermissionWithDetails(
|
||||
permission, requesting_frame, origin, std::move(details));
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::RequestFullscreenPermission(
|
||||
@@ -313,6 +313,7 @@ void WebContentsPermissionHelper::RequestOpenExternalPermission(
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckMediaAccessPermission(
|
||||
content::RenderFrameHost* requesting_frame,
|
||||
const url::Origin& security_origin,
|
||||
blink::mojom::MediaStreamType type) const {
|
||||
base::DictValue details;
|
||||
@@ -321,14 +322,16 @@ bool WebContentsPermissionHelper::CheckMediaAccessPermission(
|
||||
auto blink_type = type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE
|
||||
? blink::PermissionType::AUDIO_CAPTURE
|
||||
: blink::PermissionType::VIDEO_CAPTURE;
|
||||
return CheckPermission(blink_type, std::move(details));
|
||||
return CheckPermission(requesting_frame, blink_type, std::move(details));
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckSerialAccessPermission(
|
||||
const url::Origin& embedding_origin) const {
|
||||
content::RenderFrameHost* requesting_frame) const {
|
||||
base::DictValue details;
|
||||
details.Set("securityOrigin", embedding_origin.GetURL().spec());
|
||||
return CheckPermission(blink::PermissionType::SERIAL, std::move(details));
|
||||
details.Set("securityOrigin",
|
||||
requesting_frame->GetLastCommittedOrigin().GetURL().spec());
|
||||
return CheckPermission(requesting_frame, blink::PermissionType::SERIAL,
|
||||
std::move(details));
|
||||
}
|
||||
|
||||
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper);
|
||||
|
||||
@@ -47,9 +47,11 @@ class WebContentsPermissionHelper
|
||||
const GURL& url);
|
||||
|
||||
// Synchronous Checks
|
||||
bool CheckMediaAccessPermission(const url::Origin& security_origin,
|
||||
bool CheckMediaAccessPermission(content::RenderFrameHost* requesting_frame,
|
||||
const url::Origin& security_origin,
|
||||
blink::mojom::MediaStreamType type) const;
|
||||
bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
|
||||
bool CheckSerialAccessPermission(
|
||||
content::RenderFrameHost* requesting_frame) const;
|
||||
|
||||
private:
|
||||
explicit WebContentsPermissionHelper(content::WebContents* web_contents);
|
||||
@@ -61,7 +63,8 @@ class WebContentsPermissionHelper
|
||||
bool user_gesture = false,
|
||||
base::DictValue details = {});
|
||||
|
||||
bool CheckPermission(blink::PermissionType permission,
|
||||
bool CheckPermission(content::RenderFrameHost* requesting_frame,
|
||||
blink::PermissionType permission,
|
||||
base::DictValue details) const;
|
||||
|
||||
// TODO(clavin): refactor to use the WebContents provided by the
|
||||
|
||||
@@ -266,7 +266,11 @@ gfx::Image Clipboard::ReadImage(gin::Arguments* const args) {
|
||||
[](std::optional<gfx::Image>* image, base::RepeatingClosure cb,
|
||||
const std::vector<uint8_t>& result) {
|
||||
SkBitmap bitmap = gfx::PNGCodec::Decode(result);
|
||||
image->emplace(gfx::Image::CreateFrom1xBitmap(bitmap));
|
||||
if (bitmap.isNull()) {
|
||||
image->emplace();
|
||||
} else {
|
||||
image->emplace(gfx::Image::CreateFrom1xBitmap(bitmap));
|
||||
}
|
||||
std::move(cb).Run();
|
||||
},
|
||||
&image, std::move(callback)));
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_FILE_PATH_CONVERTER_H_
|
||||
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_FILE_PATH_CONVERTER_H_
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "base/files/file_path.h"
|
||||
#include "gin/converter.h"
|
||||
#include "shell/common/gin_converters/std_converter.h"
|
||||
@@ -30,6 +32,11 @@ struct Converter<base::FilePath> {
|
||||
|
||||
base::FilePath::StringType path;
|
||||
if (Converter<base::FilePath::StringType>::FromV8(isolate, val, &path)) {
|
||||
bool has_control_chars = std::any_of(
|
||||
path.begin(), path.end(),
|
||||
[](base::FilePath::CharType c) { return c >= 0 && c < 0x20; });
|
||||
if (has_control_chars)
|
||||
return false;
|
||||
*out = base::FilePath(path);
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -607,6 +607,9 @@ bool Converter<scoped_refptr<network::ResourceRequestBody>>::FromV8(
|
||||
const std::string* file = dict.FindString("filePath");
|
||||
if (!file)
|
||||
return false;
|
||||
if (std::any_of(file->begin(), file->end(),
|
||||
[](char c) { return c >= 0 && c < 0x20; }))
|
||||
return false;
|
||||
double modification_time =
|
||||
dict.FindDouble("modificationTime").value_or(0.0);
|
||||
int offset = dict.FindInt("offset").value_or(0);
|
||||
|
||||
@@ -153,9 +153,12 @@ v8::Local<v8::Value> Converter<electron::OffscreenSharedTextureValue>::ToV8(
|
||||
root.Set("textureInfo", ConvertToV8(isolate, dict));
|
||||
auto root_local = ConvertToV8(isolate, root);
|
||||
|
||||
// Create a persistent reference of the object, so that we can check the
|
||||
// monitor again when GC collects this object.
|
||||
auto* tex_persistent = monitor->CreatePersistent(isolate, root_local);
|
||||
// Create a weak persistent that tracks the release function rather than the
|
||||
// texture object. The release function holds a raw pointer to |monitor| via
|
||||
// its v8::External data, so |monitor| must outlive it. Since the texture
|
||||
// keeps |release| alive via its property, this also covers the case where
|
||||
// the texture itself is leaked without calling release().
|
||||
auto* tex_persistent = monitor->CreatePersistent(isolate, releaser);
|
||||
tex_persistent->SetWeak(
|
||||
monitor,
|
||||
[](const v8::WeakCallbackInfo<OffscreenReleaseHolderMonitor>& data) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "shell/common/gin_helper/wrappable.h"
|
||||
|
||||
#include "base/task/sequenced_task_runner.h"
|
||||
#include "gin/object_template_builder.h"
|
||||
#include "gin/public/isolate_holder.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
@@ -90,7 +91,22 @@ void WrappableBase::SecondWeakCallback(
|
||||
if (gin::IsolateHolder::DestroyedMicrotasksRunner()) {
|
||||
return;
|
||||
}
|
||||
delete static_cast<WrappableBase*>(data.GetInternalField(0));
|
||||
// Defer destruction to a posted task. V8's second-pass weak callbacks run
|
||||
// inside a DisallowJavascriptExecutionScope (they may touch the V8 API but
|
||||
// must not invoke JS). Several Electron Wrappables (e.g. WebContents) emit
|
||||
// JS events from their destructors, so deleting synchronously here can
|
||||
// crash with "Invoke in DisallowJavascriptExecutionScope" — see
|
||||
// https://github.com/electron/electron/issues/47420. Posting via the
|
||||
// current sequence's task runner ensures the destructor runs once V8 has
|
||||
// left the GC scope. If no task runner is available (e.g. early/late in
|
||||
// process lifetime), fall back to synchronous deletion.
|
||||
auto* wrappable = static_cast<WrappableBase*>(data.GetInternalField(0));
|
||||
if (base::SequencedTaskRunner::HasCurrentDefault()) {
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
|
||||
wrappable);
|
||||
} else {
|
||||
delete wrappable;
|
||||
}
|
||||
}
|
||||
|
||||
DeprecatedWrappableBase::DeprecatedWrappableBase() = default;
|
||||
@@ -126,9 +142,19 @@ void DeprecatedWrappableBase::SecondWeakCallback(
|
||||
const v8::WeakCallbackInfo<DeprecatedWrappableBase>& data) {
|
||||
if (gin::IsolateHolder::DestroyedMicrotasksRunner())
|
||||
return;
|
||||
// See WrappableBase::SecondWeakCallback for why deletion is posted: V8's
|
||||
// second-pass weak callbacks run inside a DisallowJavascriptExecutionScope,
|
||||
// and several Wrappables emit JS events from their destructors.
|
||||
// https://github.com/electron/electron/issues/47420
|
||||
DeprecatedWrappableBase* wrappable = data.GetParameter();
|
||||
if (wrappable)
|
||||
if (!wrappable)
|
||||
return;
|
||||
if (base::SequencedTaskRunner::HasCurrentDefault()) {
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
|
||||
wrappable);
|
||||
} else {
|
||||
delete wrappable;
|
||||
}
|
||||
}
|
||||
|
||||
v8::MaybeLocal<v8::Object> DeprecatedWrappableBase::GetWrapperImpl(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#define ELECTRON_SHELL_COMMON_GIN_HELPER_WRAPPABLE_BASE_H_
|
||||
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/task/sequenced_task_runner_helpers.h"
|
||||
#include "v8/include/v8-forward.h"
|
||||
|
||||
namespace gin {
|
||||
@@ -75,6 +76,11 @@ class DeprecatedWrappableBase {
|
||||
DeprecatedWrappableBase();
|
||||
virtual ~DeprecatedWrappableBase();
|
||||
|
||||
// SecondWeakCallback posts destruction via DeleteSoon so that destructors
|
||||
// (which may emit JS events) run outside V8's GC scope. DeleteSoon needs
|
||||
// access to the protected destructor.
|
||||
friend class base::DeleteHelper<DeprecatedWrappableBase>;
|
||||
|
||||
// Overrides of this method should be declared final and not overridden again.
|
||||
virtual gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate);
|
||||
|
||||
@@ -268,6 +268,9 @@ inline constexpr base::cstring_view kStreamingSchemes = "streaming-schemes";
|
||||
// Register schemes as supporting V8 code cache.
|
||||
inline constexpr base::cstring_view kCodeCacheSchemes = "code-cache-schemes";
|
||||
|
||||
// Register schemes as supporting extensions.
|
||||
inline constexpr base::cstring_view kExtensionSchemes = "extension-schemes";
|
||||
|
||||
// The browser process app model ID
|
||||
inline constexpr base::cstring_view kAppUserModelId = "app-user-model-id";
|
||||
|
||||
|
||||
@@ -162,6 +162,11 @@ RendererClientBase::RendererClientBase() {
|
||||
ParseSchemesCLISwitch(command_line, switches::kSecureSchemes);
|
||||
for (const std::string& scheme : secure_schemes_list)
|
||||
url::AddSecureScheme(scheme.data());
|
||||
// Parse --extension-schemes=scheme1,scheme2
|
||||
std::vector<std::string> extension_schemes_list =
|
||||
ParseSchemesCLISwitch(command_line, switches::kExtensionSchemes);
|
||||
for (const std::string& scheme : extension_schemes_list)
|
||||
url::AddExtensionScheme(scheme.c_str());
|
||||
// We rely on the unique process host id which is notified to the
|
||||
// renderer process via command line switch from the content layer,
|
||||
// if this switch is removed from the content layer for some reason,
|
||||
|
||||
@@ -1671,6 +1671,32 @@ describe('BrowserWindow module', () => {
|
||||
expectBoundsEqual(w.getMaximumSize(), [900, 600]);
|
||||
});
|
||||
|
||||
it('creates window at min size when a smaller size is requested', () => {
|
||||
const w1 = new BrowserWindow({
|
||||
show: false,
|
||||
width: 200,
|
||||
height: 200,
|
||||
minWidth: 300,
|
||||
minHeight: 300
|
||||
});
|
||||
const size = w1.getSize();
|
||||
expect(size[0]).to.equal(300);
|
||||
expect(size[1]).to.equal(300);
|
||||
});
|
||||
|
||||
it('creates window at max size when a larger size is requested', () => {
|
||||
const w1 = new BrowserWindow({
|
||||
show: false,
|
||||
width: 300,
|
||||
height: 300,
|
||||
maxWidth: 200,
|
||||
maxHeight: 200
|
||||
});
|
||||
const size = w1.getSize();
|
||||
expect(size[0]).to.equal(200);
|
||||
expect(size[1]).to.equal(200);
|
||||
});
|
||||
|
||||
it('enforces minimum size', async () => {
|
||||
w.setMinimumSize(300, 300);
|
||||
const resize = once(w, 'resize');
|
||||
@@ -6902,6 +6928,54 @@ describe('BrowserWindow module', () => {
|
||||
expect(w.webContents.frameRate).to.equal(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared texture', () => {
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
|
||||
it('does not crash when release() is called after the texture is garbage collected', async () => {
|
||||
const sw = new BrowserWindow({
|
||||
width: 100,
|
||||
height: 100,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
offscreen: {
|
||||
useSharedTexture: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const paint = once(sw.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
sw.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [event] = await paint;
|
||||
sw.webContents.stopPainting();
|
||||
|
||||
if (!event.texture) {
|
||||
// GPU shared texture not available on this host; skip.
|
||||
sw.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep only the release closure and drop the owning texture object.
|
||||
const staleRelease = event.texture.release;
|
||||
const weakTexture = new WeakRef(event.texture);
|
||||
event.texture = undefined;
|
||||
|
||||
// Force GC until the texture object is collected.
|
||||
let collected = false;
|
||||
for (let i = 0; i < 30 && !collected; ++i) {
|
||||
await setTimeout();
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
collected = weakTexture.deref() === undefined;
|
||||
}
|
||||
expect(collected).to.be.true('texture should be garbage collected');
|
||||
|
||||
// This should return safely and not crash the main process.
|
||||
expect(() => staleRelease()).to.not.throw();
|
||||
|
||||
sw.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"transparent" option', () => {
|
||||
|
||||
@@ -132,6 +132,36 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTraceBufferUsage', function () {
|
||||
this.timeout(10e3);
|
||||
|
||||
it('does not crash and returns valid usage data', async () => {
|
||||
await app.whenReady();
|
||||
await contentTracing.startRecording({
|
||||
categoryFilter: '*',
|
||||
traceOptions: 'record-until-full'
|
||||
});
|
||||
|
||||
// Yield to the event loop so the JS HandleScope from this tick is gone.
|
||||
// When the Mojo response arrives it fires OnTraceBufferUsageAvailable
|
||||
// as a plain Chromium task — if that callback lacks its own HandleScope
|
||||
// the process will crash with "Cannot create a handle without a HandleScope".
|
||||
const result = await contentTracing.getTraceBufferUsage();
|
||||
|
||||
expect(result).to.have.property('percentage').that.is.a('number');
|
||||
expect(result).to.have.property('value').that.is.a('number');
|
||||
|
||||
await contentTracing.stopRecording();
|
||||
});
|
||||
|
||||
it('returns zero usage when no trace is active', async () => {
|
||||
await app.whenReady();
|
||||
const result = await contentTracing.getTraceBufferUsage();
|
||||
expect(result).to.have.property('percentage').that.is.a('number');
|
||||
expect(result.percentage).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('captured events', () => {
|
||||
it('include V8 samples from the main process', async function () {
|
||||
this.timeout(60000);
|
||||
|
||||
@@ -2,9 +2,10 @@ import { dialog, BaseWindow, BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { ifit } from './lib/spec-helpers';
|
||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('dialog module', () => {
|
||||
@@ -243,4 +244,785 @@ describe('dialog module', () => {
|
||||
}).to.throw(/message must be a string/);
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin' && !process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS)('end-to-end dialog interaction (macOS)', () => {
|
||||
let dialogHelper: any;
|
||||
|
||||
before(() => {
|
||||
dialogHelper = require('@electron-ci/dialog-helper');
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
// Poll for a sheet to appear on the given window.
|
||||
async function waitForSheet (w: BrowserWindow): Promise<void> {
|
||||
const handle = w.getNativeWindowHandle();
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
if (info.type !== 'none') return;
|
||||
await setTimeout(100);
|
||||
}
|
||||
throw new Error('Timed out waiting for dialog sheet to appear');
|
||||
}
|
||||
|
||||
describe('showMessageBox', () => {
|
||||
it('shows the correct message and buttons', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Test message',
|
||||
buttons: ['OK', 'Cancel']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('message-box');
|
||||
expect(info.message).to.equal('Test message');
|
||||
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.include('OK');
|
||||
expect(buttons).to.include('Cancel');
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows detail text', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Main message',
|
||||
detail: 'Extra detail text',
|
||||
buttons: ['OK']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.message).to.equal('Main message');
|
||||
expect(info.detail).to.equal('Extra detail text');
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('returns the correct response when a specific button is clicked', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Choose a button',
|
||||
buttons: ['First', 'Second', 'Third']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
dialogHelper.clickMessageBoxButton(handle, 1);
|
||||
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(1);
|
||||
});
|
||||
|
||||
it('returns the correct response when the last button is clicked', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Choose a button',
|
||||
buttons: ['Yes', 'No', 'Maybe']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
dialogHelper.clickMessageBoxButton(handle, 2);
|
||||
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(2);
|
||||
});
|
||||
|
||||
it('shows a single button when no buttons are specified', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'No buttons specified'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('message-box');
|
||||
// macOS adds a default "OK" button when none are specified.
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.have.lengthOf(1);
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(0);
|
||||
});
|
||||
|
||||
it('renders checkbox with the correct label and initial state', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Checkbox test',
|
||||
buttons: ['OK'],
|
||||
checkboxLabel: 'Do not show again',
|
||||
checkboxChecked: false
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.checkboxLabel).to.equal('Do not show again');
|
||||
expect(info.checkboxChecked).to.be.false();
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.checkboxChecked).to.be.false();
|
||||
});
|
||||
|
||||
it('returns checkboxChecked as true when checkbox is initially checked', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Pre-checked checkbox',
|
||||
buttons: ['OK'],
|
||||
checkboxLabel: 'Remember my choice',
|
||||
checkboxChecked: true
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.checkboxLabel).to.equal('Remember my choice');
|
||||
expect(info.checkboxChecked).to.be.true();
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.checkboxChecked).to.be.true();
|
||||
});
|
||||
|
||||
it('can toggle checkbox and returns updated state', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Toggle test',
|
||||
buttons: ['OK'],
|
||||
checkboxLabel: 'Toggle me',
|
||||
checkboxChecked: false
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
// Verify initially unchecked.
|
||||
let info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.checkboxChecked).to.be.false();
|
||||
|
||||
// Click the checkbox to check it.
|
||||
dialogHelper.clickCheckbox(handle);
|
||||
info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.checkboxChecked).to.be.true();
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
const result = await p;
|
||||
expect(result.checkboxChecked).to.be.true();
|
||||
});
|
||||
|
||||
it('strips access keys on macOS with normalizeAccessKeys', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Access key test',
|
||||
buttons: ['&Save', '&Cancel'],
|
||||
normalizeAccessKeys: true
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
// On macOS, ampersands are stripped by normalizeAccessKeys.
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.include('Save');
|
||||
expect(buttons).to.include('Cancel');
|
||||
expect(buttons).not.to.include('&Save');
|
||||
expect(buttons).not.to.include('&Cancel');
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('respects defaultId by making it the default button', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Default button test',
|
||||
buttons: ['One', 'Two', 'Three'],
|
||||
defaultId: 2
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
const buttons = JSON.parse(info.buttons);
|
||||
expect(buttons).to.deep.equal(['One', 'Two', 'Three']);
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 2);
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(2);
|
||||
});
|
||||
|
||||
it('respects cancelId and returns it when cancelled via signal', async () => {
|
||||
const controller = new AbortController();
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: 'Cancel ID test',
|
||||
buttons: ['OK', 'Dismiss', 'Abort'],
|
||||
cancelId: 2,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
controller.abort();
|
||||
|
||||
const result = await p;
|
||||
expect(result.response).to.equal(2);
|
||||
});
|
||||
|
||||
it('works with all message box types', async () => {
|
||||
const types: Array<'none' | 'info' | 'warning' | 'error' | 'question'> =
|
||||
['none', 'info', 'warning', 'error', 'question'];
|
||||
|
||||
for (const type of types) {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showMessageBox(w, {
|
||||
message: `Type: ${type}`,
|
||||
type,
|
||||
buttons: ['OK']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('message-box');
|
||||
expect(info.message).to.equal(`Type: ${type}`);
|
||||
|
||||
dialogHelper.clickMessageBoxButton(handle, 0);
|
||||
await p;
|
||||
w.destroy();
|
||||
// Allow the event loop to settle between iterations to avoid
|
||||
// Chromium DCHECK failures from rapid window lifecycle churn.
|
||||
await setTimeout(100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('showOpenDialog', () => {
|
||||
it('can cancel an open dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
title: 'Test Open',
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.type).to.equal('open-dialog');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.true();
|
||||
expect(result.filePaths).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('sets a custom button label', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
buttonLabel: 'Select This',
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.prompt).to.equal('Select This');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets a message on the dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
message: 'Choose a file to import',
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.panelMessage).to.equal('Choose a file to import');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('defaults to openFile with canChooseFiles enabled', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canChooseFiles).to.be.true();
|
||||
expect(info.canChooseDirectories).to.be.false();
|
||||
expect(info.allowsMultipleSelection).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables directory selection with openDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canChooseDirectories).to.be.true();
|
||||
// openFile is not set, so canChooseFiles should be false
|
||||
expect(info.canChooseFiles).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables both file and directory selection together', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'openDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canChooseFiles).to.be.true();
|
||||
expect(info.canChooseDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables multiple selection with multiSelections', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'multiSelections']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.allowsMultipleSelection).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows hidden files with showHiddenFiles', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'showHiddenFiles']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('does not show hidden files by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('disables alias resolution with noResolveAliases', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'noResolveAliases']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.resolvesAliases).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('resolves aliases by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.resolvesAliases).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('treats packages as directories with treatPackageAsDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'treatPackageAsDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.treatsPackagesAsDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables directory creation with createDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
properties: ['openFile', 'createDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets the default path directory', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
defaultPath: defaultDir,
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.directory).to.equal(defaultDir);
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('applies multiple properties simultaneously', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
title: 'Multi-Property Test',
|
||||
buttonLabel: 'Pick',
|
||||
message: 'Select items',
|
||||
properties: [
|
||||
'openFile',
|
||||
'openDirectory',
|
||||
'multiSelections',
|
||||
'showHiddenFiles',
|
||||
'createDirectory',
|
||||
'treatPackageAsDirectory',
|
||||
'noResolveAliases'
|
||||
]
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('open-dialog');
|
||||
expect(info.prompt).to.equal('Pick');
|
||||
expect(info.panelMessage).to.equal('Select items');
|
||||
expect(info.canChooseFiles).to.be.true();
|
||||
expect(info.canChooseDirectories).to.be.true();
|
||||
expect(info.allowsMultipleSelection).to.be.true();
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
expect(info.treatsPackagesAsDirectories).to.be.true();
|
||||
expect(info.resolvesAliases).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('can accept an open dialog and return a file path', async () => {
|
||||
const targetDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showOpenDialog(w, {
|
||||
defaultPath: targetDir,
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
dialogHelper.acceptFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.false();
|
||||
expect(result.filePaths).to.have.lengthOf(1);
|
||||
expect(result.filePaths[0]).to.equal(targetDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showSaveDialog', () => {
|
||||
it('can cancel a save dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
title: 'Test Save'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.type).to.equal('save-dialog');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.true();
|
||||
expect(result.filePath).to.equal('');
|
||||
});
|
||||
|
||||
it('can accept a save dialog with a filename', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const filename = 'test-save-output.txt';
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
title: 'Test Save',
|
||||
defaultPath: path.join(defaultDir, filename)
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
|
||||
dialogHelper.acceptFileDialog(handle);
|
||||
|
||||
const result = await p;
|
||||
expect(result.canceled).to.be.false();
|
||||
expect(result.filePath).to.equal(path.join(defaultDir, filename));
|
||||
});
|
||||
|
||||
it('sets a custom button label', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
buttonLabel: 'Export'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.prompt).to.equal('Export');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets a message on the dialog', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
message: 'Choose where to save'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.panelMessage).to.equal('Choose where to save');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets a custom name field label', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
nameFieldLabel: 'Export As:'
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.nameFieldLabel).to.equal('Export As:');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets the default filename from defaultPath', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
defaultPath: path.join(__dirname, 'fixtures', 'my-document.txt')
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.nameFieldValue).to.equal('my-document.txt');
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('sets the default directory from defaultPath', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
defaultPath: path.join(defaultDir, 'some-file.txt')
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.directory).to.equal(defaultDir);
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('hides the tag field when showsTagField is false', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
showsTagField: false
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsTagField).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows the tag field by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsTagField).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('enables directory creation with createDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
properties: ['createDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('shows hidden files with showHiddenFiles', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
properties: ['showHiddenFiles']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('does not show hidden files by default', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.showsHiddenFiles).to.be.false();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('treats packages as directories with treatPackageAsDirectory', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
properties: ['treatPackageAsDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
expect(info.treatsPackagesAsDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
|
||||
it('applies multiple options simultaneously', async () => {
|
||||
const defaultDir = path.join(__dirname, 'fixtures');
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const p = dialog.showSaveDialog(w, {
|
||||
buttonLabel: 'Save Now',
|
||||
message: 'Pick a location',
|
||||
nameFieldLabel: 'File Name:',
|
||||
defaultPath: path.join(defaultDir, 'output.txt'),
|
||||
showsTagField: false,
|
||||
properties: ['showHiddenFiles', 'createDirectory']
|
||||
});
|
||||
|
||||
await waitForSheet(w);
|
||||
const handle = w.getNativeWindowHandle();
|
||||
const info = dialogHelper.getDialogInfo(handle);
|
||||
|
||||
expect(info.type).to.equal('save-dialog');
|
||||
expect(info.prompt).to.equal('Save Now');
|
||||
expect(info.panelMessage).to.equal('Pick a location');
|
||||
expect(info.nameFieldLabel).to.equal('File Name:');
|
||||
expect(info.nameFieldValue).to.equal('output.txt');
|
||||
expect(info.directory).to.equal(defaultDir);
|
||||
expect(info.showsTagField).to.be.false();
|
||||
expect(info.showsHiddenFiles).to.be.true();
|
||||
expect(info.canCreateDirectories).to.be.true();
|
||||
|
||||
dialogHelper.cancelFileDialog(handle);
|
||||
await p;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('nativeTheme module', () => {
|
||||
@@ -119,4 +120,10 @@ describe('nativeTheme module', () => {
|
||||
expect(nativeTheme.prefersReducedTransparency).to.be.a('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('nativeTheme.shouldDifferentiateWithoutColor', () => {
|
||||
it('returns a boolean', () => {
|
||||
expect(nativeTheme.shouldDifferentiateWithoutColor).to.be.a('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1123,6 +1123,8 @@ describe('protocol module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// protocol.registerSchemesAsPrivileged allowExtensions tests are in extensions-spec.ts.
|
||||
|
||||
describe('handle', () => {
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
|
||||
@@ -1764,6 +1764,60 @@ describe('session module', () => {
|
||||
expect(handlerDetails!.isMainFrame).to.be.false();
|
||||
expect(handlerDetails!.embeddingOrigin).to.equal('file:///');
|
||||
});
|
||||
|
||||
it('provides iframe origin as requestingOrigin for media check from cross-origin subFrame', async () => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
partition: 'very-temp-permission-handler-media'
|
||||
}
|
||||
});
|
||||
const ses = w.webContents.session;
|
||||
const iframeUrl = 'https://myfakesite/';
|
||||
let capturedOrigin: string | undefined;
|
||||
let capturedIsMainFrame: boolean | undefined;
|
||||
let capturedRequestingUrl: string | undefined;
|
||||
let capturedSecurityOrigin: string | undefined;
|
||||
|
||||
ses.protocol.interceptStringProtocol('https', (req, cb) => {
|
||||
cb('<html><body>iframe</body></html>');
|
||||
});
|
||||
|
||||
ses.setPermissionCheckHandler((wc, permission, requestingOrigin, details) => {
|
||||
if (permission === 'media') {
|
||||
capturedOrigin = requestingOrigin;
|
||||
capturedIsMainFrame = details.isMainFrame;
|
||||
capturedRequestingUrl = details.requestingUrl;
|
||||
capturedSecurityOrigin = (details as any).securityOrigin;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
try {
|
||||
await w.loadFile(path.join(fixtures, 'api', 'blank.html'));
|
||||
w.webContents.executeJavaScript(`
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.src = '${iframeUrl}';
|
||||
iframe.allow = 'camera; microphone';
|
||||
document.body.appendChild(iframe);
|
||||
null;
|
||||
`);
|
||||
const [,, frameProcessId, frameRoutingId] = await once(w.webContents, 'did-frame-finish-load');
|
||||
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId)!;
|
||||
await frame.executeJavaScript(
|
||||
'navigator.mediaDevices.enumerateDevices().then(() => {}).catch(() => {});',
|
||||
true
|
||||
);
|
||||
|
||||
expect(capturedOrigin).to.equal(iframeUrl);
|
||||
expect(capturedIsMainFrame).to.be.false();
|
||||
expect(capturedRequestingUrl).to.equal(iframeUrl);
|
||||
expect(capturedSecurityOrigin).to.equal(iframeUrl);
|
||||
} finally {
|
||||
ses.protocol.uninterceptProtocol('https');
|
||||
ses.setPermissionCheckHandler(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ses.isPersistent()', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BaseWindow, BrowserWindow, View, WebContentsView, webContents, screen }
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
|
||||
|
||||
import { HexColors, ScreenCapture, hasCapturableScreen, nextFrameTime } from './lib/screen-helpers';
|
||||
import { defer, ifdescribe, waitUntil } from './lib/spec-helpers';
|
||||
@@ -309,6 +310,94 @@ describe('WebContentsView', () => {
|
||||
}
|
||||
expect(visibilityState).to.equal('visible');
|
||||
});
|
||||
|
||||
it('tracks visibility for multiple child WebContentsViews', async () => {
|
||||
const w = new BaseWindow({ show: false });
|
||||
const cv = new View();
|
||||
w.setContentView(cv);
|
||||
|
||||
const v1 = new WebContentsView();
|
||||
const v2 = new WebContentsView();
|
||||
cv.addChildView(v1);
|
||||
cv.addChildView(v2);
|
||||
v1.setBounds({ x: 0, y: 0, width: 400, height: 300 });
|
||||
v2.setBounds({ x: 0, y: 300, width: 400, height: 300 });
|
||||
|
||||
await v1.webContents.loadURL('about:blank');
|
||||
await v2.webContents.loadURL('about:blank');
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
|
||||
|
||||
w.show();
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
|
||||
|
||||
w.hide();
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async () => {
|
||||
const w = new BaseWindow();
|
||||
const cv = new View();
|
||||
w.setContentView(cv);
|
||||
|
||||
const v1 = new WebContentsView();
|
||||
const v2 = new WebContentsView();
|
||||
cv.addChildView(v1);
|
||||
cv.addChildView(v2);
|
||||
v1.setBounds({ x: 0, y: 0, width: 400, height: 300 });
|
||||
v2.setBounds({ x: 0, y: 300, width: 400, height: 300 });
|
||||
|
||||
await v1.webContents.loadURL('about:blank');
|
||||
await v2.webContents.loadURL('about:blank');
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
|
||||
|
||||
v1.setVisible(false);
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
|
||||
// v2 should remain visible while v1 is hidden
|
||||
expect(await v2.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
|
||||
|
||||
v1.setVisible(true);
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('fires a single visibilitychange event per show/hide transition', async () => {
|
||||
const w = new BaseWindow({ show: false });
|
||||
const v = new WebContentsView();
|
||||
w.setContentView(v);
|
||||
await v.webContents.loadURL('about:blank');
|
||||
|
||||
await v.webContents.executeJavaScript(`
|
||||
window.__visChanges = [];
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
window.__visChanges.push(document.visibilityState);
|
||||
});
|
||||
`);
|
||||
|
||||
w.show();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
|
||||
|
||||
// Give any delayed/queued occlusion updates time to fire.
|
||||
await setTimeoutAsync(1500);
|
||||
|
||||
w.hide();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v, 'hidden'))).to.eventually.be.fulfilled();
|
||||
|
||||
await setTimeoutAsync(1500);
|
||||
|
||||
const changes = await v.webContents.executeJavaScript('window.__visChanges');
|
||||
// Expect exactly one 'visible' followed by one 'hidden'. Extra events
|
||||
// would indicate the occlusion checker is causing spurious transitions.
|
||||
expect(changes).to.deep.equal(['visible', 'hidden']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBorderRadius', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user