Compare commits

..

29 Commits

Author SHA1 Message Date
John Kleinschmidt
78896775d9 ci: update actions to node24 (#50522)
ci: update actions to node24 (#50373)

* ci: update actions to node24

* chore: fixup actions/cache to 5.0.4 everywhere

(cherry picked from commit 639d3b99b7)
2026-03-31 15:26:31 +02:00
trop[bot]
40eb41656a ci: update nick-fields/retry to v4.0.0 (#50544)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
2026-03-31 14:32:29 +02:00
Michaela Laurencin
5a69e80cac ci: add functionality for programmatic add/remove needs-signed-commits label (#50316) (#50587)
* remove comment based label removal

* ci: add functionality for programmatic add/remove needs-signed-commits label

* add new line to pull-request-opened-synchronized
2026-03-31 10:30:47 +02:00
trop[bot]
90decd4eaf fix: add missing HandleScope in contentTracing.getTraceBufferUsage() (#50594)
The `OnTraceBufferUsageAvailable` callback creates V8 handles via
`Dictionary::CreateEmpty()` before `promise.Resolve()` enters its
`SettleScope` (which provides a `HandleScope`). When the callback
fires asynchronously from a Mojo response (i.e. when a trace session
is active), there is no `HandleScope` on the stack, causing a fatal
V8 error: "Cannot create a handle without a HandleScope".

Add an explicit `v8::HandleScope` at the top of the callback, matching
the pattern used by the other contentTracing APIs which resolve their
promises through `SettleScope` or the static `ResolvePromise` helper.

Made-with: Cursor

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
2026-03-31 10:15:13 +02:00
trop[bot]
ba551d265c perf: enable V8 builtins PGO (#50574)
* build: enable V8 builtins PGO

Removes the gn arg that disabled V8 builtins profile-guided optimization
and adds a V8 patch to warn instead of abort when the builtin PGO profile
data does not match. Also strips the PGO-related flags from the generated
mksnapshot_args so they are not passed through to downstream mksnapshot
invocations.

Co-authored-by: Sam Attard <sattard@anthropic.com>

* docs: clarify Node.js async_hooks as reason for promise_hooks flag

Addresses review feedback: the v8_enable_javascript_promise_hooks flag
is set to support Node.js async_hooks, not used directly by Electron.

Co-authored-by: Sam Attard <sattard@anthropic.com>

* chore: update patches

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-03-30 19:44:54 +02:00
trop[bot]
24784ed024 refactor: improve input handling in FilePath gin converter (#50547)
refactor: improve input handling in file_path_converter

Properly handle paths containing ASCII control characters in the FilePath gin converter

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>
2026-03-27 22:56:38 +00:00
trop[bot]
f49f6b1a29 docs: clarify allowed characters in protocol names (#50538)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
2026-03-27 09:58:46 -04:00
trop[bot]
c63e0d8b96 test: add interactive macOS dialog tests (#50528)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-03-27 08:20:36 -04:00
trop[bot]
33a81b40c2 fix: register PrintDialogLinuxFactory on Linux (#50486)
fix: register PrintDialogLinuxFactory on Linux

Chromium 145 refactored Linux print dialog creation to use a factory
pattern instead of directly calling LinuxUi::CreatePrintDialog().
Chrome registers this factory in
ChromeBrowserMainExtraPartsViewsLinux::ToolkitInitialized(), but
Electron did not, causing PrintingContextLinux::EnsurePrintDialog()
to leave print_dialog_ null on every call.

Without a dialog, UseDefaultSettings() and UpdatePrinterSettings()
return success but with empty/unprocessed settings, causing
PrintMsgPrintParamsIsValid() to fail. This broke both window.print()
(no dialog appears) and webContents.print() (callback stuck until
app close with "Invalid printer settings").

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-03-26 17:01:28 -04:00
trop[bot]
eb49ed962d fix: outdated execution path for COM activation (#50519)
* fix: outdated execution path

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: use stub exe when detected

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>
2026-03-26 20:32:47 +00:00
electron-roller[bot]
7e36ac67ce chore: bump chromium to 146.0.7680.166 (41-x-y) (#50458)
* chore: bump chromium in DEPS to 146.0.7680.164

* chore: update patches

* chore: bump chromium in DEPS to 146.0.7680.166

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-03-26 15:53:50 -04:00
trop[bot]
cbae32aac6 fix: [a11y] fire AXMenuOpened event when ARIA menu is added to DOM (#50506)
* fix: fire AXMenuOpened event when a visible ARIA menu instance is added to the DOM

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* fix: remove redundent FireMenuPopupEndForDeletedMenus

MENU_POPUP_END for deleted menus is already handled by
AXTreeManager::OnNodeWillBeDeleted, which
fires the event directly on the menu node before destruction.

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* chore: add feature flag (kDynamicMenuPopupEvents)

Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

* chore: update patches

Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

* chore: update patches after trop

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>
Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
2026-03-26 09:54:27 -07:00
trop[bot]
880b1e08e7 refactor: remove dead named-window lookup from guest-window-manager (#50497)
The frameNamesToWindow map was a holdover from the BrowserWindowProxy
IPC shim. Since nativeWindowOpen became the only code path, Blink's
FrameTree::FindOrCreateFrameForNavigation resolves named window targets
directly in the renderer, scoped to the opener's browsing context
group. When a matching named window exists, Blink navigates it without
ever sending a CreateNewWindow IPC to the browser, so this map was
never consulted in the legitimate same-opener case.

The only time the map found a match was when two unrelated renderers
happened to use the same target name, in which case openGuestWindow
would short-circuit before consuming the guest WebContents that
Chromium had already created for the new window, leaking it.

Adds a test verifying Blink handles same-opener named-target reuse
end-to-end without any browser-side tracking.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-03-26 11:46:12 -04:00
trop[bot]
aedea576da fix: hex-encode Windows notification icon temp filenames (#50483)
* fix: hex-encode Windows notification icon temp filenames

NotificationPresenterWin was using SHA1HashString(origin.spec()) directly
as the basename for the temporary PNG written for toast icons.

SHA1HashString returns raw digest bytes, so the generated filename could
contain invalid path characters on Windows. That caused WriteFile to fail
when saving notification icons, which left toast XML without the expected
icon path.

Hex-encode the digest before appending .png so the temporary filename is
filesystem-safe while keeping deterministic naming for a given origin.

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* Update shell/browser/notifications/win/notification_presenter_win.cc

Co-authored-by: Robo <hop2deep@gmail.com>

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-03-26 09:20:32 -04:00
trop[bot]
707541d9b2 fix: fall back to default DPI when GTK returns 0 on Linux (#50489)
GetDefaultPrinterDPI() creates a blank GtkPrintSettings and reads
its resolution, which returns 0 for uninitialized settings. With
DPI=0, SetPrintableAreaIfValid() computes a zero scale factor,
producing empty page dimensions that fail PrintMsgPrintParamsIsValid().

Fall back to kDefaultPdfDpi (72) when GTK returns 0, matching the
existing Windows fallback pattern when CreateDC fails.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-03-26 09:17:44 -04:00
trop[bot]
3dcb641a99 fix: crash calling OSR shared texture release() after texture GC'd (#50501)
The weak persistent tracking the OffscreenReleaseHolderMonitor was tied
to the texture object, but the release() closure holds a raw pointer to
the monitor via its v8::External data. If JS retained texture.release
while dropping the texture itself, the monitor would be freed on GC and
a later release() call would crash.

Track the release function instead of the texture object. Since the
texture holds release as a property, this keeps the monitor alive as
long as either is reachable.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-03-26 00:49:57 -07:00
trop[bot]
878a763344 fix: crash in clipboard.readImage() on malformed image data (#50492)
gfx::PNGCodec::Decode() returns a null SkBitmap when it fails to decode
the clipboard contents as a PNG. Passing that null bitmap to
gfx::Image::CreateFrom1xBitmap() triggers a crash.

Return an empty gfx::Image instead, matching the existing null-check
pattern in skia_util.cc.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-03-25 22:15:58 -07:00
trop[bot]
6a8d187105 feat: add accessibilityDisplayShouldDifferentiateWithoutColor on macOS (#50408)
feat: add nativeTheme.shouldDifferentiateWithoutColor on macOS

Adds nativeTheme.shouldDifferentiateWithoutColor on macOS that maps to
NSWorkspace.accessibilityDisplayShouldDifferentiateWithoutColor. If true,
the user has indicated that they prefer UI that differentiates items with
something other than color alone. This is useful for users with color
vision deficiency.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Robert Böhnke <robb@robb.is>
2026-03-25 15:53:31 -04:00
trop[bot]
29622930a0 feat: support notification priority on Windows (#50382)
* feat: support notification priority on Windows

Add Windows notifications support urgency/priority levels.
This maps the existing `urgency` option (previously Linux-only) to
Windows toast notification priorities:

- 'critical' maps to ToastNotificationPriority_High, which sorts the
  notification above default-priority items in Action Center.
- 'normal' and 'low' both map to ToastNotificationPriority_Default.

Note that on Windows, 'critical' priority does not prevent the toast
from being auto-dismissed. Users should additionally set `timeoutType`
to 'never' for that behavior.

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* chore: make linter happy

Co-authored-by: Charles Kerr <charles@charleskerr.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-03-25 15:49:00 -04:00
trop[bot]
8b9e721047 fix: don't re-parse URL unnecessarily when handling dialogs (#50399)
* fix: fallback to opaque URL when needed inside dialog callback

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* refactor: remove additional URL parsing entirely when showing dialogs

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* test: add crash test case for URL-less dialogs

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* refactor: exit on events instead of on timeout for dialog crash test

Co-authored-by: Robo <hop2deep@gmail.com>

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* style: make linter happy

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* style: make linter actually happy

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* fix: address failing `safeDialogs` tests

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Noah Gregory <noahmgregory@gmail.com>
2026-03-20 13:46:37 -04:00
trop[bot]
43bb93908c fix: correct utility process exit code on Windows (#50386)
* fix: correct utility process exit code on Windows

On Windows, process exit codes are 32-bit unsigned integers (DWORD).
When passed from Chromium to Electron as a signed int and then
implicitly converted to uint64_t, values with the high bit set
(e.g., NTSTATUS codes) undergo sign extension, producing incorrect
values.

Cast the exit code to uint32_t before widening to uint64_t to
prevent sign extension and preserve the original Windows exit code.

Fixes #49455

Co-authored-by: João Silva <joaomrsilva@tecnico.ulisboa.pt>

* fix: narrow HandleTermination and Shutdown to uint32_t, add tests

Co-authored-by: João Silva <joaomrsilva@tecnico.ulisboa.pt>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: João Silva <joaomrsilva@tecnico.ulisboa.pt>
2026-03-19 18:48:27 -07:00
trop[bot]
b0055e0500 fix: improved the appearance of shadows and borders on frameless windows on Wayland (#50213)
fix: improved the appearance of shadows and borders on frameless windows on Wayland (#50007)

* remove painting from linux frame layout

* use chromium csd strategy for frameless windows

* Apply suggestions from code review

Remove unneeded virtual methods



* removed inline destructors

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Mitchell Cohen <mitch.cohen@me.com>
2026-03-19 15:42:58 -04:00
trop[bot]
9a7381a328 ci: output build cache hit rate as GHA annotation (#50370)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-03-19 12:04:12 -04:00
trop[bot]
af3e0fca24 fix: always call the original impl in swizzled mousedown impls (#50354)
fix: always call the original implementation in swizzled mousedown implementations

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Noah Gregory <noahmgregory@gmail.com>
2026-03-18 20:11:50 -07:00
trop[bot]
99d879b52e chore: Respect HTTP(S) proxy env variable for Yarn (#50350)
Respect HTTP(S) proxy env variable for Yarn

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Filip Mösner <filip.mosner@seznam.cz>
2026-03-18 20:03:10 -07:00
electron-roller[bot]
3d8105ae7f chore: bump chromium to 146.0.7680.153 (41-x-y) (#50346)
* chore: bump chromium in DEPS to 146.0.7680.153

* chore: update patches

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-03-18 17:49:48 -04:00
trop[bot]
aba01d38dc fix: correctly track BaseWindow::IsActive() on MacOS (#50340)
fix: correctly set IsActive() in BaseWindow on MacOS

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Kyle Cutler <kycutler@microsoft.com>
2026-03-18 17:11:46 -04:00
trop[bot]
a0f01336a3 fix: ensure WebContents::WasShown runs when window is shown (#50343)
Avoids a freeze when failing to enter fullscreen on macOS.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: John Beutner <beutner.john@gmail.com>
2026-03-18 14:53:09 -04:00
trop[bot]
4a98b4e27e docs: fix markdown formatting in fuses.md (#50333)
* docs: fix markdown formatting in fuses.md

* Use bulleted list (was being run together on one line)
* Wrap ASCII diagram in code block

Co-authored-by: Ryan Zimmerman <ryan@exodus.io>

* docs: apply suggestions from code review

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

Co-authored-by: Ryan Zimmerman <ryan@exodus.io>

* docs: fix misapplied suggestion

Co-authored-by: Ryan Zimmerman <ryan@exodus.io>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Ryan Zimmerman <ryan@exodus.io>
2026-03-18 16:09:27 +01:00
78 changed files with 2290 additions and 298 deletions

View File

@@ -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
@@ -271,12 +274,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 }}

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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
@@ -101,7 +101,7 @@ runs:
- 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

View File

@@ -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

View File

@@ -289,7 +289,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

View File

@@ -0,0 +1,35 @@
name: Pull Request Opened/Synchronized
on:
pull_request_target:
types: [opened, synchronize]
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
View File

@@ -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

View File

@@ -9,4 +9,8 @@ npmMinimalAgeGate: 10080
npmPreapprovedPackages:
- "@electron/*"
httpProxy: "${HTTP_PROXY:-}"
httpsProxy: "${HTTPS_PROXY:-}"
yarnPath: .yarn/releases/yarn-4.12.0.cjs

2
DEPS
View File

@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
vars = {
'chromium_version':
'146.0.7680.80',
'146.0.7680.166',
'node_version':
'v24.14.0',
'nan_version':

View File

@@ -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

View File

@@ -245,6 +245,10 @@ static_library("chrome") {
"//chrome/browser/ui/views/dark_mode_manager_linux.cc",
"//chrome/browser/ui/views/dark_mode_manager_linux.h",
]
sources += [
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.cc",
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h",
]
public_deps += [ "//components/dbus" ]
}

View File

@@ -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).

View File

@@ -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:

View File

@@ -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:

View File

@@ -146,13 +146,15 @@ The extra privileges granted to the `file://` protocol by this fuse are incomple
The `wasmTrapHandlers` fuse controls whether V8 will use signal handlers to trap Out of Bounds memory
access from WebAssembly. The feature works by surrounding the WebAssembly memory with large guard regions
and then installing a signal handler that traps attempt to access memory in the guard region. The feature
is only supported on the following 64-bit systems.
is only supported on the following 64-bit systems:
Linux. MacOS, Windows - x86_64
Linux, MacOS - aarch64
* Linux, macOS, Windows - x86_64
* Linux, macOS - aarch64
```text
| Guard Pages | WASM heap | Guard Pages |
|-----8GB-----| |-----8GB-----|
```
When the fuse is disabled V8 will use explicit bound checks in the generated WebAssembly code to ensure
memory safety. However, this method has some downsides

View File

@@ -782,8 +782,7 @@ WebContents.prototype._init = function () {
const originCounts = new Map<string, number>();
const openDialogs = new Set<AbortController>();
this.on('-run-dialog', async (info, callback) => {
const originUrl = new URL(info.frame.url);
const origin = originUrl.protocol === 'file:' ? originUrl.href : originUrl.origin;
const origin = info.frame.origin === 'file://' ? info.frame.url : info.frame.origin;
if ((originCounts.get(origin) ?? 0) < 0) return callback(false, '');
const prefs = this.getLastWebPreferences();

View File

@@ -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

View File

@@ -147,3 +147,4 @@ 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

View File

@@ -33,10 +33,10 @@ index 4a742db71f62f9ac891ceeb0604ca0b99d1d89c1..2c5af6482e2b6905552a05b16d3df0a4
"//base",
"//build:branding_buildflags",
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 2fc3a991d89093ff9139eb09d74123197155caff..0862aa96c2a7b496338ac0593f84fcfa21f25572 100644
index a2a14349d40ce34831ab063cd5eb55cd5085c814..1a861ff7867f19935178c8368a9a720230fee026 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -4749,7 +4749,7 @@ static_library("browser") {
@@ -4751,7 +4751,7 @@ static_library("browser") {
]
}
@@ -46,10 +46,10 @@ index 2fc3a991d89093ff9139eb09d74123197155caff..0862aa96c2a7b496338ac0593f84fcfa
# than here in :chrome_dll.
deps += [ "//chrome:packed_resources_integrity_header" ]
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f564843bcef 100644
index 40ea51f97470e2b86f8d2d373ea99a2a71ad185e..db6a2291ce77d89c8e28a1435336fd939e436906 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -7728,9 +7728,12 @@ test("unit_tests") {
@@ -7731,9 +7731,12 @@ test("unit_tests") {
"//chrome/notification_helper",
]
@@ -63,7 +63,7 @@ index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f56
"//chrome//services/util_win:unit_tests",
"//chrome/app:chrome_dll_resources",
"//chrome/app:win_unit_tests",
@@ -8698,6 +8701,10 @@ test("unit_tests") {
@@ -8703,6 +8706,10 @@ test("unit_tests") {
"../browser/performance_manager/policies/background_tab_loading_policy_unittest.cc",
]
@@ -74,7 +74,7 @@ index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f56
sources += [
# The importer code is not used on Android.
"../common/importer/firefox_importer_utils_unittest.cc",
@@ -8755,7 +8762,6 @@ test("unit_tests") {
@@ -8760,7 +8767,6 @@ test("unit_tests") {
# TODO(crbug.com/417513088): Maybe merge with the non-android `deps` declaration above?
deps += [
"../browser/screen_ai:screen_ai_install_state",

View File

@@ -9,10 +9,10 @@ potentially prevent a window from being created.
TODO(loc): this patch is currently broken.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 46368e70af175d8d0ab0fb5a36d258e48270371e..8d7be769a6c76650ae999338578215dcd324c199 100644
index 2d8a70f5fc0f6c2dc2a7587b7bc2e43dbcee8f0e..a87bd09d7a12c5f003488792843cd1807ee1e30f 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9990,6 +9990,7 @@ void RenderFrameHostImpl::CreateNewWindow(
@@ -9997,6 +9997,7 @@ void RenderFrameHostImpl::CreateNewWindow(
last_committed_origin_, params->window_container_type,
params->target_url, params->referrer.To<Referrer>(),
params->frame_name, params->disposition, *params->features,

View File

@@ -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 d5fe1468676285540909ee078d5b88a9bd8e197c..427c77b17d3e130c43a69d79d330bf5c4759f4cc 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 @@

View File

@@ -28,7 +28,7 @@ The patch should be removed in favor of either:
Upstream bug https://bugs.chromium.org/p/chromium/issues/detail?id=1081397.
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 5b79df01e0a5ee81919ebed7d689e430fe7fe305..b11808a69483f4cbcc56d90cc6161984df90c1e4 100644
index 7d101d40116bf743f940f32ba4c9b507aa9a235b..2aa1584fd451fb15ec6084fb0c19724e6c63e0e3 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -11666,6 +11666,11 @@ url::Origin NavigationRequest::GetOriginForURLLoaderFactoryUnchecked() {

View File

@@ -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();

View File

@@ -17,7 +17,7 @@ Revert "Reland "Port net::CookieCryptoDelegate to os_crypt async""
This reverts commit f01b115c7e21a09cc762f65bf7fd9c6ea9d9d0f8.
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 0862aa96c2a7b496338ac0593f84fcfa21f25572..aed5a316bd3d97df715f779273ae4c283cd29c92 100644
index 1a861ff7867f19935178c8368a9a720230fee026..b1ca947122f4ea715be18a0fd4e75b30fffc5a3c 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -714,6 +714,8 @@ static_library("browser") {

View File

@@ -1189,7 +1189,7 @@ index a1068589ad844518038ee7bc15a3de9bc5cba525..1ff781c49f086ec8015c7d3c44567dbe
} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 8575983261c7b57fc85097edb94a8e6f306974f9..aae50b6830450baf27f2834a8187540d7ff6eb35 100644
index d368b2481156bb79c6e74c8b09a828eb2fa2d44c..07cbf495717714d71d977a8820e08050c3062526 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -700,6 +700,7 @@ static_library("test_support") {
@@ -1217,7 +1217,7 @@ index 8575983261c7b57fc85097edb94a8e6f306974f9..aae50b6830450baf27f2834a8187540d
]
if (!(is_chromeos && target_cpu == "arm64" && current_cpu == "arm")) {
@@ -3411,6 +3415,7 @@ test("content_unittests") {
@@ -3412,6 +3416,7 @@ test("content_unittests") {
"//ui/shell_dialogs",
"//ui/webui:test_support",
"//url",

View File

@@ -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 eecdbe8a279ef1a7d9aed4f5496e871d54092e0f..16ff3ffbe8300534cef76f857284ef92ad0a88f6 100644
index d17637a54208450504d071a3f10c20668cfbe76d..f3ffc975d794f356d9a83837fd977e758b726501 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -27059,6 +27059,21 @@
@@ -27095,6 +27095,21 @@
]
}
],

View File

@@ -15,7 +15,7 @@ Note that we also need to manually update embedder's
`api::WebContents::IsFullscreenForTabOrPending` value.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 8d7be769a6c76650ae999338578215dcd324c199..3e8021289c00ec6b15457b17173dfed386eac2fe 100644
index a87bd09d7a12c5f003488792843cd1807ee1e30f..b38240fd422163f09bfb8d4b40213a1940a72acd 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9097,6 +9097,17 @@ void RenderFrameHostImpl::EnterFullscreen(

View File

@@ -1 +1,2 @@
chore_allow_customizing_microtask_policy_per_context.patch
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch

View File

@@ -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) {

View File

@@ -32,7 +32,8 @@ async function main () {
}));
const hitRate = stats.CacheHit / (stats.Remote + stats.CacheHit + stats.LocalFallback);
console.log(`Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
const messagePrefix = process.env.GITHUB_ACTIONS ? '::notice title=Build Stats::' : '';
console.log(`${messagePrefix}Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
if (uploadStats) {
if (!process.env.DD_API_KEY) {

View File

@@ -317,6 +317,12 @@ void BaseWindow::OnWindowSheetEnd() {
Emit("sheet-end");
}
void BaseWindow::OnWindowIsKeyChanged(bool is_key) {
#if BUILDFLAG(IS_MAC)
window()->SetActive(is_key);
#endif
}
void BaseWindow::OnWindowEnterHtmlFullScreen() {
Emit("enter-html-full-screen");
}

View File

@@ -85,6 +85,7 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
void OnWindowRotateGesture(float rotation) override;
void OnWindowSheetBegin() override;
void OnWindowSheetEnd() override;
void OnWindowIsKeyChanged(bool is_key) override;
void OnWindowEnterFullScreen() override;
void OnWindowLeaveFullScreen() override;
void OnWindowEnterHtmlFullScreen() override;

View File

@@ -280,16 +280,22 @@ v8::Local<v8::Value> BrowserWindow::GetWebContents(v8::Isolate* isolate) {
}
void BrowserWindow::OnWindowShow() {
if (!web_contents_shown_) {
web_contents()->WasShown();
web_contents_shown_ = true;
}
BaseWindow::OnWindowShow();
}
void BrowserWindow::OnWindowHide() {
web_contents()->WasOccluded();
web_contents_shown_ = false;
BaseWindow::OnWindowHide();
}
void BrowserWindow::Show() {
web_contents()->WasShown();
web_contents_shown_ = true;
BaseWindow::Show();
}
@@ -298,6 +304,7 @@ void BrowserWindow::ShowInactive() {
if (IsModal())
return;
web_contents()->WasShown();
web_contents_shown_ = true;
BaseWindow::ShowInactive();
}

View File

@@ -80,6 +80,7 @@ class BrowserWindow : public BaseWindow,
// Helpers.
v8::Global<v8::Value> web_contents_;
bool web_contents_shown_ = false;
v8::Global<v8::Value> web_contents_view_;
base::WeakPtr<api::WebContents> api_web_contents_;

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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;

View File

@@ -26,4 +26,9 @@ void NativeTheme::UpdateMacOSAppearanceForOverrideValue(
[[NSApplication sharedApplication] setAppearance:new_appearance];
}
bool NativeTheme::ShouldDifferentiateWithoutColor() {
return [[NSWorkspace sharedWorkspace]
accessibilityDisplayShouldDifferentiateWithoutColor];
}
} // namespace electron::api

View File

@@ -259,7 +259,7 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
EmitWithoutEvent("spawn");
}
void UtilityProcessWrapper::HandleTermination(uint64_t exit_code) {
void UtilityProcessWrapper::HandleTermination(uint32_t exit_code) {
// HandleTermination is called from multiple callsites,
// we need to ensure we only process it for the first callsite.
if (terminated_)
@@ -327,7 +327,7 @@ void UtilityProcessWrapper::CloseConnectorPort() {
}
}
void UtilityProcessWrapper::Shutdown(uint64_t exit_code) {
void UtilityProcessWrapper::Shutdown(uint32_t exit_code) {
node_service_remote_.reset();
HandleTermination(exit_code);
}

View File

@@ -57,7 +57,7 @@ class UtilityProcessWrapper final
static gin_helper::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
void Shutdown(uint64_t exit_code);
void Shutdown(uint32_t exit_code);
// gin_helper::Wrappable
static gin::DeprecatedWrapperInfo kWrapperInfo;
@@ -77,7 +77,7 @@ class UtilityProcessWrapper final
void OnServiceProcessLaunch(const base::Process& process);
void CloseConnectorPort();
void HandleTermination(uint64_t exit_code);
void HandleTermination(uint32_t exit_code);
void PostMessage(gin::Arguments* args);
bool Kill();

View File

@@ -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();

View File

@@ -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_;
};

View File

@@ -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;

View File

@@ -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";

View File

@@ -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))) {

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -87,8 +87,8 @@ MouseDownImpl g_nsnextstepframe_mousedown;
(electron::NativeWindowMac*)[(id)self.window shell];
if (shell && !shell->has_frame())
[self cr_mouseDownOnFrameView:event];
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
}
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
}
- (void)swiz_nsnextstepframe_mouseDown:(NSEvent*)event {
@@ -98,8 +98,8 @@ MouseDownImpl g_nsnextstepframe_mousedown;
if (shell && !shell->has_frame()) {
[self cr_mouseDownOnFrameView:event];
}
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
}
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
}
- (void)swiz_nsview_swipeWithEvent:(NSEvent*)event {

View File

@@ -243,8 +243,8 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
// 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.
SkRRect rrect = layout->GetRoundedWindowContentBounds();
gfx::RectF rectf(layout->GetWindowContentBounds());
SkRRect rrect = layout->GetRoundedWindowBounds();
gfx::RectF rectf(layout->GetWindowBounds());
rectf.Scale(scale);
// It is acceptable to omit some pixels that are opaque, but the region
// must not include any translucent pixels. Therefore, we must

View File

@@ -112,7 +112,7 @@ ClientFrameViewLinux::~ClientFrameViewLinux() {
void ClientFrameViewLinux::Init(NativeWindowViews* window,
views::Widget* frame) {
FramelessView::Init(window, frame);
linux_frame_layout_ = std::make_unique<LinuxCSDFrameLayout>(window);
linux_frame_layout_ = std::make_unique<LinuxCSDNativeFrameLayout>(window);
// Unretained() is safe because the subscription is saved into an instance
// member and thus will be cancelled upon the instance's destruction.
@@ -156,7 +156,8 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
}
int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
return ResizingBorderHitTestImpl(point, RestoredFrameBorderInsets());
return ResizingBorderHitTestImpl(
point, linux_frame_layout_->GetResizeBorderInsets());
}
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
@@ -235,8 +236,11 @@ void ClientFrameViewLinux::Layout(PassKey) {
}
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
linux_frame_layout_->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds(), ShouldPaintAsActive());
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
frame_provider->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
ShouldPaintAsActive(), linux_frame_layout_->GetInputInsets());
}
}
void ClientFrameViewLinux::PaintAsActiveChanged() {
@@ -267,7 +271,7 @@ void ClientFrameViewLinux::UpdateThemeValues() {
}
theme_values_.window_border_radius =
linux_frame_layout_->GetFrameProvider()->GetTopCornerRadiusDip();
linux_frame_layout_->GetTopCornerRadiusDip();
gtk::GtkStyleContextGet(headerbar_context, "min-height",
&theme_values_.titlebar_min_height, nullptr);

View File

@@ -112,7 +112,7 @@ class ClientFrameViewLinux : public FramelessView,
gfx::Insets GetTitlebarContentInsets() const;
gfx::Rect GetTitlebarContentBounds() const;
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
std::unique_ptr<LinuxCSDNativeFrameLayout> linux_frame_layout_;
raw_ptr<ui::NativeTheme> theme_;
ThemeValues theme_values_;

View File

@@ -4,14 +4,20 @@
// found in the LICENSE file.
#include "shell/browser/ui/views/linux_frame_layout.h"
#include <algorithm>
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
#include "shell/browser/linux/x11_util.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
#include "ui/gfx/canvas.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/linux/linux_ui.h"
#include "ui/native_theme/native_theme.h"
#include "ui/linux/window_frame_provider.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/widget/widget.h"
namespace electron {
@@ -21,151 +27,174 @@ namespace {
constexpr int kResizeBorder = 10;
// This should match FramelessView's inside resize band.
constexpr int kResizeInsideBoundsSize = 5;
// These should match Chromium's restored frame edge thickness.
constexpr gfx::Insets kDefaultCustomFrameBorder = gfx::Insets::TLBR(2, 1, 1, 1);
bool CheckClientFrameShadowSupport(NativeWindowViews* window) {
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
window->GetAcceleratedWidget()));
return tree_host && tree_host->SupportsClientFrameShadow();
}
} // namespace
// static
std::unique_ptr<LinuxFrameLayout> LinuxFrameLayout::Create(
NativeWindowViews* window,
bool wants_shadow) {
bool wants_shadow,
CSDStyle csd_style) {
if (x11_util::IsX11() || window->IsTranslucent() || !wants_shadow) {
return std::make_unique<LinuxUndecoratedFrameLayout>(window);
return std::make_unique<LinuxFrameLayout>(window);
} else if (csd_style == CSDStyle::kCustom) {
return std::make_unique<LinuxCSDCustomFrameLayout>(window);
} else {
return std::make_unique<LinuxCSDFrameLayout>(window);
return std::make_unique<LinuxCSDNativeFrameLayout>(window);
}
}
LinuxCSDFrameLayout::LinuxCSDFrameLayout(NativeWindowViews* window)
: window_(window) {
host_supports_client_frame_shadow_ = SupportsClientFrameShadow();
gfx::Insets LinuxFrameLayout::GetResizeBorderInsets() const {
gfx::Insets insets = RestoredFrameBorderInsets();
return insets.IsEmpty() ? GetInputInsets() : insets;
}
bool LinuxCSDFrameLayout::tiled() const {
return tiled_;
}
void LinuxCSDFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
gfx::Insets LinuxCSDFrameLayout::RestoredFrameBorderInsets() const {
gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip();
const gfx::Insets input = GetInputInsets();
auto expand_if_visible = [](int side_thickness, int min_band) {
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
};
gfx::Insets merged;
merged.set_top(expand_if_visible(insets.top(), input.top()));
merged.set_left(expand_if_visible(insets.left(), input.left()));
merged.set_bottom(expand_if_visible(insets.bottom(), input.bottom()));
merged.set_right(expand_if_visible(insets.right(), input.right()));
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
merged.bottom(), merged.left())
: merged;
}
gfx::Insets LinuxCSDFrameLayout::GetInputInsets() const {
bool showing_shadow = host_supports_client_frame_shadow_ &&
!window_->IsMaximized() && !window_->IsFullscreen();
return gfx::Insets(showing_shadow ? kResizeBorder : 0);
}
bool LinuxCSDFrameLayout::SupportsClientFrameShadow() const {
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
window_->GetAcceleratedWidget()));
return tree_host->SupportsClientFrameShadow();
}
void LinuxCSDFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) {
GetFrameProvider()->PaintWindowFrame(
canvas, local_bounds, titlebar_bounds.bottom(), active, GetInputInsets());
}
gfx::Rect LinuxCSDFrameLayout::GetWindowContentBounds() const {
gfx::Rect content_bounds = window_->widget()->GetWindowBoundsInScreen();
content_bounds.Inset(RestoredFrameBorderInsets());
return content_bounds;
}
SkRRect LinuxCSDFrameLayout::GetRoundedWindowContentBounds() const {
SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
SkRRect LinuxFrameLayout::GetRoundedWindowBounds() const {
SkRect rect = gfx::RectToSkRect(GetWindowBounds());
SkRRect rrect;
if (!window_->IsMaximized()) {
float radius = GetFrameProvider()->GetTopCornerRadiusDip();
float radius = GetTopCornerRadiusDip();
if (radius > 0) {
SkPoint round_point{radius, radius};
SkPoint radii[] = {round_point, round_point, {}, {}};
rrect.setRectRadii(rect, radii);
} else {
rrect.setRect(rect);
}
return rrect;
}
int LinuxCSDFrameLayout::GetTranslucentTopAreaHeight() const {
// Base implementation is suitable for X11/views without shadows
LinuxFrameLayout::LinuxFrameLayout(NativeWindowViews* window)
: window_(window) {
host_supports_client_frame_shadow_ = false;
}
LinuxFrameLayout::~LinuxFrameLayout() = default;
gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
}
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
}
bool LinuxFrameLayout::IsShowingShadow() const {
return host_supports_client_frame_shadow_ && !window_->IsMaximized() &&
!window_->IsFullscreen();
}
bool LinuxFrameLayout::SupportsClientFrameShadow() const {
return host_supports_client_frame_shadow_;
}
bool LinuxFrameLayout::tiled() const {
return tiled_;
}
void LinuxFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
bounds.Inset(RestoredFrameBorderInsets());
return bounds;
}
float LinuxFrameLayout::GetTopCornerRadiusDip() const {
return 0;
}
ui::WindowFrameProvider* LinuxCSDFrameLayout::GetFrameProvider() const {
int LinuxFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
gfx::Insets LinuxFrameLayout::NormalizeBorderInsets(
const gfx::Insets& frame_insets,
const gfx::Insets& input_insets) const {
auto expand_if_visible = [](int side_thickness, int min_band) {
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
};
// Ensure hit testing for resize targets works
// even if borders/shadows are absent on some edges.
gfx::Insets merged;
merged.set_top(expand_if_visible(frame_insets.top(), input_insets.top()));
merged.set_left(expand_if_visible(frame_insets.left(), input_insets.left()));
merged.set_bottom(
expand_if_visible(frame_insets.bottom(), input_insets.bottom()));
merged.set_right(
expand_if_visible(frame_insets.right(), input_insets.right()));
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
merged.bottom(), merged.left())
: merged;
}
// Used for a native-like frame with a FrameProvider
LinuxCSDNativeFrameLayout::LinuxCSDNativeFrameLayout(NativeWindowViews* window)
: LinuxFrameLayout(window) {
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
}
LinuxCSDNativeFrameLayout::~LinuxCSDNativeFrameLayout() = default;
gfx::Insets LinuxCSDNativeFrameLayout::RestoredFrameBorderInsets() const {
const gfx::Insets input_insets = GetInputInsets();
const gfx::Insets frame_insets = GetFrameProvider()->GetFrameThicknessDip();
return NormalizeBorderInsets(frame_insets, input_insets);
}
gfx::Insets LinuxCSDNativeFrameLayout::GetInputInsets() const {
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
}
float LinuxCSDNativeFrameLayout::GetTopCornerRadiusDip() const {
return window_->IsMaximized() ? 0
: GetFrameProvider()->GetTopCornerRadiusDip();
}
ui::WindowFrameProvider* LinuxCSDNativeFrameLayout::GetFrameProvider() const {
return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
!host_supports_client_frame_shadow_, tiled(), window_->IsMaximized());
}
LinuxUndecoratedFrameLayout::LinuxUndecoratedFrameLayout(
NativeWindowViews* window)
: window_(window) {}
gfx::Insets LinuxUndecoratedFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
// Used for Chromium-like custom CSD
LinuxCSDCustomFrameLayout::LinuxCSDCustomFrameLayout(NativeWindowViews* window)
: LinuxFrameLayout(window) {
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
}
gfx::Insets LinuxUndecoratedFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
LinuxCSDCustomFrameLayout::~LinuxCSDCustomFrameLayout() = default;
gfx::Insets LinuxCSDCustomFrameLayout::RestoredFrameBorderInsets() const {
const gfx::Insets input_insets = GetInputInsets();
const bool showing_shadow = IsShowingShadow();
const auto shadow_values = (showing_shadow && !tiled())
? GetFrameShadowValuesLinux(/*active=*/true)
: gfx::ShadowValues();
const gfx::Insets frame_insets = GetRestoredFrameBorderInsetsLinux(
showing_shadow, kDefaultCustomFrameBorder, shadow_values, input_insets);
return NormalizeBorderInsets(frame_insets, input_insets);
}
bool LinuxUndecoratedFrameLayout::SupportsClientFrameShadow() const {
return false;
gfx::Insets LinuxCSDCustomFrameLayout::GetInputInsets() const {
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
}
bool LinuxUndecoratedFrameLayout::tiled() const {
return tiled_;
}
void LinuxUndecoratedFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
void LinuxUndecoratedFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) {
// No-op
}
gfx::Rect LinuxUndecoratedFrameLayout::GetWindowContentBounds() const {
// With no transparent insets, widget bounds and logical bounds match.
return window_->widget()->GetWindowBoundsInScreen();
}
SkRRect LinuxUndecoratedFrameLayout::GetRoundedWindowContentBounds() const {
SkRRect rrect;
rrect.setRect(gfx::RectToSkRect(GetWindowContentBounds()));
return rrect;
}
int LinuxUndecoratedFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
ui::WindowFrameProvider* LinuxUndecoratedFrameLayout::GetFrameProvider() const {
return nullptr;
gfx::ShadowValues GetFrameShadowValuesLinux(bool active) {
const int elevation = views::LayoutProvider::Get()->GetShadowElevationMetric(
active ? views::Emphasis::kMaximum : views::Emphasis::kMedium);
return gfx::ShadowValue::MakeMdShadowValues(elevation);
}
} // namespace electron

View File

@@ -8,110 +8,96 @@
#include <memory>
#include "base/i18n/rtl.h"
#include "shell/browser/linux/x11_util.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
#include "base/memory/raw_ptr.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/linux/linux_ui.h"
#include "ui/gfx/shadow_value.h"
#include "ui/linux/window_frame_provider.h"
namespace gfx {
class Insets;
class Rect;
} // namespace gfx
namespace electron {
class NativeWindowViews;
// Shared helper for CSD layout and frame painting on Linux (shadows, resize
// regions, titlebars, etc.). Also helps views determine insets and perform
// bounds conversions between widget and logical coordinates.
// Shared helper for CSD layout on Linux (shadows, resize regions, titlebars,
// etc.). Also helps views determine insets and perform bounds conversions
// between widget and logical coordinates.
//
// The base class is concrete and suitable as-is for the undecorated case (X11,
// translucent windows, or windows without shadows). CSD subclasses override
// the methods that differ.
class LinuxFrameLayout {
public:
virtual ~LinuxFrameLayout() = default;
enum class CSDStyle {
kNativeFrame,
kCustom,
};
explicit LinuxFrameLayout(NativeWindowViews* window);
virtual ~LinuxFrameLayout();
static std::unique_ptr<LinuxFrameLayout> Create(NativeWindowViews* window,
bool wants_shadow);
bool wants_shadow,
CSDStyle csd_style);
// Insets from the transparent widget border to the opaque part of the window
virtual gfx::Insets RestoredFrameBorderInsets() const = 0;
// Insets for parts of the surface that should be counted for user input
virtual gfx::Insets GetInputInsets() const = 0;
// Insets from the transparent widget border to the opaque part of the window.
virtual gfx::Insets RestoredFrameBorderInsets() const;
// Insets for parts of the surface that should be counted for user input.
virtual gfx::Insets GetInputInsets() const;
// Insets to use for non-client resize hit-testing.
gfx::Insets GetResizeBorderInsets() const;
virtual bool SupportsClientFrameShadow() const = 0;
bool IsShowingShadow() const;
bool SupportsClientFrameShadow() const;
virtual bool tiled() const = 0;
virtual void set_tiled(bool tiled) = 0;
bool tiled() const;
void set_tiled(bool tiled);
virtual void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) = 0;
// The logical bounds of the window interior.
gfx::Rect GetWindowBounds() const;
// The logical window bounds as a rounded rect with corner radii applied.
SkRRect GetRoundedWindowBounds() const;
// The corner radius of the top corners of the window, in DIPs.
virtual float GetTopCornerRadiusDip() const;
// The logical bounds of the window
virtual gfx::Rect GetWindowContentBounds() const = 0;
// The logical bounds as a rounded rect with corner radii applied
virtual SkRRect GetRoundedWindowContentBounds() const = 0;
int GetTranslucentTopAreaHeight() const;
virtual int GetTranslucentTopAreaHeight() const = 0;
protected:
gfx::Insets NormalizeBorderInsets(const gfx::Insets& frame_insets,
const gfx::Insets& input_insets) const;
virtual ui::WindowFrameProvider* GetFrameProvider() const = 0;
};
// Client-side decoration (CSD) Linux frame layout implementation.
class LinuxCSDFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDFrameLayout(NativeWindowViews* window);
~LinuxCSDFrameLayout() override = default;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
bool SupportsClientFrameShadow() const override;
bool tiled() const override;
void set_tiled(bool tiled) override;
void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) override;
gfx::Rect GetWindowContentBounds() const override;
SkRRect GetRoundedWindowContentBounds() const override;
int GetTranslucentTopAreaHeight() const override;
ui::WindowFrameProvider* GetFrameProvider() const override;
private:
raw_ptr<NativeWindowViews> window_;
bool tiled_ = false;
bool host_supports_client_frame_shadow_ = false;
};
// No-decoration Linux frame layout implementation.
//
// Intended for cases where we do not allocate a transparent inset area around
// the window (e.g. X11 / server-side decorations, or when insets are disabled).
// All inset math returns 0 and frame painting is skipped.
class LinuxUndecoratedFrameLayout : public LinuxFrameLayout {
// CSD strategy that uses the GTK window frame provider for metrics.
class LinuxCSDNativeFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxUndecoratedFrameLayout(NativeWindowViews* window);
~LinuxUndecoratedFrameLayout() override = default;
explicit LinuxCSDNativeFrameLayout(NativeWindowViews* window);
~LinuxCSDNativeFrameLayout() override;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
bool SupportsClientFrameShadow() const override;
bool tiled() const override;
void set_tiled(bool tiled) override;
void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) override;
gfx::Rect GetWindowContentBounds() const override;
SkRRect GetRoundedWindowContentBounds() const override;
int GetTranslucentTopAreaHeight() const override;
ui::WindowFrameProvider* GetFrameProvider() const override;
private:
raw_ptr<NativeWindowViews> window_;
bool tiled_ = false;
float GetTopCornerRadiusDip() const override;
ui::WindowFrameProvider* GetFrameProvider() const;
};
// CSD strategy that uses custom metrics, similar to those used in Chromium.
class LinuxCSDCustomFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDCustomFrameLayout(NativeWindowViews* window);
~LinuxCSDCustomFrameLayout() override;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
};
gfx::ShadowValues GetFrameShadowValuesLinux(bool active);
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_LINUX_FRAME_LAYOUT_H_

View File

@@ -5,22 +5,24 @@
#include "shell/browser/ui/views/opaque_frame_view.h"
#include "base/containers/adapters.h"
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
#include "chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h" // nogncheck
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/font_list.h"
#include "ui/linux/linux_ui.h"
#include "ui/gfx/geometry/insets_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/frame_background.h"
#include "ui/views/window/frame_caption_button.h"
#include "ui/views/window/vector_icons/vector_icons.h"
@@ -55,12 +57,14 @@ const int kCaptionButtonBottomPadding = 3;
// The content edge images have a shadow built into them.
const int OpaqueFrameView::kContentEdgeShadowThickness = 2;
OpaqueFrameView::OpaqueFrameView() = default;
OpaqueFrameView::OpaqueFrameView()
: frame_background_(std::make_unique<views::FrameBackground>()) {}
OpaqueFrameView::~OpaqueFrameView() = default;
void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
FramelessView::Init(window, frame);
linux_frame_layout_ = LinuxFrameLayout::Create(window, window->HasShadow());
linux_frame_layout_ = LinuxFrameLayout::Create(
window, window->HasShadow(), LinuxFrameLayout::CSDStyle::kCustom);
// Unretained() is safe because the subscription is saved into an instance
// member and thus will be cancelled upon the instance's destruction.
@@ -98,9 +102,8 @@ void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
}
int OpaqueFrameView::ResizingBorderHitTest(const gfx::Point& point) {
auto insets = RestoredFrameBorderInsets();
return ResizingBorderHitTestImpl(
point, insets.IsEmpty() ? linux_frame_layout_->GetInputInsets() : insets);
point, linux_frame_layout_->GetResizeBorderInsets());
}
void OpaqueFrameView::InvalidateCaptionButtons() {
@@ -200,14 +203,31 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
if (frame()->IsFullscreen())
return;
// Titlebar height must be at least the frame border insets to avoid
// a negative height calculation in the GTK frame provider. We add 1 to
// ensure it's always positive even when insets are 0.
int top_area_height = RestoredFrameBorderInsets().top() + 1;
const bool active = ShouldPaintAsActive();
const gfx::Insets border = RestoredFrameBorderInsets();
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
gfx::RectF bounds_dip(GetLocalBounds());
if (showing_shadow) {
bounds_dip.Inset(gfx::InsetsF(border));
}
linux_frame_layout_->PaintWindowFrame(
canvas, GetLocalBounds(), gfx::Rect(0, 0, width(), top_area_height),
ShouldPaintAsActive());
// TODO: support roundedCorners.
float radius_dip = 0;
SkVector radii[4]{{radius_dip, radius_dip}, {radius_dip, radius_dip}, {}, {}};
SkRRect clip;
clip.setRectRadii(gfx::RectFToSkRect(bounds_dip), radii);
frame_background_->set_frame_color(GetFrameColor());
frame_background_->set_use_custom_frame(true);
frame_background_->set_is_active(active);
frame_background_->set_top_area_height(GetTopAreaHeight());
const bool draw_shadow = showing_shadow && !linux_frame_layout_->tiled();
auto shadow_values =
draw_shadow ? GetFrameShadowValuesLinux(active) : gfx::ShadowValues();
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
showing_shadow, active, border, shadow_values,
linux_frame_layout_->tiled());
if (!window()->IsWindowControlsOverlayEnabled())
return;

View File

@@ -20,6 +20,10 @@
class CaptionButtonPlaceholderContainer;
namespace views {
class FrameBackground;
}
namespace electron {
class NativeWindowViews;
@@ -166,6 +170,7 @@ class OpaqueFrameView : public FramelessView {
bool is_leading_button) const;
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
std::unique_ptr<views::FrameBackground> frame_background_;
// Window controls.
raw_ptr<views::Button> minimize_button_;

View File

@@ -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)));

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -6902,6 +6902,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', () => {

View File

@@ -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);

View File

@@ -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;
});
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -129,6 +129,22 @@ describe('utilityProcess module', () => {
expect(code).to.equal(exitCode);
});
ifit(process.platform === 'win32')('emits correct exit code when high bit is set on Windows', async () => {
// NTSTATUS code with high bit set should not be mangled by sign extension.
const exitCode = 0xC0000005;
const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
const [code] = await once(child, 'exit');
expect(code).to.equal(exitCode);
});
ifit(process.platform !== 'win32')('emits correct exit code when child process crashes on posix', async () => {
// Crash exit codes should not be sign-extended to large 64-bit values.
const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
const [code] = await once(child, 'exit');
expect(code).to.not.equal(0);
expect(code).to.be.lessThanOrEqual(0xFFFFFFFF);
});
it('does not run JS after process.exit is called', async () => {
const file = path.join(os.tmpdir(), `no-js-after-exit-log-${Math.random()}`);
const child = utilityProcess.fork(path.join(fixturesPath, 'no-js-after-exit.js'), [`--testPath=${file}`]);

View File

@@ -0,0 +1,7 @@
<html>
<body>
<script>
window.open('javascript:alert()');
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
const { app, BrowserWindow } = require('electron');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error(reason);
process.exit(1);
});
app.on('browser-window-created', (_, window) => {
window.webContents.once('did-frame-navigate', () => {
process.exit(0);
});
});
app.whenReady().then(() => {
const win = new BrowserWindow({ show: false });
win.loadFile('index.html');
});

View File

@@ -0,0 +1,7 @@
/node_modules
/build
*.swp
*.log
*~
.node-version
package-lock.json

View File

@@ -0,0 +1,23 @@
{
'targets': [
{
'target_name': 'dialog_helper',
'conditions': [
['OS=="mac"', {
'sources': [
'src/main.cc',
'src/dialog_helper_mac.mm',
],
'libraries': [
'$(SDKROOT)/System/Library/Frameworks/AppKit.framework',
],
'xcode_settings': {
'OTHER_CFLAGS': ['-fobjc-arc'],
},
}, {
'type': 'none',
}],
],
}
]
}

View File

@@ -0,0 +1,2 @@
const binding = require('../build/Release/dialog_helper.node');
module.exports = binding;

View File

@@ -0,0 +1,10 @@
{
"name": "@electron-ci/dialog-helper",
"version": "0.0.1",
"main": "./lib/index.js",
"private": true,
"licenses": "MIT",
"scripts": {
"install": "node-gyp configure && node-gyp build"
}
}

View File

@@ -0,0 +1,68 @@
#ifndef SRC_DIALOG_HELPER_H_
#define SRC_DIALOG_HELPER_H_
#include <cstddef>
#include <string>
namespace dialog_helper {
struct DialogInfo {
// "message-box", "open-dialog", "save-dialog", or "none"
std::string type;
// Button titles for message boxes
std::string buttons; // JSON array string, e.g. '["OK","Cancel"]'
// Message text (NSAlert messageText or panel title)
std::string message;
// Detail / informative text (NSAlert informativeText)
std::string detail;
// Checkbox (suppression button) label, empty if none
std::string checkbox_label;
// Whether the checkbox is checked
bool checkbox_checked = false;
// File dialog properties (open/save panels)
std::string prompt; // Button label (NSSavePanel prompt)
std::string panel_message; // Panel message text (NSSavePanel message)
std::string directory; // Current directory URL path
// NSSavePanel-specific properties
std::string name_field_label; // Label for the name field
std::string name_field_value; // Current value of the name field
bool shows_tag_field = true;
// NSOpenPanel-specific properties
bool can_choose_files = false;
bool can_choose_directories = false;
bool allows_multiple_selection = false;
// Shared panel properties (open and save)
bool shows_hidden_files = false;
bool resolves_aliases = true;
bool treats_packages_as_directories = false;
bool can_create_directories = false;
};
// Get information about the sheet dialog attached to the window identified
// by the given native handle buffer (NSView* on macOS).
DialogInfo GetDialogInfo(char* handle, size_t size);
// Click a button at the given index on an NSAlert sheet attached to the window.
// Returns true if a message box was found and the button was clicked.
bool ClickMessageBoxButton(char* handle, size_t size, int button_index);
// Toggle the checkbox (suppression button) on an NSAlert sheet.
// Returns true if a checkbox was found and clicked.
bool ClickCheckbox(char* handle, size_t size);
// Cancel the file dialog (NSOpenPanel/NSSavePanel) sheet attached to the window.
// Returns true if a file dialog was found and cancelled.
bool CancelFileDialog(char* handle, size_t size);
// Accept the file dialog sheet attached to the window.
// For save dialogs, |filename| is set in the name field before accepting.
// Returns true if a file dialog was found and accepted.
bool AcceptFileDialog(char* handle, size_t size, const std::string& filename);
} // namespace dialog_helper
#endif // SRC_DIALOG_HELPER_H_

View File

@@ -0,0 +1,320 @@
#include "dialog_helper.h"
#import <Cocoa/Cocoa.h>
namespace {
// Extract the NSWindow* from the native handle buffer.
// The buffer contains an NSView* (the content view of the window).
NSWindow* GetNSWindowFromHandle(char* handle, size_t size) {
if (size != sizeof(void*))
return nil;
// Read the raw pointer from the buffer, then bridge to ARC.
void* raw = *reinterpret_cast<void**>(handle);
NSView* view = (__bridge NSView*)raw;
if (!view || ![view isKindOfClass:[NSView class]])
return nil;
return [view window];
}
} // namespace
namespace dialog_helper {
DialogInfo GetDialogInfo(char* handle, size_t size) {
DialogInfo info;
info.type = "none";
NSWindow* window = GetNSWindowFromHandle(handle, size);
if (!window)
return info;
NSWindow* sheet = [window attachedSheet];
if (!sheet)
return info;
// NSOpenPanel is a subclass of NSSavePanel, so check NSOpenPanel first.
if ([sheet isKindOfClass:[NSOpenPanel class]]) {
info.type = "open-dialog";
NSOpenPanel* panel = (NSOpenPanel*)sheet;
info.message = [[panel title] UTF8String] ?: "";
info.prompt = [[panel prompt] UTF8String] ?: "";
info.panel_message = [[panel message] UTF8String] ?: "";
if ([panel directoryURL])
info.directory = [[[panel directoryURL] path] UTF8String] ?: "";
info.can_choose_files = [panel canChooseFiles];
info.can_choose_directories = [panel canChooseDirectories];
info.allows_multiple_selection = [panel allowsMultipleSelection];
info.shows_hidden_files = [panel showsHiddenFiles];
info.resolves_aliases = [panel resolvesAliases];
info.treats_packages_as_directories = [panel treatsFilePackagesAsDirectories];
info.can_create_directories = [panel canCreateDirectories];
return info;
}
if ([sheet isKindOfClass:[NSSavePanel class]]) {
info.type = "save-dialog";
NSSavePanel* panel = (NSSavePanel*)sheet;
info.message = [[panel title] UTF8String] ?: "";
info.prompt = [[panel prompt] UTF8String] ?: "";
info.panel_message = [[panel message] UTF8String] ?: "";
if ([panel directoryURL])
info.directory = [[[panel directoryURL] path] UTF8String] ?: "";
info.name_field_label = [[panel nameFieldLabel] UTF8String] ?: "";
info.name_field_value = [[panel nameFieldStringValue] UTF8String] ?: "";
info.shows_tag_field = [panel showsTagField];
info.shows_hidden_files = [panel showsHiddenFiles];
info.treats_packages_as_directories =
[panel treatsFilePackagesAsDirectories];
info.can_create_directories = [panel canCreateDirectories];
return info;
}
// For NSAlert, the sheet window is not an NSSavePanel.
// Check if it contains typical NSAlert button structure.
// NSAlert's window contains buttons as subviews in its content view.
NSView* contentView = [sheet contentView];
NSMutableArray<NSButton*>* buttons = [NSMutableArray array];
// Recursively find all NSButton instances in the view hierarchy.
NSMutableArray<NSView*>* stack =
[NSMutableArray arrayWithObject:contentView];
while ([stack count] > 0) {
NSView* current = [stack lastObject];
[stack removeLastObject];
if ([current isKindOfClass:[NSButton class]]) {
NSButton* btn = (NSButton*)current;
// Filter to push-type buttons (not checkboxes, radio buttons, etc.)
if ([btn bezelStyle] == NSBezelStyleRounded ||
[btn bezelStyle] == NSBezelStyleRegularSquare) {
[buttons addObject:btn];
}
}
for (NSView* subview in [current subviews]) {
[stack addObject:subview];
}
}
if ([buttons count] > 0) {
info.type = "message-box";
// Sort buttons by tag to maintain the order they were added.
[buttons sortUsingComparator:^NSComparisonResult(NSButton* a, NSButton* b) {
if ([a tag] < [b tag])
return NSOrderedAscending;
if ([a tag] > [b tag])
return NSOrderedDescending;
return NSOrderedSame;
}];
std::string btn_json = "[";
for (NSUInteger i = 0; i < [buttons count]; i++) {
if (i > 0)
btn_json += ",";
btn_json += "\"";
NSString* title = [[buttons objectAtIndex:i] title];
btn_json += [title UTF8String] ?: "";
btn_json += "\"";
}
btn_json += "]";
info.buttons = btn_json;
// NSAlert's content view contains static NSTextFields for message and
// detail text. The first non-editable text field with content is the
// message; the second is the detail (informative text).
int text_field_index = 0;
// Walk all subviews (non-recursive — NSAlert places labels directly).
for (NSView* subview in [contentView subviews]) {
if ([subview isKindOfClass:[NSTextField class]]) {
NSTextField* field = (NSTextField*)subview;
if (![field isEditable] && [[field stringValue] length] > 0) {
if (text_field_index == 0) {
info.message = [[field stringValue] UTF8String];
} else if (text_field_index == 1) {
info.detail = [[field stringValue] UTF8String];
}
text_field_index++;
}
}
}
// Check for the suppression (checkbox) button.
// NSAlert's suppression button is a non-bordered NSButton, unlike
// push buttons which are bordered. This reliably identifies it
// across macOS versions where the accessibility role may differ.
NSMutableArray<NSView*>* cbStack =
[NSMutableArray arrayWithObject:contentView];
while ([cbStack count] > 0) {
NSView* current = [cbStack lastObject];
[cbStack removeLastObject];
if ([current isKindOfClass:[NSButton class]]) {
NSButton* btn = (NSButton*)current;
if (![btn isBordered]) {
NSString* title = [btn title];
if (title && [title length] > 0) {
info.checkbox_label = [title UTF8String];
info.checkbox_checked =
([btn state] == NSControlStateValueOn);
}
}
}
for (NSView* sub in [current subviews]) {
[cbStack addObject:sub];
}
}
}
return info;
}
bool ClickMessageBoxButton(char* handle, size_t size, int button_index) {
NSWindow* window = GetNSWindowFromHandle(handle, size);
if (!window)
return false;
NSWindow* sheet = [window attachedSheet];
if (!sheet)
return false;
// Find buttons in the sheet, sorted by tag.
NSView* contentView = [sheet contentView];
NSMutableArray<NSButton*>* buttons = [NSMutableArray array];
NSMutableArray<NSView*>* stack =
[NSMutableArray arrayWithObject:contentView];
while ([stack count] > 0) {
NSView* current = [stack lastObject];
[stack removeLastObject];
if ([current isKindOfClass:[NSButton class]]) {
NSButton* btn = (NSButton*)current;
if ([btn bezelStyle] == NSBezelStyleRounded ||
[btn bezelStyle] == NSBezelStyleRegularSquare) {
[buttons addObject:btn];
}
}
for (NSView* subview in [current subviews]) {
[stack addObject:subview];
}
}
[buttons sortUsingComparator:^NSComparisonResult(NSButton* a, NSButton* b) {
if ([a tag] < [b tag])
return NSOrderedAscending;
if ([a tag] > [b tag])
return NSOrderedDescending;
return NSOrderedSame;
}];
if (button_index < 0 || button_index >= (int)[buttons count])
return false;
NSButton* target = [buttons objectAtIndex:button_index];
[target performClick:nil];
return true;
}
bool ClickCheckbox(char* handle, size_t size) {
NSWindow* window = GetNSWindowFromHandle(handle, size);
if (!window)
return false;
NSWindow* sheet = [window attachedSheet];
if (!sheet)
return false;
// Find the suppression/checkbox button — it is a non-bordered NSButton,
// unlike the push buttons which are bordered.
NSView* contentView = [sheet contentView];
NSMutableArray<NSView*>* stack =
[NSMutableArray arrayWithObject:contentView];
while ([stack count] > 0) {
NSView* current = [stack lastObject];
[stack removeLastObject];
if ([current isKindOfClass:[NSButton class]]) {
NSButton* btn = (NSButton*)current;
if (![btn isBordered] && [[btn title] length] > 0) {
[btn performClick:nil];
return true;
}
}
for (NSView* subview in [current subviews]) {
[stack addObject:subview];
}
}
return false;
}
bool CancelFileDialog(char* handle, size_t size) {
NSWindow* window = GetNSWindowFromHandle(handle, size);
if (!window)
return false;
NSWindow* sheet = [window attachedSheet];
if (!sheet)
return false;
// sheet is the NSSavePanel/NSOpenPanel window itself when presented as a
// sheet. We need to find the actual panel object. On macOS, when an
// NSSavePanel is run as a sheet, [window attachedSheet] returns the panel's
// window. The panel can be retrieved because NSSavePanel IS the window.
if ([sheet isKindOfClass:[NSSavePanel class]]) {
NSSavePanel* panel = (NSSavePanel*)sheet;
[panel cancel:nil];
return true;
}
// If it's not a recognized panel type, try ending the sheet directly.
[NSApp endSheet:sheet returnCode:NSModalResponseCancel];
return true;
}
bool AcceptFileDialog(char* handle, size_t size, const std::string& filename) {
NSWindow* window = GetNSWindowFromHandle(handle, size);
if (!window)
return false;
NSWindow* sheet = [window attachedSheet];
if (!sheet)
return false;
if (![sheet isKindOfClass:[NSSavePanel class]])
return false;
NSSavePanel* panel = (NSSavePanel*)sheet;
// Set the filename if provided (for save dialogs).
if (!filename.empty()) {
NSString* name = [NSString stringWithUTF8String:filename.c_str()];
[panel setNameFieldStringValue:name];
// Resign first responder to commit the name field edit. Without this,
// the panel may still use the previous value (e.g. "Untitled") when
// the accept button is clicked immediately after.
[sheet makeFirstResponder:nil];
}
NSView* contentView = [sheet contentView];
// Search for the default button (key equivalent "\r") in the view hierarchy.
NSMutableArray<NSView*>* stack =
[NSMutableArray arrayWithObject:contentView];
while ([stack count] > 0) {
NSView* current = [stack lastObject];
[stack removeLastObject];
if ([current isKindOfClass:[NSButton class]]) {
NSButton* btn = (NSButton*)current;
if ([[btn keyEquivalent] isEqualToString:@"\r"]) {
[btn performClick:nil];
return true;
}
}
for (NSView* subview in [current subviews]) {
[stack addObject:subview];
}
}
[NSApp endSheet:sheet returnCode:NSModalResponseOK];
return true;
}
} // namespace dialog_helper

View File

@@ -0,0 +1,231 @@
#include <js_native_api.h>
#include <node_api.h>
#include <string>
#include "dialog_helper.h"
namespace {
// Helper: extract (char* data, size_t length) from the first Buffer argument.
bool GetHandleArg(napi_env env, napi_callback_info info, size_t expected_argc,
napi_value* args, char** data, size_t* length) {
size_t argc = expected_argc;
napi_status status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
if (status != napi_ok || argc < 1)
return false;
bool is_buffer;
status = napi_is_buffer(env, args[0], &is_buffer);
if (status != napi_ok || !is_buffer) {
napi_throw_error(env, NULL, "First argument must be a Buffer (native window handle)");
return false;
}
status = napi_get_buffer_info(env, args[0], (void**)data, length);
return status == napi_ok;
}
napi_value GetDialogInfo(napi_env env, napi_callback_info info) {
napi_value args[1];
char* data;
size_t length;
if (!GetHandleArg(env, info, 1, args, &data, &length))
return NULL;
dialog_helper::DialogInfo di = dialog_helper::GetDialogInfo(data, length);
napi_value result;
napi_create_object(env, &result);
// Message box properties
napi_value type_val;
napi_create_string_utf8(env, di.type.c_str(), di.type.size(), &type_val);
napi_set_named_property(env, result, "type", type_val);
napi_value buttons_val;
napi_create_string_utf8(env, di.buttons.c_str(), di.buttons.size(), &buttons_val);
napi_set_named_property(env, result, "buttons", buttons_val);
napi_value message_val;
napi_create_string_utf8(env, di.message.c_str(), di.message.size(), &message_val);
napi_set_named_property(env, result, "message", message_val);
napi_value detail_val;
napi_create_string_utf8(env, di.detail.c_str(), di.detail.size(), &detail_val);
napi_set_named_property(env, result, "detail", detail_val);
napi_value checkbox_label_val;
napi_create_string_utf8(env, di.checkbox_label.c_str(),
di.checkbox_label.size(), &checkbox_label_val);
napi_set_named_property(env, result, "checkboxLabel", checkbox_label_val);
napi_value checkbox_checked_val;
napi_get_boolean(env, di.checkbox_checked, &checkbox_checked_val);
napi_set_named_property(env, result, "checkboxChecked", checkbox_checked_val);
// File dialog properties
napi_value prompt_val;
napi_create_string_utf8(env, di.prompt.c_str(), di.prompt.size(), &prompt_val);
napi_set_named_property(env, result, "prompt", prompt_val);
napi_value panel_message_val;
napi_create_string_utf8(env, di.panel_message.c_str(),
di.panel_message.size(), &panel_message_val);
napi_set_named_property(env, result, "panelMessage", panel_message_val);
napi_value directory_val;
napi_create_string_utf8(env, di.directory.c_str(), di.directory.size(),
&directory_val);
napi_set_named_property(env, result, "directory", directory_val);
// NSSavePanel-specific string/boolean properties
napi_value name_field_label_val;
napi_create_string_utf8(env, di.name_field_label.c_str(),
di.name_field_label.size(), &name_field_label_val);
napi_set_named_property(env, result, "nameFieldLabel", name_field_label_val);
napi_value name_field_value_val;
napi_create_string_utf8(env, di.name_field_value.c_str(),
di.name_field_value.size(), &name_field_value_val);
napi_set_named_property(env, result, "nameFieldValue", name_field_value_val);
napi_value shows_tag_field_val;
napi_get_boolean(env, di.shows_tag_field, &shows_tag_field_val);
napi_set_named_property(env, result, "showsTagField", shows_tag_field_val);
// NSOpenPanel-specific properties
napi_value can_choose_files_val;
napi_get_boolean(env, di.can_choose_files, &can_choose_files_val);
napi_set_named_property(env, result, "canChooseFiles", can_choose_files_val);
napi_value can_choose_dirs_val;
napi_get_boolean(env, di.can_choose_directories, &can_choose_dirs_val);
napi_set_named_property(env, result, "canChooseDirectories",
can_choose_dirs_val);
napi_value allows_multi_val;
napi_get_boolean(env, di.allows_multiple_selection, &allows_multi_val);
napi_set_named_property(env, result, "allowsMultipleSelection",
allows_multi_val);
// Shared panel properties (open and save)
napi_value shows_hidden_val;
napi_get_boolean(env, di.shows_hidden_files, &shows_hidden_val);
napi_set_named_property(env, result, "showsHiddenFiles", shows_hidden_val);
napi_value resolves_aliases_val;
napi_get_boolean(env, di.resolves_aliases, &resolves_aliases_val);
napi_set_named_property(env, result, "resolvesAliases", resolves_aliases_val);
napi_value treats_packages_val;
napi_get_boolean(env, di.treats_packages_as_directories, &treats_packages_val);
napi_set_named_property(env, result, "treatsPackagesAsDirectories",
treats_packages_val);
napi_value can_create_dirs_val;
napi_get_boolean(env, di.can_create_directories, &can_create_dirs_val);
napi_set_named_property(env, result, "canCreateDirectories",
can_create_dirs_val);
return result;
}
napi_value ClickMessageBoxButton(napi_env env, napi_callback_info info) {
napi_value args[2];
char* data;
size_t length;
if (!GetHandleArg(env, info, 2, args, &data, &length))
return NULL;
int32_t button_index;
napi_status status = napi_get_value_int32(env, args[1], &button_index);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Second argument must be a number (button index)");
return NULL;
}
bool ok = dialog_helper::ClickMessageBoxButton(data, length, button_index);
napi_value result;
napi_get_boolean(env, ok, &result);
return result;
}
napi_value ClickCheckbox(napi_env env, napi_callback_info info) {
napi_value args[1];
char* data;
size_t length;
if (!GetHandleArg(env, info, 1, args, &data, &length))
return NULL;
bool ok = dialog_helper::ClickCheckbox(data, length);
napi_value result;
napi_get_boolean(env, ok, &result);
return result;
}
napi_value CancelFileDialog(napi_env env, napi_callback_info info) {
napi_value args[1];
char* data;
size_t length;
if (!GetHandleArg(env, info, 1, args, &data, &length))
return NULL;
bool ok = dialog_helper::CancelFileDialog(data, length);
napi_value result;
napi_get_boolean(env, ok, &result);
return result;
}
napi_value AcceptFileDialog(napi_env env, napi_callback_info info) {
napi_value args[2];
char* data;
size_t length;
if (!GetHandleArg(env, info, 2, args, &data, &length))
return NULL;
std::string filename;
// Second argument (filename) is optional.
napi_valuetype vtype;
napi_typeof(env, args[1], &vtype);
if (vtype == napi_string) {
size_t str_len;
napi_get_value_string_utf8(env, args[1], NULL, 0, &str_len);
filename.resize(str_len);
napi_get_value_string_utf8(env, args[1], &filename[0], str_len + 1,
&str_len);
}
bool ok = dialog_helper::AcceptFileDialog(data, length, filename);
napi_value result;
napi_get_boolean(env, ok, &result);
return result;
}
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = {
{"getDialogInfo", NULL, GetDialogInfo, NULL, NULL, NULL,
napi_enumerable, NULL},
{"clickMessageBoxButton", NULL, ClickMessageBoxButton, NULL, NULL, NULL,
napi_enumerable, NULL},
{"clickCheckbox", NULL, ClickCheckbox, NULL, NULL, NULL,
napi_enumerable, NULL},
{"cancelFileDialog", NULL, CancelFileDialog, NULL, NULL, NULL,
napi_enumerable, NULL},
{"acceptFileDialog", NULL, AcceptFileDialog, NULL, NULL, NULL,
napi_enumerable, NULL},
};
napi_define_properties(env, exports,
sizeof(descriptors) / sizeof(*descriptors),
descriptors);
return exports;
}
} // namespace
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -186,6 +186,39 @@ describe('webContents.setWindowOpenHandler', () => {
await once(browserWindow.webContents, 'did-create-window');
});
it('reuses an existing window when window.open is called with the same frame name', async () => {
let handlerCallCount = 0;
browserWindow.webContents.setWindowOpenHandler(() => {
handlerCallCount++;
return { action: 'allow' };
});
const didCreateWindow = once(browserWindow.webContents, 'did-create-window') as Promise<[BrowserWindow, Electron.DidCreateWindowDetails]>;
await browserWindow.webContents.executeJavaScript("window.open('about:blank?one', 'named-target', 'show=no') && true");
const [childWindow] = await didCreateWindow;
expect(handlerCallCount).to.equal(1);
expect(childWindow.webContents.getURL()).to.equal('about:blank?one');
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not fire when reusing a named window');
});
const didNavigate = once(childWindow.webContents, 'did-navigate');
const sameWindow = await browserWindow.webContents.executeJavaScript(`
(() => {
const first = window.open('about:blank?one', 'named-target', 'show=no');
const second = window.open('about:blank?two', 'named-target', 'show=no');
return first === second;
})()
`);
await didNavigate;
expect(sameWindow).to.be.true('window.open with matching frame name should return the same window proxy');
expect(handlerCallCount).to.equal(1, 'setWindowOpenHandler should not be called when Blink resolves the named target');
expect(childWindow.webContents.getURL()).to.equal('about:blank?two');
expect(BrowserWindow.getAllWindows()).to.have.lengthOf(2);
});
it('can change webPreferences of child windows', async () => {
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));

View File

@@ -7,6 +7,7 @@
"node-gyp-install": "node-gyp install"
},
"devDependencies": {
"@electron-ci/dialog-helper": "*",
"@electron-ci/echo": "*",
"@electron-ci/external-ab": "*",
"@electron-ci/is-valid-window": "*",

View File

@@ -647,6 +647,12 @@ __metadata:
languageName: unknown
linkType: soft
"@electron-ci/dialog-helper@npm:*, @electron-ci/dialog-helper@workspace:spec/fixtures/native-addon/dialog-helper":
version: 0.0.0-use.local
resolution: "@electron-ci/dialog-helper@workspace:spec/fixtures/native-addon/dialog-helper"
languageName: unknown
linkType: soft
"@electron-ci/echo@npm:*, @electron-ci/echo@workspace:spec/fixtures/native-addon/echo":
version: 0.0.0-use.local
resolution: "@electron-ci/echo@workspace:spec/fixtures/native-addon/echo"
@@ -4899,6 +4905,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "electron-test-main@workspace:spec"
dependencies:
"@electron-ci/dialog-helper": "npm:*"
"@electron-ci/echo": "npm:*"
"@electron-ci/external-ab": "npm:*"
"@electron-ci/is-valid-window": "npm:*"