mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
Compare commits
135 Commits
ci/replace
...
sam/vitest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bc4bc55f1 | ||
|
|
9ef4ca2a7b | ||
|
|
2a6960541e | ||
|
|
e90456a9d6 | ||
|
|
8c92be4037 | ||
|
|
76900132a9 | ||
|
|
3100799343 | ||
|
|
d9c2df194f | ||
|
|
9a6e0a0251 | ||
|
|
06f54c3ef1 | ||
|
|
2e175b96a0 | ||
|
|
b337bdd789 | ||
|
|
3e0125da61 | ||
|
|
e363fd2136 | ||
|
|
aed406000e | ||
|
|
28fb9f0965 | ||
|
|
8c887520fd | ||
|
|
5f7a2f99e1 | ||
|
|
032cb1d26c | ||
|
|
ce203ae0fc | ||
|
|
56d213bc96 | ||
|
|
b6494c70b1 | ||
|
|
a23a4b8f5c | ||
|
|
dd93cd1fa9 | ||
|
|
dc8cd08dc7 | ||
|
|
6bbc801fc2 | ||
|
|
4a506e7e25 | ||
|
|
59b3e45d0c | ||
|
|
2276abbd96 | ||
|
|
5b10d20d38 | ||
|
|
88ec0f4bef | ||
|
|
cb60689173 | ||
|
|
34c05cd1a5 | ||
|
|
4929a267e6 | ||
|
|
7d31ad9297 | ||
|
|
8cebedc7b6 | ||
|
|
69c9ff51eb | ||
|
|
64f3c1c199 | ||
|
|
0d321ccae0 | ||
|
|
36e5f6a2a5 | ||
|
|
65f2dee205 | ||
|
|
c2304737f1 | ||
|
|
8e51023155 | ||
|
|
7cdd1314d2 | ||
|
|
984f5c5023 | ||
|
|
abadff1643 | ||
|
|
6beb0f3de3 | ||
|
|
2e826cdaa4 | ||
|
|
dc02d7907b | ||
|
|
3bb67099de | ||
|
|
6fea5e8691 | ||
|
|
7a5dc9e725 | ||
|
|
2067a6b975 | ||
|
|
3dbd48441e | ||
|
|
6eee70a432 | ||
|
|
07cdd3543b | ||
|
|
e7d867952c | ||
|
|
e4474b232e | ||
|
|
d39cbc204b | ||
|
|
b1f3befe21 | ||
|
|
6d99359799 | ||
|
|
b5c9d0bdff | ||
|
|
46464ace53 | ||
|
|
46ce6bdc94 | ||
|
|
295deb50e0 | ||
|
|
2e8b3fb5bf | ||
|
|
4304cd88b6 | ||
|
|
b93c6dd11a | ||
|
|
6c9447f2e6 | ||
|
|
1127cd3b6c | ||
|
|
0608bc93a0 | ||
|
|
0a1e6119a6 | ||
|
|
d69cee4a05 | ||
|
|
9c6df31551 | ||
|
|
178456f69e | ||
|
|
8eac3fc601 | ||
|
|
971d4369eb | ||
|
|
8dc0354881 | ||
|
|
4d6349eeca | ||
|
|
24fc081b2f | ||
|
|
1e0e9a41f3 | ||
|
|
d2a5c72b5f | ||
|
|
2e7aaacbbe | ||
|
|
0419a43b84 | ||
|
|
0faef330c1 | ||
|
|
25a4d53dcc | ||
|
|
644bc84146 | ||
|
|
040fa8cb6a | ||
|
|
6fb8c72b96 | ||
|
|
8881184167 | ||
|
|
c34e07fae5 | ||
|
|
8cdb30f0f0 | ||
|
|
b58390fe1f | ||
|
|
86589f707f | ||
|
|
62a6b8118e | ||
|
|
7e9d88bd74 | ||
|
|
562330457e | ||
|
|
913304c5bb | ||
|
|
85643d44e5 | ||
|
|
442cc4e005 | ||
|
|
6c8102697a | ||
|
|
b9b6285b5d | ||
|
|
1d2e635ab2 | ||
|
|
b474e10633 | ||
|
|
3a030f81c2 | ||
|
|
f8d0d2d599 | ||
|
|
220171a697 | ||
|
|
ad5dd349f4 | ||
|
|
8799634083 | ||
|
|
29ffaae7a1 | ||
|
|
1932404af2 | ||
|
|
2c23564a7a | ||
|
|
ad1b1d4b0b | ||
|
|
e6c6e38b24 | ||
|
|
d8345f4054 | ||
|
|
f6e164dadf | ||
|
|
68883ece6f | ||
|
|
c9625bea13 | ||
|
|
c28e50d10f | ||
|
|
f945aa5e39 | ||
|
|
046631a8fe | ||
|
|
f124d7f261 | ||
|
|
5d6ebd6e4a | ||
|
|
827ad50c53 | ||
|
|
c46433d1cd | ||
|
|
515adf3399 | ||
|
|
8d7d778ab7 | ||
|
|
93d9fe675d | ||
|
|
d64680ff0d | ||
|
|
689d8ddc11 | ||
|
|
22b79ba2f9 | ||
|
|
02541f8606 | ||
|
|
034ac4fc04 | ||
|
|
d8fe4fd091 | ||
|
|
7a446b0f84 |
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -81,7 +81,7 @@ When working on the `roller/chromium/main` branch for Chromium upgrades, use `e
|
||||
|
||||
- JS/TS files: kebab-case (`file-name.ts`)
|
||||
- C++ files: snake_case with `electron_api_` prefix (`electron_api_safe_storage.cc`)
|
||||
- Test files: `api-{module-name}-spec.ts` in `spec/`
|
||||
- Test files: `api-{module-name}.spec.ts` in `spec/`
|
||||
- Source file lists are maintained in `filenames.gni` (with platform-specific sections)
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -269,7 +269,7 @@ jobs:
|
||||
needs: checkout-macos
|
||||
with:
|
||||
build-runs-on: macos-15-xlarge
|
||||
test-runs-on: macos-15
|
||||
test-runs-on: macos-15-xlarge
|
||||
target-platform: macos
|
||||
target-arch: arm64
|
||||
is-release: false
|
||||
@@ -290,7 +290,7 @@ jobs:
|
||||
with:
|
||||
build-runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
clang-tidy-runs-on: electron-arc-centralus-linux-amd64-8core
|
||||
test-runs-on: electron-arc-centralus-linux-amd64-4core
|
||||
test-runs-on: electron-arc-centralus-linux-amd64-8core
|
||||
build-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
|
||||
clang-tidy-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
|
||||
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
|
||||
@@ -312,7 +312,7 @@ jobs:
|
||||
if: ${{ needs.setup.outputs.src == 'true' }}
|
||||
with:
|
||||
build-runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
test-runs-on: electron-arc-centralus-linux-amd64-4core
|
||||
test-runs-on: electron-arc-centralus-linux-amd64-8core
|
||||
build-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
|
||||
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
|
||||
target-platform: linux
|
||||
|
||||
@@ -57,8 +57,6 @@ jobs:
|
||||
- name: Run Electron Tests in QEMU 64k Container
|
||||
shell: bash
|
||||
env:
|
||||
MOCHA_REPORTER: mocha-multi-reporters
|
||||
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
|
||||
ELECTRON_DISABLE_SECURITY_WARNINGS: 1
|
||||
DISPLAY: ':99.0'
|
||||
run: |
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build-type: ${{ inputs.target-platform == 'macos' && fromJSON('["darwin","mas"]') || (inputs.target-platform == 'win' && fromJSON('["win"]') || fromJSON('["linux"]')) }}
|
||||
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.target-platform == 'linux', fromJSON('[1, 2, 3]'), inputs.target-platform == 'macos' && inputs.target-arch == 'x64', fromJSON('[1, 2, 3]'), fromJSON('[1, 2]')) }}
|
||||
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.is-asan == true, fromJSON('[1, 2]'), inputs.target-platform == 'linux', fromJSON('[1]'), fromJSON('[1, 2]')) }}
|
||||
env:
|
||||
BUILD_TYPE: ${{ matrix.build-type }}
|
||||
TARGET_ARCH: ${{ inputs.target-arch }}
|
||||
@@ -218,16 +218,15 @@ jobs:
|
||||
shell: bash
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
MOCHA_REPORTER: mocha-multi-reporters
|
||||
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
|
||||
ELECTRON_DISABLE_SECURITY_WARNINGS: 1
|
||||
DISPLAY: ':99.0'
|
||||
NPM_CONFIG_MSVS_VERSION: '2022'
|
||||
run: |
|
||||
cd src/electron
|
||||
export ELECTRON_TEST_RESULTS_DIR=`pwd`/junit
|
||||
export ELECTRON_EXTRA_ARGS="--trace-uncaught --enable-logging"
|
||||
JUNIT_ARGS="--reporter=default --reporter=junit --outputFile.junit=junit/test-results-main.xml"
|
||||
# Get which tests are on this shard
|
||||
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.target-platform == 'linux', 3, inputs.target-platform == 'macos' && inputs.target-arch == 'x64', 3, 2) }})
|
||||
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.is-asan == true, 2, inputs.target-platform == 'linux', 1, 2) }})
|
||||
if [ "${{ inputs.display-server }}" = "wayland" ]; then
|
||||
allowlist_file=script/wayland-test-allowlist.txt
|
||||
filtered_tests=""
|
||||
@@ -251,11 +250,8 @@ jobs:
|
||||
if [ "${{ inputs.target-arch }}" = "x86" ]; then
|
||||
export npm_config_arch="ia32"
|
||||
fi
|
||||
if [ "${{ inputs.target-arch }}" = "arm64" ]; then
|
||||
export ELECTRON_FORCE_TEST_SUITE_EXIT="true"
|
||||
fi
|
||||
fi
|
||||
node script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
|
||||
node script/yarn.js test $JUNIT_ARGS $tests_files
|
||||
else
|
||||
chown :builduser .. && chmod g+w ..
|
||||
chown -R :builduser . && chmod -R g+w .
|
||||
@@ -269,21 +265,19 @@ jobs:
|
||||
export NSS_DISABLE_ARENA_FREE_LIST=1
|
||||
export NSS_DISABLE_UNLOAD=1
|
||||
export LLVM_SYMBOLIZER_PATH=$PWD/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer
|
||||
export MOCHA_TIMEOUT=180000
|
||||
echo "Piping output to ASAN_SYMBOLIZE ($ASAN_SYMBOLIZE)"
|
||||
cd electron
|
||||
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --trace-uncaught --enable-logging --files $tests_files | $ASAN_SYMBOLIZE
|
||||
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --testTimeout=180000 $JUNIT_ARGS $tests_files | $ASAN_SYMBOLIZE
|
||||
else
|
||||
if [ "${{ inputs.target-arch }}" = "arm" ]; then
|
||||
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
|
||||
else
|
||||
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall $JUNIT_ARGS $tests_files
|
||||
else
|
||||
if [ "${{ inputs.display-server }}" = "wayland" ]; then
|
||||
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
|
||||
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test $JUNIT_ARGS $tests_files
|
||||
else
|
||||
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
|
||||
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test $JUNIT_ARGS $tests_files
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
fi
|
||||
- name: Take screenshot on timeout or cancellation
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
{
|
||||
"name": "no-only-tests",
|
||||
"specifier": "./script/lint-plugins/no-only-tests.mjs"
|
||||
},
|
||||
{
|
||||
"name": "remote-tools-imports",
|
||||
"specifier": "./script/lint-plugins/remote-tools-imports.mjs"
|
||||
},
|
||||
{
|
||||
"name": "no-nested-tests",
|
||||
"specifier": "./script/lint-plugins/no-nested-tests.mjs"
|
||||
},
|
||||
{
|
||||
"name": "no-unawaited-load",
|
||||
"specifier": "./script/lint-plugins/no-unawaited-load.mjs"
|
||||
}
|
||||
],
|
||||
"categories": {
|
||||
@@ -315,7 +327,18 @@
|
||||
{
|
||||
"files": ["spec/**/*.ts", "spec/**/*.js", "spec/**/*.mjs"],
|
||||
"rules": {
|
||||
"no-only-tests/no-only-tests": "error"
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"remote-tools-imports/no-foreign-imports-in-remote-closure": "error",
|
||||
"no-nested-tests/no-nested-tests": "error",
|
||||
"no-unawaited-load/no-unawaited-load": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["spec/fixtures/**"],
|
||||
"rules": {
|
||||
"no-unawaited-load/no-unawaited-load": "off",
|
||||
"remote-tools-imports/no-foreign-imports-in-remote-closure": "off",
|
||||
"no-nested-tests/no-nested-tests": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"@datadog/datadog-ci": "^5.9.1",
|
||||
"@electron/asar": "^4.0.1",
|
||||
"@electron/docs-parser": "^2.0.0",
|
||||
"@electron/fiddle-core": "^1.3.4",
|
||||
"@electron/github-app-auth": "^3.2.0",
|
||||
"@electron/lint-roller": "^3.2.0",
|
||||
"@electron/typescript-definitions": "^9.1.5",
|
||||
@@ -21,12 +20,10 @@
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/stream-json": "^1.7.8",
|
||||
"@types/temp": "^0.9.4",
|
||||
"@xmldom/xmldom": "^0.8.12",
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^4.1.0",
|
||||
"check-for-leaks": "^1.2.1",
|
||||
"events": "^3.2.0",
|
||||
"folder-hash": "^4.1.2",
|
||||
"got": "^11.8.5",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
@@ -46,6 +43,7 @@
|
||||
"ts-node": "6.2.0",
|
||||
"typescript": "^5.8.3",
|
||||
"url": "^0.11.4",
|
||||
"vitest": "^4.1.2",
|
||||
"webpack": "^5.104.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"wrapper-webpack-plugin": "^2.2.0",
|
||||
@@ -86,7 +84,7 @@
|
||||
"prepare": "husky",
|
||||
"repl": "node ./script/start.js --interactive",
|
||||
"start": "node ./script/start.js",
|
||||
"test": "node ./script/spec-runner.js",
|
||||
"test": "node ./spec/_vitest_runner/run.js",
|
||||
"tsc": "tsc",
|
||||
"webpack": "webpack"
|
||||
},
|
||||
|
||||
@@ -29,4 +29,5 @@ for _ in {1..100}; do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
node "$@" --ozone-platform=wayland
|
||||
export ELECTRON_EXTRA_ARGS="${ELECTRON_EXTRA_ARGS:-} --ozone-platform=wayland"
|
||||
node "$@"
|
||||
|
||||
69
script/lint-plugins/no-nested-tests.mjs
Normal file
69
script/lint-plugins/no-nested-tests.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
// Flags it()/ifit()/itremote() calls that appear inside the body of another
|
||||
// it()/ifit()/itremote() call. Mocha tolerated this; vitest does not.
|
||||
|
||||
const TEST_CALLEES = new Set(['it', 'itremote', 'test', 'specify']);
|
||||
|
||||
function isTestCall(node) {
|
||||
if (node.type !== 'CallExpression') return null;
|
||||
let callee = node.callee;
|
||||
// unwrap it.only / it.skip / it.runIf(cond)(...)
|
||||
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
|
||||
callee = callee.object;
|
||||
}
|
||||
// ifit(cond)(name, fn) — callee is CallExpression whose callee is Identifier 'ifit'
|
||||
if (callee.type === 'CallExpression') {
|
||||
const inner = callee.callee;
|
||||
if (inner.type === 'Identifier' && (inner.name === 'ifit' || inner.name === 'it')) {
|
||||
return { name: inner.name, fn: node.arguments.find((a) => isFunctionLike(a)) };
|
||||
}
|
||||
if (
|
||||
inner.type === 'MemberExpression' &&
|
||||
inner.object.type === 'Identifier' &&
|
||||
(inner.object.name === 'it' || inner.object.name === 'test')
|
||||
) {
|
||||
return { name: inner.object.name, fn: node.arguments.find((a) => isFunctionLike(a)) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (callee.type === 'Identifier' && TEST_CALLEES.has(callee.name)) {
|
||||
return { name: callee.name, fn: node.arguments.find((a) => isFunctionLike(a)) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isFunctionLike(node) {
|
||||
return (
|
||||
node &&
|
||||
(node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression' ||
|
||||
(node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'withDone'))
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: { name: 'no-nested-tests' },
|
||||
rules: {
|
||||
'no-nested-tests': {
|
||||
meta: { type: 'problem' },
|
||||
create(context) {
|
||||
let depth = 0;
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const test = isTestCall(node);
|
||||
if (!test) return;
|
||||
if (depth > 0) {
|
||||
context.report({
|
||||
node: node.callee,
|
||||
message: `'${test.name}()' is nested inside another test body. vitest does not allow nested test definitions; hoist this to the enclosing describe().`
|
||||
});
|
||||
}
|
||||
depth++;
|
||||
},
|
||||
'CallExpression:exit'(node) {
|
||||
if (isTestCall(node)) depth--;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
79
script/lint-plugins/no-unawaited-load.mjs
Normal file
79
script/lint-plugins/no-unawaited-load.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
// Flags loadURL()/loadFile() calls whose returned promise is neither awaited,
|
||||
// returned, .then()/.catch()'d, nor assigned. These reject as unhandled when
|
||||
// the load is aborted (e.g. the test moves on or the window closes).
|
||||
|
||||
const LOAD_METHODS = new Set(['loadURL', 'loadFile']);
|
||||
|
||||
function isHandled(node, parent) {
|
||||
if (!parent) return false;
|
||||
switch (parent.type) {
|
||||
case 'AwaitExpression':
|
||||
return true;
|
||||
case 'ReturnStatement':
|
||||
return true;
|
||||
case 'ArrowFunctionExpression':
|
||||
// Implicit return: (…) => w.loadURL(…)
|
||||
return parent.body === node;
|
||||
case 'VariableDeclarator':
|
||||
// const p = w.loadURL(…) — assume the promise is handled later.
|
||||
return parent.init === node;
|
||||
case 'AssignmentExpression':
|
||||
return parent.right === node;
|
||||
case 'MemberExpression':
|
||||
// w.loadURL(…).catch(…) / .then(…) — the MemberExpression is the object
|
||||
// of a surrounding CallExpression; treat any property access as handled
|
||||
// to avoid false positives on .then/.catch/.finally chains.
|
||||
return parent.object === node;
|
||||
case 'CallExpression':
|
||||
// Passed as an argument, e.g. expect(w.loadURL(...)).to.eventually…,
|
||||
// Promise.all([w.loadURL(...)]), once(w, 'x').then(w.loadURL(...))
|
||||
return parent.arguments?.includes(node) || parent.callee === node;
|
||||
case 'ArrayExpression':
|
||||
// [w.loadURL(...)] — typically Promise.all input.
|
||||
return true;
|
||||
case 'ConditionalExpression':
|
||||
case 'LogicalExpression':
|
||||
// cond ? w.loadURL(a) : w.loadURL(b) — handled if the whole expr is.
|
||||
return true;
|
||||
case 'UnaryExpression':
|
||||
// void w.loadURL(...) — explicit discard.
|
||||
return parent.operator === 'void';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: { name: 'no-unawaited-load' },
|
||||
rules: {
|
||||
'no-unawaited-load': {
|
||||
meta: { type: 'suggestion' },
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
|
||||
const ancestorsOf = (n) => sourceCode?.getAncestors?.(n) ?? context.getAncestors?.() ?? [];
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
if (
|
||||
callee.type !== 'MemberExpression' ||
|
||||
callee.property.type !== 'Identifier' ||
|
||||
!LOAD_METHODS.has(callee.property.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const ancestors = ancestorsOf(node);
|
||||
const parent = ancestors[ancestors.length - 1] ?? node.parent;
|
||||
if (isHandled(node, parent)) return;
|
||||
context.report({
|
||||
node: callee.property,
|
||||
message:
|
||||
`'${callee.property.name}()' returns a promise that is not awaited, ` +
|
||||
`returned, assigned, or .catch()'d. If the load may be aborted, add ` +
|
||||
`'.catch(() => {})' or 'void' to suppress the unhandled rejection.`
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
226
script/lint-plugins/remote-tools-imports.mjs
Normal file
226
script/lint-plugins/remote-tools-imports.mjs
Normal file
@@ -0,0 +1,226 @@
|
||||
// Flags imports (other than from spec/lib/remote-tools) that are referenced
|
||||
// inside itremote()/remotely() closures. vite's SSR transform rewrites import
|
||||
// bindings to __vite_ssr_import_N__, which breaks when the closure is
|
||||
// stringified and eval'd in a renderer/child process. Importing from
|
||||
// remote-tools instead lets runRemote()'s __rt shim resolve them.
|
||||
|
||||
const REMOTE_TOOLS_RE = /[./]lib\/remote-tools(\.ts)?$/;
|
||||
const REMOTE_TAG_RE = /@remote\b([^\n*]*)/;
|
||||
|
||||
function getRemoteTag(node, sourceCode) {
|
||||
const comments = sourceCode.getCommentsBefore?.(node) ?? node.leadingComments ?? [];
|
||||
for (const c of comments) {
|
||||
const m = REMOTE_TAG_RE.exec(c.value);
|
||||
if (m) return { noLocals: /\bno-locals\b/.test(m[1]) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isRemoteCall(callee, taggedCallees) {
|
||||
// itremote(name, fn), itremote.only(name, fn), itremote.skip(name, fn)
|
||||
if (callee.type === 'Identifier' && callee.name === 'itremote') return { fnArg: 1 };
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'itremote' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
(callee.property.name === 'only' || callee.property.name === 'skip')
|
||||
) {
|
||||
return { fnArg: 1 };
|
||||
}
|
||||
// <anything>.remotely(fn, ...args)
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'remotely'
|
||||
) {
|
||||
return { fnArg: 0 };
|
||||
}
|
||||
// Calls to identifiers tagged /** @remote */ (function decls, const bindings,
|
||||
// or for-of loop vars). fn arg is assumed to be the first function-expression
|
||||
// argument, resolved at the call site.
|
||||
if (callee.type === 'Identifier' && taggedCallees.has(callee.name)) {
|
||||
return { fnArg: 'firstFunction', noLocals: taggedCallees.get(callee.name).noLocals };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectDeclared(node, declared) {
|
||||
if (!node) return;
|
||||
if (node.type === 'Identifier') {
|
||||
declared.add(node.name);
|
||||
} else if (node.type === 'ObjectPattern') {
|
||||
for (const p of node.properties) collectDeclared(p.value ?? p.argument, declared);
|
||||
} else if (node.type === 'ArrayPattern') {
|
||||
for (const e of node.elements) collectDeclared(e, declared);
|
||||
} else if (node.type === 'RestElement') {
|
||||
collectDeclared(node.argument, declared);
|
||||
} else if (node.type === 'AssignmentPattern') {
|
||||
collectDeclared(node.left, declared);
|
||||
}
|
||||
}
|
||||
|
||||
function walkClosure(node, onIdentifierRef, declared) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
switch (node.type) {
|
||||
case 'Identifier':
|
||||
if (!declared.has(node.name)) onIdentifierRef(node);
|
||||
return;
|
||||
case 'MemberExpression':
|
||||
walkClosure(node.object, onIdentifierRef, declared);
|
||||
if (node.computed) walkClosure(node.property, onIdentifierRef, declared);
|
||||
return;
|
||||
case 'Property':
|
||||
if (node.computed) walkClosure(node.key, onIdentifierRef, declared);
|
||||
walkClosure(node.value, onIdentifierRef, declared);
|
||||
return;
|
||||
case 'VariableDeclarator':
|
||||
collectDeclared(node.id, declared);
|
||||
walkClosure(node.init, onIdentifierRef, declared);
|
||||
return;
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression': {
|
||||
const inner = new Set(declared);
|
||||
if (node.id) inner.add(node.id.name);
|
||||
for (const p of node.params) collectDeclared(p, inner);
|
||||
walkClosure(node.body, onIdentifierRef, inner);
|
||||
return;
|
||||
}
|
||||
case 'CatchClause': {
|
||||
const inner = new Set(declared);
|
||||
collectDeclared(node.param, inner);
|
||||
walkClosure(node.body, onIdentifierRef, inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(node)) {
|
||||
if (
|
||||
key === 'type' ||
|
||||
key === 'loc' ||
|
||||
key === 'range' ||
|
||||
key === 'start' ||
|
||||
key === 'end' ||
|
||||
key === 'parent' ||
|
||||
key === 'typeAnnotation' ||
|
||||
key === 'typeParameters' ||
|
||||
key === 'returnType'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const child = node[key];
|
||||
if (Array.isArray(child)) {
|
||||
for (const c of child) walkClosure(c, onIdentifierRef, declared);
|
||||
} else if (child && typeof child === 'object' && typeof child.type === 'string') {
|
||||
walkClosure(child, onIdentifierRef, declared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: { name: 'remote-tools-imports' },
|
||||
rules: {
|
||||
'no-foreign-imports-in-remote-closure': {
|
||||
meta: { type: 'problem' },
|
||||
create(context) {
|
||||
// Map of import-binding name -> { source, node } for everything NOT
|
||||
// from remote-tools.
|
||||
const foreignImports = new Map();
|
||||
// Every identifier declared in this file (imports from anywhere,
|
||||
// plus const/let/var/function/class at any scope). Used by no-locals.
|
||||
const fileLocals = new Set();
|
||||
// Names of functions/bindings tagged /** @remote */ in this file,
|
||||
// mapped to {noLocals:boolean}.
|
||||
const taggedCallees = new Map();
|
||||
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
|
||||
|
||||
const addLocal = (idNode) => {
|
||||
if (idNode?.type === 'Identifier') fileLocals.add(idNode.name);
|
||||
};
|
||||
|
||||
const collectTagged = (idNode, commentTarget) => {
|
||||
if (idNode?.type !== 'Identifier') return;
|
||||
const tag = getRemoteTag(commentTarget, sourceCode);
|
||||
if (tag) taggedCallees.set(idNode.name, tag);
|
||||
};
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const source = node.source.value;
|
||||
for (const spec of node.specifiers) addLocal(spec.local);
|
||||
if (REMOTE_TOOLS_RE.test(source)) return;
|
||||
if (node.importKind === 'type') return;
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.importKind === 'type') continue;
|
||||
foreignImports.set(spec.local.name, { source, node: spec.local });
|
||||
}
|
||||
},
|
||||
FunctionDeclaration(node) {
|
||||
addLocal(node.id);
|
||||
collectTagged(node.id, node);
|
||||
},
|
||||
ClassDeclaration(node) {
|
||||
addLocal(node.id);
|
||||
},
|
||||
VariableDeclaration(node) {
|
||||
for (const d of node.declarations) collectDeclared(d.id, fileLocals);
|
||||
if (!getRemoteTag(node, sourceCode)) return;
|
||||
for (const d of node.declarations) collectTagged(d.id, node);
|
||||
},
|
||||
ForOfStatement(node) {
|
||||
if (!getRemoteTag(node, sourceCode)) return;
|
||||
const decl = node.left;
|
||||
if (decl.type === 'VariableDeclaration') {
|
||||
for (const d of decl.declarations) collectTagged(d.id, node);
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
const match = isRemoteCall(node.callee, taggedCallees);
|
||||
if (!match) return;
|
||||
const fnArg =
|
||||
match.fnArg === 'firstFunction'
|
||||
? node.arguments.find(
|
||||
(a) => a && (a.type === 'FunctionExpression' || a.type === 'ArrowFunctionExpression')
|
||||
)
|
||||
: node.arguments[match.fnArg];
|
||||
if (!fnArg || (fnArg.type !== 'FunctionExpression' && fnArg.type !== 'ArrowFunctionExpression')) {
|
||||
return;
|
||||
}
|
||||
const reported = new Set();
|
||||
walkClosure(
|
||||
fnArg,
|
||||
(id) => {
|
||||
if (reported.has(id.name)) return;
|
||||
if (match.noLocals) {
|
||||
if (!fileLocals.has(id.name)) return;
|
||||
reported.add(id.name);
|
||||
context.report({
|
||||
node: id,
|
||||
message:
|
||||
`'${id.name}' is declared in this file but referenced inside a ` +
|
||||
`/** @remote no-locals */ closure. These closures are stringified and ` +
|
||||
`evaluated with no access to the enclosing scope; use only parameters ` +
|
||||
`and JS/DOM globals.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
const hit = foreignImports.get(id.name);
|
||||
if (hit) {
|
||||
reported.add(id.name);
|
||||
context.report({
|
||||
node: id,
|
||||
message:
|
||||
`'${id.name}' is imported from '${hit.source}' but used inside a ` +
|
||||
`stringified remote closure. Import it from './lib/remote-tools' ` +
|
||||
`instead (add it there if missing).`
|
||||
});
|
||||
}
|
||||
},
|
||||
new Set()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,472 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { ElectronVersions, Installer } = require('@electron/fiddle-core');
|
||||
|
||||
const { DOMParser } = require('@xmldom/xmldom');
|
||||
const chalk = require('chalk');
|
||||
const { hashElement } = require('folder-hash');
|
||||
const minimist = require('minimist');
|
||||
|
||||
const childProcess = require('node:child_process');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const unknownFlags = [];
|
||||
|
||||
const pass = chalk.green('✓');
|
||||
const fail = chalk.red('✗');
|
||||
|
||||
const FAILURE_STATUS_KEY = 'Electron_Spec_Runner_Failures';
|
||||
|
||||
const args = minimist(process.argv, {
|
||||
boolean: ['skipYarnInstall'],
|
||||
string: ['runners', 'target', 'electronVersion'],
|
||||
number: ['enableRerun'],
|
||||
unknown: (arg) => unknownFlags.push(arg)
|
||||
});
|
||||
|
||||
const unknownArgs = [];
|
||||
for (const flag of unknownFlags) {
|
||||
unknownArgs.push(flag);
|
||||
const onlyFlag = flag.replace(/^-+/, '');
|
||||
if (args[onlyFlag]) {
|
||||
unknownArgs.push(args[onlyFlag]);
|
||||
}
|
||||
}
|
||||
|
||||
const utils = require('./lib/utils');
|
||||
const { YARN_SCRIPT_PATH } = require('./yarn');
|
||||
|
||||
const BASE = path.resolve(__dirname, '../..');
|
||||
|
||||
const runners = new Map([['main', { description: 'Main process specs', run: runMainProcessElectronTests }]]);
|
||||
|
||||
const specHashPath = path.resolve(__dirname, '../spec/.hash');
|
||||
|
||||
if (args.electronVersion) {
|
||||
if (args.runners && args.runners !== 'main') {
|
||||
console.log(`${fail} only 'main' runner can be used with --electronVersion`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
args.runners = 'main';
|
||||
}
|
||||
|
||||
let runnersToRun = null;
|
||||
if (args.runners !== undefined) {
|
||||
runnersToRun = args.runners.split(',').filter((value) => value);
|
||||
if (!runnersToRun.every((r) => [...runners.keys()].includes(r))) {
|
||||
console.log(`${fail} ${runnersToRun} must be a subset of [${[...runners.keys()].join(' | ')}]`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Only running:', runnersToRun);
|
||||
} else {
|
||||
console.log(`Triggering runners: ${[...runners.keys()].join(', ')}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.electronVersion) {
|
||||
const versions = await ElectronVersions.create();
|
||||
if (args.electronVersion === 'latest') {
|
||||
args.electronVersion = versions.latest.version;
|
||||
} else if (args.electronVersion.startsWith('latest@')) {
|
||||
const majorVersion = parseInt(args.electronVersion.slice('latest@'.length));
|
||||
const ver = versions.inMajor(majorVersion).slice(-1)[0];
|
||||
if (ver) {
|
||||
args.electronVersion = ver.version;
|
||||
} else {
|
||||
console.log(`${fail} '${majorVersion}' is not a recognized Electron major version`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (!versions.isVersion(args.electronVersion)) {
|
||||
console.log(`${fail} '${args.electronVersion}' is not a recognized Electron version`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const versionString = `v${args.electronVersion}`;
|
||||
console.log(`Running against Electron ${chalk.green(versionString)}`);
|
||||
}
|
||||
|
||||
const [lastSpecHash, lastSpecInstallHash] = loadLastSpecHash();
|
||||
const [currentSpecHash, currentSpecInstallHash] = await getSpecHash();
|
||||
const somethingChanged = currentSpecHash !== lastSpecHash || lastSpecInstallHash !== currentSpecInstallHash;
|
||||
|
||||
if (somethingChanged && !args.skipYarnInstall) {
|
||||
await installSpecModules(path.resolve(__dirname, '..', 'spec'));
|
||||
await getSpecHash().then(saveSpecHash);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.resolve(__dirname, '../electron.d.ts'))) {
|
||||
console.log('Generating electron.d.ts as it is missing');
|
||||
generateTypeDefinitions();
|
||||
}
|
||||
|
||||
await runElectronTests();
|
||||
}
|
||||
|
||||
function generateTypeDefinitions() {
|
||||
const { status } = childProcess.spawnSync('npm', ['run', 'create-typescript-definitions'], {
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
if (status !== 0) {
|
||||
throw new Error(`Electron typescript definition generation failed with exit code: ${status}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadLastSpecHash() {
|
||||
return fs.existsSync(specHashPath) ? fs.readFileSync(specHashPath, 'utf8').split('\n') : [null, null];
|
||||
}
|
||||
|
||||
function saveSpecHash([newSpecHash, newSpecInstallHash]) {
|
||||
fs.writeFileSync(specHashPath, `${newSpecHash}\n${newSpecInstallHash}`);
|
||||
}
|
||||
|
||||
async function runElectronTests() {
|
||||
const errors = [];
|
||||
|
||||
const testResultsDir = process.env.ELECTRON_TEST_RESULTS_DIR;
|
||||
for (const [runnerId, { description, run }] of runners) {
|
||||
if (runnersToRun && !runnersToRun.includes(runnerId)) {
|
||||
console.info('\nSkipping:', description);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
console.info('\nRunning:', description);
|
||||
if (testResultsDir) {
|
||||
process.env.MOCHA_FILE = path.join(testResultsDir, `test-results-${runnerId}.xml`);
|
||||
}
|
||||
await run();
|
||||
} catch (err) {
|
||||
errors.push([runnerId, err]);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length !== 0) {
|
||||
for (const err of errors) {
|
||||
console.error('\n\nRunner Failed:', err[0]);
|
||||
console.error(err[1]);
|
||||
}
|
||||
console.log(`${fail} Electron test runners have failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function asyncSpawn(exe, runnerArgs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let forceExitResult = 0;
|
||||
const child = childProcess.spawn(exe, runnerArgs, {
|
||||
cwd: path.resolve(__dirname, '../..')
|
||||
});
|
||||
if (process.env.ELECTRON_TEST_PID_DUMP_PATH && child.pid) {
|
||||
fs.writeFileSync(process.env.ELECTRON_TEST_PID_DUMP_PATH, child.pid.toString());
|
||||
}
|
||||
child.stdout.pipe(process.stdout);
|
||||
child.stderr.pipe(process.stderr);
|
||||
if (process.env.ELECTRON_FORCE_TEST_SUITE_EXIT) {
|
||||
child.stdout.on('data', (data) => {
|
||||
const failureRE = RegExp(`${FAILURE_STATUS_KEY}: (\\d.*)`);
|
||||
const failures = data.toString().match(failureRE);
|
||||
if (failures) {
|
||||
forceExitResult = parseInt(failures[1], 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
child.on('error', (error) => reject(error));
|
||||
child.on('close', (status, signal) => {
|
||||
let returnStatus = 0;
|
||||
if (process.env.ELECTRON_FORCE_TEST_SUITE_EXIT) {
|
||||
returnStatus = forceExitResult;
|
||||
} else {
|
||||
returnStatus = status;
|
||||
}
|
||||
resolve({ status: returnStatus, signal });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseJUnitXML(specDir) {
|
||||
if (!fs.existsSync(process.env.MOCHA_FILE)) {
|
||||
console.error('JUnit XML file not found:', process.env.MOCHA_FILE);
|
||||
return [];
|
||||
}
|
||||
|
||||
const xmlContent = fs.readFileSync(process.env.MOCHA_FILE, 'utf8');
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
|
||||
|
||||
const failedTests = [];
|
||||
// find failed tests by looking for all testsuite nodes with failure > 0
|
||||
const testSuites = xmlDoc.getElementsByTagName('testsuite');
|
||||
for (let i = 0; i < testSuites.length; i++) {
|
||||
const testSuite = testSuites[i];
|
||||
const failures = testSuite.getAttribute('failures');
|
||||
if (failures > 0) {
|
||||
const testcases = testSuite.getElementsByTagName('testcase');
|
||||
|
||||
for (let i = 0; i < testcases.length; i++) {
|
||||
const testcase = testcases[i];
|
||||
const failures = testcase.getElementsByTagName('failure');
|
||||
const errors = testcase.getElementsByTagName('error');
|
||||
|
||||
if (failures.length > 0 || errors.length > 0) {
|
||||
const testName = testcase.getAttribute('name');
|
||||
const filePath = testSuite.getAttribute('file');
|
||||
const fileName = filePath ? path.relative(specDir, filePath) : 'unknown file';
|
||||
const failureInfo = {
|
||||
name: testName,
|
||||
file: fileName,
|
||||
filePath
|
||||
};
|
||||
if (failures.length > 0) {
|
||||
failureInfo.failure = failures[0].textContent || failures[0].nodeValue || 'No failure message';
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
failureInfo.error = errors[0].textContent || errors[0].nodeValue || 'No error message';
|
||||
}
|
||||
|
||||
failedTests.push(failureInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failedTests;
|
||||
}
|
||||
|
||||
async function rerunFailedTest(specDir, testName, testInfo) {
|
||||
console.log('\n========================================');
|
||||
console.log(`Rerunning failed test: ${testInfo.name} (${testInfo.file})`);
|
||||
console.log('========================================');
|
||||
|
||||
let grepPattern = testInfo.name;
|
||||
|
||||
// Escape special regex characters in test name
|
||||
grepPattern = grepPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
const args = [];
|
||||
if (testInfo.filePath) {
|
||||
args.push('--files', testInfo.filePath);
|
||||
}
|
||||
args.push('-g', grepPattern);
|
||||
|
||||
const success = await runTestUsingElectron(specDir, testName, false, args);
|
||||
|
||||
if (success) {
|
||||
console.log(`✅ Test passed: ${testInfo.name}`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`❌ Test failed again: ${testInfo.name}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function rerunFailedTests(specDir, testName) {
|
||||
console.log('\n📋 Parsing JUnit XML for failed tests...');
|
||||
const failedTests = parseJUnitXML(specDir);
|
||||
|
||||
if (failedTests.length === 0) {
|
||||
console.log('No failed tests could be found.');
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save off the original junit xml file
|
||||
if (fs.existsSync(process.env.MOCHA_FILE)) {
|
||||
fs.copyFileSync(process.env.MOCHA_FILE, `${process.env.MOCHA_FILE}.save`);
|
||||
}
|
||||
|
||||
console.log(`\n📊 Found ${failedTests.length} failed test(s):`);
|
||||
failedTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.name} (${test.file})`);
|
||||
});
|
||||
|
||||
// Step 3: Rerun each failed test individually
|
||||
console.log('\n🔄 Rerunning failed tests individually...\n');
|
||||
|
||||
const results = {
|
||||
total: failedTests.length,
|
||||
passed: 0,
|
||||
failed: 0
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
for (const testInfo of failedTests) {
|
||||
let runCount = 0;
|
||||
let success = false;
|
||||
let retryTest = false;
|
||||
while (!success && runCount < args.enableRerun) {
|
||||
success = await rerunFailedTest(specDir, testName, testInfo);
|
||||
if (success) {
|
||||
results.passed++;
|
||||
} else {
|
||||
if (runCount === args.enableRerun - 1) {
|
||||
results.failed++;
|
||||
} else {
|
||||
retryTest = true;
|
||||
console.log(`Retrying test (${runCount + 1}/${args.enableRerun})...`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a small delay between tests
|
||||
if (retryTest || index < failedTests.length - 1) {
|
||||
console.log('\nWaiting 2 seconds before next test...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
runCount++;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// Step 4: Summary
|
||||
console.log('\n📈 Summary:');
|
||||
console.log(`Total failed tests: ${results.total}`);
|
||||
console.log(`Passed on rerun: ${results.passed}`);
|
||||
console.log(`Still failing: ${results.failed}`);
|
||||
|
||||
// Restore the original junit xml file
|
||||
if (fs.existsSync(`${process.env.MOCHA_FILE}.save`)) {
|
||||
fs.renameSync(`${process.env.MOCHA_FILE}.save`, process.env.MOCHA_FILE);
|
||||
}
|
||||
|
||||
if (results.failed === 0) {
|
||||
console.log('🎉 All previously failed tests now pass!');
|
||||
} else {
|
||||
console.log(`⚠️ ${results.failed} test(s) are still failing`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTestUsingElectron(specDir, testName, shouldRerun, additionalArgs = []) {
|
||||
let exe;
|
||||
if (args.electronVersion) {
|
||||
const installer = new Installer();
|
||||
exe = await installer.install(args.electronVersion);
|
||||
} else {
|
||||
exe = path.resolve(BASE, utils.getElectronExec());
|
||||
}
|
||||
let argsToPass = unknownArgs.slice(2);
|
||||
if (additionalArgs.includes('--files')) {
|
||||
argsToPass = argsToPass.filter(
|
||||
(arg) => arg.toString().indexOf('--files') === -1 && arg.toString().indexOf('spec/') === -1
|
||||
);
|
||||
}
|
||||
const runnerArgs = [`electron/${specDir}`, ...argsToPass, ...additionalArgs];
|
||||
if (process.platform === 'linux') {
|
||||
runnerArgs.unshift(path.resolve(__dirname, 'dbus_mock.py'), exe);
|
||||
exe = 'python3';
|
||||
}
|
||||
console.log(`Running: ${exe} ${runnerArgs.join(' ')}`);
|
||||
const { status, signal } = await asyncSpawn(exe, runnerArgs);
|
||||
if (status !== 0) {
|
||||
if (status) {
|
||||
const textStatus = process.platform === 'win32' ? `0x${status.toString(16)}` : status.toString();
|
||||
console.log(`${fail} Electron tests failed with code ${textStatus}.`);
|
||||
} else {
|
||||
console.log(`${fail} Electron tests failed with kill signal ${signal}.`);
|
||||
}
|
||||
if (shouldRerun) {
|
||||
await rerunFailedTests(specDir, testName);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log(`${pass} Electron ${testName} process tests passed.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function runMainProcessElectronTests() {
|
||||
let shouldRerun = false;
|
||||
if (args.enableRerun && args.enableRerun > 0) {
|
||||
shouldRerun = true;
|
||||
}
|
||||
await runTestUsingElectron('spec', 'main', shouldRerun);
|
||||
}
|
||||
|
||||
async function installSpecModules(dir) {
|
||||
const env = {
|
||||
npm_config_msvs_version: '2022',
|
||||
...process.env,
|
||||
CXXFLAGS: process.env.CXXFLAGS,
|
||||
npm_config_yes: 'true'
|
||||
};
|
||||
if (args.electronVersion) {
|
||||
env.npm_config_target = args.electronVersion;
|
||||
env.npm_config_disturl = 'https://electronjs.org/headers';
|
||||
env.npm_config_runtime = 'electron';
|
||||
env.npm_config_devdir = path.join(os.homedir(), '.electron-gyp');
|
||||
env.npm_config_build_from_source = 'true';
|
||||
const { status } = childProcess.spawnSync('npm', ['run', 'node-gyp-install', '--ensure'], {
|
||||
env,
|
||||
cwd: dir,
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
if (status !== 0) {
|
||||
console.log(`${fail} Failed to "npm run node-gyp-install" install in '${dir}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
env.npm_config_nodedir = path.resolve(BASE, `out/${utils.getOutDir({ shouldLog: true })}/gen/node_headers`);
|
||||
}
|
||||
if (fs.existsSync(path.resolve(dir, 'node_modules'))) {
|
||||
await fs.promises.rm(path.resolve(dir, 'node_modules'), { force: true, recursive: true });
|
||||
}
|
||||
const yarnArgs = [YARN_SCRIPT_PATH, 'install', '--immutable'];
|
||||
const { status } = childProcess.spawnSync(process.execPath, yarnArgs, {
|
||||
env,
|
||||
cwd: dir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
if (status !== 0 && !process.env.IGNORE_YARN_INSTALL_ERROR) {
|
||||
console.log(`${fail} Failed to yarn install in '${dir}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
const { status: rebuildStatus } = childProcess.spawnSync('npm', ['rebuild', 'abstract-socket'], {
|
||||
env,
|
||||
cwd: dir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
if (rebuildStatus !== 0) {
|
||||
console.log(`${fail} Failed to rebuild abstract-socket native module`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSpecHash() {
|
||||
return Promise.all([
|
||||
(async () => {
|
||||
const hasher = crypto.createHash('SHA256');
|
||||
hasher.update(fs.readFileSync(path.resolve(__dirname, '../yarn.lock')));
|
||||
hasher.update(fs.readFileSync(path.resolve(__dirname, '../spec/package.json')));
|
||||
hasher.update(fs.readFileSync(path.resolve(__dirname, '../script/spec-runner.js')));
|
||||
return hasher.digest('hex');
|
||||
})(),
|
||||
(async () => {
|
||||
const specNodeModulesPath = path.resolve(__dirname, '../spec/node_modules');
|
||||
if (!fs.existsSync(specNodeModulesPath)) {
|
||||
return null;
|
||||
}
|
||||
const { hash } = await hashElement(specNodeModulesPath, {
|
||||
folders: {
|
||||
exclude: ['.bin']
|
||||
}
|
||||
});
|
||||
return hash;
|
||||
})()
|
||||
]);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('An error occurred inside the spec runner:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -6,29 +6,29 @@ const path = require('node:path');
|
||||
const currentShard = parseInt(process.argv[2], 10);
|
||||
const shardCount = parseInt(process.argv[3], 10);
|
||||
|
||||
const specFiles = glob.sync('spec/*-spec.ts');
|
||||
|
||||
const buckets = [];
|
||||
|
||||
for (let i = 0; i < shardCount; i++) {
|
||||
buckets.push([]);
|
||||
}
|
||||
|
||||
const testsInSpecFile = Object.create(null);
|
||||
for (const specFile of specFiles) {
|
||||
const testContent = fs.readFileSync(specFile, 'utf8');
|
||||
testsInSpecFile[specFile] = testContent.split('it(').length;
|
||||
function testCountIn(file) {
|
||||
return fs.readFileSync(file, 'utf8').split('it(').length;
|
||||
}
|
||||
|
||||
specFiles.sort((a, b) => {
|
||||
return testsInSpecFile[b] - testsInSpecFile[a];
|
||||
});
|
||||
|
||||
let shard = 0;
|
||||
for (const specFile of specFiles) {
|
||||
buckets[shard].push(path.normalize(specFile));
|
||||
shard++;
|
||||
if (shard === shardCount) shard = 0;
|
||||
function distribute(files) {
|
||||
files.sort((a, b) => testCountIn(b) - testCountIn(a));
|
||||
let shard = 0;
|
||||
for (const file of files) {
|
||||
buckets[shard].push(path.normalize(file));
|
||||
shard++;
|
||||
if (shard === shardCount) shard = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Parallel and serial sets are distributed independently so each shard gets a
|
||||
// proportional slice of both; run.js routes spec/serial/* to the
|
||||
// --no-file-parallelism phase.
|
||||
distribute(glob.sync('spec/*.spec.ts'));
|
||||
distribute(glob.sync('spec/serial/*.spec.ts'));
|
||||
|
||||
console.log(buckets[currentShard - 1].join(' '));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
spec/parse-features-string-spec.ts
|
||||
spec/types-spec.ts
|
||||
spec/version-bump-spec.ts
|
||||
spec/api-app-spec.ts
|
||||
spec/api-browser-window-spec.ts
|
||||
spec/parse-features-string.spec.ts
|
||||
spec/types.spec.ts
|
||||
spec/version-bump.spec.ts
|
||||
spec/api-app.spec.ts
|
||||
spec/api-browser-window.spec.ts
|
||||
|
||||
118
spec/_vitest_runner/electron-pool.ts
Normal file
118
spec/_vitest_runner/electron-pool.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { ForksPoolWorker, type PoolOptions, type WorkerRequest } from 'vitest/node';
|
||||
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { getAbsoluteElectronExec } from '../../script/lib/utils';
|
||||
|
||||
/**
|
||||
* A vitest PoolWorker that spawns each worker as a full Electron main
|
||||
* process (not a node-mode fork) so tests can exercise real Electron APIs.
|
||||
*
|
||||
* We extend the built-in ForksPoolWorker so we inherit all IPC / teardown
|
||||
* plumbing and only override how the child process is created.
|
||||
*/
|
||||
class ElectronPoolWorker extends ForksPoolWorker {
|
||||
override name = 'electron';
|
||||
|
||||
private readonly distPath: string;
|
||||
|
||||
constructor(options: PoolOptions) {
|
||||
super(options);
|
||||
this.distPath = options.distPath;
|
||||
}
|
||||
|
||||
private userDataDir: string | undefined;
|
||||
|
||||
override async start() {
|
||||
if ((this as any)._fork) return;
|
||||
|
||||
const electronExec = process.env.ELECTRON_TESTS_EXECUTABLE || getAbsoluteElectronExec();
|
||||
if (!fs.existsSync(electronExec)) {
|
||||
throw new Error(
|
||||
`Electron binary not found at '${electronExec}'. ` +
|
||||
`Build Electron first (e build) or set ELECTRON_TESTS_EXECUTABLE.`
|
||||
);
|
||||
}
|
||||
|
||||
// Allocate a guaranteed-unique userData dir for this worker. The pool owns
|
||||
// its lifecycle so it can be removed once the worker exits.
|
||||
this.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-vitest-'));
|
||||
|
||||
const env = {
|
||||
...(this as any).env,
|
||||
// Make sure the child is a real Electron main process, not node-mode.
|
||||
ELECTRON_RUN_AS_NODE: undefined,
|
||||
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',
|
||||
// Tell worker-entry.js where vitest's fork worker lives.
|
||||
VITEST_DIST_PATH: this.distPath,
|
||||
ELECTRON_VITEST_USER_DATA_DIR: this.userDataDir
|
||||
};
|
||||
|
||||
const extraArgs = process.env.ELECTRON_EXTRA_ARGS ? process.env.ELECTRON_EXTRA_ARGS.split(' ').filter(Boolean) : [];
|
||||
|
||||
// spawn() + an 'ipc' stdio slot gives the child process.send/on('message'),
|
||||
// which is what vitest's fork worker protocol relies on.
|
||||
const child: ChildProcess = spawn(electronExec, [path.resolve(__dirname), ...extraArgs], {
|
||||
env,
|
||||
execArgv: (this as any).execArgv,
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
serialization: 'advanced'
|
||||
} as any);
|
||||
|
||||
(this as any)._fork = child;
|
||||
|
||||
const stdout = (this as any).stdout;
|
||||
const stderr = (this as any).stderr;
|
||||
if (child.stdout) {
|
||||
stdout.setMaxListeners(1 + stdout.getMaxListeners());
|
||||
child.stdout.pipe(stdout);
|
||||
}
|
||||
if (child.stderr) {
|
||||
stderr.setMaxListeners(1 + stderr.getMaxListeners());
|
||||
child.stderr.pipe(stderr);
|
||||
}
|
||||
|
||||
child.once('exit', (code, signal) => {
|
||||
if (!this.stopping && (code !== 0 || signal)) {
|
||||
console.error(
|
||||
`[electron-pool] worker pid=${child.pid} exited unexpectedly (code=${code} signal=${signal}) ` +
|
||||
`while running: ${this.lastFiles.join(', ') || '<no files assigned yet>'}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private lastFiles: string[] = [];
|
||||
private stopping = false;
|
||||
|
||||
override send(message: WorkerRequest) {
|
||||
if (message.type === 'run' || message.type === 'collect') {
|
||||
this.lastFiles = message.context.files.map((f) =>
|
||||
typeof f === 'string'
|
||||
? f
|
||||
: ((f as { filepath?: string; file?: string }).filepath ?? (f as { file?: string }).file ?? String(f))
|
||||
);
|
||||
}
|
||||
super.send(message);
|
||||
}
|
||||
|
||||
override async stop() {
|
||||
this.stopping = true;
|
||||
try {
|
||||
await super.stop();
|
||||
} finally {
|
||||
if (this.userDataDir) {
|
||||
fs.rmSync(this.userDataDir, { recursive: true, force: true });
|
||||
this.userDataDir = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'electron',
|
||||
createPoolWorker: (options: PoolOptions) => new ElectronPoolWorker(options)
|
||||
};
|
||||
4
spec/_vitest_runner/electron-shim.cjs
Normal file
4
spec/_vitest_runner/electron-shim.cjs
Normal file
@@ -0,0 +1,4 @@
|
||||
// Vitest externalises bare specifiers to native `import()`, but Electron only
|
||||
// hooks CJS `require('electron')`. Alias 'electron' here so the runner ends up
|
||||
// in CJS-land and gets the real module.
|
||||
module.exports = require('electron');
|
||||
6
spec/_vitest_runner/package.json
Normal file
6
spec/_vitest_runner/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "electron-test-main",
|
||||
"productName": "Electron Test Main",
|
||||
"version": "0.1.0",
|
||||
"main": "worker-entry.js"
|
||||
}
|
||||
155
spec/_vitest_runner/run.js
Normal file
155
spec/_vitest_runner/run.js
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const childProcess = require('node:child_process');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { getAbsoluteElectronExec, getOutDir } = require('../../script/lib/utils');
|
||||
const { YARN_SCRIPT_PATH } = require('../../script/yarn');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..', '..');
|
||||
const SPEC_DIR = path.resolve(ROOT, 'spec');
|
||||
const SPEC_HASH_PATH = path.resolve(SPEC_DIR, '.hash');
|
||||
const VITEST_BIN = path.join(ROOT, 'node_modules', '.bin', 'vitest');
|
||||
|
||||
const rawArgs = process.argv.slice(2);
|
||||
const skipYarnInstall = rawArgs.includes('--skipYarnInstall');
|
||||
const vitestArgs = rawArgs.filter((a) => a !== '--skipYarnInstall');
|
||||
|
||||
function getSpecHash() {
|
||||
const hasher = crypto.createHash('SHA256');
|
||||
hasher.update(fs.readFileSync(path.resolve(ROOT, 'yarn.lock')));
|
||||
hasher.update(fs.readFileSync(path.resolve(SPEC_DIR, 'package.json')));
|
||||
hasher.update(fs.readFileSync(__filename));
|
||||
return hasher.digest('hex');
|
||||
}
|
||||
|
||||
function installSpecModules() {
|
||||
const env = {
|
||||
npm_config_msvs_version: '2022',
|
||||
...process.env,
|
||||
npm_config_nodedir: path.resolve(ROOT, '..', `out/${getOutDir({ shouldLog: true })}/gen/node_headers`),
|
||||
npm_config_yes: 'true'
|
||||
};
|
||||
const nodeModules = path.resolve(SPEC_DIR, 'node_modules');
|
||||
if (fs.existsSync(nodeModules)) {
|
||||
fs.rmSync(nodeModules, { force: true, recursive: true });
|
||||
}
|
||||
const { status } = childProcess.spawnSync(process.execPath, [YARN_SCRIPT_PATH, 'install', '--immutable'], {
|
||||
env,
|
||||
cwd: SPEC_DIR,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
if (status !== 0 && !process.env.IGNORE_YARN_INSTALL_ERROR) {
|
||||
console.error(`Failed to yarn install in '${SPEC_DIR}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
const { status: rebuildStatus } = childProcess.spawnSync('npm', ['rebuild', 'abstract-socket'], {
|
||||
env,
|
||||
cwd: SPEC_DIR,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (rebuildStatus !== 0) {
|
||||
console.error('Failed to rebuild abstract-socket native module');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateTypeDefinitions() {
|
||||
const { status } = childProcess.spawnSync('npm', ['run', 'create-typescript-definitions'], {
|
||||
cwd: ROOT,
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
if (status !== 0) {
|
||||
throw new Error(`Electron typescript definition generation failed with exit code: ${status}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipYarnInstall) {
|
||||
const currentHash = getSpecHash();
|
||||
const lastHash = fs.existsSync(SPEC_HASH_PATH) ? fs.readFileSync(SPEC_HASH_PATH, 'utf8') : null;
|
||||
if (currentHash !== lastHash || !fs.existsSync(path.resolve(SPEC_DIR, 'node_modules'))) {
|
||||
installSpecModules();
|
||||
fs.writeFileSync(SPEC_HASH_PATH, getSpecHash());
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.resolve(ROOT, 'electron.d.ts'))) {
|
||||
console.log('Generating electron.d.ts as it is missing');
|
||||
generateTypeDefinitions();
|
||||
}
|
||||
|
||||
const exe = process.env.ELECTRON_TESTS_EXECUTABLE || getAbsoluteElectronExec();
|
||||
console.log(`Electron binary: ${exe}`);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
ELECTRON_TESTS_EXECUTABLE: exe
|
||||
};
|
||||
|
||||
const configPath = path.join(__dirname, 'vitest.config.ts');
|
||||
|
||||
function runVitest(extraArgs) {
|
||||
let command = VITEST_BIN;
|
||||
let args = ['run', '--config', configPath, ...extraArgs];
|
||||
// On Linux, run vitest under a mocked D-Bus so every spawned Electron worker
|
||||
// inherits the session/system bus addresses.
|
||||
if (process.platform === 'linux') {
|
||||
args = [path.resolve(ROOT, 'script', 'dbus_mock.py'), command, ...args];
|
||||
command = 'python3';
|
||||
}
|
||||
const result = childProcess.spawnSync(command, args, { cwd: ROOT, stdio: 'inherit', env });
|
||||
return result.status ?? 1;
|
||||
}
|
||||
|
||||
const SERIAL_DIR = path.join('spec', 'serial');
|
||||
|
||||
function isSerialPath(a) {
|
||||
const rel = path.relative(ROOT, path.resolve(ROOT, a));
|
||||
return rel === SERIAL_DIR || rel.startsWith(SERIAL_DIR + path.sep);
|
||||
}
|
||||
|
||||
function isTestPath(a) {
|
||||
return !a.startsWith('-') && (a.includes('/') || a.includes(path.sep) || /\.spec\.[cm]?[tj]s$/.test(a));
|
||||
}
|
||||
|
||||
function serialArgs(args) {
|
||||
const out = [];
|
||||
let hasPositional = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a.startsWith('--outputFile.junit=')) {
|
||||
out.push(a.replace(/\.xml$/, '-serial.xml'));
|
||||
} else if (a === '--outputFile.junit') {
|
||||
out.push(a, args[++i].replace(/\.xml$/, '-serial.xml'));
|
||||
} else if (!isTestPath(a)) {
|
||||
out.push(a);
|
||||
} else {
|
||||
hasPositional = true;
|
||||
if (isSerialPath(a)) out.push(a);
|
||||
}
|
||||
}
|
||||
return { args: out, hasPositional };
|
||||
}
|
||||
|
||||
const { args: sArgs, hasPositional } = serialArgs(vitestArgs);
|
||||
const positionalSerial = hasPositional && sArgs.some((a) => isTestPath(a));
|
||||
const positionalParallel = hasPositional && vitestArgs.some((a) => isTestPath(a) && !isSerialPath(a));
|
||||
|
||||
let parallelStatus = 0;
|
||||
if (!hasPositional || positionalParallel) {
|
||||
parallelStatus = runVitest(['--exclude', 'spec/serial/**', ...vitestArgs]);
|
||||
}
|
||||
|
||||
let serialStatus = 0;
|
||||
if (!hasPositional || positionalSerial) {
|
||||
console.log('\nRunning spec/serial/** without file parallelism...');
|
||||
serialStatus = runVitest(['--no-file-parallelism', ...sArgs, ...(hasPositional ? [] : ['spec/serial/**/*.spec.ts'])]);
|
||||
}
|
||||
|
||||
process.exit(parallelStatus || serialStatus);
|
||||
44
spec/_vitest_runner/setup.ts
Normal file
44
spec/_vitest_runner/setup.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as chai from 'chai';
|
||||
import { afterAll, beforeEach } from 'vitest';
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { runCleanupFunctions } from '../lib/defer-helpers';
|
||||
import { assertNoWindowsLeaked } from '../lib/window-helpers';
|
||||
|
||||
import chaiAsPromised = require('chai-as-promised');
|
||||
import dirtyChai = require('dirty-chai');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
chai.use(dirtyChai);
|
||||
|
||||
// Show full object diff.
|
||||
// https://github.com/chaijs/chai/issues/469
|
||||
chai.config.truncateThreshold = 0;
|
||||
|
||||
// Skip any tests listed in disabled-tests.json.
|
||||
const disabledTests = new Set(JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'disabled-tests.json'), 'utf8')));
|
||||
beforeEach((ctx) => {
|
||||
// Fallback drain for defer()ed cleanups. Most tests use afterEach(closeAllWindows)
|
||||
// which already runs these first; this catches tests that defer() without it.
|
||||
// Note: onTestFinished runs *after* afterEach, so callbacks reaching here must
|
||||
// not assume windows still exist.
|
||||
ctx.onTestFinished(runCleanupFunctions);
|
||||
|
||||
const parts: string[] = [ctx.task.name];
|
||||
let suite = ctx.task.suite;
|
||||
while (suite) {
|
||||
if (suite.name) parts.unshift(suite.name);
|
||||
suite = suite.suite;
|
||||
}
|
||||
if (disabledTests.has(parts.join(' '))) {
|
||||
ctx.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// Runs once per file, after all test-file-level afterAll hooks (setupFiles
|
||||
// hooks are outermost). Using afterAll rather than afterEach so suites that
|
||||
// intentionally share a window across tests (useRemoteContext, etc.) are not
|
||||
// flagged on every test.
|
||||
afterAll(assertNoWindowsLeaked);
|
||||
42
spec/_vitest_runner/vitest.config.ts
Normal file
42
spec/_vitest_runner/vitest.config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import * as path from 'node:path';
|
||||
|
||||
import electronPool from './electron-pool';
|
||||
|
||||
const electronShim = path.resolve(__dirname, 'electron-shim.cjs');
|
||||
|
||||
// Each worker is a full Electron main process (GPU process, network service,
|
||||
// etc.), so the usual cpus-1 default can starve the smaller hosted runners.
|
||||
function ciMaxWorkers(): number | undefined {
|
||||
if (!process.env.CI || process.platform !== 'darwin') return undefined;
|
||||
return process.arch === 'arm64' ? 3 : 6;
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: [{ find: /^electron(\/(main|common|renderer))?$/, replacement: electronShim }]
|
||||
},
|
||||
test: {
|
||||
include: ['spec/**/*.spec.ts'],
|
||||
exclude: ['spec/fixtures/**', 'spec/node_modules/**'],
|
||||
setupFiles: ['./spec/_vitest_runner/setup.ts'],
|
||||
// Custom pool: each worker is a real Electron main process.
|
||||
pool: electronPool as any,
|
||||
// Run test *files* in parallel across workers...
|
||||
fileParallelism: true,
|
||||
isolate: true,
|
||||
// ...but keep tests *within* a file sequential.
|
||||
sequence: { concurrent: false },
|
||||
allowOnly: !process.env.CI,
|
||||
retry: process.env.CI ? 3 : 0,
|
||||
maxWorkers: ciMaxWorkers(),
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
server: {
|
||||
deps: {
|
||||
external: [/electron-shim\.cjs$/]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
98
spec/_vitest_runner/worker-entry.js
Normal file
98
spec/_vitest_runner/worker-entry.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// This file is the Electron main-process entry for each vitest pool worker.
|
||||
// It is launched via `spawn(electron, [<spec-v2 dir>])` with an IPC channel,
|
||||
// so `process.send` is available and vitest's fork-worker protocol works.
|
||||
|
||||
const { app, protocol } = require('electron');
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const v8 = require('node:v8');
|
||||
|
||||
// Catch-and-exit only while bootstrapping. Once vitest's fork worker is
|
||||
// loaded it patches process.exit (to throw) and installs its own handlers,
|
||||
// so calling process.exit from here would re-throw inside an uncaughtException
|
||||
// handler and exit the process with Node's code 7, hiding the real error.
|
||||
function bootstrapUncaughtHandler(err) {
|
||||
console.error('Unhandled exception in vitest worker bootstrap:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
process.on('uncaughtException', bootstrapUncaughtHandler);
|
||||
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
|
||||
|
||||
if (process.env.ELECTRON_TEST_DISABLE_HARDWARE_ACCELERATION) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// The pool allocates (mkdtemp) and cleans up the parent directory; the worker
|
||||
// adds app.name so tests that assert getPath('userData') contains the app name
|
||||
// still hold.
|
||||
const userDataBase = process.env.ELECTRON_VITEST_USER_DATA_DIR;
|
||||
if (!userDataBase) {
|
||||
throw new Error('ELECTRON_VITEST_USER_DATA_DIR was not provided by the pool');
|
||||
}
|
||||
const userDataDir = path.join(userDataBase, app.name);
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
app.setPath('userData', userDataDir);
|
||||
|
||||
v8.setFlagsFromString('--expose_gc');
|
||||
app.commandLine.appendSwitch('js-flags', '--expose_gc');
|
||||
app.on('window-all-closed', () => null);
|
||||
|
||||
// Use fake device for Media Stream to replace actual camera and microphone.
|
||||
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
|
||||
app.commandLine.appendSwitch(
|
||||
'host-resolver-rules',
|
||||
[
|
||||
'MAP localhost2 127.0.0.1',
|
||||
'MAP ipv4.localhost2 10.0.0.1',
|
||||
'MAP ipv6.localhost2 [::1]',
|
||||
'MAP notfound.localhost2 ~NOTFOUND'
|
||||
].join(', ')
|
||||
);
|
||||
|
||||
// Enable features required by tests.
|
||||
app.commandLine.appendSwitch(
|
||||
'enable-features',
|
||||
[
|
||||
// spec/api-web-frame-main.spec.ts
|
||||
'DocumentPolicyIncludeJSCallStacksInCrashReports',
|
||||
// spec/spellchecker.spec.ts
|
||||
'UnrestrictSpellingAndGrammarForTesting'
|
||||
].join(',')
|
||||
);
|
||||
|
||||
global.standardScheme = 'app';
|
||||
global.zoomScheme = 'zoom';
|
||||
global.serviceWorkerScheme = 'sw';
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } },
|
||||
{ scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
|
||||
{ scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } },
|
||||
{ scheme: 'http-like', privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true } },
|
||||
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
|
||||
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
|
||||
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },
|
||||
{ scheme: 'no-fetch', privileges: { corsEnabled: true } },
|
||||
{ scheme: 'stream', privileges: { standard: true, stream: true } },
|
||||
{ scheme: 'foo', privileges: { standard: true } },
|
||||
{ scheme: 'bar', privileges: { standard: true } }
|
||||
]);
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(async () => {
|
||||
const distPath = process.env.VITEST_DIST_PATH;
|
||||
if (!distPath) {
|
||||
throw new Error('VITEST_DIST_PATH was not provided by the pool');
|
||||
}
|
||||
// Importing this registers the process.on('message') handler that speaks
|
||||
// vitest's pool protocol and actually runs the test files.
|
||||
await import(path.join(distPath, 'workers/forks.js'));
|
||||
// vitest's worker patches process.exit and owns error handling from here.
|
||||
process.removeListener('uncaughtException', bootstrapUncaughtHandler);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to bootstrap vitest worker:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { app, BrowserWindow, Menu, session, net as electronNet, WebContents, uti
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import * as semver from 'semver';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -15,7 +16,15 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { collectStreamBody, getResponse } from './lib/net-helpers';
|
||||
import { ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
|
||||
import {
|
||||
ifdescribe,
|
||||
ifit,
|
||||
isWayland,
|
||||
listen,
|
||||
waitUntil,
|
||||
withDone,
|
||||
dangerouslyIgnoreWebContentsLoadResult
|
||||
} from './lib/spec-helpers';
|
||||
import { closeWindow, closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
@@ -41,7 +50,7 @@ describe('app module', () => {
|
||||
let secureUrl: string;
|
||||
const certPath = path.join(fixturesPath, 'certificates');
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
const options = {
|
||||
key: fs.readFileSync(path.join(certPath, 'server.key')),
|
||||
cert: fs.readFileSync(path.join(certPath, 'server.pem')),
|
||||
@@ -66,9 +75,11 @@ describe('app module', () => {
|
||||
secureUrl = (await listen(server)).url;
|
||||
});
|
||||
|
||||
after((done) => {
|
||||
server.close(() => done());
|
||||
});
|
||||
afterAll(
|
||||
withDone((done) => {
|
||||
server.close(() => done());
|
||||
})
|
||||
);
|
||||
|
||||
describe('app.getVersion()', () => {
|
||||
it('returns the version field of package.json', () => {
|
||||
@@ -244,8 +255,7 @@ describe('app module', () => {
|
||||
expectedAdditionalData: unknown;
|
||||
}
|
||||
|
||||
it('prevents the second launch of app', async function () {
|
||||
this.timeout(120000);
|
||||
it('prevents the second launch of app', { timeout: 120000 }, async () => {
|
||||
const appPath = path.join(fixturesPath, 'api', 'singleton-data');
|
||||
const first = cp.spawn(process.execPath, [appPath]);
|
||||
await once(first.stdout, 'data');
|
||||
@@ -425,53 +435,60 @@ describe('app module', () => {
|
||||
const socketPath =
|
||||
process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch';
|
||||
|
||||
beforeEach((done) => {
|
||||
fs.unlink(socketPath, () => {
|
||||
server = net.createServer();
|
||||
server.listen(socketPath);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
server!.close(() => {
|
||||
if (process.platform === 'win32') {
|
||||
beforeEach(
|
||||
withDone((done) => {
|
||||
fs.unlink(socketPath, () => {
|
||||
server = net.createServer();
|
||||
server.listen(socketPath);
|
||||
done();
|
||||
} else {
|
||||
fs.unlink(socketPath, () => done());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it('relaunches the app', function (done) {
|
||||
this.timeout(120000);
|
||||
|
||||
let state = 'none';
|
||||
server!.once('error', (error) => done(error));
|
||||
server!.on('connection', (client) => {
|
||||
client.once('data', (data) => {
|
||||
if (String(data) === '--first' && state === 'none') {
|
||||
state = 'first-launch';
|
||||
} else if (String(data) === '--second' && state === 'first-launch') {
|
||||
state = 'second-launch';
|
||||
} else if (String(data) === '--third' && state === 'second-launch') {
|
||||
afterEach(
|
||||
withDone((done) => {
|
||||
server!.close(() => {
|
||||
if (process.platform === 'win32') {
|
||||
done();
|
||||
} else {
|
||||
done(`Unexpected state: "${state}", data: "${data}"`);
|
||||
fs.unlink(socketPath, () => done());
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const appPath = path.join(fixturesPath, 'api', 'relaunch');
|
||||
const child = cp.spawn(process.execPath, [appPath, '--first']);
|
||||
child.stdout.on('data', (c) => console.log(c.toString()));
|
||||
child.stderr.on('data', (c) => console.log(c.toString()));
|
||||
child.on('exit', (code, signal) => {
|
||||
if (code !== 0) {
|
||||
console.log(`Process exited with code "${code}" signal "${signal}"`);
|
||||
}
|
||||
});
|
||||
});
|
||||
it(
|
||||
'relaunches the app',
|
||||
{ timeout: 120000 },
|
||||
() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let state = 'none';
|
||||
server!.once('error', (error) => reject(error));
|
||||
server!.on('connection', (client) => {
|
||||
client.once('data', (data) => {
|
||||
if (String(data) === '--first' && state === 'none') {
|
||||
state = 'first-launch';
|
||||
} else if (String(data) === '--second' && state === 'first-launch') {
|
||||
state = 'second-launch';
|
||||
} else if (String(data) === '--third' && state === 'second-launch') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Unexpected state: "${state}", data: "${data}"`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const appPath = path.join(fixturesPath, 'api', 'relaunch');
|
||||
const child = cp.spawn(process.execPath, [appPath, '--first']);
|
||||
child.stdout.on('data', (c) => console.log(c.toString()));
|
||||
child.stderr.on('data', (c) => console.log(c.toString()));
|
||||
child.on('exit', (code, signal) => {
|
||||
if (code !== 0) {
|
||||
console.log(`Process exited with code "${code}" signal "${signal}"`);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('app.setUserActivity(type, userInfo)', () => {
|
||||
@@ -485,35 +502,35 @@ describe('app module', () => {
|
||||
afterEach(closeAllWindows);
|
||||
it('is emitted when visiting a server with a self-signed cert', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL(secureUrl);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(secureUrl));
|
||||
await once(app, 'certificate-error');
|
||||
});
|
||||
|
||||
describe('when denied', () => {
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
callback(false);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
app.removeAllListeners('certificate-error');
|
||||
});
|
||||
|
||||
it('causes did-fail-load', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL(secureUrl);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(secureUrl));
|
||||
await once(w.webContents, 'did-fail-load');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// xdescribe('app.importCertificate', () => {
|
||||
// describe.skip('app.importCertificate', () => {
|
||||
// let w = null
|
||||
|
||||
// before(function () {
|
||||
// beforeAll((ctx) => {
|
||||
// if (process.platform !== 'linux') {
|
||||
// this.skip()
|
||||
// ctx.skip()
|
||||
// }
|
||||
// })
|
||||
|
||||
@@ -629,7 +646,7 @@ describe('app module', () => {
|
||||
|
||||
const expectedBadgeCount = 42;
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
app.badgeCount = 0;
|
||||
});
|
||||
|
||||
@@ -1354,7 +1371,7 @@ describe('app module', () => {
|
||||
ifdescribe(process.platform !== 'linux')('select-client-certificate event', () => {
|
||||
let w: BrowserWindow;
|
||||
|
||||
before(function () {
|
||||
beforeAll(function () {
|
||||
session.fromPartition('empty-certificate').setCertificateVerifyProc((req, cb) => {
|
||||
cb(0);
|
||||
});
|
||||
@@ -1376,7 +1393,7 @@ describe('app module', () => {
|
||||
})
|
||||
);
|
||||
|
||||
after(() => session.fromPartition('empty-certificate').setCertificateVerifyProc(null));
|
||||
afterAll(() => session.fromPartition('empty-certificate').setCertificateVerifyProc(null));
|
||||
|
||||
it('can respond with empty certificate list', async () => {
|
||||
app.once('select-client-certificate', function (event, webContents, url, list, callback) {
|
||||
@@ -1402,7 +1419,7 @@ describe('app module', () => {
|
||||
let Winreg: any;
|
||||
let classesKey: any;
|
||||
|
||||
before(function () {
|
||||
beforeAll(function () {
|
||||
Winreg = require('winreg');
|
||||
|
||||
classesKey = new Winreg({
|
||||
@@ -1411,20 +1428,22 @@ describe('app module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
if (process.platform !== 'win32') {
|
||||
done();
|
||||
} else {
|
||||
const protocolKey = new Winreg({
|
||||
hive: Winreg.HKCU,
|
||||
key: `\\Software\\Classes\\${protocol}`
|
||||
});
|
||||
afterAll(
|
||||
withDone((done) => {
|
||||
if (process.platform !== 'win32') {
|
||||
done();
|
||||
} else {
|
||||
const protocolKey = new Winreg({
|
||||
hive: Winreg.HKCU,
|
||||
key: `\\Software\\Classes\\${protocol}`
|
||||
});
|
||||
|
||||
// The last test leaves the registry dirty,
|
||||
// delete the protocol key for those of us who test at home
|
||||
protocolKey.destroy(() => done());
|
||||
}
|
||||
});
|
||||
// The last test leaves the registry dirty,
|
||||
// delete the protocol key for those of us who test at home
|
||||
protocolKey.destroy(() => done());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
app.removeAsDefaultProtocolClient(protocol);
|
||||
@@ -1766,82 +1785,92 @@ describe('app module', () => {
|
||||
const socketPath =
|
||||
process.platform === 'win32' ? '\\\\.\\pipe\\electron-mixed-sandbox' : '/tmp/electron-mixed-sandbox';
|
||||
|
||||
beforeEach(function (done) {
|
||||
fs.unlink(socketPath, () => {
|
||||
server = net.createServer();
|
||||
server.listen(socketPath);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
if (appProcess != null) appProcess.kill();
|
||||
|
||||
if (server) {
|
||||
server.close(() => {
|
||||
if (process.platform === 'win32') {
|
||||
done();
|
||||
} else {
|
||||
fs.unlink(socketPath, () => done());
|
||||
}
|
||||
beforeEach(
|
||||
withDone((done) => {
|
||||
fs.unlink(socketPath, () => {
|
||||
server = net.createServer();
|
||||
server.listen(socketPath);
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
afterEach(
|
||||
withDone((done) => {
|
||||
if (appProcess != null) appProcess.kill();
|
||||
|
||||
if (server) {
|
||||
server.close(() => {
|
||||
if (process.platform === 'win32') {
|
||||
done();
|
||||
} else {
|
||||
fs.unlink(socketPath, () => done());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
describe('when app.enableSandbox() is called', () => {
|
||||
it('adds --enable-sandbox to all renderer processes', (done) => {
|
||||
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
|
||||
appProcess = cp.spawn(process.execPath, [appPath, '--app-enable-sandbox'], { stdio: 'inherit' });
|
||||
it(
|
||||
'adds --enable-sandbox to all renderer processes',
|
||||
withDone((done) => {
|
||||
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
|
||||
appProcess = cp.spawn(process.execPath, [appPath, '--app-enable-sandbox'], { stdio: 'inherit' });
|
||||
|
||||
server.once('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
|
||||
server.on('connection', (client) => {
|
||||
client.once('data', (data) => {
|
||||
const argv = JSON.parse(data.toString());
|
||||
expect(argv.sandbox).to.include('--enable-sandbox');
|
||||
expect(argv.sandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandbox).to.include('--enable-sandbox');
|
||||
expect(argv.noSandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandboxDevtools).to.equal(true);
|
||||
expect(argv.sandboxDevtools).to.equal(true);
|
||||
|
||||
done();
|
||||
server.once('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.on('connection', (client) => {
|
||||
client.once('data', (data) => {
|
||||
const argv = JSON.parse(data.toString());
|
||||
expect(argv.sandbox).to.include('--enable-sandbox');
|
||||
expect(argv.sandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandbox).to.include('--enable-sandbox');
|
||||
expect(argv.noSandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandboxDevtools).to.equal(true);
|
||||
expect(argv.sandboxDevtools).to.equal(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('when the app is launched with --enable-sandbox', () => {
|
||||
it('adds --enable-sandbox to all renderer processes', (done) => {
|
||||
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
|
||||
appProcess = cp.spawn(process.execPath, [appPath, '--enable-sandbox'], { stdio: 'inherit' });
|
||||
it(
|
||||
'adds --enable-sandbox to all renderer processes',
|
||||
withDone((done) => {
|
||||
const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app');
|
||||
appProcess = cp.spawn(process.execPath, [appPath, '--enable-sandbox'], { stdio: 'inherit' });
|
||||
|
||||
server.once('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
|
||||
server.on('connection', (client) => {
|
||||
client.once('data', (data) => {
|
||||
const argv = JSON.parse(data.toString());
|
||||
expect(argv.sandbox).to.include('--enable-sandbox');
|
||||
expect(argv.sandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandbox).to.include('--enable-sandbox');
|
||||
expect(argv.noSandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandboxDevtools).to.equal(true);
|
||||
expect(argv.sandboxDevtools).to.equal(true);
|
||||
|
||||
done();
|
||||
server.once('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.on('connection', (client) => {
|
||||
client.once('data', (data) => {
|
||||
const argv = JSON.parse(data.toString());
|
||||
expect(argv.sandbox).to.include('--enable-sandbox');
|
||||
expect(argv.sandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandbox).to.include('--enable-sandbox');
|
||||
expect(argv.noSandbox).to.not.include('--no-sandbox');
|
||||
|
||||
expect(argv.noSandboxDevtools).to.equal(true);
|
||||
expect(argv.sandboxDevtools).to.equal(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1858,7 +1887,7 @@ describe('app module', () => {
|
||||
describe('app.isActive', () => {
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('returns true when the app becomes active', async () => {
|
||||
it('returns true when the app becomes active', async (ctx) => {
|
||||
expect(app.isActive()).to.equal(false);
|
||||
|
||||
const w = new BrowserWindow({
|
||||
@@ -1869,7 +1898,7 @@ describe('app module', () => {
|
||||
|
||||
w.show();
|
||||
|
||||
await expect(waitUntil(() => app.isActive())).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(() => app.isActive(), ctx.signal)).to.eventually.be.fulfilled();
|
||||
|
||||
w.close();
|
||||
app.hide();
|
||||
@@ -1879,19 +1908,19 @@ describe('app module', () => {
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('app hide and show API', () => {
|
||||
describe('app.isHidden', () => {
|
||||
it('returns true when the app is hidden', async () => {
|
||||
it('returns true when the app is hidden', async (ctx) => {
|
||||
app.hide();
|
||||
await expect(waitUntil(() => app.isHidden())).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(() => app.isHidden(), ctx.signal)).to.eventually.be.fulfilled();
|
||||
});
|
||||
it('returns false when the app is shown', async () => {
|
||||
it('returns false when the app is shown', async (ctx) => {
|
||||
app.show();
|
||||
await expect(waitUntil(() => !app.isHidden())).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(() => !app.isHidden(), ctx.signal)).to.eventually.be.fulfilled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('dock APIs', () => {
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await app.dock?.show();
|
||||
});
|
||||
|
||||
@@ -1944,7 +1973,7 @@ describe('app module', () => {
|
||||
});
|
||||
|
||||
describe('dock.setBadge', () => {
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
app.dock?.setBadge('');
|
||||
});
|
||||
|
||||
@@ -2083,7 +2112,7 @@ describe('app module', () => {
|
||||
});
|
||||
|
||||
describe('configureHostResolver', () => {
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
// Returns to the default configuration.
|
||||
app.configureHostResolver({});
|
||||
});
|
||||
@@ -2258,8 +2287,15 @@ describe('app module', () => {
|
||||
|
||||
it('impacts proxy for requests made from utility process', async () => {
|
||||
const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
|
||||
// This closure is stringified and evaluated inside the utility process
|
||||
// fixture, so it must not capture any imports from this file (vite's SSR
|
||||
// transform rewrites those to __vite_ssr_import_N__ bindings that don't
|
||||
// exist in the fixture). Use inline require() instead.
|
||||
const fn = async () => {
|
||||
const urlRequest = electronNet.request('http://example.com/');
|
||||
const { net } = require('electron');
|
||||
const { expect } = require('chai');
|
||||
const { getResponse, collectStreamBody } = require('../../../lib/net-helpers');
|
||||
const urlRequest = net.request('http://example.com/');
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const message = await collectStreamBody(response);
|
||||
@@ -2277,11 +2313,15 @@ describe('app module', () => {
|
||||
const child = utilityProcess.fork(utilityFixturePath, [], {
|
||||
execArgv: ['--expose-gc']
|
||||
});
|
||||
const [ready] = await once(child, 'message');
|
||||
expect(ready?.type).to.equal('ready');
|
||||
child.postMessage({ fn: `(${fn})()` });
|
||||
const [data] = await once(child, 'message');
|
||||
expect(data.ok).to.be.true(data.message);
|
||||
// Cleanup.
|
||||
const [code] = await once(child, 'exit');
|
||||
const exited = once(child, 'exit');
|
||||
child.postMessage({ type: 'shutdown' });
|
||||
const [code] = await exited;
|
||||
expect(code).to.equal(0);
|
||||
});
|
||||
|
||||
@@ -2350,7 +2390,7 @@ describe('default behavior', () => {
|
||||
describe('user agent fallback', () => {
|
||||
let initialValue: string;
|
||||
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
initialValue = app.userAgentFallback!;
|
||||
});
|
||||
|
||||
@@ -2376,7 +2416,7 @@ describe('default behavior', () => {
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((request, response) => {
|
||||
if (request.headers.authorization) {
|
||||
return response.end('ok');
|
||||
@@ -2387,13 +2427,13 @@ describe('default behavior', () => {
|
||||
serverUrl = (await listen(server)).url;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('should emit a login event on app when a WebContents hits a 401', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL(serverUrl);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(serverUrl));
|
||||
const [, webContents] = (await once(app, 'login')) as [any, WebContents];
|
||||
expect(webContents).to.equal(w.webContents);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { autoUpdater } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { autoUpdater, systemPreferences } from 'electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as express from 'express';
|
||||
import * as psList from 'ps-list';
|
||||
import * as uuid from 'uuid';
|
||||
import { afterAll, afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import express = require('express');
|
||||
import psList = require('ps-list');
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
@@ -21,18 +23,16 @@ import {
|
||||
unsignApp
|
||||
} from './lib/codesign-helpers';
|
||||
import { withTempDirectory } from './lib/fs-helpers';
|
||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||
import { ifdescribe, ifit, withDone } from './lib/spec-helpers';
|
||||
|
||||
// We can only test the auto updater on darwin non-component builds
|
||||
ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||
this.timeout(120000);
|
||||
|
||||
ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', { timeout: 120000 }, () => {
|
||||
let identity = '';
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach((ctx) => {
|
||||
const result = getCodesignIdentity();
|
||||
if (result === null) {
|
||||
this.skip();
|
||||
ctx.skip();
|
||||
} else {
|
||||
identity = result;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||
return cachedZips[key];
|
||||
};
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
for (const version of Object.keys(cachedZips)) {
|
||||
cp.spawnSync('rm', ['-r', path.dirname(cachedZips[version])]);
|
||||
}
|
||||
@@ -226,18 +226,20 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||
let httpServer: http.Server = null as any;
|
||||
let requests: express.Request[] = [];
|
||||
|
||||
beforeEach((done) => {
|
||||
requests = [];
|
||||
server = express();
|
||||
server.use((req, res, next) => {
|
||||
requests.push(req);
|
||||
next();
|
||||
});
|
||||
httpServer = server.listen(0, '127.0.0.1', () => {
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
done();
|
||||
});
|
||||
});
|
||||
beforeEach(
|
||||
withDone((done) => {
|
||||
requests = [];
|
||||
server = express();
|
||||
server.use((req, res, next) => {
|
||||
requests.push(req);
|
||||
next();
|
||||
});
|
||||
httpServer = server.listen(0, '127.0.0.1', () => {
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
if (httpServer) {
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect } from 'chai';
|
||||
import * as express from 'express';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as http from 'node:http';
|
||||
import { AddressInfo } from 'node:net';
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
uninstallMsixPackage,
|
||||
unregisterExecutableWithIdentity
|
||||
} from './lib/msix-helpers';
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
import { ifdescribe, withDone } from './lib/spec-helpers';
|
||||
|
||||
const ELECTRON_MSIX_ALIAS = 'ElectronMSIX.exe';
|
||||
const MAIN_JS_PATH = getMainJsFixturePath();
|
||||
@@ -25,17 +26,15 @@ const MSIX_V1 = getMsixFixturePath('v1');
|
||||
const MSIX_V2 = getMsixFixturePath('v2');
|
||||
|
||||
// We can only test the MSIX updater on Windows
|
||||
ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
|
||||
this.timeout(120000);
|
||||
|
||||
before(async function () {
|
||||
ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', { timeout: 120000 }, () => {
|
||||
beforeAll(async function () {
|
||||
await installMsixCertificate();
|
||||
|
||||
const electronExec = getElectronExecutable();
|
||||
await registerExecutableWithIdentity(electronExec);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
afterAll(async function () {
|
||||
await unregisterExecutableWithIdentity();
|
||||
});
|
||||
|
||||
@@ -75,18 +74,20 @@ ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
|
||||
let httpServer: http.Server = null as any;
|
||||
let requests: express.Request[] = [];
|
||||
|
||||
beforeEach((done) => {
|
||||
requests = [];
|
||||
server = express();
|
||||
server.use((req, res, next) => {
|
||||
requests.push(req);
|
||||
next();
|
||||
});
|
||||
httpServer = server.listen(0, '127.0.0.1', () => {
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
done();
|
||||
});
|
||||
});
|
||||
beforeEach(
|
||||
withDone((done) => {
|
||||
requests = [];
|
||||
server = express();
|
||||
server.use((req, res, next) => {
|
||||
requests.push(req);
|
||||
next();
|
||||
});
|
||||
httpServer = server.listen(0, '127.0.0.1', () => {
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
if (httpServer) {
|
||||
@@ -1,12 +1,18 @@
|
||||
import { BrowserView, BrowserWindow, screen, session, webContents } from 'electron/main';
|
||||
import { BrowserView, BrowserWindow, session, webContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ScreenCapture, hasCapturableScreen } from './lib/screen-helpers';
|
||||
import { defer, ifit, startRemoteControlApp } from './lib/spec-helpers';
|
||||
import {
|
||||
defer,
|
||||
runCleanupFunctions,
|
||||
startRemoteControlApp,
|
||||
withDone,
|
||||
dangerouslyIgnoreWebContentsLoadResult
|
||||
} from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
describe('BrowserView module', () => {
|
||||
@@ -32,6 +38,7 @@ describe('BrowserView module', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await runCleanupFunctions();
|
||||
if (w && !w.isDestroyed()) {
|
||||
const p = once(w.webContents, 'destroyed');
|
||||
await closeWindow(w);
|
||||
@@ -82,43 +89,8 @@ describe('BrowserView module', () => {
|
||||
}).not.to.throw();
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('sets the background color to transparent if none is set', async () => {
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const WINDOW_BACKGROUND_COLOR = '#55ccbb';
|
||||
|
||||
w.show();
|
||||
w.setBounds(display.bounds);
|
||||
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
|
||||
await w.loadURL('data:text/html,<html></html>');
|
||||
|
||||
view = new BrowserView();
|
||||
view.setBounds(display.bounds);
|
||||
w.setBrowserView(view);
|
||||
await view.webContents.loadURL('data:text/html,hello there');
|
||||
|
||||
const screenCapture = new ScreenCapture(display);
|
||||
await screenCapture.expectColorAtCenterMatches(WINDOW_BACKGROUND_COLOR);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('successfully applies the background color', async () => {
|
||||
const WINDOW_BACKGROUND_COLOR = '#55ccbb';
|
||||
const VIEW_BACKGROUND_COLOR = '#ff00ff';
|
||||
const display = screen.getPrimaryDisplay();
|
||||
|
||||
w.show();
|
||||
w.setBounds(display.bounds);
|
||||
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
|
||||
await w.loadURL('data:text/html,<html></html>');
|
||||
|
||||
view = new BrowserView();
|
||||
view.setBounds(display.bounds);
|
||||
w.setBrowserView(view);
|
||||
w.setBackgroundColor(VIEW_BACKGROUND_COLOR);
|
||||
await view.webContents.loadURL('data:text/html,hello there');
|
||||
|
||||
const screenCapture = new ScreenCapture(display);
|
||||
await screenCapture.expectColorAtCenterMatches(VIEW_BACKGROUND_COLOR);
|
||||
});
|
||||
// Screen-capture tests for setBackgroundColor live in
|
||||
// spec/serial/browser-view-background-color.spec.ts.
|
||||
});
|
||||
|
||||
describe('BrowserView.setAutoResize()', () => {
|
||||
@@ -409,13 +381,15 @@ describe('BrowserView module', () => {
|
||||
w.addBrowserView(view);
|
||||
});
|
||||
|
||||
it('does not crash if the webContents is destroyed after a URL is loaded', () => {
|
||||
it('does not crash if the webContents is destroyed after a URL is loaded', async () => {
|
||||
view = new BrowserView();
|
||||
expect(async () => {
|
||||
view.setBounds({ x: 0, y: 0, width: 400, height: 300 });
|
||||
await view.webContents.loadURL('data:text/html,hello there');
|
||||
view.webContents.destroy();
|
||||
}).to.not.throw();
|
||||
await expect(
|
||||
(async () => {
|
||||
view.setBounds({ x: 0, y: 0, width: 400, height: 300 });
|
||||
await view.webContents.loadURL('data:text/html,hello there');
|
||||
view.webContents.destroy();
|
||||
})()
|
||||
).to.not.be.rejected();
|
||||
});
|
||||
|
||||
it('can handle BrowserView reparenting', async () => {
|
||||
@@ -424,7 +398,7 @@ describe('BrowserView module', () => {
|
||||
expect(view.ownerWindow).to.be.null('ownerWindow');
|
||||
|
||||
w.addBrowserView(view);
|
||||
view.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(view.webContents.loadURL('about:blank'));
|
||||
await once(view.webContents, 'did-finish-load');
|
||||
|
||||
expect(view.ownerWindow).to.equal(w);
|
||||
@@ -436,7 +410,7 @@ describe('BrowserView module', () => {
|
||||
|
||||
w.close();
|
||||
|
||||
view.webContents.loadURL(`file://${fixtures}/pages/blank.html`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(view.webContents.loadURL(`file://${fixtures}/pages/blank.html`));
|
||||
await once(view.webContents, 'did-finish-load');
|
||||
|
||||
// Clean up - the afterEach hook assumes the webContents on w is still alive.
|
||||
@@ -454,7 +428,7 @@ describe('BrowserView module', () => {
|
||||
w2.addBrowserView(view);
|
||||
expect(view.ownerWindow).to.equal(w2);
|
||||
|
||||
w2.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w2.webContents.loadURL('about:blank'));
|
||||
await once(w2.webContents, 'did-finish-load');
|
||||
w2.close();
|
||||
});
|
||||
@@ -467,7 +441,7 @@ describe('BrowserView module', () => {
|
||||
w.addBrowserView(view);
|
||||
expect(view.ownerWindow).to.equal(w);
|
||||
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
await once(w.webContents, 'did-finish-load');
|
||||
});
|
||||
|
||||
@@ -711,7 +685,7 @@ describe('BrowserView module', () => {
|
||||
await rc.remotely(() => {
|
||||
const { app, BrowserView, BrowserWindow } = require('electron');
|
||||
const bv = new BrowserView();
|
||||
bv.webContents.loadURL('about:blank');
|
||||
bv.webContents.loadURL('about:blank').catch(() => {});
|
||||
const bw = new BrowserWindow({ show: false });
|
||||
bw.addBrowserView(bv);
|
||||
setTimeout(() => {
|
||||
@@ -742,17 +716,22 @@ describe('BrowserView module', () => {
|
||||
});
|
||||
|
||||
describe('window.open()', () => {
|
||||
it('works in BrowserView', (done) => {
|
||||
view = new BrowserView();
|
||||
w.setBrowserView(view);
|
||||
view.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
||||
expect(url).to.equal('http://host/');
|
||||
expect(frameName).to.equal('host');
|
||||
done();
|
||||
return { action: 'deny' };
|
||||
});
|
||||
view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
|
||||
});
|
||||
it(
|
||||
'works in BrowserView',
|
||||
withDone((done) => {
|
||||
view = new BrowserView();
|
||||
w.setBrowserView(view);
|
||||
view.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
||||
expect(url).to.equal('http://host/');
|
||||
expect(frameName).to.equal('host');
|
||||
done();
|
||||
return { action: 'deny' };
|
||||
});
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('BrowserView.capturePage(rect)', () => {
|
||||
@@ -778,7 +757,7 @@ describe('BrowserView module', () => {
|
||||
expect(image.isEmpty()).to.equal(true);
|
||||
});
|
||||
|
||||
xit('resolves after the window is hidden and capturer count is non-zero', async () => {
|
||||
it.skip('resolves after the window is hidden and capturer count is non-zero', async () => {
|
||||
view = new BrowserView({
|
||||
webPreferences: {
|
||||
backgroundThrottling: false
|
||||
1273
spec/api-browser-window-spec.ts → spec/api-browser-window.spec.ts
Executable file → Normal file
1273
spec/api-browser-window-spec.ts → spec/api-browser-window.spec.ts
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import { app, contentTracing, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -31,14 +32,8 @@ ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'lin
|
||||
}
|
||||
});
|
||||
|
||||
describe('startRecording', function () {
|
||||
if (process.platform === 'win32' && process.arch === 'arm64') {
|
||||
// WOA needs more time
|
||||
this.timeout(10e3);
|
||||
} else {
|
||||
this.timeout(5e3);
|
||||
}
|
||||
|
||||
// WOA needs more time
|
||||
describe('startRecording', { timeout: process.platform === 'win32' && process.arch === 'arm64' ? 10e3 : 5e3 }, () => {
|
||||
const getFileSizeInKiloBytes = (filePath: string) => {
|
||||
const stats = fs.statSync(filePath);
|
||||
const fileSizeInBytes = stats.size;
|
||||
@@ -95,50 +90,48 @@ ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'lin
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform !== 'linux')('stopRecording', function () {
|
||||
if (process.platform === 'win32' && process.arch === 'arm64') {
|
||||
// WOA needs more time
|
||||
this.timeout(10e3);
|
||||
} else {
|
||||
this.timeout(5e3);
|
||||
// WOA needs more time
|
||||
ifdescribe(process.platform !== 'linux')(
|
||||
'stopRecording',
|
||||
{ timeout: process.platform === 'win32' && process.arch === 'arm64' ? 10e3 : 5e3 },
|
||||
() => {
|
||||
// FIXME(samuelmaddock): this test regularly flakes
|
||||
it.skip('does not crash on empty string', async () => {
|
||||
const options = {
|
||||
categoryFilter: '*',
|
||||
traceOptions: 'record-until-full,enable-sampling'
|
||||
};
|
||||
|
||||
await contentTracing.startRecording(options);
|
||||
const path = await contentTracing.stopRecording('');
|
||||
expect(path).to.be.a('string').that.is.not.empty('result path');
|
||||
expect(fs.statSync(path).isFile()).to.be.true('output exists');
|
||||
});
|
||||
|
||||
it('calls its callback with a result file path', async () => {
|
||||
const resultFilePath = await record(/* options */ {}, outputFilePath);
|
||||
expect(resultFilePath).to.be.a('string').and.be.equal(outputFilePath);
|
||||
});
|
||||
|
||||
it('creates a temporary file when an empty string is passed', async function () {
|
||||
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ '');
|
||||
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
|
||||
});
|
||||
|
||||
it('creates a temporary file when no path is passed', async function () {
|
||||
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ undefined);
|
||||
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
|
||||
});
|
||||
|
||||
it('rejects if no trace is happening', async () => {
|
||||
await expect(contentTracing.stopRecording()).to.be.rejectedWith(
|
||||
'Failed to stop tracing - no trace in progress'
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// FIXME(samuelmaddock): this test regularly flakes
|
||||
it.skip('does not crash on empty string', async () => {
|
||||
const options = {
|
||||
categoryFilter: '*',
|
||||
traceOptions: 'record-until-full,enable-sampling'
|
||||
};
|
||||
|
||||
await contentTracing.startRecording(options);
|
||||
const path = await contentTracing.stopRecording('');
|
||||
expect(path).to.be.a('string').that.is.not.empty('result path');
|
||||
expect(fs.statSync(path).isFile()).to.be.true('output exists');
|
||||
});
|
||||
|
||||
it('calls its callback with a result file path', async () => {
|
||||
const resultFilePath = await record(/* options */ {}, outputFilePath);
|
||||
expect(resultFilePath).to.be.a('string').and.be.equal(outputFilePath);
|
||||
});
|
||||
|
||||
it('creates a temporary file when an empty string is passed', async function () {
|
||||
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ '');
|
||||
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
|
||||
});
|
||||
|
||||
it('creates a temporary file when no path is passed', async function () {
|
||||
const resultFilePath = await record(/* options */ {}, /* outputFilePath */ undefined);
|
||||
expect(resultFilePath).to.be.a('string').that.is.not.empty('result path');
|
||||
});
|
||||
|
||||
it('rejects if no trace is happening', async () => {
|
||||
await expect(contentTracing.stopRecording()).to.be.rejectedWith('Failed to stop tracing - no trace in progress');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTraceBufferUsage', function () {
|
||||
this.timeout(10e3);
|
||||
|
||||
describe('getTraceBufferUsage', { timeout: 10e3 }, () => {
|
||||
it('does not crash and returns valid usage data', async () => {
|
||||
await app.whenReady();
|
||||
await contentTracing.startRecording({
|
||||
@@ -167,8 +160,7 @@ ifdescribe(!['arm', 'arm64'].includes(process.arch) || process.platform !== 'lin
|
||||
});
|
||||
|
||||
describe('captured events', () => {
|
||||
it('include V8 samples from the main process', async function () {
|
||||
this.timeout(60000);
|
||||
it('include V8 samples from the main process', { timeout: 60000 }, async () => {
|
||||
await contentTracing.startRecording({
|
||||
categoryFilter: 'disabled-by-default-v8.cpu_profiler',
|
||||
traceOptions: 'record-until-full'
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron/main';
|
||||
import { contextBridge } from 'electron/renderer';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -10,7 +10,8 @@ import * as http from 'node:http';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { listen } from './lib/spec-helpers';
|
||||
import { contextBridge, rewriteForRemoteEval } from './lib/remote-tools';
|
||||
import { listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
|
||||
@@ -21,7 +22,7 @@ describe('contextBridge', () => {
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('');
|
||||
@@ -29,13 +30,13 @@ describe('contextBridge', () => {
|
||||
serverUrl = (await listen(server)).url;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
if (server) await new Promise((resolve) => server.close(resolve));
|
||||
server = null as any;
|
||||
await closeWindow(w);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closeWindow(w);
|
||||
if (dir) await fs.promises.rm(dir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
@@ -47,9 +48,12 @@ describe('contextBridge', () => {
|
||||
preload: path.resolve(fixturesPath, 'can-bind-preload.js')
|
||||
}
|
||||
});
|
||||
w.loadFile(path.resolve(fixturesPath, 'empty.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.resolve(fixturesPath, 'empty.html')));
|
||||
const [, bound] = await once(ipcMain, 'context-bridge-bound');
|
||||
expect(bound).to.equal(false);
|
||||
|
||||
await closeWindow(w);
|
||||
w = null as unknown as BrowserWindow;
|
||||
});
|
||||
|
||||
it('should be accessible when contextIsolation is enabled', async () => {
|
||||
@@ -60,15 +64,34 @@ describe('contextBridge', () => {
|
||||
preload: path.resolve(fixturesPath, 'can-bind-preload.js')
|
||||
}
|
||||
});
|
||||
w.loadFile(path.resolve(fixturesPath, 'empty.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.resolve(fixturesPath, 'empty.html')));
|
||||
const [, bound] = await once(ipcMain, 'context-bridge-bound');
|
||||
expect(bound).to.equal(true);
|
||||
|
||||
await closeWindow(w);
|
||||
w = null as unknown as BrowserWindow;
|
||||
});
|
||||
|
||||
const generateTests = (useSandbox: boolean) => {
|
||||
describe(`with sandbox=${useSandbox}`, () => {
|
||||
let registeredPreloads: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const registeredPreload of registeredPreloads) {
|
||||
w.webContents.session.unregisterPreloadScript(registeredPreload);
|
||||
}
|
||||
registeredPreloads = [];
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeWindow(w);
|
||||
w = null as unknown as BrowserWindow;
|
||||
});
|
||||
|
||||
/** @remote */
|
||||
const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => {
|
||||
const bindingSrc = rewriteForRemoteEval(bindingCreator);
|
||||
const preloadContentForMainWorld = `const renderer_1 = require('electron');
|
||||
const __rt = renderer_1;
|
||||
${
|
||||
useSandbox
|
||||
? ''
|
||||
@@ -78,9 +101,10 @@ describe('contextBridge', () => {
|
||||
run: () => gc()
|
||||
});`
|
||||
}
|
||||
(${bindingCreator.toString()})();`;
|
||||
(${bindingSrc})();`;
|
||||
|
||||
const preloadContentForIsolatedWorld = `const renderer_1 = require('electron');
|
||||
const __rt = renderer_1;
|
||||
${
|
||||
useSandbox
|
||||
? ''
|
||||
@@ -93,7 +117,7 @@ describe('contextBridge', () => {
|
||||
run: () => gc()
|
||||
});`
|
||||
}
|
||||
(${bindingCreator.toString()})();`;
|
||||
(${bindingSrc})();`;
|
||||
|
||||
const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
|
||||
dir = tmpDir;
|
||||
@@ -101,19 +125,27 @@ describe('contextBridge', () => {
|
||||
path.resolve(tmpDir, 'preload.js'),
|
||||
worldId === 0 ? preloadContentForMainWorld : preloadContentForIsolatedWorld
|
||||
);
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: true,
|
||||
sandbox: useSandbox,
|
||||
preload: path.resolve(tmpDir, 'preload.js'),
|
||||
additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
|
||||
}
|
||||
});
|
||||
await w.loadURL(serverUrl);
|
||||
w =
|
||||
w ||
|
||||
new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: true,
|
||||
sandbox: useSandbox,
|
||||
additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
|
||||
}
|
||||
});
|
||||
registeredPreloads.push(
|
||||
w.webContents.session.registerPreloadScript({
|
||||
filePath: path.resolve(tmpDir, 'preload.js'),
|
||||
type: 'frame'
|
||||
})
|
||||
);
|
||||
await w.loadURL(serverUrl).catch(() => {});
|
||||
};
|
||||
|
||||
/** @remote no-locals */
|
||||
const callWithBindings = (fn: Function, worldId: number = 0) =>
|
||||
worldId === 0
|
||||
? w.webContents.executeJavaScript(`(${fn.toString()})(window)`)
|
||||
@@ -2,6 +2,7 @@ import { NativeImage, nativeImage } from 'electron/common';
|
||||
import { BrowserWindow } from 'electron/main';
|
||||
|
||||
import { AssertionError, expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import path = require('node:path');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { app } from 'electron/main';
|
||||
import * as Busboy from 'busboy';
|
||||
import { expect } from 'chai';
|
||||
import * as uuid from 'uuid';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import * as childProcess from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
@@ -11,7 +12,15 @@ import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { ifdescribe, ifit, defer, startRemoteControlApp, repeatedly, listen } from './lib/spec-helpers';
|
||||
import {
|
||||
ifdescribe,
|
||||
ifit,
|
||||
defer,
|
||||
startRemoteControlApp,
|
||||
repeatedly,
|
||||
listen,
|
||||
dangerouslyIgnoreWebContentsLoadResult
|
||||
} from './lib/spec-helpers';
|
||||
|
||||
const isWindowsOnArm = process.platform === 'win32' && process.arch === 'arm64';
|
||||
const isLinuxOnArm = process.platform === 'linux' && process.arch.includes('arm');
|
||||
@@ -263,10 +272,9 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
// Regression: base::circular_deque relocates elements on growth,
|
||||
// corrupting crashpad::Annotation's self-referential pointers and
|
||||
// causing missing crash keys or a hung handler. See crash_keys.cc.
|
||||
it('does not corrupt the crashpad annotation list after deque reallocation', async function () {
|
||||
it('does not corrupt the crashpad annotation list after deque reallocation', { timeout: 45000 }, async () => {
|
||||
// Tight timeout so a hanging handler fails fast instead of waiting
|
||||
// for the mocha default of 120s.
|
||||
this.timeout(45000);
|
||||
// for the suite default.
|
||||
const { port, waitForCrash } = await startServer();
|
||||
runCrashApp('renderer-dynamic-keys', port);
|
||||
const crash = await Promise.race([
|
||||
@@ -314,7 +322,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
bw.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
|
||||
bw.webContents.executeJavaScript(
|
||||
"process._linkedBinding('electron_common_v8_util').triggerFatalErrorForTesting()"
|
||||
);
|
||||
@@ -330,20 +338,22 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
});
|
||||
|
||||
describe('OOM crash keys', () => {
|
||||
it('reports OOM stack trace and heap statistics when renderer runs out of memory', async function () {
|
||||
this.timeout(120000);
|
||||
const { port, waitForCrash } = await startServer();
|
||||
runCrashApp('renderer-oom', port, ['--js-flags=--max-old-space-size=128']);
|
||||
const crash = await waitForCrash();
|
||||
expect(crash.process_type).to.equal('renderer');
|
||||
expect(crash['electron.v8-oom.stack']).to.be.a('string');
|
||||
expect(crash['electron.v8-oom.stack']).to.include('oomTrigger');
|
||||
expect(crash['electron.v8-oom.heap.used']).to.be.a('string');
|
||||
expect(crash['electron.v8-oom.heap.limit']).to.be.a('string');
|
||||
});
|
||||
it(
|
||||
'reports OOM stack trace and heap statistics when renderer runs out of memory',
|
||||
{ timeout: 120000 },
|
||||
async () => {
|
||||
const { port, waitForCrash } = await startServer();
|
||||
runCrashApp('renderer-oom', port, ['--js-flags=--max-old-space-size=128']);
|
||||
const crash = await waitForCrash();
|
||||
expect(crash.process_type).to.equal('renderer');
|
||||
expect(crash['electron.v8-oom.stack']).to.be.a('string');
|
||||
expect(crash['electron.v8-oom.stack']).to.include('oomTrigger');
|
||||
expect(crash['electron.v8-oom.heap.used']).to.be.a('string');
|
||||
expect(crash['electron.v8-oom.heap.limit']).to.be.a('string');
|
||||
}
|
||||
);
|
||||
|
||||
it('captures the calling function on JSON.stringify OOM', async function () {
|
||||
this.timeout(120000);
|
||||
it('captures the calling function on JSON.stringify OOM', { timeout: 120000 }, async () => {
|
||||
const { port, waitForCrash } = await startServer();
|
||||
runCrashApp('renderer-oom-json', port, ['--js-flags=--max-old-space-size=128']);
|
||||
const crash = await waitForCrash();
|
||||
@@ -352,8 +362,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
expect(crash['electron.v8-oom.stack']).to.include('serializeData');
|
||||
});
|
||||
|
||||
it('captures OOM crash keys inside a web worker', async function () {
|
||||
this.timeout(120000);
|
||||
it('captures OOM crash keys inside a web worker', { timeout: 120000 }, async () => {
|
||||
const { port, waitForCrash } = await startServer();
|
||||
runCrashApp('renderer-oom-worker', port, ['--js-flags=--max-old-space-size=128']);
|
||||
const crash = await waitForCrash();
|
||||
@@ -521,7 +530,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
bw.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
|
||||
bw.webContents.executeJavaScript('process.crash()');
|
||||
});
|
||||
await waitForCrash();
|
||||
@@ -578,7 +587,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
bw.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
|
||||
await bw.webContents.executeJavaScript(
|
||||
"require('electron').crashReporter.addExtraParameter('hello', 'world')"
|
||||
);
|
||||
@@ -630,7 +639,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
bw.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
|
||||
bw.webContents.executeJavaScript('process.crash()');
|
||||
});
|
||||
} else if (processType === 'sandboxed-renderer') {
|
||||
@@ -641,7 +650,7 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
|
||||
show: false,
|
||||
webPreferences: { sandbox: true, preload, contextIsolation: false }
|
||||
});
|
||||
bw.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(bw.loadURL('about:blank'));
|
||||
}, preloadPath);
|
||||
} else if (processType === 'node') {
|
||||
const crashScriptPath = path.join(__dirname, 'fixtures', 'apps', 'crash', 'node-crash.js');
|
||||
@@ -1,13 +1,14 @@
|
||||
import { BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { emittedUntil } from './lib/events-helpers';
|
||||
import { listen } from './lib/spec-helpers';
|
||||
import { listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('debugger module', () => {
|
||||
@@ -54,7 +55,7 @@ describe('debugger module', () => {
|
||||
});
|
||||
|
||||
it("doesn't disconnect an active devtools session", async () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
const detach = once(w.webContents.debugger, 'detach');
|
||||
w.webContents.debugger.attach();
|
||||
w.webContents.openDevTools();
|
||||
@@ -78,7 +79,7 @@ describe('debugger module', () => {
|
||||
});
|
||||
|
||||
it('returns response', async () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
w.webContents.debugger.attach();
|
||||
|
||||
const params = { expression: '4+2' };
|
||||
@@ -91,7 +92,7 @@ describe('debugger module', () => {
|
||||
});
|
||||
|
||||
it('returns response when devtools is opened', async () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
w.webContents.debugger.attach();
|
||||
|
||||
const opened = once(w.webContents, 'devtools-opened');
|
||||
@@ -112,7 +113,7 @@ describe('debugger module', () => {
|
||||
process.platform !== 'win32'
|
||||
? `file://${path.join(fixtures, 'pages', 'a.html')}`
|
||||
: `file:///${path.join(fixtures, 'pages', 'a.html').replaceAll('\\', '/')}`;
|
||||
w.webContents.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL(url));
|
||||
w.webContents.debugger.attach();
|
||||
const message = emittedUntil(
|
||||
w.webContents.debugger,
|
||||
@@ -128,7 +129,7 @@ describe('debugger module', () => {
|
||||
});
|
||||
|
||||
it('returns error message when command fails', async () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
w.webContents.debugger.attach();
|
||||
|
||||
const promise = w.webContents.debugger.sendCommand('Test');
|
||||
@@ -144,7 +145,7 @@ describe('debugger module', () => {
|
||||
});
|
||||
|
||||
const { url } = await listen(server);
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
// If we do this synchronously, it's fast enough to attach and enable
|
||||
// network capture before the load. If we do it before the loadURL, for
|
||||
// some reason network capture doesn't get enabled soon enough and we get
|
||||
@@ -187,7 +188,7 @@ describe('debugger module', () => {
|
||||
|
||||
const { url } = await listen(server);
|
||||
w.webContents.debugger.sendCommand('Network.enable');
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
|
||||
await loadingFinished;
|
||||
});
|
||||
@@ -225,7 +226,7 @@ describe('debugger module', () => {
|
||||
});
|
||||
|
||||
it('uses empty sessionId by default', async () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
w.webContents.debugger.attach();
|
||||
const onMessage = once(w.webContents.debugger, 'message');
|
||||
await w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
|
||||
@@ -236,33 +237,38 @@ describe('debugger module', () => {
|
||||
w.webContents.debugger.detach();
|
||||
});
|
||||
|
||||
it('creates unique session id for each target', (done) => {
|
||||
w.webContents.loadFile(path.join(__dirname, 'fixtures', 'sub-frames', 'debug-frames.html'));
|
||||
it('creates unique session id for each target', async () => {
|
||||
await w.webContents.loadFile(path.join(__dirname, 'fixtures', 'sub-frames', 'debug-frames.html'));
|
||||
w.webContents.debugger.attach();
|
||||
let debuggerSessionId: string;
|
||||
|
||||
w.webContents.debugger.on('message', (_event, ...args) => {
|
||||
const [method, params, sessionId] = args;
|
||||
if (method === 'Target.targetCreated') {
|
||||
w.webContents.debugger
|
||||
.sendCommand('Target.attachToTarget', { targetId: params.targetInfo.targetId, flatten: true })
|
||||
.then((result) => {
|
||||
debuggerSessionId = result.sessionId;
|
||||
w.webContents.debugger.sendCommand('Debugger.enable', {}, result.sessionId);
|
||||
|
||||
// Ensure debugger finds a script to pause to possibly reduce flaky
|
||||
// tests.
|
||||
w.webContents.mainFrame.executeJavaScript('void 0;');
|
||||
});
|
||||
}
|
||||
if (method === 'Debugger.scriptParsed') {
|
||||
if (sessionId === debuggerSessionId) {
|
||||
w.webContents.debugger.detach();
|
||||
done();
|
||||
const targetCreated = new Promise<{ targetId: string }>((resolve) => {
|
||||
w.webContents.debugger.on('message', (_event, method, params) => {
|
||||
if (method === 'Target.targetCreated') {
|
||||
resolve({ targetId: params.targetInfo.targetId });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
|
||||
await w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
|
||||
const { targetId } = await targetCreated;
|
||||
|
||||
const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
|
||||
targetId,
|
||||
flatten: true
|
||||
});
|
||||
expect(sessionId).to.be.a('string').and.not.be.empty();
|
||||
|
||||
const scriptParsed = new Promise<string>((resolve) => {
|
||||
w.webContents.debugger.on('message', (_event, method, _params, messageSessionId) => {
|
||||
if (method === 'Debugger.scriptParsed') resolve(messageSessionId);
|
||||
});
|
||||
});
|
||||
await w.webContents.debugger.sendCommand('Debugger.enable', {}, sessionId);
|
||||
// Evaluate via CDP on the attached session so a script is parsed after
|
||||
// Debugger.enable has been acknowledged, regardless of page load timing.
|
||||
await w.webContents.debugger.sendCommand('Runtime.evaluate', { expression: 'void 0;' }, sessionId);
|
||||
|
||||
expect(await scriptParsed).to.equal(sessionId);
|
||||
w.webContents.debugger.detach();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { screen, desktopCapturer, BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||
import { ifit } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
function getSourceTypes(): ('window' | 'screen')[] {
|
||||
@@ -173,143 +173,7 @@ describe('desktopCapturer', () => {
|
||||
});
|
||||
|
||||
// Linux doesn't return any window sources.
|
||||
ifit(process.platform !== 'linux')('moveAbove should move the window at the requested place', async function () {
|
||||
// DesktopCapturer.getSources() is guaranteed to return in the correct
|
||||
// z-order from foreground to background.
|
||||
const MAX_WIN = 4;
|
||||
const wList: BrowserWindow[] = [];
|
||||
|
||||
const destroyWindows = () => {
|
||||
for (const w of wList) {
|
||||
w.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
for (let i = 0; i < MAX_WIN; i++) {
|
||||
const w = new BrowserWindow({ show: false, width: 100, height: 100 });
|
||||
wList.push(w);
|
||||
}
|
||||
expect(wList.length).to.equal(MAX_WIN);
|
||||
|
||||
// Show and focus all the windows.
|
||||
for (const w of wList) {
|
||||
const wShown = once(w, 'show');
|
||||
const wFocused = once(w, 'focus');
|
||||
|
||||
w.show();
|
||||
w.focus();
|
||||
|
||||
await wShown;
|
||||
await wFocused;
|
||||
}
|
||||
|
||||
// At this point our windows should be showing from bottom to top.
|
||||
|
||||
// DesktopCapturer.getSources() returns sources sorted from foreground to
|
||||
// background, i.e. top to bottom.
|
||||
let sources = await desktopCapturer.getSources({
|
||||
types: ['window'],
|
||||
thumbnailSize: { width: 0, height: 0 }
|
||||
});
|
||||
|
||||
expect(sources).to.be.an('array').that.is.not.empty();
|
||||
expect(sources.length).to.gte(MAX_WIN);
|
||||
|
||||
// Only keep our windows, they must be in the MAX_WIN first windows.
|
||||
sources.splice(MAX_WIN, sources.length - MAX_WIN);
|
||||
expect(sources.length).to.equal(MAX_WIN);
|
||||
expect(sources.length).to.equal(wList.length);
|
||||
|
||||
// Check that the sources and wList are sorted in the reverse order.
|
||||
// If they're not, skip remaining checks because either focus or
|
||||
// window placement are not reliable in the running test environment.
|
||||
const wListReversed = wList.slice().reverse();
|
||||
const proceed = sources.every((source, index) => source.id === wListReversed[index].getMediaSourceId());
|
||||
if (!proceed) return;
|
||||
|
||||
// Move windows so wList is sorted from foreground to background.
|
||||
for (const [i, w] of wList.entries()) {
|
||||
if (i < wList.length - 1) {
|
||||
const next = wList[wList.length - 1];
|
||||
w.focus();
|
||||
w.moveAbove(next.getMediaSourceId());
|
||||
// Ensure the window has time to move.
|
||||
await setTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
sources = await desktopCapturer.getSources({
|
||||
types: ['window'],
|
||||
thumbnailSize: { width: 0, height: 0 }
|
||||
});
|
||||
|
||||
sources.splice(MAX_WIN, sources.length);
|
||||
expect(sources.length).to.equal(MAX_WIN);
|
||||
expect(sources.length).to.equal(wList.length);
|
||||
|
||||
// Check that the sources and wList are sorted in the same order.
|
||||
for (const [index, source] of sources.entries()) {
|
||||
const wID = wList[index].getMediaSourceId();
|
||||
expect(source.id).to.equal(wID);
|
||||
}
|
||||
} finally {
|
||||
destroyWindows();
|
||||
}
|
||||
});
|
||||
|
||||
// Linux doesn't return any window sources.
|
||||
ifdescribe(process.platform !== 'linux')('fetchWindowIcons', function () {
|
||||
// Tests are sequentially dependent
|
||||
this.bail(true);
|
||||
let w: BrowserWindow;
|
||||
let testSource: Electron.DesktopCapturerSource | undefined;
|
||||
let appIcon: Electron.NativeImage | undefined;
|
||||
|
||||
before(async () => {
|
||||
w = new BrowserWindow({
|
||||
width: 200,
|
||||
height: 200,
|
||||
show: true,
|
||||
title: 'desktop-capturer-test-window'
|
||||
});
|
||||
await w.loadURL('about:blank');
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['window'],
|
||||
fetchWindowIcons: true
|
||||
});
|
||||
testSource = sources.find((s) => s.name === 'desktop-capturer-test-window');
|
||||
appIcon = testSource?.appIcon;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (w) w.destroy();
|
||||
});
|
||||
|
||||
it('should find the test window in the list of captured sources', () => {
|
||||
expect(testSource, `The ${w.getTitle()} window was not found by desktopCapturer`).to.exist();
|
||||
});
|
||||
|
||||
it('should return a non-null appIcon for the captured window', () => {
|
||||
expect(appIcon, 'appIcon property is null or undefined').to.exist();
|
||||
});
|
||||
|
||||
it('should return an appIcon that is not an empty image', () => {
|
||||
expect(appIcon?.isEmpty()).to.be.false();
|
||||
});
|
||||
|
||||
it('should return an appIcon that encodes to a valid PNG data URL', () => {
|
||||
const url = appIcon?.toDataURL();
|
||||
expect(url).to.be.a('string');
|
||||
// This is header 'data:image/png;base64,' length;
|
||||
expect(url?.length).to.be.greaterThan(22);
|
||||
expect(url?.startsWith('data:image/png;base64,')).to.be.true();
|
||||
});
|
||||
|
||||
it('should return an appIcon with dimensions greater than 0x0 pixels', () => {
|
||||
const { width, height } = appIcon?.getSize() || { width: 0, height: 0 };
|
||||
expect(width).to.be.greaterThan(0);
|
||||
expect(height).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
// Tests are sequentially dependent; later tests will fail if an earlier one does.
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { nativeImage } from 'electron/common';
|
||||
import { BaseWindow, BrowserWindow, ImageView } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { inAppPurchase } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { it } from 'vitest';
|
||||
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
|
||||
describe('inAppPurchase module', function () {
|
||||
if (process.platform !== 'darwin') return;
|
||||
|
||||
this.timeout(3 * 60 * 1000);
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('inAppPurchase module', { timeout: 3 * 60 * 1000 }, () => {
|
||||
it('canMakePayments() returns a boolean', () => {
|
||||
const canMakePayments = inAppPurchase.canMakePayments();
|
||||
expect(canMakePayments).to.be.a('boolean');
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ipcMain, BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { defer } from './lib/spec-helpers';
|
||||
import { defer, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('ipc main module', () => {
|
||||
@@ -19,38 +20,44 @@ describe('ipc main module', () => {
|
||||
ipcMain.removeAllListeners('send-sync-message');
|
||||
});
|
||||
|
||||
it('does not crash when reply is not sent and browser is destroyed', (done) => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
ipcMain.once('send-sync-message', (event) => {
|
||||
event.returnValue = null;
|
||||
done();
|
||||
});
|
||||
w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html'));
|
||||
});
|
||||
it(
|
||||
'does not crash when reply is not sent and browser is destroyed',
|
||||
withDone((done) => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
ipcMain.once('send-sync-message', (event) => {
|
||||
event.returnValue = null;
|
||||
done();
|
||||
});
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html')));
|
||||
})
|
||||
);
|
||||
|
||||
it('does not crash when reply is sent by multiple listeners', (done) => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
ipcMain.on('send-sync-message', (event) => {
|
||||
event.returnValue = null;
|
||||
});
|
||||
ipcMain.on('send-sync-message', (event) => {
|
||||
event.returnValue = null;
|
||||
done();
|
||||
});
|
||||
w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html'));
|
||||
});
|
||||
it(
|
||||
'does not crash when reply is sent by multiple listeners',
|
||||
withDone((done) => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
ipcMain.on('send-sync-message', (event) => {
|
||||
event.returnValue = null;
|
||||
});
|
||||
ipcMain.on('send-sync-message', (event) => {
|
||||
event.returnValue = null;
|
||||
done();
|
||||
});
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.join(fixtures, 'api', 'send-sync-message.html')));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('ipcMain.on', () => {
|
||||
@@ -85,7 +92,7 @@ describe('ipc main module', () => {
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const v = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('test-echo', 'hello')
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ipcMain, BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
|
||||
@@ -8,7 +9,7 @@ import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
describe('ipcRenderer module', () => {
|
||||
let w: BrowserWindow;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
@@ -20,7 +21,7 @@ describe('ipcRenderer module', () => {
|
||||
await w.loadURL('about:blank');
|
||||
w.webContents.on('console-message', (_event, ...args) => console.error(...args));
|
||||
});
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await closeWindow(w);
|
||||
w = null as unknown as BrowserWindow;
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import { EventEmitter, once } from 'node:events';
|
||||
import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { defer, listen } from './lib/spec-helpers';
|
||||
import { defer, listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
@@ -16,11 +17,11 @@ describe('ipc module', () => {
|
||||
describe('invoke', () => {
|
||||
let w: BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
await w.loadURL('about:blank');
|
||||
});
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
@@ -147,11 +148,11 @@ describe('ipc module', () => {
|
||||
describe('ordering', () => {
|
||||
let w: BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
await w.loadURL('about:blank');
|
||||
});
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
@@ -246,7 +247,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('can send a port to the main process', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const p = once(ipcMain, 'port');
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
@@ -265,7 +266,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('can sent a message without a transfer', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const p = once(ipcMain, 'port');
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
@@ -280,7 +281,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('throws when the transferable is invalid', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const p = once(ipcMain, 'port');
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
@@ -298,7 +299,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('can communicate between main and renderer', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const p = once(ipcMain, 'port');
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
@@ -321,7 +322,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('can receive a port from a renderer over a MessagePort connection', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
function fn() {
|
||||
const channel1 = new MessageChannel();
|
||||
const channel2 = new MessageChannel();
|
||||
@@ -349,8 +350,8 @@ describe('ipc module', () => {
|
||||
it('can forward a port from one renderer to another renderer', async () => {
|
||||
const w1 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
const w2 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w1.loadURL('about:blank');
|
||||
w2.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w1.loadURL('about:blank'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w2.loadURL('about:blank'));
|
||||
w1.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
const channel = new MessageChannel();
|
||||
@@ -384,7 +385,7 @@ describe('ipc module', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
const { ipcRenderer } = require('electron');
|
||||
@@ -408,7 +409,7 @@ describe('ipc module', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${async function () {
|
||||
const { port2 } = new MessageChannel();
|
||||
@@ -427,7 +428,7 @@ describe('ipc module', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
ipcMain.once('do-a-gc', () => v8Util.requestGarbageCollectionForTesting());
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${async function () {
|
||||
@@ -570,7 +571,7 @@ describe('ipc module', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
const { ipcRenderer } = require('electron');
|
||||
@@ -593,7 +594,7 @@ describe('ipc module', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
const { ipcRenderer } = require('electron');
|
||||
@@ -695,7 +696,7 @@ describe('ipc module', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await w.webContents.executeJavaScript(
|
||||
`(${function () {
|
||||
const { ipcRenderer } = require('electron');
|
||||
@@ -797,7 +798,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('receives ipc messages sent from the WebContents', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.executeJavaScript("require('electron').ipcRenderer.send('test', 42)");
|
||||
const [, num] = await once(w.webContents.ipc, 'test');
|
||||
expect(num).to.equal(42);
|
||||
@@ -805,7 +806,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('receives sync-ipc messages sent from the WebContents', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.ipc.on('test', (event, arg) => {
|
||||
event.returnValue = arg * 2;
|
||||
});
|
||||
@@ -815,7 +816,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.executeJavaScript(
|
||||
"require('electron').ipcRenderer.postMessage('test', null, [(new MessageChannel).port1])"
|
||||
);
|
||||
@@ -825,7 +826,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('handles invoke messages sent from the WebContents', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
|
||||
const result = await w.webContents.executeJavaScript("require('electron').ipcRenderer.invoke('test', 42)");
|
||||
expect(result).to.equal(42 * 2);
|
||||
@@ -833,7 +834,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('cascades to ipcMain', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
let gotFromIpcMain = false;
|
||||
const ipcMainReceived = new Promise<void>((resolve) =>
|
||||
ipcMain.on('test', () => {
|
||||
@@ -856,7 +857,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('overrides ipcMain handlers', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
|
||||
ipcMain.handle('test', () => {
|
||||
throw new Error('should not be called');
|
||||
@@ -868,7 +869,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('falls back to ipcMain handlers', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
ipcMain.handle('test', (_event, arg) => {
|
||||
return arg * 2;
|
||||
});
|
||||
@@ -905,7 +906,7 @@ describe('ipc module', () => {
|
||||
afterEach(closeAllWindows);
|
||||
it('responds to ipc messages in the main frame', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.executeJavaScript("require('electron').ipcRenderer.send('test', 42)");
|
||||
const [, arg] = await once(w.webContents.mainFrame.ipc, 'test');
|
||||
expect(arg).to.equal(42);
|
||||
@@ -913,7 +914,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('responds to sync ipc messages in the main frame', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.mainFrame.ipc.on('test', (event, arg) => {
|
||||
event.returnValue = arg * 2;
|
||||
});
|
||||
@@ -923,7 +924,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.executeJavaScript(
|
||||
"require('electron').ipcRenderer.postMessage('test', null, [(new MessageChannel).port1])"
|
||||
);
|
||||
@@ -933,7 +934,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('handles invoke messages sent from the WebContents', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
|
||||
const result = await w.webContents.executeJavaScript("require('electron').ipcRenderer.invoke('test', 42)");
|
||||
expect(result).to.equal(42 * 2);
|
||||
@@ -941,7 +942,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('cascades to WebContents and ipcMain', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
let gotFromIpcMain = false;
|
||||
let gotFromWebContents = false;
|
||||
const ipcMainReceived = new Promise<void>((resolve) =>
|
||||
@@ -972,7 +973,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('overrides ipcMain handlers', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
|
||||
ipcMain.handle('test', () => {
|
||||
throw new Error('should not be called');
|
||||
@@ -984,7 +985,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('overrides WebContents handlers', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.ipc.handle('test', () => {
|
||||
throw new Error('should not be called');
|
||||
});
|
||||
@@ -999,7 +1000,7 @@ describe('ipc module', () => {
|
||||
|
||||
it('falls back to WebContents handlers', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
w.webContents.ipc.handle('test', (_event, arg) => {
|
||||
return arg * 2;
|
||||
});
|
||||
@@ -1048,7 +1049,7 @@ describe('ipc module', () => {
|
||||
"window.onunload = () => require('electron').ipcRenderer.send('unload'); void 0"
|
||||
);
|
||||
const onUnloadIpc = once(w.webContents.mainFrame.ipc, 'unload');
|
||||
w.loadURL(`http://127.0.0.1:${port}`); // cross-origin navigation
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`http://127.0.0.1:${port}`)); // cross-origin navigation
|
||||
const [{ senderFrame }] = await onUnloadIpc;
|
||||
expect(senderFrame.detached).to.be.true();
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrowserWindow, session, desktopCapturer } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import * as http from 'node:http';
|
||||
|
||||
@@ -13,20 +14,20 @@ describe('setDisplayMediaRequestHandler', () => {
|
||||
// requires a secure context.
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('');
|
||||
});
|
||||
serverUrl = (await listen(server)).url;
|
||||
});
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
ifit(process.platform !== 'darwin')('works when calling getDisplayMedia', async function () {
|
||||
ifit(process.platform !== 'darwin')('works when calling getDisplayMedia', async (ctx) => {
|
||||
if ((await desktopCapturer.getSources({ types: ['screen'] })).length === 0) {
|
||||
return this.skip();
|
||||
return ctx.skip();
|
||||
}
|
||||
const ses = session.fromPartition('' + Math.random());
|
||||
let requestHandlerCalled = false;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BrowserWindow, app, Menu, MenuItem, MenuItemConstructorOptions } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { roleList, execute } from '../lib/browser/api/menu-item-roles';
|
||||
import { ifit, ifdescribe } from './lib/spec-helpers';
|
||||
import { ifit, ifdescribe, withDone } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
function keys<Key extends string, Value>(record: Record<Key, Value>) {
|
||||
@@ -108,23 +109,26 @@ describe('MenuItems', () => {
|
||||
});
|
||||
|
||||
describe('MenuItem.click', () => {
|
||||
it('should be called with the item object passed', (done) => {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'text',
|
||||
click: (item) => {
|
||||
try {
|
||||
expect(item.constructor.name).to.equal('MenuItem');
|
||||
expect(item.label).to.equal('text');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
it(
|
||||
'should be called with the item object passed',
|
||||
withDone((done) => {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'text',
|
||||
click: (item) => {
|
||||
try {
|
||||
expect(item.constructor.name).to.equal('MenuItem');
|
||||
expect(item.label).to.equal('text');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
menu._executeCommand({}, menu.items[0].commandId);
|
||||
});
|
||||
]);
|
||||
menu._executeCommand({}, menu.items[0].commandId);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('MenuItem with checked/radio property', () => {
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrowserWindow, Menu, MenuItem } from 'electron/main';
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -9,7 +10,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { sortMenuItems } from '../lib/browser/api/menu-utils';
|
||||
import { singleModifierCombinations } from './lib/accelerator-helpers';
|
||||
import { ifit } from './lib/spec-helpers';
|
||||
import { ifit, withDone } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
@@ -848,23 +849,29 @@ describe('Menu module', function () {
|
||||
expect(y).to.equal(101);
|
||||
});
|
||||
|
||||
it('works with a given BrowserWindow, options and callback', (done) => {
|
||||
const { x, y } = menu.popup({
|
||||
window: w,
|
||||
x: 100,
|
||||
y: 101,
|
||||
callback: () => done()
|
||||
}) as unknown as { x: number; y: number };
|
||||
it(
|
||||
'works with a given BrowserWindow, options and callback',
|
||||
withDone((done) => {
|
||||
const { x, y } = menu.popup({
|
||||
window: w,
|
||||
x: 100,
|
||||
y: 101,
|
||||
callback: () => done()
|
||||
}) as unknown as { x: number; y: number };
|
||||
|
||||
expect(x).to.equal(100);
|
||||
expect(y).to.equal(101);
|
||||
menu.closePopup();
|
||||
});
|
||||
expect(x).to.equal(100);
|
||||
expect(y).to.equal(101);
|
||||
menu.closePopup();
|
||||
})
|
||||
);
|
||||
|
||||
it('works with a given BrowserWindow, no options, and a callback', (done) => {
|
||||
menu.popup({ window: w, callback: () => done() });
|
||||
menu.closePopup();
|
||||
});
|
||||
it(
|
||||
'works with a given BrowserWindow, no options, and a callback',
|
||||
withDone((done) => {
|
||||
menu.popup({ window: w, callback: () => done() });
|
||||
menu.closePopup();
|
||||
})
|
||||
);
|
||||
|
||||
it('prevents menu from getting garbage-collected when popuping', async () => {
|
||||
const menu = Menu.buildFromTemplate([{ role: 'paste' }]);
|
||||
@@ -893,44 +900,47 @@ describe('Menu module', function () {
|
||||
// https://github.com/electron/electron/issues/35724
|
||||
// Maximizing window is enough to trigger the bug
|
||||
// FIXME(dsanders11): Test always passes on CI, even pre-fix
|
||||
ifit(process.platform === 'linux' && !process.env.CI)('does not trigger issue #35724', (done) => {
|
||||
const showAndCloseMenu = async () => {
|
||||
await setTimeout(1000);
|
||||
menu.popup({ window: w, x: 50, y: 50 });
|
||||
await setTimeout(500);
|
||||
const closed = once(menu, 'menu-will-close');
|
||||
menu.closePopup();
|
||||
await closed;
|
||||
};
|
||||
ifit(process.platform === 'linux' && !process.env.CI)(
|
||||
'does not trigger issue #35724',
|
||||
withDone((done) => {
|
||||
const showAndCloseMenu = async () => {
|
||||
await setTimeout(1000);
|
||||
menu.popup({ window: w, x: 50, y: 50 });
|
||||
await setTimeout(500);
|
||||
const closed = once(menu, 'menu-will-close');
|
||||
menu.closePopup();
|
||||
await closed;
|
||||
};
|
||||
|
||||
const failOnEvent = () => {
|
||||
done(new Error('Menu closed prematurely'));
|
||||
};
|
||||
const failOnEvent = () => {
|
||||
done(new Error('Menu closed prematurely'));
|
||||
};
|
||||
|
||||
assert(!w.isVisible());
|
||||
w.on('show', async () => {
|
||||
assert(!w.isMaximized());
|
||||
// Show the menu once, then maximize window
|
||||
await showAndCloseMenu();
|
||||
// NOTE - 'maximize' event never fires on CI for Linux
|
||||
const maximized = once(w, 'maximize');
|
||||
w.maximize();
|
||||
await maximized;
|
||||
assert(!w.isVisible());
|
||||
w.on('show', async () => {
|
||||
assert(!w.isMaximized());
|
||||
// Show the menu once, then maximize window
|
||||
await showAndCloseMenu();
|
||||
// NOTE - 'maximize' event never fires on CI for Linux
|
||||
const maximized = once(w, 'maximize');
|
||||
w.maximize();
|
||||
await maximized;
|
||||
|
||||
// Bug only seems to trigger programmatically after showing the menu once more
|
||||
await showAndCloseMenu();
|
||||
// Bug only seems to trigger programmatically after showing the menu once more
|
||||
await showAndCloseMenu();
|
||||
|
||||
// Now ensure the menu stays open until we close it
|
||||
await setTimeout(500);
|
||||
menu.once('menu-will-close', failOnEvent);
|
||||
menu.popup({ window: w, x: 50, y: 50 });
|
||||
await setTimeout(1500);
|
||||
menu.off('menu-will-close', failOnEvent);
|
||||
menu.once('menu-will-close', () => done());
|
||||
menu.closePopup();
|
||||
});
|
||||
w.show();
|
||||
});
|
||||
// Now ensure the menu stays open until we close it
|
||||
await setTimeout(500);
|
||||
menu.once('menu-will-close', failOnEvent);
|
||||
menu.popup({ window: w, x: 50, y: 50 });
|
||||
await setTimeout(1500);
|
||||
menu.off('menu-will-close', failOnEvent);
|
||||
menu.once('menu-will-close', () => done());
|
||||
menu.closePopup();
|
||||
});
|
||||
w.show();
|
||||
})
|
||||
);
|
||||
|
||||
const chunkSize = 10;
|
||||
let chunkCount = 0;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { nativeImage } from 'electron/common';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { expect } from './lib/remote-tools';
|
||||
import { ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
|
||||
import { expectDeprecationMessages } from './lib/warning-helpers';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { nativeTheme, BrowserWindow, ipcMain } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { net, protocol } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
import * as url from 'node:url';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { session, net } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as ChildProcess from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -10,7 +11,7 @@ import { Socket } from 'node:net';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ifit, listen } from './lib/spec-helpers';
|
||||
import { ifit, listen, withDone } from './lib/spec-helpers';
|
||||
|
||||
const appPath = path.join(__dirname, 'fixtures', 'api', 'net-log');
|
||||
const dumpFile = path.join(os.tmpdir(), 'net_log.json');
|
||||
@@ -23,7 +24,7 @@ describe('netLog module', () => {
|
||||
let serverUrl: string;
|
||||
const connections: Set<Socket> = new Set();
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer();
|
||||
server.on('connection', (connection) => {
|
||||
connections.add(connection);
|
||||
@@ -37,15 +38,17 @@ describe('netLog module', () => {
|
||||
serverUrl = (await listen(server)).url;
|
||||
});
|
||||
|
||||
after((done) => {
|
||||
for (const connection of connections) {
|
||||
connection.destroy();
|
||||
}
|
||||
server.close(() => {
|
||||
server = null as any;
|
||||
done();
|
||||
});
|
||||
});
|
||||
afterAll(
|
||||
withDone((done) => {
|
||||
for (const connection of connections) {
|
||||
connection.destroy();
|
||||
}
|
||||
server.close(() => {
|
||||
server = null as any;
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
expect(testNetLog().currentlyLogging).to.be.false('currently logging');
|
||||
@@ -1,6 +1,7 @@
|
||||
import { net, session, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as dns from 'node:dns';
|
||||
|
||||
@@ -14,14 +15,12 @@ describe('net module (session)', () => {
|
||||
beforeEach(() => {
|
||||
respondNTimes.routeFailure = false;
|
||||
});
|
||||
afterEach(async function () {
|
||||
afterEach(async (ctx) => {
|
||||
await session.defaultSession.clearCache();
|
||||
if (respondNTimes.routeFailure && this.test) {
|
||||
if (!this.test.isFailed()) {
|
||||
throw new Error(
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
);
|
||||
}
|
||||
if (respondNTimes.routeFailure && ctx.task.result?.state !== 'fail') {
|
||||
throw new Error(
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,50 +1,75 @@
|
||||
import { net, session, ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main';
|
||||
import { ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it, type TestFunction } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import * as http2 from 'node:http2';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import {
|
||||
collectStreamBody,
|
||||
collectStreamBodyBuffer,
|
||||
defer,
|
||||
expect,
|
||||
getResponse,
|
||||
http,
|
||||
kOneKiloByte,
|
||||
kOneMegaByte,
|
||||
net,
|
||||
once,
|
||||
randomBuffer,
|
||||
randomString,
|
||||
respondNTimes,
|
||||
respondOnce
|
||||
} from './lib/net-helpers';
|
||||
import { listen, defer, ifdescribe, isTestingBindingAvailable } from './lib/spec-helpers';
|
||||
respondOnce,
|
||||
rewriteForRemoteEval,
|
||||
session,
|
||||
setTimeout
|
||||
} from './lib/remote-tools';
|
||||
import { listen, ifdescribe, isTestingBindingAvailable } from './lib/spec-helpers';
|
||||
|
||||
const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
let sharedUtilityChild: Electron.UtilityProcess | null = null;
|
||||
|
||||
async function getUtilityChild() {
|
||||
if (sharedUtilityChild) return sharedUtilityChild;
|
||||
const child = utilityProcess.fork(utilityFixturePath, [], { execArgv: ['--expose-gc'] });
|
||||
const [ready] = await once(child, 'message');
|
||||
expect(ready?.type).to.equal('ready');
|
||||
child.once('exit', (code) => {
|
||||
if (sharedUtilityChild === child && code !== 0) {
|
||||
console.error(`api-net utility process exited unexpectedly with code ${code}`);
|
||||
}
|
||||
if (sharedUtilityChild === child) sharedUtilityChild = null;
|
||||
});
|
||||
sharedUtilityChild = child;
|
||||
return child;
|
||||
}
|
||||
|
||||
async function closeUtilityChild() {
|
||||
const child = sharedUtilityChild;
|
||||
if (!child) return;
|
||||
sharedUtilityChild = null;
|
||||
const exited = once(child, 'exit');
|
||||
child.postMessage({ type: 'shutdown' });
|
||||
await exited;
|
||||
}
|
||||
|
||||
/** @remote */
|
||||
async function itUtility(name: string, fn?: Function, args?: { [key: string]: any }) {
|
||||
it(`${name} in utility process`, async () => {
|
||||
const child = utilityProcess.fork(utilityFixturePath, [], {
|
||||
execArgv: ['--expose-gc']
|
||||
});
|
||||
if (fn) {
|
||||
child.postMessage({ fn: `(${fn})()`, args });
|
||||
} else {
|
||||
child.postMessage({ fn: '(() => {})()', args });
|
||||
}
|
||||
const [data] = await once(child, 'message');
|
||||
const child = await getUtilityChild();
|
||||
const body = fn ? `(${rewriteForRemoteEval(fn)})()` : '(() => {})()';
|
||||
const result = once(child, 'message');
|
||||
child.postMessage({ fn: body, args });
|
||||
const [data] = await result;
|
||||
expect(data.ok).to.be.true(data.message);
|
||||
// Cleanup.
|
||||
const [code] = await once(child, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function itIgnoringArgs(name: string, fn?: Mocha.Func | Mocha.AsyncFunc, args?: { [key: string]: any }) {
|
||||
async function itIgnoringArgs(name: string, fn?: TestFunction, args?: { [key: string]: any }) {
|
||||
it(name, fn);
|
||||
}
|
||||
|
||||
@@ -52,13 +77,11 @@ describe('net module', () => {
|
||||
beforeEach(() => {
|
||||
respondNTimes.routeFailure = false;
|
||||
});
|
||||
afterEach(async function () {
|
||||
if (respondNTimes.routeFailure && this.test) {
|
||||
if (!this.test.isFailed()) {
|
||||
throw new Error(
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
);
|
||||
}
|
||||
afterEach(async (ctx) => {
|
||||
if (respondNTimes.routeFailure && ctx.task.result?.state !== 'fail') {
|
||||
throw new Error(
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,14 +110,16 @@ describe('net module', () => {
|
||||
}
|
||||
);
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
http2URL = (await listen(h2server)).url + '/';
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(async () => {
|
||||
h2server.close();
|
||||
await closeUtilityChild();
|
||||
});
|
||||
|
||||
/** @remote — `test` may be itUtility, which stringifies its closure */
|
||||
for (const test of [itIgnoringArgs, itUtility]) {
|
||||
describe('HTTP basics', () => {
|
||||
test('should be able to issue a basic GET request', async () => {
|
||||
@@ -2,7 +2,7 @@
|
||||
// with the session bus. This requires python-dbusmock to be installed and
|
||||
// running at $DBUS_SESSION_BUS_ADDRESS.
|
||||
//
|
||||
// script/spec-runner.js spawns dbusmock, which sets DBUS_SESSION_BUS_ADDRESS.
|
||||
// spec/_vitest_runner/run.js spawns dbusmock, which sets DBUS_SESSION_BUS_ADDRESS.
|
||||
//
|
||||
// See https://pypi.python.org/pypi/python-dbusmock to read about dbusmock.
|
||||
|
||||
@@ -11,12 +11,13 @@ import { app } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as dbus from 'dbus-native';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
import { ifdescribe, withDone } from './lib/spec-helpers';
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
|
||||
@@ -33,7 +34,7 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
const appName = 'api-notification-dbus-spec';
|
||||
const serviceName = 'org.freedesktop.Notifications';
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
// init app
|
||||
app.name = appName;
|
||||
app.setDesktopName(`${appName}.desktop`);
|
||||
@@ -63,7 +64,7 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
// cleanup dbus
|
||||
if (reset) await reset();
|
||||
// cleanup app
|
||||
@@ -106,21 +107,23 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
};
|
||||
}
|
||||
|
||||
before((done) => {
|
||||
mock.on('MethodCalled', onMethodCalled(done));
|
||||
// lazy load Notification after we listen to MethodCalled mock signal
|
||||
Notification = require('electron').Notification;
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
body: 'body',
|
||||
icon: nativeImage.createFromPath(path.join(fixturesPath, 'assets', 'notification_icon.png')),
|
||||
replyPlaceholder: 'replyPlaceholder',
|
||||
sound: 'sound',
|
||||
closeButtonText: 'closeButtonText'
|
||||
});
|
||||
n.show();
|
||||
});
|
||||
beforeAll(
|
||||
withDone((done) => {
|
||||
mock.on('MethodCalled', onMethodCalled(done));
|
||||
// lazy load Notification after we listen to MethodCalled mock signal
|
||||
Notification = require('electron').Notification;
|
||||
const n = new Notification({
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
body: 'body',
|
||||
icon: nativeImage.createFromPath(path.join(fixturesPath, 'assets', 'notification_icon.png')),
|
||||
replyPlaceholder: 'replyPlaceholder',
|
||||
sound: 'sound',
|
||||
closeButtonText: 'closeButtonText'
|
||||
});
|
||||
n.show();
|
||||
})
|
||||
);
|
||||
|
||||
it(`should call ${serviceName} to show notifications`, async () => {
|
||||
const calls = await getCalls();
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Notification } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
|
||||
@@ -282,63 +283,6 @@ describe('Notification module', () => {
|
||||
expect(n.toastXml).to.equal('<xml/>');
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('emits show and close events', async () => {
|
||||
const n = new Notification({
|
||||
title: 'test notification',
|
||||
body: 'test body',
|
||||
silent: true
|
||||
});
|
||||
{
|
||||
const e = once(n, 'show');
|
||||
n.show();
|
||||
await e;
|
||||
}
|
||||
{
|
||||
const e = once(n, 'close');
|
||||
n.close();
|
||||
await e;
|
||||
}
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('emits show and close events with custom id', async () => {
|
||||
const n = new Notification({
|
||||
id: 'test-custom-id',
|
||||
title: 'test notification',
|
||||
body: 'test body',
|
||||
silent: true
|
||||
});
|
||||
{
|
||||
const e = once(n, 'show');
|
||||
n.show();
|
||||
await e;
|
||||
}
|
||||
{
|
||||
const e = once(n, 'close');
|
||||
n.close();
|
||||
await e;
|
||||
}
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('emits show and close events with custom id and groupId', async () => {
|
||||
const n = new Notification({
|
||||
id: 'E017VKL2N8H|C07RBMNS9EK|1772656675.039',
|
||||
groupId: 'E017VKL2N8H|C07RBMNS9EK',
|
||||
title: 'test notification',
|
||||
body: 'test body',
|
||||
silent: true
|
||||
});
|
||||
{
|
||||
const e = once(n, 'show');
|
||||
n.show();
|
||||
await e;
|
||||
}
|
||||
{
|
||||
const e = once(n, 'close');
|
||||
n.close();
|
||||
await e;
|
||||
}
|
||||
});
|
||||
|
||||
ifit(process.platform === 'win32')('can show notification with custom id and groupId', () => {
|
||||
const n = new Notification({
|
||||
id: 'test-custom-id',
|
||||
@@ -1,19 +1,20 @@
|
||||
// For these tests we use a fake DBus daemon to verify powerMonitor module
|
||||
// interaction with the system bus. This requires python-dbusmock installed and
|
||||
// running (with the DBUS_SYSTEM_BUS_ADDRESS environment variable set).
|
||||
// script/spec-runner.js will take care of spawning the fake DBus daemon and setting
|
||||
// spec/_vitest_runner/run.js will take care of spawning the fake DBus daemon and setting
|
||||
// DBUS_SYSTEM_BUS_ADDRESS when python-dbusmock is installed.
|
||||
//
|
||||
// See https://pypi.python.org/pypi/python-dbusmock for more information about
|
||||
// python-dbusmock.
|
||||
import { expect } from 'chai';
|
||||
import * as dbus from 'dbus-native';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { ifdescribe, startRemoteControlApp } from './lib/spec-helpers';
|
||||
import { ifdescribe, startRemoteControlApp, withDone } from './lib/spec-helpers';
|
||||
|
||||
describe('powerMonitor', () => {
|
||||
let logindMock: any, dbusMockPowerMonitor: any, getCalls: any, emitSignal: any, reset: any;
|
||||
@@ -21,7 +22,7 @@ describe('powerMonitor', () => {
|
||||
ifdescribe(process.platform === 'linux' && process.env.DBUS_SYSTEM_BUS_ADDRESS != null)(
|
||||
'when powerMonitor module is loaded with dbus mock',
|
||||
() => {
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
const systemBus = dbus.systemBus();
|
||||
const loginService = systemBus.getService('org.freedesktop.login1');
|
||||
const getInterface = promisify(loginService.getInterface.bind(loginService));
|
||||
@@ -31,7 +32,7 @@ describe('powerMonitor', () => {
|
||||
reset = promisify(logindMock.Reset.bind(logindMock));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await reset();
|
||||
});
|
||||
|
||||
@@ -43,11 +44,13 @@ describe('powerMonitor', () => {
|
||||
return cb;
|
||||
}
|
||||
|
||||
before((done) => {
|
||||
logindMock.on('MethodCalled', onceMethodCalled(done));
|
||||
// lazy load powerMonitor after we listen to MethodCalled mock signal
|
||||
dbusMockPowerMonitor = require('electron').powerMonitor;
|
||||
});
|
||||
beforeAll(
|
||||
withDone((done) => {
|
||||
logindMock.on('MethodCalled', onceMethodCalled(done));
|
||||
// lazy load powerMonitor after we listen to MethodCalled mock signal
|
||||
dbusMockPowerMonitor = require('electron').powerMonitor;
|
||||
})
|
||||
);
|
||||
|
||||
it('should call Inhibit to delay suspend once a listener is added', async () => {
|
||||
// No calls to dbus until a listener is added
|
||||
@@ -113,7 +116,7 @@ describe('powerMonitor', () => {
|
||||
});
|
||||
|
||||
describe('when a listener is added to shutdown event', () => {
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
const calls = await getCalls();
|
||||
expect(calls).to.be.an('array').that.has.lengthOf(2);
|
||||
dbusMockPowerMonitor.once('shutdown', () => {});
|
||||
@@ -134,12 +137,15 @@ describe('powerMonitor', () => {
|
||||
});
|
||||
|
||||
describe('when PrepareForShutdown(true) signal is sent by logind', () => {
|
||||
it('should emit "shutdown" event', (done) => {
|
||||
dbusMockPowerMonitor.once('shutdown', () => {
|
||||
done();
|
||||
});
|
||||
emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown', 'b', [['b', true]]);
|
||||
});
|
||||
it(
|
||||
'should emit "shutdown" event',
|
||||
withDone((done) => {
|
||||
dbusMockPowerMonitor.once('shutdown', () => {
|
||||
done();
|
||||
});
|
||||
emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown', 'b', [['b', true]]);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -154,7 +160,7 @@ describe('powerMonitor', () => {
|
||||
|
||||
describe('when powerMonitor module is loaded', () => {
|
||||
let powerMonitor: typeof Electron.powerMonitor;
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
powerMonitor = require('electron').powerMonitor;
|
||||
});
|
||||
describe('powerMonitor.getSystemIdleState', () => {
|
||||
@@ -1,6 +1,7 @@
|
||||
import { powerSaveBlocker } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
describe('powerSaveBlocker module', () => {
|
||||
it('can be started and stopped', () => {
|
||||
@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron';
|
||||
import { app } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -108,11 +109,11 @@ describe('process module', () => {
|
||||
|
||||
describe('renderer process', () => {
|
||||
let w: BrowserWindow;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
await w.loadURL('about:blank');
|
||||
});
|
||||
after(closeAllWindows);
|
||||
afterAll(closeAllWindows);
|
||||
|
||||
generateSpecs((fn, ...args) => {
|
||||
const jsonArgs = args.map((value) => JSON.stringify(value)).join(',');
|
||||
@@ -2,6 +2,7 @@ import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain, ne
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as ChildProcess from 'node:child_process';
|
||||
import { EventEmitter, once } from 'node:events';
|
||||
@@ -16,7 +17,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import * as url from 'node:url';
|
||||
|
||||
import { collectStreamBody, getResponse } from './lib/net-helpers';
|
||||
import { listen, defer, ifit } from './lib/spec-helpers';
|
||||
import { listen, defer, ifit, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { WebmGenerator } from './lib/video-helpers';
|
||||
import { closeAllWindows, closeWindow } from './lib/window-helpers';
|
||||
|
||||
@@ -83,10 +84,10 @@ function deferPromise(): Promise<any> & { resolve: Function; reject: Function }
|
||||
describe('protocol module', () => {
|
||||
let contents: WebContents;
|
||||
// NB. sandbox: true is used because it makes navigations much (~8x) faster.
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
|
||||
});
|
||||
after(() => contents.destroy());
|
||||
afterAll(() => contents.destroy());
|
||||
|
||||
async function ajax(url: string, options = {}) {
|
||||
// Note that we need to do navigation every time after a protocol is
|
||||
@@ -289,7 +290,9 @@ describe('protocol module', () => {
|
||||
});
|
||||
|
||||
const loaded = once(ipcMain, 'loaded-iframe-custom-protocol');
|
||||
w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'iframe-protocol.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'iframe-protocol.html'))
|
||||
);
|
||||
await loaded;
|
||||
});
|
||||
|
||||
@@ -356,7 +359,7 @@ describe('protocol module', () => {
|
||||
res.end(text);
|
||||
}
|
||||
});
|
||||
after(() => server.close());
|
||||
defer(() => server.close());
|
||||
const { port } = await listen(server);
|
||||
const url = `${protocolName}://fake-host`;
|
||||
const redirectURL = `http://127.0.0.1:${port}/serverRedirect`;
|
||||
@@ -366,17 +369,20 @@ describe('protocol module', () => {
|
||||
expect(r.data).to.equal(text);
|
||||
});
|
||||
|
||||
it('can access request headers', (done) => {
|
||||
protocol.registerHttpProtocol(protocolName, (request) => {
|
||||
try {
|
||||
expect(request).to.have.property('headers');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
ajax(protocolName + '://fake-host').catch(() => {});
|
||||
});
|
||||
it(
|
||||
'can access request headers',
|
||||
withDone((done) => {
|
||||
protocol.registerHttpProtocol(protocolName, (request) => {
|
||||
try {
|
||||
expect(request).to.have.property('headers');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
ajax(protocolName + '://fake-host').catch(() => {});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -644,7 +650,7 @@ describe('protocol module', () => {
|
||||
// FIXME(zcbenz): This test was passing because the test itself was wrong,
|
||||
// I don't know whether it ever passed before and we should take a look at
|
||||
// it in future.
|
||||
xit('can send POST request', async () => {
|
||||
it.skip('can send POST request', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
@@ -655,7 +661,7 @@ describe('protocol module', () => {
|
||||
});
|
||||
server.close();
|
||||
});
|
||||
after(() => server.close());
|
||||
defer(() => server.close());
|
||||
const { url } = await listen(server);
|
||||
interceptHttpProtocol('http', (request, callback) => {
|
||||
const data: Electron.ProtocolResponse = {
|
||||
@@ -679,7 +685,7 @@ describe('protocol module', () => {
|
||||
expect(details.url).to.equal('http://fake-host/');
|
||||
callback({ cancel: true });
|
||||
});
|
||||
after(() => customSession.webRequest.onBeforeRequest(null));
|
||||
defer(() => customSession.webRequest.onBeforeRequest(null));
|
||||
|
||||
interceptHttpProtocol('http', (request, callback) => {
|
||||
callback({
|
||||
@@ -690,17 +696,20 @@ describe('protocol module', () => {
|
||||
await expect(ajax('http://fake-host')).to.be.eventually.rejectedWith(Error);
|
||||
});
|
||||
|
||||
it('can access request headers', (done) => {
|
||||
protocol.interceptHttpProtocol('http', (request) => {
|
||||
try {
|
||||
expect(request).to.have.property('headers');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
ajax('http://fake-host').catch(() => {});
|
||||
});
|
||||
it(
|
||||
'can access request headers',
|
||||
withDone((done) => {
|
||||
protocol.interceptHttpProtocol('http', (request) => {
|
||||
try {
|
||||
expect(request).to.have.property('headers');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
ajax('http://fake-host').catch(() => {});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('protocol.interceptStreamProtocol', () => {
|
||||
@@ -812,7 +821,7 @@ describe('protocol module', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
after(() => protocol.unregisterProtocol(serviceWorkerScheme));
|
||||
afterAll(() => protocol.unregisterProtocol(serviceWorkerScheme));
|
||||
|
||||
it('should fail when registering invalid service worker', async () => {
|
||||
await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
|
||||
@@ -889,21 +898,27 @@ describe('protocol module', () => {
|
||||
await requestReceived;
|
||||
});
|
||||
|
||||
it('can access files through the FileSystem API', (done) => {
|
||||
const filePath = path.join(fixturesPath, 'pages', 'filesystem.html');
|
||||
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
|
||||
w.loadURL(origin);
|
||||
ipcMain.once('file-system-error', (event, err) => done(err));
|
||||
ipcMain.once('file-system-write-end', () => done());
|
||||
});
|
||||
it(
|
||||
'can access files through the FileSystem API',
|
||||
withDone((done) => {
|
||||
const filePath = path.join(fixturesPath, 'pages', 'filesystem.html');
|
||||
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(origin));
|
||||
ipcMain.once('file-system-error', (event, err) => done(err));
|
||||
ipcMain.once('file-system-write-end', () => done());
|
||||
})
|
||||
);
|
||||
|
||||
it('registers secure, when {secure: true}', (done) => {
|
||||
const filePath = path.join(fixturesPath, 'pages', 'cache-storage.html');
|
||||
ipcMain.once('success', () => done());
|
||||
ipcMain.once('failure', (event, err) => done(err));
|
||||
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
|
||||
w.loadURL(origin);
|
||||
});
|
||||
it(
|
||||
'registers secure, when {secure: true}',
|
||||
withDone((done) => {
|
||||
const filePath = path.join(fixturesPath, 'pages', 'cache-storage.html');
|
||||
ipcMain.once('success', () => done());
|
||||
ipcMain.once('failure', (event, err) => done(err));
|
||||
protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(origin));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('protocol.registerSchemesAsPrivileged cors-fetch', function () {
|
||||
@@ -996,7 +1011,7 @@ describe('protocol module', () => {
|
||||
const consoleMessages: string[] = [];
|
||||
newContents.on('console-message', (e) => consoleMessages.push(e.message));
|
||||
try {
|
||||
newContents.loadURL(standardScheme + '://fake-host');
|
||||
dangerouslyIgnoreWebContentsLoadResult(newContents.loadURL(standardScheme + '://fake-host'));
|
||||
const [, response] = await once(ipcMain, 'response');
|
||||
expect(response).to.deep.equal(expected);
|
||||
expect(consoleMessages.join('\n')).to.match(expectedConsole);
|
||||
@@ -1016,7 +1031,7 @@ describe('protocol module', () => {
|
||||
const videoPath = path.join(fixturesPath, 'video.webm');
|
||||
let w: BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
// generate test video
|
||||
const imageBase64 = await fs.promises.readFile(videoSourceImagePath, 'base64');
|
||||
const imageDataUrl = `data:image/webp;base64,${imageBase64}`;
|
||||
@@ -1031,11 +1046,11 @@ describe('protocol module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await fs.promises.unlink(videoPath);
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
beforeEach(async (ctx) => {
|
||||
w = new BrowserWindow({ show: false });
|
||||
await w.loadURL('about:blank');
|
||||
if (
|
||||
@@ -1043,7 +1058,7 @@ describe('protocol module', () => {
|
||||
"document.createElement('video').canPlayType('video/webm; codecs=\"vp8.0\"')"
|
||||
))
|
||||
) {
|
||||
this.skip();
|
||||
ctx.skip();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1103,7 +1118,7 @@ describe('protocol module', () => {
|
||||
});
|
||||
|
||||
try {
|
||||
newContents.loadURL(testingScheme + '://fake-host');
|
||||
dangerouslyIgnoreWebContentsLoadResult(newContents.loadURL(testingScheme + '://fake-host'));
|
||||
const [, response] = await once(ipcMain, 'result');
|
||||
expect(response).to.deep.equal(expected);
|
||||
} finally {
|
||||
@@ -1183,28 +1198,31 @@ describe('protocol module', () => {
|
||||
expect(body).to.equal('hello https://foo/');
|
||||
});
|
||||
|
||||
it('receives requests to the existing file scheme', (done) => {
|
||||
const filePath = path.join(__dirname, 'fixtures', 'pages', 'a.html');
|
||||
it(
|
||||
'receives requests to the existing file scheme',
|
||||
withDone((done) => {
|
||||
const filePath = path.join(__dirname, 'fixtures', 'pages', 'a.html');
|
||||
|
||||
protocol.handle('file', (req) => {
|
||||
let file;
|
||||
if (process.platform === 'win32') {
|
||||
file = `file:///${filePath.replaceAll('\\', '/')}`;
|
||||
} else {
|
||||
file = `file://${filePath}`;
|
||||
}
|
||||
protocol.handle('file', (req) => {
|
||||
let file;
|
||||
if (process.platform === 'win32') {
|
||||
file = `file:///${filePath.replaceAll('\\', '/')}`;
|
||||
} else {
|
||||
file = `file://${filePath}`;
|
||||
}
|
||||
|
||||
if (req.url === file) done();
|
||||
return new Response(req.url);
|
||||
});
|
||||
if (req.url === file) done();
|
||||
return new Response(req.url);
|
||||
});
|
||||
|
||||
defer(() => {
|
||||
protocol.unhandle('file');
|
||||
});
|
||||
defer(() => {
|
||||
protocol.unhandle('file');
|
||||
});
|
||||
|
||||
const w = new BrowserWindow();
|
||||
w.loadFile(filePath);
|
||||
});
|
||||
const w = new BrowserWindow();
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(filePath));
|
||||
})
|
||||
);
|
||||
|
||||
it('receives requests to an existing scheme when navigating', async () => {
|
||||
protocol.handle('https', (req) => new Response('hello ' + req.url));
|
||||
@@ -1517,7 +1535,7 @@ describe('protocol module', () => {
|
||||
protocol.unhandle('http-like');
|
||||
});
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const expectedHashChunks = await w.webContents.executeJavaScript(`
|
||||
const dataStream = () =>
|
||||
new ReadableStream({
|
||||
@@ -1,8 +1,7 @@
|
||||
import { safeStorage } from 'electron/main';
|
||||
|
||||
import * as chai from 'chai';
|
||||
import { expect } from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -11,16 +10,14 @@ import * as path from 'node:path';
|
||||
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
describe('safeStorage module', () => {
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
if (process.platform === 'linux') {
|
||||
safeStorage.setUsePlainTextEncryption(true);
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
const pathToEncryptedString = path.resolve(__dirname, 'fixtures', 'api', 'safe-storage', 'encrypted.txt');
|
||||
if (fs.existsSync(pathToEncryptedString)) {
|
||||
await fs.promises.rm(pathToEncryptedString, { force: true, recursive: true });
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Display, screen, desktopCapturer } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
describe('screen module', () => {
|
||||
describe('methods reassignment', () => {
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ipcMain, session, webContents as webContentsModule, WebContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once, on } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { listen, waitUntil } from './lib/spec-helpers';
|
||||
import { listen, waitUntil, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
|
||||
// Toggle to add extra debug output
|
||||
const DEBUG = !process.env.CI;
|
||||
@@ -75,7 +76,9 @@ describe('ServiceWorkerMain module', () => {
|
||||
|
||||
async function loadWorkerScript(scriptUrl?: string) {
|
||||
const scriptParams = scriptUrl ? `?scriptUrl=${scriptUrl}` : '';
|
||||
return wc.loadURL(`${baseUrl}/index.html${scriptParams}`);
|
||||
// Call sites never await this (they await waitForServiceWorker instead),
|
||||
// so a load aborted by teardown would otherwise reject unhandled.
|
||||
return dangerouslyIgnoreWebContentsLoadResult(wc.loadURL(`${baseUrl}/index.html${scriptParams}`));
|
||||
}
|
||||
|
||||
async function unregisterAllServiceWorkers() {
|
||||
@@ -143,7 +146,7 @@ describe('ServiceWorkerMain module', () => {
|
||||
});
|
||||
|
||||
it('does not crash on script error', async () => {
|
||||
wc.loadURL(`${baseUrl}/index.html?scriptUrl=sw-script-error.js`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL(`${baseUrl}/index.html?scriptUrl=sw-script-error.js`));
|
||||
let serviceWorker;
|
||||
const actualStatuses = [];
|
||||
for await (const [{ versionId, runningStatus }] of on(serviceWorkers, 'running-status-changed')) {
|
||||
@@ -159,12 +162,12 @@ describe('ServiceWorkerMain module', () => {
|
||||
expect(serviceWorker).to.not.be.undefined();
|
||||
});
|
||||
|
||||
it('does not find unregistered service worker', async () => {
|
||||
it('does not find unregistered service worker', async (ctx) => {
|
||||
loadWorkerScript();
|
||||
const runningServiceWorker = await waitForServiceWorker('running');
|
||||
const { versionId } = runningServiceWorker;
|
||||
unregisterAllServiceWorkers();
|
||||
await waitUntil(() => runningServiceWorker.isDestroyed());
|
||||
await waitUntil(() => runningServiceWorker.isDestroyed(), ctx.signal);
|
||||
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
|
||||
expect(serviceWorker).to.be.undefined();
|
||||
});
|
||||
@@ -177,20 +180,20 @@ describe('ServiceWorkerMain module', () => {
|
||||
expect(serviceWorker.isDestroyed()).to.be.false();
|
||||
});
|
||||
|
||||
it('is destroyed after being unregistered', async () => {
|
||||
it('is destroyed after being unregistered', async (ctx) => {
|
||||
loadWorkerScript();
|
||||
const serviceWorker = await waitForServiceWorker();
|
||||
expect(serviceWorker.isDestroyed()).to.be.false();
|
||||
await unregisterAllServiceWorkers();
|
||||
await waitUntil(() => serviceWorker.isDestroyed());
|
||||
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"running-status-changed" event', () => {
|
||||
it('handles when content::ServiceWorkerVersion has been destroyed', async () => {
|
||||
it('handles when content::ServiceWorkerVersion has been destroyed', async (ctx) => {
|
||||
loadWorkerScript('sw-unregister-self.js');
|
||||
const serviceWorker = await waitForServiceWorker('running');
|
||||
await waitUntil(() => serviceWorker.isDestroyed());
|
||||
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -279,20 +282,20 @@ describe('ServiceWorkerMain module', () => {
|
||||
expect(serviceWorker._countExternalRequests()).to.equal(0);
|
||||
});
|
||||
|
||||
it('throws when starting task after destroyed', async () => {
|
||||
it('throws when starting task after destroyed', async (ctx) => {
|
||||
loadWorkerScript();
|
||||
const serviceWorker = await waitForServiceWorker();
|
||||
await unregisterAllServiceWorkers();
|
||||
await waitUntil(() => serviceWorker.isDestroyed());
|
||||
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
|
||||
expect(() => serviceWorker.startTask()).to.throw();
|
||||
});
|
||||
|
||||
it('throws when ending task after destroyed', async () => {
|
||||
it('throws when ending task after destroyed', async (ctx) => {
|
||||
loadWorkerScript();
|
||||
const serviceWorker = await waitForServiceWorker();
|
||||
const task = serviceWorker.startTask();
|
||||
await unregisterAllServiceWorkers();
|
||||
await waitUntil(() => serviceWorker.isDestroyed());
|
||||
await waitUntil(() => serviceWorker.isDestroyed(), ctx.signal);
|
||||
expect(() => task.end()).to.throw();
|
||||
});
|
||||
});
|
||||
@@ -300,7 +303,7 @@ describe('ServiceWorkerMain module', () => {
|
||||
describe("'versionId' property", () => {
|
||||
it('matches the expected value', async () => {
|
||||
const runningStatusChanged = once(serviceWorkers, 'running-status-changed');
|
||||
wc.loadURL(`${baseUrl}/index.html`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL(`${baseUrl}/index.html`));
|
||||
const [{ versionId }] = await runningStatusChanged;
|
||||
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
|
||||
expect(serviceWorker).to.not.be.undefined();
|
||||
@@ -360,9 +363,11 @@ describe('ServiceWorkerMain module', () => {
|
||||
const abortController = new AbortController();
|
||||
try {
|
||||
let pingReceived = false;
|
||||
once(ipcMain, 'ping', { signal: abortController.signal }).then(() => {
|
||||
pingReceived = true;
|
||||
});
|
||||
once(ipcMain, 'ping', { signal: abortController.signal })
|
||||
.then(() => {
|
||||
pingReceived = true;
|
||||
})
|
||||
.catch(() => {});
|
||||
runTest(serviceWorker, { name: 'testSend', args: ['ping'] });
|
||||
await once(ses, '-ipc-message');
|
||||
await new Promise<void>(queueMicrotask);
|
||||
@@ -2,13 +2,14 @@ import { session, webContents, WebContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { on, once } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { listen } from './lib/spec-helpers';
|
||||
import { listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
|
||||
const partition = 'service-workers-spec';
|
||||
|
||||
@@ -18,7 +19,7 @@ describe('session.serviceWorkers', () => {
|
||||
let baseUrl: string;
|
||||
let w: WebContents;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
ses = session.fromPartition(partition);
|
||||
await ses.clearStorageData();
|
||||
});
|
||||
@@ -54,7 +55,7 @@ describe('session.serviceWorkers', () => {
|
||||
});
|
||||
|
||||
it('should report one as running once you load a page with a service worker', async () => {
|
||||
w.loadURL(`${baseUrl}/index.html`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${baseUrl}/index.html`));
|
||||
await once(ses.serviceWorkers, 'console-message');
|
||||
const workers = ses.serviceWorkers.getAllRunning();
|
||||
const ids = Object.keys(workers) as any[] as number[];
|
||||
@@ -64,7 +65,7 @@ describe('session.serviceWorkers', () => {
|
||||
|
||||
describe('getFromVersionID()', () => {
|
||||
it('should report the correct script url and scope', async () => {
|
||||
w.loadURL(`${baseUrl}/index.html`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${baseUrl}/index.html`));
|
||||
const eventInfo = await once(ses.serviceWorkers, 'console-message');
|
||||
const details: Electron.MessageDetails = eventInfo[1];
|
||||
const worker = ses.serviceWorkers.getFromVersionID(details.versionId);
|
||||
@@ -77,7 +78,7 @@ describe('session.serviceWorkers', () => {
|
||||
describe('console-message event', () => {
|
||||
it('should correctly keep the source, message and level', async () => {
|
||||
const messages: Record<string, Electron.MessageDetails> = {};
|
||||
w.loadURL(`${baseUrl}/index.html?scriptUrl=sw-logs.js`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${baseUrl}/index.html?scriptUrl=sw-logs.js`));
|
||||
for await (const [, details] of on(ses.serviceWorkers, 'console-message')) {
|
||||
messages[details.message] = details;
|
||||
expect(details).to.have.property('source', 'console-api');
|
||||
@@ -3,6 +3,7 @@ import { app, session, BrowserWindow, net, ipcMain, Session, webFrameMain, WebFr
|
||||
import * as auth from 'basic-auth';
|
||||
import { expect } from 'chai';
|
||||
import * as send from 'send';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as ChildProcess from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -12,7 +13,7 @@ import * as https from 'node:https';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { defer, ifit, listen, waitUntil } from './lib/spec-helpers';
|
||||
import { defer, ifit, listen, waitUntil, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('session module', () => {
|
||||
@@ -324,8 +325,7 @@ describe('session module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should survive an app restart for persistent partition', async function () {
|
||||
this.timeout(60000);
|
||||
it('should survive an app restart for persistent partition', { timeout: 60000 }, async () => {
|
||||
const appPath = path.join(fixtures, 'api', 'cookie-app');
|
||||
|
||||
const runAppWithPhase = (phase: string) => {
|
||||
@@ -686,7 +686,7 @@ describe('session module', () => {
|
||||
resolve({ itemUrl: item.getURL(), itemFilename: item.getFilename(), item });
|
||||
});
|
||||
});
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
const { item, itemUrl, itemFilename } = await downloadPrevented;
|
||||
expect(itemUrl).to.equal(url + '/');
|
||||
expect(itemFilename).to.equal('mockFile.txt');
|
||||
@@ -735,7 +735,7 @@ describe('session module', () => {
|
||||
});
|
||||
customSession = session.fromPartition(partitionName);
|
||||
await customSession.protocol.registerStringProtocol(protocolName, handler);
|
||||
w.loadURL(`${protocolName}://fake-host`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`${protocolName}://fake-host`));
|
||||
await once(ipcMain, 'hello');
|
||||
});
|
||||
});
|
||||
@@ -936,17 +936,19 @@ describe('session module', () => {
|
||||
const scheme = 'cors-blob';
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const url = `${scheme}://host`;
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await protocol.unregisterProtocol(scheme);
|
||||
});
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('returns blob data for uuid', (done) => {
|
||||
const postData = JSON.stringify({
|
||||
type: 'blob',
|
||||
value: 'hello'
|
||||
});
|
||||
const content = `<html>
|
||||
it(
|
||||
'returns blob data for uuid',
|
||||
withDone((done) => {
|
||||
const postData = JSON.stringify({
|
||||
type: 'blob',
|
||||
value: 'hello'
|
||||
});
|
||||
const content = `<html>
|
||||
<script>
|
||||
let fd = new FormData();
|
||||
fd.append('file', new Blob(['${postData}'], {type:'application/json'}));
|
||||
@@ -954,29 +956,30 @@ describe('session module', () => {
|
||||
</script>
|
||||
</html>`;
|
||||
|
||||
protocol.registerStringProtocol(scheme, (request, callback) => {
|
||||
try {
|
||||
if (request.method === 'GET') {
|
||||
callback({ data: content, mimeType: 'text/html' });
|
||||
} else if (request.method === 'POST') {
|
||||
const uuid = request.uploadData![1].blobUUID;
|
||||
expect(uuid).to.be.a('string');
|
||||
session.defaultSession.getBlobData(uuid!).then((result) => {
|
||||
try {
|
||||
expect(result.toString()).to.equal(postData);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
protocol.registerStringProtocol(scheme, (request, callback) => {
|
||||
try {
|
||||
if (request.method === 'GET') {
|
||||
callback({ data: content, mimeType: 'text/html' });
|
||||
} else if (request.method === 'POST') {
|
||||
const uuid = request.uploadData![1].blobUUID;
|
||||
expect(uuid).to.be.a('string');
|
||||
session.defaultSession.getBlobData(uuid!).then((result) => {
|
||||
try {
|
||||
expect(result.toString()).to.equal(postData);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL(url);
|
||||
});
|
||||
});
|
||||
const w = new BrowserWindow({ show: false });
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('ses.getBlobData() (gc)', () => {
|
||||
@@ -984,7 +987,7 @@ describe('session module', () => {
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
|
||||
const waitForBlobDataRejection = (uuid: string) =>
|
||||
const waitForBlobDataRejection = (uuid: string, signal: AbortSignal) =>
|
||||
waitUntil(async () => {
|
||||
const attempt = session.defaultSession
|
||||
.getBlobData(uuid)
|
||||
@@ -993,14 +996,14 @@ describe('session module', () => {
|
||||
const deadline = setTimeout(1000).then(() => false);
|
||||
const rejected = await Promise.race([attempt, deadline]);
|
||||
return rejected;
|
||||
});
|
||||
}, signal);
|
||||
|
||||
const waitForGarbageCollection = (weak: WeakRef<object>) =>
|
||||
const waitForGarbageCollection = (weak: WeakRef<object>, signal: AbortSignal) =>
|
||||
waitUntil(() => {
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
v8Util.runUntilIdle();
|
||||
return weak.deref() === undefined;
|
||||
});
|
||||
}, signal);
|
||||
|
||||
const makeContent = (url: string, postData: string) => `<html>
|
||||
<script>
|
||||
@@ -1057,7 +1060,7 @@ describe('session module', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects after wrapper is collected', async () => {
|
||||
it('rejects after wrapper is collected', async (ctx) => {
|
||||
const url = `${scheme}://gc-released-${Date.now()}`;
|
||||
const postData = 'payload';
|
||||
const content = makeContent(url, postData);
|
||||
@@ -1075,8 +1078,8 @@ describe('session module', () => {
|
||||
const weak = new WeakRef(heldDataPipe as object);
|
||||
heldDataPipe = null;
|
||||
|
||||
await waitForGarbageCollection(weak);
|
||||
await waitForBlobDataRejection(uuid);
|
||||
await waitForGarbageCollection(weak, ctx.signal);
|
||||
await waitForBlobDataRejection(uuid, ctx.signal);
|
||||
} finally {
|
||||
await protocol.unregisterProtocol(scheme);
|
||||
}
|
||||
@@ -1087,13 +1090,15 @@ describe('session module', () => {
|
||||
const scheme = 'cors-blob';
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const url = `${scheme}://host`;
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await protocol.unregisterProtocol(scheme);
|
||||
});
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('returns blob data for uuid', (done) => {
|
||||
const content = `<html>
|
||||
it(
|
||||
'returns blob data for uuid',
|
||||
withDone((done) => {
|
||||
const content = `<html>
|
||||
<script>
|
||||
let fd = new FormData();
|
||||
fd.append("data", new Blob(new Array(65_537).fill('a')));
|
||||
@@ -1101,30 +1106,31 @@ describe('session module', () => {
|
||||
</script>
|
||||
</html>`;
|
||||
|
||||
protocol.registerStringProtocol(scheme, (request, callback) => {
|
||||
try {
|
||||
if (request.method === 'GET') {
|
||||
callback({ data: content, mimeType: 'text/html' });
|
||||
} else if (request.method === 'POST') {
|
||||
const uuid = request.uploadData![1].blobUUID;
|
||||
expect(uuid).to.be.a('string');
|
||||
session.defaultSession.getBlobData(uuid!).then((result) => {
|
||||
try {
|
||||
const data = new Array(65_537).fill('a');
|
||||
expect(result.toString()).to.equal(data.join(''));
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
protocol.registerStringProtocol(scheme, (request, callback) => {
|
||||
try {
|
||||
if (request.method === 'GET') {
|
||||
callback({ data: content, mimeType: 'text/html' });
|
||||
} else if (request.method === 'POST') {
|
||||
const uuid = request.uploadData![1].blobUUID;
|
||||
expect(uuid).to.be.a('string');
|
||||
session.defaultSession.getBlobData(uuid!).then((result) => {
|
||||
try {
|
||||
const data = new Array(65_537).fill('a');
|
||||
expect(result.toString()).to.equal(data.join(''));
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL(url);
|
||||
});
|
||||
});
|
||||
const w = new BrowserWindow({ show: false });
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('ses.setCertificateVerifyProc(callback)', () => {
|
||||
@@ -1150,9 +1156,11 @@ describe('session module', () => {
|
||||
serverUrl = (await listen(server)).url;
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
server.close(done);
|
||||
});
|
||||
afterEach(
|
||||
withDone((done) => {
|
||||
server.close(done);
|
||||
})
|
||||
);
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('accepts the request when the callback is called with 0', async () => {
|
||||
@@ -1310,7 +1318,7 @@ describe('session module', () => {
|
||||
let port: number;
|
||||
let downloadServer: http.Server;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
downloadServer = http.createServer((req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
@@ -1322,7 +1330,7 @@ describe('session module', () => {
|
||||
port = (await listen(downloadServer)).port;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await new Promise((resolve) => downloadServer.close(resolve));
|
||||
});
|
||||
|
||||
@@ -1854,14 +1862,14 @@ describe('session module', () => {
|
||||
// requires a secure context.
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('');
|
||||
});
|
||||
serverUrl = (await listen(server)).url;
|
||||
});
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { BaseWindow } from 'electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
import { ifdescribe, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
const fixtures = path.resolve(__dirname, 'fixtures');
|
||||
@@ -31,8 +32,7 @@ ifdescribe(!skip)('sharedTexture module', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('successfully imported and rendered with subtle api', async function () {
|
||||
this.timeout(debugSpec ? 100000 : 10000);
|
||||
it('successfully imported and rendered with subtle api', { timeout: debugSpec ? 100000 : 10000 }, async () => {
|
||||
type CapturedTextureHolder = {
|
||||
importedSubtle: Electron.SharedTextureImportedSubtle;
|
||||
texture: Electron.OffscreenSharedTexture;
|
||||
@@ -142,8 +142,8 @@ ifdescribe(!skip)('sharedTexture module', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
win.loadFile(htmlPath);
|
||||
osr.loadFile(osrPath);
|
||||
dangerouslyIgnoreWebContentsLoadResult(win.loadFile(htmlPath));
|
||||
dangerouslyIgnoreWebContentsLoadResult(osr.loadFile(osrPath));
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
@@ -256,8 +256,8 @@ ifdescribe(!skip)('sharedTexture module', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
win.loadFile(htmlPath);
|
||||
osr.loadFile(osrPath);
|
||||
dangerouslyIgnoreWebContentsLoadResult(win.loadFile(htmlPath));
|
||||
dangerouslyIgnoreWebContentsLoadResult(osr.loadFile(osrPath));
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
@@ -266,12 +266,20 @@ ifdescribe(!skip)('sharedTexture module', () => {
|
||||
});
|
||||
};
|
||||
|
||||
it('successfully imported and rendered with managed api, without iframe', async () => {
|
||||
return runSharedTextureManagedTest(false);
|
||||
}).timeout(debugSpec ? 100000 : 10000);
|
||||
it(
|
||||
'successfully imported and rendered with managed api, without iframe',
|
||||
{ timeout: debugSpec ? 100000 : 10000 },
|
||||
async () => {
|
||||
return runSharedTextureManagedTest(false);
|
||||
}
|
||||
);
|
||||
|
||||
it('successfully imported and rendered with managed api, with iframe', async () => {
|
||||
return runSharedTextureManagedTest(true);
|
||||
}).timeout(debugSpec ? 100000 : 10000);
|
||||
it(
|
||||
'successfully imported and rendered with managed api, with iframe',
|
||||
{ timeout: debugSpec ? 100000 : 10000 },
|
||||
async () => {
|
||||
return runSharedTextureManagedTest(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { shell } from 'electron/common';
|
||||
import { BrowserWindow, app } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -10,7 +11,7 @@ import * as http from 'node:http';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ifdescribe, ifit, listen } from './lib/spec-helpers';
|
||||
import { ifdescribe, ifit, listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('shell module', () => {
|
||||
@@ -18,14 +19,13 @@ describe('shell module', () => {
|
||||
let envVars: Record<string, string | undefined> = {};
|
||||
let server: http.Server;
|
||||
|
||||
after(function () {
|
||||
this.timeout(60000);
|
||||
afterAll(() => {
|
||||
if (process.env.CI && process.platform === 'win32') {
|
||||
// Edge may cause issues with visibility tests, so make sure it is closed after testing.
|
||||
const killEdge = 'Get-Process | Where Name -Like "msedge" | Stop-Process';
|
||||
execSync(killEdge, { shell: 'powershell.exe' });
|
||||
}
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
beforeEach(function () {
|
||||
envVars = {
|
||||
@@ -96,21 +96,6 @@ describe('shell module', () => {
|
||||
requestReceived
|
||||
]);
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')(
|
||||
'removes focus from the electron window after opening an external link',
|
||||
async () => {
|
||||
const url = 'http://127.0.0.1';
|
||||
const w = new BrowserWindow({ show: true });
|
||||
|
||||
await once(w, 'focus');
|
||||
expect(w.isFocused()).to.be.true();
|
||||
|
||||
await Promise.all<void>([shell.openExternal(url), once(w, 'blur') as Promise<any>]);
|
||||
|
||||
expect(w.isFocused()).to.be.false();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('shell.trashItem()', () => {
|
||||
@@ -131,7 +116,7 @@ describe('shell module', () => {
|
||||
|
||||
ifit(!(process.platform === 'win32' && process.arch === 'ia32'))('works in the renderer process', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(
|
||||
w.webContents.executeJavaScript("require('electron').shell.trashItem('does-not-exist')")
|
||||
).to.be.rejectedWith(/does-not-exist|Failed to move item|Failed to create FileOperation/);
|
||||
@@ -1,13 +1,14 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { emittedNTimes } from './lib/events-helpers';
|
||||
import { ifdescribe, listen } from './lib/spec-helpers';
|
||||
import { defer, ifdescribe, listen, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
@@ -33,7 +34,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should load preload scripts in top level iframes', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [event1, event2] = await detailsPromise;
|
||||
expect(event1[0].senderFrame.frameToken).to.not.equal(event2[0].senderFrame.frameToken);
|
||||
expect(event1[0].senderFrame.frameToken).to.equal(event1[2]);
|
||||
@@ -42,7 +45,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should load preload scripts in nested iframes', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [event1, event2, event3] = await detailsPromise;
|
||||
expect(event1[0].senderFrame.frameToken).to.not.equal(event2[0].senderFrame.frameToken);
|
||||
expect(event1[0].senderFrame.frameToken).to.not.equal(event3[0].senderFrame.frameToken);
|
||||
@@ -54,7 +59,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should correctly reply to the main frame with using event.reply', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [event1] = await detailsPromise;
|
||||
const pongPromise = once(ipcMain, 'preload-pong');
|
||||
event1[0].reply('preload-ping');
|
||||
@@ -64,7 +71,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should correctly reply to the main frame with using event.senderFrame.send', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [event1] = await detailsPromise;
|
||||
const pongPromise = once(ipcMain, 'preload-pong');
|
||||
event1[0].senderFrame.send('preload-ping');
|
||||
@@ -74,7 +83,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should correctly reply to the sub-frames with using event.reply', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [, event2] = await detailsPromise;
|
||||
const pongPromise = once(ipcMain, 'preload-pong');
|
||||
event2[0].reply('preload-ping');
|
||||
@@ -84,7 +95,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should correctly reply to the sub-frames with using event.senderFrame.send', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [, event2] = await detailsPromise;
|
||||
const pongPromise = once(ipcMain, 'preload-pong');
|
||||
event2[0].senderFrame.send('preload-ping');
|
||||
@@ -94,7 +107,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should correctly reply to the nested sub-frames with using event.reply', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [, , event3] = await detailsPromise;
|
||||
const pongPromise = once(ipcMain, 'preload-pong');
|
||||
event3[0].reply('preload-ping');
|
||||
@@ -104,7 +119,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should correctly reply to the nested sub-frames with using event.senderFrame.send', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const [, , event3] = await detailsPromise;
|
||||
const pongPromise = once(ipcMain, 'preload-pong');
|
||||
event3[0].senderFrame.send('preload-ping');
|
||||
@@ -114,7 +131,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
|
||||
it('should not expose globals in main world', async () => {
|
||||
const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2);
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`));
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`))
|
||||
);
|
||||
const details = await detailsPromise;
|
||||
const senders = details.map((event) => event[0].sender);
|
||||
const isolatedGlobals = await Promise.all(
|
||||
@@ -216,6 +235,7 @@ describe('renderer nodeIntegrationInSubFrames', () => {
|
||||
describe('subframe with non-standard schemes', () => {
|
||||
it('should not crash when changing subframe src to about:blank and back', async () => {
|
||||
const w = new BrowserWindow({ show: false, width: 400, height: 400 });
|
||||
defer(() => closeWindow(w));
|
||||
|
||||
const fwfPath = path.resolve(__dirname, 'fixtures/sub-frames/frame-with-frame.html');
|
||||
await w.loadFile(fwfPath);
|
||||
@@ -253,7 +273,7 @@ ifdescribe(process.platform !== 'linux')('cross-site frame sandboxing', () => {
|
||||
let crossSiteUrl: string;
|
||||
let serverUrl: string;
|
||||
|
||||
before(async function () {
|
||||
beforeAll(async function () {
|
||||
server = http.createServer((req, res) => {
|
||||
res.end(`<iframe name="frame" src="${crossSiteUrl}" />`);
|
||||
});
|
||||
@@ -261,7 +281,7 @@ ifdescribe(process.platform !== 'linux')('cross-site frame sandboxing', () => {
|
||||
crossSiteUrl = serverUrl.replace('127.0.0.1', 'localhost');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
server = null as unknown as http.Server;
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { systemPreferences } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { BaseWindow, BrowserWindow, TouchBar } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { withDone } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
const {
|
||||
@@ -117,29 +119,35 @@ describe('TouchBar module', () => {
|
||||
touchBar.escapeItem = null;
|
||||
});
|
||||
|
||||
it('calls the callback on the items when a window interaction event fires', (done) => {
|
||||
const button = new TouchBarButton({
|
||||
label: 'bar',
|
||||
click: () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
const touchBar = new TouchBar({ items: [button] });
|
||||
window.setTouchBar(touchBar);
|
||||
window.emit('-touch-bar-interaction', {}, (button as any).id);
|
||||
});
|
||||
it(
|
||||
'calls the callback on the items when a window interaction event fires',
|
||||
withDone((done) => {
|
||||
const button = new TouchBarButton({
|
||||
label: 'bar',
|
||||
click: () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
const touchBar = new TouchBar({ items: [button] });
|
||||
window.setTouchBar(touchBar);
|
||||
window.emit('-touch-bar-interaction', {}, (button as any).id);
|
||||
})
|
||||
);
|
||||
|
||||
it('calls the callback on the escape item when a window interaction event fires', (done) => {
|
||||
const button = new TouchBarButton({
|
||||
label: 'bar',
|
||||
click: () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
const touchBar = new TouchBar({ escapeItem: button });
|
||||
window.setTouchBar(touchBar);
|
||||
window.emit('-touch-bar-interaction', {}, (button as any).id);
|
||||
});
|
||||
it(
|
||||
'calls the callback on the escape item when a window interaction event fires',
|
||||
withDone((done) => {
|
||||
const button = new TouchBarButton({
|
||||
label: 'bar',
|
||||
click: () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
const touchBar = new TouchBar({ escapeItem: button });
|
||||
window.setTouchBar(touchBar);
|
||||
window.emit('-touch-bar-interaction', {}, (button as any).id);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { nativeImage } from 'electron/common';
|
||||
import { Menu, Tray } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
@@ -2,6 +2,7 @@ import { systemPreferences } from 'electron';
|
||||
import { BrowserWindow, MessageChannelMain, utilityProcess, app } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import * as childProcess from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -12,7 +13,7 @@ import { setImmediate } from 'node:timers/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { respondOnce, randomString, kOneKiloByte } from './lib/net-helpers';
|
||||
import { ifit, startRemoteControlApp } from './lib/spec-helpers';
|
||||
import { ifit, startRemoteControlApp, withDone } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process');
|
||||
@@ -60,21 +61,27 @@ describe('utilityProcess module', () => {
|
||||
await once(child, 'spawn');
|
||||
});
|
||||
|
||||
it("emits 'exit' when child process exits gracefully", (done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
|
||||
child.on('exit', (code) => {
|
||||
expect(code).to.equal(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it(
|
||||
"emits 'exit' when child process exits gracefully",
|
||||
withDone((done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
|
||||
child.on('exit', (code) => {
|
||||
expect(code).to.equal(0);
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it("emits 'exit' when the child process file does not exist", (done) => {
|
||||
const child = utilityProcess.fork('nonexistent');
|
||||
child.on('exit', (code) => {
|
||||
expect(code).to.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it(
|
||||
"emits 'exit' when the child process file does not exist",
|
||||
withDone((done) => {
|
||||
const child = utilityProcess.fork('nonexistent');
|
||||
child.on('exit', (code) => {
|
||||
expect(code).to.equal(1);
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
ifit(!isWindows32Bit)('emits the correct error code when child process exits nonzero', async () => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
|
||||
@@ -457,86 +464,95 @@ describe('utilityProcess module', () => {
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('supports starting the v8 inspector with --inspect-brk', (done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
|
||||
stdio: 'pipe',
|
||||
execArgv: ['--inspect-brk']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr!.removeListener('data', listener);
|
||||
child.stdout!.removeListener('data', listener);
|
||||
child.once('exit', () => {
|
||||
done();
|
||||
it(
|
||||
'supports starting the v8 inspector with --inspect-brk',
|
||||
withDone((done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
|
||||
stdio: 'pipe',
|
||||
execArgv: ['--inspect-brk']
|
||||
});
|
||||
child.kill();
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/Debugger listening on ws:/m.test(output)) {
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr!.removeListener('data', listener);
|
||||
child.stdout!.removeListener('data', listener);
|
||||
child.once('exit', () => {
|
||||
done();
|
||||
});
|
||||
child.kill();
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/Debugger listening on ws:/m.test(output)) {
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr!.on('data', listener);
|
||||
child.stdout!.on('data', listener);
|
||||
})
|
||||
);
|
||||
|
||||
it(
|
||||
'supports starting the v8 inspector with --inspect and a provided port',
|
||||
withDone((done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
|
||||
stdio: 'pipe',
|
||||
execArgv: ['--inspect=17364']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr!.removeListener('data', listener);
|
||||
child.stdout!.removeListener('data', listener);
|
||||
child.once('exit', () => {
|
||||
done();
|
||||
});
|
||||
child.kill();
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/Debugger listening on ws:/m.test(output)) {
|
||||
expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr!.on('data', listener);
|
||||
child.stdout!.on('data', listener);
|
||||
})
|
||||
);
|
||||
|
||||
it(
|
||||
'supports changing dns verbatim with --dns-result-order',
|
||||
withDone((done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'dns-result-order.js'), [], {
|
||||
stdio: 'pipe',
|
||||
execArgv: ['--dns-result-order=ipv4first']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr!.removeListener('data', listener);
|
||||
child.stdout!.removeListener('data', listener);
|
||||
child.once('exit', () => {
|
||||
done();
|
||||
});
|
||||
child.kill();
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
expect(output.trim()).to.contain('ipv4first', 'default verbatim should be ipv4first');
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
child.stderr!.on('data', listener);
|
||||
child.stdout!.on('data', listener);
|
||||
});
|
||||
|
||||
it('supports starting the v8 inspector with --inspect and a provided port', (done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
|
||||
stdio: 'pipe',
|
||||
execArgv: ['--inspect=17364']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr!.removeListener('data', listener);
|
||||
child.stdout!.removeListener('data', listener);
|
||||
child.once('exit', () => {
|
||||
done();
|
||||
});
|
||||
child.kill();
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/Debugger listening on ws:/m.test(output)) {
|
||||
expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr!.on('data', listener);
|
||||
child.stdout!.on('data', listener);
|
||||
});
|
||||
|
||||
it('supports changing dns verbatim with --dns-result-order', (done) => {
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'dns-result-order.js'), [], {
|
||||
stdio: 'pipe',
|
||||
execArgv: ['--dns-result-order=ipv4first']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr!.removeListener('data', listener);
|
||||
child.stdout!.removeListener('data', listener);
|
||||
child.once('exit', () => {
|
||||
done();
|
||||
});
|
||||
child.kill();
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
expect(output.trim()).to.contain('ipv4first', 'default verbatim should be ipv4first');
|
||||
cleanup();
|
||||
};
|
||||
|
||||
child.stderr!.on('data', listener);
|
||||
child.stdout!.on('data', listener);
|
||||
});
|
||||
child.stderr!.on('data', listener);
|
||||
child.stdout!.on('data', listener);
|
||||
})
|
||||
);
|
||||
|
||||
ifit(process.platform !== 'win32')('supports redirecting stdout to parent process', async () => {
|
||||
const result = 'Output from utility process';
|
||||
@@ -1,7 +1,9 @@
|
||||
import { BaseWindow, View } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import { withDone } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
describe('View', () => {
|
||||
@@ -39,7 +41,7 @@ describe('View', () => {
|
||||
});
|
||||
|
||||
it('can be added as a child of another View', async () => {
|
||||
const w = new BaseWindow();
|
||||
w = new BaseWindow();
|
||||
const v1 = new View();
|
||||
const v2 = new View();
|
||||
|
||||
@@ -136,20 +138,23 @@ describe('View', () => {
|
||||
expect(child.getBounds()).to.deep.equal({ x: 10, y: 15, width: 25, height: 30 });
|
||||
});
|
||||
|
||||
it('can set bounds with animation', (done) => {
|
||||
const v = new View();
|
||||
v.setBounds(
|
||||
{ x: 0, y: 0, width: 100, height: 100 },
|
||||
{
|
||||
animate: {
|
||||
duration: 300
|
||||
it(
|
||||
'can set bounds with animation',
|
||||
withDone((done) => {
|
||||
const v = new View();
|
||||
v.setBounds(
|
||||
{ x: 0, y: 0, width: 100, height: 100 },
|
||||
{
|
||||
animate: {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(v.getBounds()).to.deep.equal({ x: 0, y: 0, width: 100, height: 100 });
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(v.getBounds()).to.deep.equal({ x: 0, y: 0, width: 100, height: 100 });
|
||||
done();
|
||||
}, 350);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { BaseWindow, BrowserWindow, View, WebContentsView, webContents, screen } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
|
||||
|
||||
import { HexColors, ScreenCapture, hasCapturableScreen, nextFrameTime } from './lib/screen-helpers';
|
||||
import { defer, ifdescribe, waitUntil } from './lib/spec-helpers';
|
||||
import { defer, ifdescribe, waitUntil, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('WebContentsView', () => {
|
||||
@@ -27,7 +28,7 @@ describe('WebContentsView', () => {
|
||||
});
|
||||
|
||||
it('accepts existing webContents object', async () => {
|
||||
const currentWebContentsCount = webContents.getAllWebContents().length;
|
||||
const before = new Set(webContents.getAllWebContents().map((c) => c.id));
|
||||
|
||||
const wc = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
|
||||
defer(() => wc.destroy());
|
||||
@@ -38,10 +39,9 @@ describe('WebContentsView', () => {
|
||||
});
|
||||
|
||||
expect(webContentsView.webContents).to.eq(wc);
|
||||
expect(webContents.getAllWebContents().length).to.equal(
|
||||
currentWebContentsCount + 1,
|
||||
'expected only single webcontents to be created'
|
||||
);
|
||||
const created = webContents.getAllWebContents().filter((c) => !before.has(c.id));
|
||||
expect(created).to.have.lengthOf(1, 'expected only single webcontents to be created');
|
||||
expect(created[0].id).to.equal(wc.id);
|
||||
});
|
||||
|
||||
it('should throw error when created with already attached webContents to BrowserWindow', () => {
|
||||
@@ -67,7 +67,7 @@ describe('WebContentsView', () => {
|
||||
|
||||
const webContentsView = new WebContentsView();
|
||||
const wc = webContentsView.webContents;
|
||||
wc.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL('about:blank'));
|
||||
wc.destroy();
|
||||
|
||||
const destroyed = once(wc, 'destroyed');
|
||||
@@ -82,7 +82,7 @@ describe('WebContentsView', () => {
|
||||
|
||||
const webContentsView = new WebContentsView();
|
||||
defer(() => webContentsView.webContents.destroy());
|
||||
webContentsView.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(webContentsView.webContents.loadURL('about:blank'));
|
||||
|
||||
expect(
|
||||
() =>
|
||||
@@ -163,23 +163,28 @@ describe('WebContentsView', () => {
|
||||
return arr;
|
||||
}
|
||||
|
||||
it("doesn't crash when GCed during allocation", (done) => {
|
||||
// eslint-disable-next-line no-new
|
||||
new WebContentsView();
|
||||
setTimeout(() => {
|
||||
// NB. the crash we're testing for is the lack of a current `v8::Context`
|
||||
// when emitting an event in WebContents's destructor. V8 is inconsistent
|
||||
// about whether or not there's a current context during garbage
|
||||
// collection, and it seems that `v8Util.requestGarbageCollectionForTesting`
|
||||
// causes a GC in which there _is_ a current context, so the crash isn't
|
||||
// triggered. Thus, we force a GC by other means: namely, by allocating a
|
||||
// bunch of stuff.
|
||||
triggerGCByAllocation();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it(
|
||||
"doesn't crash when GCed during allocation",
|
||||
withDone((done) => {
|
||||
// eslint-disable-next-line no-new
|
||||
new WebContentsView();
|
||||
setTimeout(() => {
|
||||
// NB. the crash we're testing for is the lack of a current `v8::Context`
|
||||
// when emitting an event in WebContents's destructor. V8 is inconsistent
|
||||
// about whether or not there's a current context during garbage
|
||||
// collection, and it seems that `v8Util.requestGarbageCollectionForTesting`
|
||||
// causes a GC in which there _is_ a current context, so the crash isn't
|
||||
// triggered. Thus, we force a GC by other means: namely, by allocating a
|
||||
// bunch of stuff.
|
||||
triggerGCByAllocation();
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it('does not crash when closed via window.close()', async () => {
|
||||
// TODO(#50982): re-enable once the native blur-during-destruction DCHECK is
|
||||
// resolved. This test's blur handler is the re-entry vector.
|
||||
it.skip('does not crash when closed via window.close()', async () => {
|
||||
const bw = new BrowserWindow();
|
||||
const wcv = new WebContentsView();
|
||||
const wc = wcv.webContents;
|
||||
@@ -194,7 +199,7 @@ describe('WebContentsView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
wc.loadURL('data:text/html,<script>window.close()</script>');
|
||||
dangerouslyIgnoreWebContentsLoadResult(wc.loadURL('data:text/html,<script>window.close()</script>'));
|
||||
|
||||
const open = await dto;
|
||||
expect(open).to.be.false();
|
||||
@@ -265,12 +270,14 @@ describe('WebContentsView', () => {
|
||||
expect(await v.webContents.executeJavaScript('initialVisibility')).to.equal('visible');
|
||||
});
|
||||
|
||||
it('becomes hidden when parent window is hidden', async () => {
|
||||
it('becomes hidden when parent window is hidden', async (ctx) => {
|
||||
const w = new BaseWindow();
|
||||
const v = new WebContentsView();
|
||||
w.setContentView(v);
|
||||
await v.webContents.loadURL('about:blank');
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
const p = v.webContents.executeJavaScript(
|
||||
'new Promise(resolve => document.addEventListener("visibilitychange", resolve))'
|
||||
);
|
||||
@@ -301,12 +308,14 @@ describe('WebContentsView', () => {
|
||||
expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
|
||||
});
|
||||
|
||||
it('does not change when view is moved between two visible windows', async () => {
|
||||
it('does not change when view is moved between two visible windows', async (ctx) => {
|
||||
const w = new BaseWindow();
|
||||
const v = new WebContentsView();
|
||||
w.setContentView(v);
|
||||
await v.webContents.loadURL('about:blank');
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
|
||||
const p = v.webContents.executeJavaScript(
|
||||
'new Promise(resolve => document.addEventListener("visibilitychange", () => resolve(document.visibilityState)))'
|
||||
@@ -330,7 +339,7 @@ describe('WebContentsView', () => {
|
||||
expect(visibilityState).to.equal('visible');
|
||||
});
|
||||
|
||||
it('tracks visibility for multiple child WebContentsViews', async () => {
|
||||
it('tracks visibility for multiple child WebContentsViews', async (ctx) => {
|
||||
const w = new BaseWindow({ show: false });
|
||||
const cv = new View();
|
||||
w.setContentView(cv);
|
||||
@@ -345,21 +354,33 @@ describe('WebContentsView', () => {
|
||||
await v1.webContents.loadURL('about:blank');
|
||||
await v2.webContents.loadURL('about:blank');
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v1, 'hidden'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v2, 'hidden'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
|
||||
w.show();
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v1, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v2, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
|
||||
w.hide();
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v1, 'hidden'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v2, 'hidden'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async () => {
|
||||
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async (ctx) => {
|
||||
const w = new BaseWindow();
|
||||
const cv = new View();
|
||||
w.setContentView(cv);
|
||||
@@ -374,21 +395,29 @@ describe('WebContentsView', () => {
|
||||
await v1.webContents.loadURL('about:blank');
|
||||
await v2.webContents.loadURL('about:blank');
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v1, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v2, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
|
||||
v1.setVisible(false);
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v1, 'hidden'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
// v2 should remain visible while v1 is hidden
|
||||
expect(await v2.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
|
||||
|
||||
v1.setVisible(true);
|
||||
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v1, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('fires a single visibilitychange event per show/hide transition', async () => {
|
||||
it('fires a single visibilitychange event per show/hide transition', async (ctx) => {
|
||||
const w = new BaseWindow({ show: false });
|
||||
const v = new WebContentsView();
|
||||
w.setContentView(v);
|
||||
@@ -402,13 +431,17 @@ describe('WebContentsView', () => {
|
||||
`);
|
||||
|
||||
w.show();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v, 'visible'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
|
||||
// Give any delayed/queued occlusion updates time to fire.
|
||||
await setTimeoutAsync(1500);
|
||||
|
||||
w.hide();
|
||||
await expect(waitUntil(async () => await haveVisibilityState(v, 'hidden'))).to.eventually.be.fulfilled();
|
||||
await expect(
|
||||
waitUntil(async () => await haveVisibilityState(v, 'hidden'), ctx.signal)
|
||||
).to.eventually.be.fulfilled();
|
||||
|
||||
await setTimeoutAsync(1500);
|
||||
|
||||
@@ -445,7 +478,7 @@ describe('WebContentsView', () => {
|
||||
v.setBorderRadius(100);
|
||||
|
||||
const readyForCapture = once(v.webContents, 'ready-to-show');
|
||||
v.webContents.loadURL(backgroundUrl);
|
||||
dangerouslyIgnoreWebContentsLoadResult(v.webContents.loadURL(backgroundUrl));
|
||||
|
||||
const inset = 10;
|
||||
// Adjust for macOS menu bar height which seems to be about 24px
|
||||
@@ -497,7 +530,7 @@ describe('WebContentsView', () => {
|
||||
w.setContentView(v);
|
||||
|
||||
const readyForCapture = once(v.webContents, 'ready-to-show');
|
||||
v.webContents.loadURL(backgroundUrl);
|
||||
dangerouslyIgnoreWebContentsLoadResult(v.webContents.loadURL(backgroundUrl));
|
||||
await readyForCapture;
|
||||
|
||||
const corner = corners[0];
|
||||
@@ -512,38 +545,4 @@ describe('WebContentsView', () => {
|
||||
v.setBorderRadius(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('focusOnNavigation webPreference', () => {
|
||||
it('focuses the webContents on navigation by default', async () => {
|
||||
const w = new BrowserWindow();
|
||||
await once(w, 'focus');
|
||||
const v = new WebContentsView();
|
||||
w.setContentView(v);
|
||||
await v.webContents.loadURL('about:blank');
|
||||
const devToolsFocused = once(v.webContents, 'devtools-focused');
|
||||
v.webContents.openDevTools({ mode: 'right' });
|
||||
await devToolsFocused;
|
||||
expect(v.webContents.isFocused()).to.be.false();
|
||||
await v.webContents.loadURL('data:text/html,<body>test</body>');
|
||||
expect(v.webContents.isFocused()).to.be.true();
|
||||
});
|
||||
|
||||
it('does not focus the webContents on navigation when focusOnNavigation is false', async () => {
|
||||
const w = new BrowserWindow();
|
||||
await once(w, 'focus');
|
||||
const v = new WebContentsView({
|
||||
webPreferences: {
|
||||
focusOnNavigation: false
|
||||
}
|
||||
});
|
||||
w.setContentView(v);
|
||||
await v.webContents.loadURL('about:blank');
|
||||
const devToolsFocused = once(v.webContents, 'devtools-focused');
|
||||
v.webContents.openDevTools({ mode: 'right' });
|
||||
await devToolsFocused;
|
||||
expect(v.webContents.isFocused()).to.be.false();
|
||||
await v.webContents.loadURL('data:text/html,<body>test</body>');
|
||||
expect(v.webContents.isFocused()).to.be.false();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { clipboard } from 'electron/common';
|
||||
import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain, app, WebContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as http from 'node:http';
|
||||
@@ -10,7 +10,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import * as url from 'node:url';
|
||||
|
||||
import { emittedNTimes } from './lib/events-helpers';
|
||||
import { defer, ifit, listen, waitUntil } from './lib/spec-helpers';
|
||||
import { defer, ifit, listen, waitUntil, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('webFrameMain module', () => {
|
||||
@@ -108,12 +108,12 @@ describe('webFrameMain module', () => {
|
||||
let serverA: Server;
|
||||
let serverB: Server;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
serverA = await createServer();
|
||||
serverB = await createServer();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
serverA.server.close();
|
||||
serverB.server.close();
|
||||
});
|
||||
@@ -206,14 +206,14 @@ describe('webFrameMain module', () => {
|
||||
|
||||
describe('WebFrame.visibilityState', () => {
|
||||
// DISABLED-FIXME(MarshallOfSound): Fix flaky test
|
||||
it('should match window state', async () => {
|
||||
it('should match window state', async (ctx) => {
|
||||
const w = new BrowserWindow({ show: true });
|
||||
await w.loadURL('about:blank');
|
||||
const webFrame = w.webContents.mainFrame;
|
||||
|
||||
expect(webFrame.visibilityState).to.equal('visible');
|
||||
w.hide();
|
||||
await expect(waitUntil(() => webFrame.visibilityState === 'hidden')).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(() => webFrame.visibilityState === 'hidden', ctx.signal)).to.eventually.be.fulfilled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,10 +296,10 @@ describe('webFrameMain module', () => {
|
||||
let server: Awaited<ReturnType<typeof createServer>>;
|
||||
let w: BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = await createServer();
|
||||
});
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.server.close();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
@@ -355,7 +355,7 @@ describe('webFrameMain module', () => {
|
||||
console.log('mainFrame.url', mainFrame.url);
|
||||
});
|
||||
|
||||
it('returns null upon accessing senderFrame after cross-origin navigation', async () => {
|
||||
it('returns null upon accessing senderFrame after cross-origin navigation', async (ctx) => {
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
@@ -368,9 +368,9 @@ describe('webFrameMain module', () => {
|
||||
await w.webContents.loadURL(server.crossOriginUrl);
|
||||
// senderFrame now points to a disposed RenderFrameHost. It should
|
||||
// be null when attempting to access the lazily evaluated property.
|
||||
waitUntil(() => {
|
||||
await waitUntil(() => {
|
||||
return event.senderFrame === null;
|
||||
});
|
||||
}, ctx.signal);
|
||||
});
|
||||
|
||||
it('is detached when unload handler sends IPC', async () => {
|
||||
@@ -477,7 +477,7 @@ describe('webFrameMain module', () => {
|
||||
|
||||
// frame-with-frame-container.html, frame-with-frame.html, frame.html
|
||||
const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);
|
||||
w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')));
|
||||
|
||||
for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) {
|
||||
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
|
||||
@@ -504,14 +504,14 @@ describe('webFrameMain module', () => {
|
||||
|
||||
describe('webFrameMain.collectJavaScriptCallStack', () => {
|
||||
let server: Server;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = await createServer({
|
||||
headers: {
|
||||
'Document-Policy': 'include-js-call-stacks-in-crash-reports'
|
||||
}
|
||||
});
|
||||
});
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.server.close();
|
||||
});
|
||||
|
||||
@@ -525,102 +525,11 @@ describe('webFrameMain module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('webFrameMain.copyVideoFrameAt', () => {
|
||||
const insertVideoInFrame = async (frame: WebFrameMain) => {
|
||||
const videoFilePath = url.pathToFileURL(path.join(fixtures, 'cat-spin.mp4')).href;
|
||||
await frame.executeJavaScript(`
|
||||
const video = document.createElement('video');
|
||||
video.src = '${videoFilePath}';
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.play();
|
||||
document.body.appendChild(video);
|
||||
`);
|
||||
};
|
||||
|
||||
const getFramePosition = async (frame: WebFrameMain) => {
|
||||
const point = (await frame.executeJavaScript(
|
||||
`(${() => {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (!iframe) return;
|
||||
const rect = iframe.getBoundingClientRect();
|
||||
return { x: Math.floor(rect.x), y: Math.floor(rect.y) };
|
||||
}})()`
|
||||
)) as Electron.Point;
|
||||
expect(point).to.be.an('object');
|
||||
return point;
|
||||
};
|
||||
|
||||
const copyVideoFrameInFrame = async (frame: WebFrameMain) => {
|
||||
const point = (await frame.executeJavaScript(
|
||||
`(${() => {
|
||||
const video = document.querySelector('video');
|
||||
if (!video) return;
|
||||
const rect = video.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.floor(rect.x + rect.width / 2),
|
||||
y: Math.floor(rect.y + rect.height / 2)
|
||||
};
|
||||
}})()`
|
||||
)) as Electron.Point;
|
||||
|
||||
expect(point).to.be.an('object');
|
||||
|
||||
// Translate coordinate to be relative of main frame
|
||||
if (frame.parent) {
|
||||
const framePosition = await getFramePosition(frame.parent);
|
||||
point.x += framePosition.x;
|
||||
point.y += framePosition.y;
|
||||
}
|
||||
|
||||
expect(clipboard.readImage().isEmpty()).to.be.true();
|
||||
// wait for video to load
|
||||
await frame.executeJavaScript(
|
||||
`(${() => {
|
||||
const video = document.querySelector('video');
|
||||
if (!video) return;
|
||||
return new Promise((resolve) => {
|
||||
if (video.readyState >= 4) resolve(null);
|
||||
else video.addEventListener('canplaythrough', resolve, { once: true });
|
||||
});
|
||||
}})()`
|
||||
);
|
||||
frame.copyVideoFrameAt(point.x, point.y);
|
||||
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
|
||||
expect(clipboard.readImage().isEmpty()).to.be.false();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
clipboard.clear();
|
||||
});
|
||||
|
||||
// TODO: Re-enable on Windows CI once Chromium fixes the intermittent
|
||||
// backwards-time DCHECK hit while copying video frames:
|
||||
// DCHECK failed: !delta.is_negative().
|
||||
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in main frame', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.webContents.loadFile(path.join(fixtures, 'blank.html'));
|
||||
await insertVideoInFrame(w.webContents.mainFrame);
|
||||
await copyVideoFrameInFrame(w.webContents.mainFrame);
|
||||
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
|
||||
});
|
||||
|
||||
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in subframe', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
|
||||
const subframe = w.webContents.mainFrame.frames[0];
|
||||
expect(subframe).to.exist();
|
||||
await insertVideoInFrame(subframe);
|
||||
await copyVideoFrameInFrame(subframe);
|
||||
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('"frame-created" event', () => {
|
||||
it('emits when the main frame is created', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const promise = once(w.webContents, 'frame-created') as Promise<[any, Electron.FrameCreatedDetails]>;
|
||||
w.webContents.loadFile(path.join(subframesPath, 'frame.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadFile(path.join(subframesPath, 'frame.html')));
|
||||
const [, details] = await promise;
|
||||
expect(details.frame).to.equal(w.webContents.mainFrame);
|
||||
});
|
||||
@@ -630,7 +539,7 @@ describe('webFrameMain module', () => {
|
||||
const promise = emittedNTimes(w.webContents, 'frame-created', 2) as Promise<
|
||||
[any, Electron.FrameCreatedDetails][]
|
||||
>;
|
||||
w.webContents.loadFile(path.join(subframesPath, 'frame-container.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadFile(path.join(subframesPath, 'frame-container.html')));
|
||||
const [[, mainDetails], [, nestedDetails]] = await promise;
|
||||
expect(mainDetails.frame).to.equal(w.webContents.mainFrame);
|
||||
expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]);
|
||||
@@ -661,7 +570,7 @@ describe('webFrameMain module', () => {
|
||||
it('emits for top-level frame', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const promise = once(w.webContents.mainFrame, 'dom-ready');
|
||||
w.webContents.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadURL('about:blank'));
|
||||
await promise;
|
||||
});
|
||||
|
||||
@@ -676,7 +585,7 @@ describe('webFrameMain module', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html')));
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import { BrowserWindow, ipcMain, WebContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { defer } from './lib/spec-helpers';
|
||||
import { defer, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
describe('webFrame module', () => {
|
||||
const fixtures = path.resolve(__dirname, 'fixtures');
|
||||
@@ -19,9 +21,9 @@ describe('webFrame module', () => {
|
||||
preload: path.join(fixtures, 'pages', 'world-safe-preload.js')
|
||||
}
|
||||
});
|
||||
defer(() => w.close());
|
||||
defer(() => closeWindow(w));
|
||||
const isSafe = once(ipcMain, 'executejs-safe');
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const [, wasSafe] = await isSafe;
|
||||
expect(wasSafe).to.equal(true);
|
||||
});
|
||||
@@ -35,9 +37,9 @@ describe('webFrame module', () => {
|
||||
preload: path.join(fixtures, 'pages', 'world-safe-preload-error.js')
|
||||
}
|
||||
});
|
||||
defer(() => w.close());
|
||||
defer(() => closeWindow(w));
|
||||
const execError = once(ipcMain, 'executejs-safe');
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const [, error] = await execError;
|
||||
expect(error).to.not.equal(null, 'Error should not be null');
|
||||
expect(error).to.have.property('message', 'Uncaught Error: An object could not be cloned.');
|
||||
@@ -51,7 +53,7 @@ describe('webFrame module', () => {
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
defer(() => w.close());
|
||||
defer(() => closeWindow(w));
|
||||
await w.loadFile(path.join(fixtures, 'pages', 'webframe-spell-check.html'));
|
||||
w.focus();
|
||||
await w.webContents.executeJavaScript('document.querySelector("input").focus()', true);
|
||||
@@ -77,7 +79,7 @@ describe('webFrame module', () => {
|
||||
describe('api', () => {
|
||||
let w: WebContents;
|
||||
let win: BrowserWindow;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
win = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, nodeIntegration: true } });
|
||||
await win.loadURL('data:text/html,<iframe name="test"></iframe>');
|
||||
w = win.webContents;
|
||||
@@ -89,8 +91,8 @@ describe('webFrame module', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
win.close();
|
||||
afterAll(async () => {
|
||||
await closeWindow(win);
|
||||
win = null as unknown as BrowserWindow;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ipcMain, net, protocol, session, WebContents, webContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';
|
||||
import * as WebSocket from 'ws';
|
||||
|
||||
import { once } from 'node:events';
|
||||
@@ -13,7 +14,7 @@ import * as qs from 'node:querystring';
|
||||
import { ReadableStream } from 'node:stream/web';
|
||||
import * as url from 'node:url';
|
||||
|
||||
import { listen, defer, startRemoteControlApp } from './lib/spec-helpers';
|
||||
import { listen, defer, startRemoteControlApp, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
@@ -60,14 +61,14 @@ describe('webRequest module', () => {
|
||||
}
|
||||
);
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
protocol.registerStringProtocol('cors', (req, cb) => cb(''));
|
||||
defaultURL = (await listen(server)).url + '/';
|
||||
http2URL = (await listen(h2server)).url + '/';
|
||||
console.log(http2URL);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
h2server.close();
|
||||
protocol.unregisterProtocol('cors');
|
||||
@@ -75,13 +76,13 @@ describe('webRequest module', () => {
|
||||
|
||||
let contents: WebContents;
|
||||
// NB. sandbox: true is used because it makes navigations much (~8x) faster.
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
|
||||
// const w = new BrowserWindow({webPreferences: {sandbox: true}})
|
||||
// contents = w.webContents
|
||||
await contents.loadFile(path.join(fixturesPath, 'pages', 'fetch.html'));
|
||||
});
|
||||
after(() => contents.destroy());
|
||||
afterAll(() => contents.destroy());
|
||||
|
||||
async function ajax(url: string, options = {}) {
|
||||
return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`);
|
||||
@@ -895,7 +896,9 @@ describe('webRequest module', () => {
|
||||
ses.webRequest.onCompleted(null);
|
||||
});
|
||||
|
||||
contents.loadFile(path.join(fixturesPath, 'api', 'webrequest.html'), { query: { port: `${port}` } });
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
contents.loadFile(path.join(fixturesPath, 'api', 'webrequest.html'), { query: { port: `${port}` } })
|
||||
);
|
||||
await once(ipcMain, 'websocket-success');
|
||||
|
||||
expect(receivedHeaders['/websocket'].Upgrade[0]).to.equal('websocket');
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BrowserWindow } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { defer } from './lib/spec-helpers';
|
||||
import { closeWindow } from './lib/window-helpers';
|
||||
|
||||
// import { once } from 'node:events';
|
||||
|
||||
@@ -21,7 +23,7 @@ describe('webUtils module', () => {
|
||||
sandbox: false
|
||||
}
|
||||
});
|
||||
defer(() => w.close());
|
||||
defer(() => closeWindow(w));
|
||||
await w.loadFile(path.resolve(fixtures, 'pages', 'file-input.html'));
|
||||
const pathFromWebUtils = await w.webContents.executeJavaScript(
|
||||
'require("electron").webUtils.getPathForFile(new Blob([1, 2, 3]))'
|
||||
@@ -38,7 +40,7 @@ describe('webUtils module', () => {
|
||||
sandbox: false
|
||||
}
|
||||
});
|
||||
defer(() => w.close());
|
||||
defer(() => closeWindow(w));
|
||||
await w.loadFile(path.resolve(fixtures, 'pages', 'file-input.html'));
|
||||
const { debugger: debug } = w.webContents;
|
||||
debug.attach();
|
||||
@@ -3,6 +3,7 @@ import { flipFuses, FuseV1Config, FuseV1Options, FuseVersion } from '@electron/f
|
||||
import { resedit } from '@electron/packager/dist/resedit';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import * as nodeCrypto from 'node:crypto';
|
||||
@@ -60,9 +61,7 @@ const expectToHaveCrashed = (res: SpawnResult) => {
|
||||
}
|
||||
};
|
||||
|
||||
describe('fuses', function () {
|
||||
this.timeout(120000);
|
||||
|
||||
describe('fuses', { timeout: 120000 }, () => {
|
||||
let tmpDir: string;
|
||||
let appPath: string;
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as importedFs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as url from 'node:url';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
|
||||
import { expect, fs, path, url } from './lib/remote-tools';
|
||||
import {
|
||||
defer,
|
||||
getRemoteContext,
|
||||
ifdescribe,
|
||||
ifit,
|
||||
itremote,
|
||||
useRemoteContext,
|
||||
withDone,
|
||||
dangerouslyIgnoreWebContentsLoadResult
|
||||
} from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('asar package', () => {
|
||||
@@ -18,8 +25,8 @@ describe('asar package', () => {
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
describe('asar protocol', () => {
|
||||
it('sets __dirname correctly', async function () {
|
||||
after(function () {
|
||||
it('sets __dirname correctly', async () => {
|
||||
defer(() => {
|
||||
ipcMain.removeAllListeners('dirname');
|
||||
});
|
||||
|
||||
@@ -34,13 +41,13 @@ describe('asar package', () => {
|
||||
});
|
||||
const p = path.resolve(asarDir, 'web.asar', 'index.html');
|
||||
const dirnameEvent = once(ipcMain, 'dirname');
|
||||
w.loadFile(p);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(p));
|
||||
const [, dirname] = await dirnameEvent;
|
||||
expect(dirname).to.equal(path.dirname(p));
|
||||
});
|
||||
|
||||
it('loads script tag in html', async function () {
|
||||
after(function () {
|
||||
it('loads script tag in html', async () => {
|
||||
defer(() => {
|
||||
ipcMain.removeAllListeners('ping');
|
||||
});
|
||||
|
||||
@@ -55,15 +62,13 @@ describe('asar package', () => {
|
||||
});
|
||||
const p = path.resolve(asarDir, 'script.asar', 'index.html');
|
||||
const ping = once(ipcMain, 'ping');
|
||||
w.loadFile(p);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(p));
|
||||
const [, message] = await ping;
|
||||
expect(message).to.equal('pong');
|
||||
});
|
||||
|
||||
it('loads video tag in html', async function () {
|
||||
this.timeout(60000);
|
||||
|
||||
after(function () {
|
||||
it('loads video tag in html', { timeout: 60000 }, async () => {
|
||||
defer(() => {
|
||||
ipcMain.removeAllListeners('asar-video');
|
||||
});
|
||||
|
||||
@@ -77,7 +82,7 @@ describe('asar package', () => {
|
||||
}
|
||||
});
|
||||
const p = path.resolve(asarDir, 'video.asar', 'index.html');
|
||||
w.loadFile(p);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadFile(p));
|
||||
const [, message, error] = await once(ipcMain, 'asar-video');
|
||||
if (message === 'ended') {
|
||||
expect(error).to.be.null();
|
||||
@@ -117,18 +122,21 @@ describe('asar package', () => {
|
||||
|
||||
describe('worker threads', function () {
|
||||
// DISABLED-FIXME(#38192): only disabled for ASan.
|
||||
ifit(!process.env.IS_ASAN)('should start worker thread from asar file', function (callback) {
|
||||
const p = path.join(asarDir, 'worker_threads.asar', 'worker.js');
|
||||
const w = new Worker(p);
|
||||
ifit(!process.env.IS_ASAN)(
|
||||
'should start worker thread from asar file',
|
||||
withDone((done) => {
|
||||
const p = path.join(asarDir, 'worker_threads.asar', 'worker.js');
|
||||
const w = new Worker(p);
|
||||
|
||||
w.on('error', (err) => callback(err));
|
||||
w.on('message', (message) => {
|
||||
expect(message).to.equal('ping');
|
||||
w.terminate();
|
||||
w.on('error', (err) => done(err));
|
||||
w.on('message', (message) => {
|
||||
expect(message).to.equal('ping');
|
||||
w.terminate();
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,7 +153,6 @@ function promisify(_f: Function): any {
|
||||
describe('asar package', function () {
|
||||
const fixtures = path.join(__dirname, 'fixtures');
|
||||
const asarDir = path.join(fixtures, 'test.asar');
|
||||
const fs = require('node:fs') as typeof importedFs; // dummy, to fool typescript
|
||||
|
||||
useRemoteContext({
|
||||
url: url.pathToFileURL(path.join(fixtures, 'pages', 'blank.html')),
|
||||
@@ -1171,31 +1178,37 @@ describe('asar package', function () {
|
||||
expect(err.code).to.equal('ENOENT');
|
||||
});
|
||||
|
||||
it('handles null for options', function (done) {
|
||||
const p = path.join(asarDir, 'a.asar', 'dir1');
|
||||
fs.readdir(p, null, function (err, dirs) {
|
||||
try {
|
||||
expect(err).to.be.null();
|
||||
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
it(
|
||||
'handles null for options',
|
||||
withDone((done) => {
|
||||
const p = path.join(asarDir, 'a.asar', 'dir1');
|
||||
fs.readdir(p, null, function (err, dirs) {
|
||||
try {
|
||||
expect(err).to.be.null();
|
||||
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it('handles undefined for options', function (done) {
|
||||
const p = path.join(asarDir, 'a.asar', 'dir1');
|
||||
fs.readdir(p, undefined, function (err, dirs) {
|
||||
try {
|
||||
expect(err).to.be.null();
|
||||
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
it(
|
||||
'handles undefined for options',
|
||||
withDone((done) => {
|
||||
const p = path.join(asarDir, 'a.asar', 'dir1');
|
||||
fs.readdir(p, undefined, function (err, dirs) {
|
||||
try {
|
||||
expect(err).to.be.null();
|
||||
expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('fs.promises.readdir', function () {
|
||||
@@ -1707,7 +1720,7 @@ describe('asar package', function () {
|
||||
|
||||
/*
|
||||
describe('process.env.ELECTRON_NO_ASAR', function () {
|
||||
itremote('disables asar support in forked processes', function (done) {
|
||||
itremote('disables asar support in forked processes', withDone((done) => {
|
||||
const forked = ChildProcess.fork(path.join(__dirname, 'fixtures', 'module', 'no-asar.js'), [], {
|
||||
env: {
|
||||
ELECTRON_NO_ASAR: true
|
||||
@@ -1722,9 +1735,9 @@ describe('asar package', function () {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
itremote('disables asar support in spawned processes', function (done) {
|
||||
itremote('disables asar support in spawned processes', withDone((done) => {
|
||||
const spawned = ChildProcess.spawn(process.execPath, [path.join(__dirname, 'fixtures', 'module', 'no-asar.js')], {
|
||||
env: {
|
||||
ELECTRON_NO_ASAR: true,
|
||||
@@ -1746,7 +1759,7 @@ describe('asar package', function () {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
*/
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
@@ -1,7 +1,9 @@
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ifit, waitUntil } from './lib/spec-helpers';
|
||||
@@ -9,10 +11,16 @@ import { ifit, waitUntil } from './lib/spec-helpers';
|
||||
const fixturePath = path.resolve(__dirname, 'fixtures', 'crash-cases');
|
||||
|
||||
let children: cp.ChildProcessWithoutNullStreams[] = [];
|
||||
const userDataDirs: string[] = [];
|
||||
|
||||
const runFixtureAndEnsureCleanExit = async (args: string[], customEnv: NodeJS.ProcessEnv) => {
|
||||
let out = '';
|
||||
const child = cp.spawn(process.execPath, args, {
|
||||
// Give each fixture child its own profile so parallel workers (and the
|
||||
// multiple crash cases within this worker) don't contend on the default
|
||||
// Chromium userData singleton lock.
|
||||
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-crash-case-'));
|
||||
userDataDirs.push(userDataDir);
|
||||
const child = cp.spawn(process.execPath, [...args, `--user-data-dir=${userDataDir}`], {
|
||||
env: {
|
||||
...process.env,
|
||||
...customEnv
|
||||
@@ -62,12 +70,15 @@ const shouldRunCase = (crashCase: string) => {
|
||||
};
|
||||
|
||||
describe('crash cases', () => {
|
||||
afterEach(async () => {
|
||||
afterEach(async (ctx) => {
|
||||
for (const child of children) {
|
||||
child.kill();
|
||||
}
|
||||
await waitUntil(() => children.length === 0);
|
||||
await waitUntil(() => children.length === 0, ctx.signal);
|
||||
children.length = 0;
|
||||
for (const dir of userDataDirs.splice(0, userDataDirs.length)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
const cases = fs.readdirSync(fixturePath);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as deprecate from '../lib/common/deprecate';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
import * as WebSocket from 'ws';
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
@@ -21,7 +22,13 @@ import * as http from 'node:http';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { emittedNTimes, emittedUntil } from './lib/events-helpers';
|
||||
import { ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpers';
|
||||
import {
|
||||
ifit,
|
||||
listen,
|
||||
startRemoteControlApp,
|
||||
waitUntil,
|
||||
dangerouslyIgnoreWebContentsLoadResult
|
||||
} from './lib/spec-helpers';
|
||||
import { expectWarningMessages } from './lib/warning-helpers';
|
||||
import { closeAllWindows, closeWindow, cleanupWebContents } from './lib/window-helpers';
|
||||
|
||||
@@ -37,7 +44,7 @@ describe('chrome extensions', () => {
|
||||
let url: string;
|
||||
let port: number;
|
||||
let wss: WebSocket.Server;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
if (req.url === '/cors') {
|
||||
res.setHeader('Access-Control-Allow-Origin', 'http://example.com');
|
||||
@@ -56,7 +63,7 @@ describe('chrome extensions', () => {
|
||||
|
||||
({ port, url } = await listen(server));
|
||||
});
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
server.close();
|
||||
wss.close();
|
||||
await cleanupWebContents();
|
||||
@@ -417,10 +424,10 @@ describe('chrome extensions', () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
it('can cancel http requests', async () => {
|
||||
it('can cancel http requests', async (ctx) => {
|
||||
await w.loadURL(url);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
|
||||
await expect(waitUntil(haveRejectedFetch)).to.eventually.be.fulfilled();
|
||||
await expect(waitUntil(haveRejectedFetch, ctx.signal)).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('does not cancel http requests when no extension loaded', async () => {
|
||||
@@ -431,6 +438,9 @@ describe('chrome extensions', () => {
|
||||
|
||||
it('does not take precedence over Electron webRequest - http', async () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
// onBeforeRequest fires (and cancels) on the navigation itself, so the
|
||||
// outer promise resolves before loadURL settles; the IIFE then rejects
|
||||
// with ERR_FAILED.
|
||||
(async () => {
|
||||
customSession.webRequest.onBeforeRequest((details, callback) => {
|
||||
resolve();
|
||||
@@ -440,7 +450,7 @@ describe('chrome extensions', () => {
|
||||
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
|
||||
fetch(w.webContents, url);
|
||||
})();
|
||||
})().catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -452,7 +462,7 @@ describe('chrome extensions', () => {
|
||||
});
|
||||
await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port: `${port}` } });
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
|
||||
})();
|
||||
})().catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -471,7 +481,7 @@ describe('chrome extensions', () => {
|
||||
|
||||
describe('chrome.tabs', () => {
|
||||
let customSession: Session;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
|
||||
});
|
||||
@@ -551,7 +561,7 @@ describe('chrome extensions', () => {
|
||||
webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
try {
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
const [, resp] = await once(ipcMain, 'bg-page-message-response');
|
||||
expect(resp.message).to.deep.equal({ some: 'message' });
|
||||
expect(resp.sender.id).to.be.a('string');
|
||||
@@ -719,12 +729,12 @@ describe('chrome extensions', () => {
|
||||
const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
|
||||
expect(result).to.equal('red');
|
||||
});
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
});
|
||||
|
||||
it('should run content script at document_idle', async () => {
|
||||
await addExtension('content-script-document-idle');
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
const result = await w.webContents.executeJavaScript('document.body.style.backgroundColor');
|
||||
expect(result).to.equal('red');
|
||||
});
|
||||
@@ -735,7 +745,7 @@ describe('chrome extensions', () => {
|
||||
const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
|
||||
expect(result).to.equal('red');
|
||||
});
|
||||
w.loadURL(url);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(url));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -750,7 +760,7 @@ describe('chrome extensions', () => {
|
||||
|
||||
let server: http.Server;
|
||||
let port: number;
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer(async (_, res) => {
|
||||
try {
|
||||
const content = await fs.readFile(contentPath, 'utf-8');
|
||||
@@ -767,7 +777,7 @@ describe('chrome extensions', () => {
|
||||
session.defaultSession.extensions.loadExtension(contentScript);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
session.defaultSession.extensions.removeExtension('content-script-test');
|
||||
server.close();
|
||||
});
|
||||
@@ -792,7 +802,7 @@ describe('chrome extensions', () => {
|
||||
it('applies matching rules in subframes', async () => {
|
||||
const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2);
|
||||
|
||||
w.loadURL(`http://127.0.0.1:${port}`);
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL(`http://127.0.0.1:${port}`));
|
||||
const frameEvents = await detailsPromise;
|
||||
await Promise.all(
|
||||
frameEvents.map(async (frameEvent) => {
|
||||
@@ -991,7 +1001,7 @@ describe('chrome extensions', () => {
|
||||
let customSession: Session;
|
||||
let w = null as unknown as BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v3'));
|
||||
});
|
||||
@@ -1083,7 +1093,7 @@ describe('chrome extensions', () => {
|
||||
let customSession: Session;
|
||||
let w = null as unknown as BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-action-fail'));
|
||||
});
|
||||
@@ -1140,7 +1150,7 @@ describe('chrome extensions', () => {
|
||||
let customSession: Session;
|
||||
let w = null as unknown as BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
|
||||
});
|
||||
@@ -1423,7 +1433,7 @@ describe('chrome extensions', () => {
|
||||
let customSession: Session;
|
||||
let w = null as unknown as BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-scripting'));
|
||||
});
|
||||
@@ -1507,7 +1517,7 @@ describe('chrome extensions', () => {
|
||||
let driver: BrowserWindow;
|
||||
let victim: BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
extSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
otherSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await extSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'tabs-cross-session'));
|
||||
3
spec/fixtures/api/close.html
vendored
3
spec/fixtures/api/close.html
vendored
@@ -2,7 +2,8 @@
|
||||
<body>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
window.addEventListener('unload', function (e) {
|
||||
require('node:fs').writeFileSync(__dirname + '/close', 'close');
|
||||
const out = new URLSearchParams(location.search).get('out');
|
||||
require('node:fs').writeFileSync(out, 'close');
|
||||
}, false);
|
||||
window.onload = () => window.close();
|
||||
</script>
|
||||
|
||||
3
spec/fixtures/api/unload.html
vendored
3
spec/fixtures/api/unload.html
vendored
@@ -2,7 +2,8 @@
|
||||
<body>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
window.addEventListener('unload', function (e) {
|
||||
require('node:fs').writeFileSync(__dirname + '/unload', 'unload');
|
||||
const out = new URLSearchParams(location.search).get('out');
|
||||
require('node:fs').writeFileSync(out, 'unload');
|
||||
}, false);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -18,12 +18,31 @@ v8.setFlagsFromString('--expose_gc');
|
||||
chai_1.use(require('chai-as-promised'));
|
||||
chai_1.use(require('dirty-chai'));
|
||||
|
||||
// Mirror of spec/lib/remote-tools.ts for closures rewritten by
|
||||
// rewriteForRemoteEval() — each __vite_ssr_import_N__ becomes __rt.
|
||||
const __rt = {
|
||||
...net_helpers_1,
|
||||
...main_1,
|
||||
expect: chai_1.expect,
|
||||
once: node_events_1.once,
|
||||
setTimeout: promises_1.setTimeout,
|
||||
defer: require('../../../lib/defer-helpers').defer,
|
||||
path: require('node:path'),
|
||||
fs: require('node:fs'),
|
||||
url,
|
||||
http
|
||||
};
|
||||
|
||||
function fail(message) {
|
||||
process.parentPort.postMessage({ ok: false, message });
|
||||
}
|
||||
|
||||
process.parentPort.on('message', async (e) => {
|
||||
// Equivalent of beforeEach in spec/api-net-spec.ts
|
||||
if (e.data?.type === 'shutdown') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Equivalent of beforeEach in spec/api-net.spec.ts
|
||||
net_helpers_1.respondNTimes.routeFailure = false;
|
||||
|
||||
try {
|
||||
@@ -37,18 +56,19 @@ process.parentPort.on('message', async (e) => {
|
||||
await eval(e.data.fn);
|
||||
} catch (err) {
|
||||
fail(`${err}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Equivalent of afterEach in spec/api-net-spec.ts
|
||||
// Equivalent of afterEach in spec/api-net.spec.ts
|
||||
if (net_helpers_1.respondNTimes.routeFailure) {
|
||||
fail(
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test passed
|
||||
process.parentPort.postMessage({ ok: true });
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.parentPort.postMessage({ type: 'ready' });
|
||||
|
||||
6
spec/fixtures/apps/refresh-page/main.html
vendored
6
spec/fixtures/apps/refresh-page/main.html
vendored
@@ -1,9 +1,9 @@
|
||||
<html>
|
||||
<body>
|
||||
<!-- Use mocha which has a large enough js file -->
|
||||
<script src="mocha.js"></script>
|
||||
<!-- Use chai which has a large enough js file -->
|
||||
<script src="chai.js"></script>
|
||||
<script>
|
||||
mocha.setup('bdd');
|
||||
void chai;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4
spec/fixtures/apps/refresh-page/main.js
vendored
4
spec/fixtures/apps/refresh-page/main.js
vendored
@@ -25,8 +25,8 @@ app.once('ready', async () => {
|
||||
|
||||
protocol.handle('atom', (request) => {
|
||||
let { pathname } = new URL(request.url);
|
||||
if (pathname === '/mocha.js') {
|
||||
pathname = path.resolve(__dirname, '../../../node_modules/mocha/mocha.js');
|
||||
if (pathname === '/chai.js') {
|
||||
pathname = path.resolve(__dirname, '../../../node_modules/chai/chai.js');
|
||||
} else {
|
||||
pathname = path.join(__dirname, pathname);
|
||||
}
|
||||
|
||||
14
spec/fixtures/module/electron-esm-vs-cjs.mjs
vendored
Normal file
14
spec/fixtures/module/electron-esm-vs-cjs.mjs
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as esmElectron from 'electron';
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const cjsElectron = require('electron');
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
esm: Object.keys(esmElectron).sort(),
|
||||
cjs: Object.keys(cjsElectron).sort()
|
||||
})
|
||||
);
|
||||
process.exit(0);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import path = require('node:path');
|
||||
|
||||
import { BrowserWindow } from './lib/remote-tools';
|
||||
import { startRemoteControlApp } from './lib/spec-helpers';
|
||||
|
||||
describe('fuses', () => {
|
||||
@@ -1,12 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export async function getFiles(
|
||||
dir: string,
|
||||
test: (file: string) => boolean = (_: string) => true // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
): Promise<string[]> {
|
||||
return fs.promises
|
||||
.readdir(dir)
|
||||
.then((files) => files.map((file) => path.join(dir, file)))
|
||||
.then((files) => files.filter((file) => test(file)));
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { BrowserWindow, screen } from 'electron';
|
||||
|
||||
import { expect, assert } from 'chai';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as http from 'node:http';
|
||||
import * as nodePath from 'node:path';
|
||||
|
||||
import { HexColors, ScreenCapture, hasCapturableScreen } from './lib/screen-helpers';
|
||||
import { ifit, listen } from './lib/spec-helpers';
|
||||
import { ifit, listen, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
describe('webContents.setWindowOpenHandler', () => {
|
||||
@@ -20,34 +21,37 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('does not fire window creation events if the handler callback throws an error', (done) => {
|
||||
const error = new Error('oh no');
|
||||
const listeners = process.listeners('uncaughtException');
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.on('uncaughtException', (thrown) => {
|
||||
try {
|
||||
expect(thrown).to.equal(error);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
} finally {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
for (const listener of listeners) {
|
||||
process.on('uncaughtException', listener);
|
||||
it(
|
||||
'does not fire window creation events if the handler callback throws an error',
|
||||
withDone((done) => {
|
||||
const error = new Error('oh no');
|
||||
const listeners = process.listeners('uncaughtException');
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.on('uncaughtException', (thrown) => {
|
||||
try {
|
||||
expect(thrown).to.equal(error);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
} finally {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
for (const listener of listeners) {
|
||||
process.on('uncaughtException', listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
throw error;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it('does not fire window creation events if the handler callback returns a bad result', async () => {
|
||||
const bad = new Promise((resolve) => {
|
||||
@@ -171,13 +175,15 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
browserWindow.webContents.loadURL(
|
||||
`data:text/html,${encodeURIComponent(`
|
||||
dangerouslyIgnoreWebContentsLoadResult(
|
||||
browserWindow.webContents.loadURL(
|
||||
`data:text/html,${encodeURIComponent(`
|
||||
<form action="http://example.com" target="_blank" method="POST" id="form">
|
||||
<input name="key" value="value"></input>
|
||||
</form>
|
||||
<script>form.submit()</script>
|
||||
`)}`
|
||||
)
|
||||
);
|
||||
});
|
||||
const { url, frameName, features, disposition, referrer, postBody } = details;
|
||||
@@ -391,7 +397,7 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
let server: http.Server;
|
||||
let url: string;
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((request, response) => {
|
||||
switch (request.url) {
|
||||
case '/index':
|
||||
@@ -414,7 +420,7 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
url = (await listen(server)).url;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
210
spec/index.js
210
spec/index.js
@@ -1,210 +0,0 @@
|
||||
const { app, protocol } = require('electron');
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const v8 = require('node:v8');
|
||||
|
||||
const FAILURE_STATUS_KEY = 'Electron_Spec_Runner_Failures';
|
||||
|
||||
// We want to terminate on errors, not throw up a dialog
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Unhandled exception in main spec runner:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Tell ts-node which tsconfig to use
|
||||
process.env.TS_NODE_PROJECT = path.resolve(__dirname, '../tsconfig.spec.json');
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
|
||||
|
||||
// Some Linux machines have broken hardware acceleration support.
|
||||
if (process.env.ELECTRON_TEST_DISABLE_HARDWARE_ACCELERATION) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
v8.setFlagsFromString('--expose_gc');
|
||||
app.commandLine.appendSwitch('js-flags', '--expose_gc');
|
||||
// Prevent the spec runner quitting when the first window closes
|
||||
app.on('window-all-closed', () => null);
|
||||
|
||||
// Use fake device for Media Stream to replace actual camera and microphone.
|
||||
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
|
||||
app.commandLine.appendSwitch(
|
||||
'host-resolver-rules',
|
||||
[
|
||||
'MAP localhost2 127.0.0.1',
|
||||
'MAP ipv4.localhost2 10.0.0.1',
|
||||
'MAP ipv6.localhost2 [::1]',
|
||||
'MAP notfound.localhost2 ~NOTFOUND'
|
||||
].join(', ')
|
||||
);
|
||||
|
||||
// Enable features required by tests.
|
||||
app.commandLine.appendSwitch(
|
||||
'enable-features',
|
||||
[
|
||||
// spec/api-web-frame-main-spec.ts
|
||||
'DocumentPolicyIncludeJSCallStacksInCrashReports',
|
||||
// spec/spellchecker-spec.ts - allows spellcheck without user gesture
|
||||
// https://chromium-review.googlesource.com/c/chromium/src/+/7452579
|
||||
'UnrestrictSpellingAndGrammarForTesting'
|
||||
].join(',')
|
||||
);
|
||||
|
||||
global.standardScheme = 'app';
|
||||
global.zoomScheme = 'zoom';
|
||||
global.serviceWorkerScheme = 'sw';
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } },
|
||||
{ scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
|
||||
{ scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } },
|
||||
{ scheme: 'http-like', privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true } },
|
||||
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
|
||||
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
|
||||
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },
|
||||
{ scheme: 'no-fetch', privileges: { corsEnabled: true } },
|
||||
{ scheme: 'stream', privileges: { standard: true, stream: true } },
|
||||
{ scheme: 'foo', privileges: { standard: true } },
|
||||
{ scheme: 'bar', privileges: { standard: true } }
|
||||
]);
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(async () => {
|
||||
require('ts-node/register');
|
||||
|
||||
const argv = require('yargs')
|
||||
.boolean('ci')
|
||||
.array('files')
|
||||
.string('g')
|
||||
.alias('g', 'grep')
|
||||
.boolean('i')
|
||||
.alias('i', 'invert').argv;
|
||||
|
||||
const Mocha = require('mocha');
|
||||
const mochaOptions = {
|
||||
forbidOnly: process.env.CI
|
||||
};
|
||||
if (process.env.CI) {
|
||||
mochaOptions.retries = 3;
|
||||
}
|
||||
if (process.env.MOCHA_REPORTER) {
|
||||
mochaOptions.reporter = process.env.MOCHA_REPORTER;
|
||||
}
|
||||
if (process.env.MOCHA_MULTI_REPORTERS) {
|
||||
mochaOptions.reporterOptions = {
|
||||
reporterEnabled: process.env.MOCHA_MULTI_REPORTERS
|
||||
};
|
||||
}
|
||||
// The MOCHA_GREP and MOCHA_INVERT are used in some vendor builds for sharding
|
||||
// tests.
|
||||
if (process.env.MOCHA_GREP) {
|
||||
mochaOptions.grep = process.env.MOCHA_GREP;
|
||||
}
|
||||
if (process.env.MOCHA_INVERT) {
|
||||
mochaOptions.invert = process.env.MOCHA_INVERT === 'true';
|
||||
}
|
||||
const mocha = new Mocha(mochaOptions);
|
||||
|
||||
// Add a root hook on mocha to skip any tests that are disabled
|
||||
const disabledTests = new Set(JSON.parse(fs.readFileSync(path.join(__dirname, 'disabled-tests.json'), 'utf8')));
|
||||
mocha.suite.beforeEach(function () {
|
||||
// TODO(clavin): add support for disabling *suites* by title, not just tests
|
||||
if (disabledTests.has(this.currentTest?.fullTitle())) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// The cleanup method is registered this way rather than through an
|
||||
// `afterEach` at the top level so that it can run before other `afterEach`
|
||||
// methods.
|
||||
//
|
||||
// The order of events is:
|
||||
// 1. test completes,
|
||||
// 2. `defer()`-ed methods run, in reverse order,
|
||||
// 3. regular `afterEach` hooks run.
|
||||
const { runCleanupFunctions } = require('./lib/spec-helpers');
|
||||
mocha.suite.on('suite', function attach(suite) {
|
||||
suite.afterEach('cleanup', runCleanupFunctions);
|
||||
suite.on('suite', attach);
|
||||
});
|
||||
|
||||
if (!process.env.MOCHA_REPORTER) {
|
||||
mocha.ui('bdd').reporter('tap');
|
||||
}
|
||||
const mochaTimeout = process.env.MOCHA_TIMEOUT || 30000;
|
||||
mocha.timeout(mochaTimeout);
|
||||
|
||||
if (argv.grep) mocha.grep(argv.grep);
|
||||
if (argv.invert) mocha.invert();
|
||||
|
||||
const baseElectronDir = path.resolve(__dirname, '..');
|
||||
const validTestPaths =
|
||||
argv.files &&
|
||||
argv.files.map((file) => (path.isAbsolute(file) ? path.relative(baseElectronDir, file) : path.normalize(file)));
|
||||
const filter = (file) => {
|
||||
if (!/-spec\.[tj]s$/.test(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This allows you to run specific modules only:
|
||||
// npm run test -match=menu
|
||||
const moduleMatch = process.env.npm_config_match ? new RegExp(process.env.npm_config_match, 'g') : null;
|
||||
if (moduleMatch && !moduleMatch.test(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validTestPaths && !validTestPaths.includes(path.relative(baseElectronDir, file))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const { getFiles } = require('./get-files');
|
||||
const testFiles = await getFiles(__dirname, filter);
|
||||
for (const file of testFiles.sort()) {
|
||||
mocha.addFile(file);
|
||||
}
|
||||
|
||||
if (validTestPaths && validTestPaths.length > 0 && testFiles.length === 0) {
|
||||
console.error('Test files were provided, but they did not match any searched files');
|
||||
console.error('provided file paths (relative to electron/):', validTestPaths);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cb = () => {
|
||||
// Ensure the callback is called after runner is defined
|
||||
process.nextTick(() => {
|
||||
if (process.env.ELECTRON_FORCE_TEST_SUITE_EXIT === 'true') {
|
||||
console.log(`${FAILURE_STATUS_KEY}: ${runner.failures}`);
|
||||
process.kill(process.pid);
|
||||
} else {
|
||||
process.exit(runner.failures);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set up chai in the correct order
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('dirty-chai'));
|
||||
|
||||
// Show full object diff
|
||||
// https://github.com/chaijs/chai/issues/469
|
||||
chai.config.truncateThreshold = 0;
|
||||
|
||||
const runner = mocha.run(cb);
|
||||
|
||||
const RETRY_EVENT = Mocha?.Runner?.constants?.EVENT_TEST_RETRY || 'retry';
|
||||
|
||||
runner.on(RETRY_EVENT, (test, err) => {
|
||||
console.log(`Failure in test: "${test.fullTitle()}"`);
|
||||
if (err?.stack) console.log(err.stack.split('\n').slice(0, 3).join('\n'));
|
||||
console.log(`Retrying test (${test.currentRetry() + 1}/${test.retries()})...`);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('An error occurred while running the spec runner');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -26,7 +26,7 @@ export function getCodesignIdentity() {
|
||||
export async function copyMacOSFixtureApp(newDir: string, fixture: string | null = 'initial') {
|
||||
const appBundlePath = path.resolve(process.execPath, '../../..');
|
||||
const newPath = path.resolve(newDir, 'Electron.app');
|
||||
cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)]);
|
||||
cp.spawnSync('cp', ['-cR', appBundlePath, path.dirname(newPath)]);
|
||||
if (fixture) {
|
||||
const appDir = path.resolve(newPath, 'Contents/Resources/app');
|
||||
await fs.promises.mkdir(appDir, { recursive: true });
|
||||
|
||||
25
spec/lib/defer-helpers.ts
Normal file
25
spec/lib/defer-helpers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
type CleanupFunction = (() => void) | (() => Promise<void>);
|
||||
const cleanupFunctions: CleanupFunction[] = [];
|
||||
|
||||
export async function runCleanupFunctions() {
|
||||
// Drain before running so a throwing cleanup can't leave stale entries to
|
||||
// be re-run on the next test.
|
||||
const pending = cleanupFunctions.splice(0, cleanupFunctions.length);
|
||||
const errors: unknown[] = [];
|
||||
for (const cleanup of pending) {
|
||||
try {
|
||||
const r = cleanup();
|
||||
if (r instanceof Promise) {
|
||||
await r;
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
if (errors.length === 1) throw errors[0];
|
||||
if (errors.length > 1) throw new AggregateError(errors, `${errors.length} defer() cleanups failed`);
|
||||
}
|
||||
|
||||
export function defer(f: CleanupFunction) {
|
||||
cleanupFunctions.unshift(f);
|
||||
}
|
||||
@@ -2,9 +2,20 @@ import { expect } from 'chai';
|
||||
|
||||
import * as dns from 'node:dns';
|
||||
import * as http from 'node:http';
|
||||
import { Socket } from 'node:net';
|
||||
import * as http2 from 'node:http2';
|
||||
import * as https from 'node:https';
|
||||
import { AddressInfo, Socket } from 'node:net';
|
||||
import * as url from 'node:url';
|
||||
|
||||
import { defer, listen } from './spec-helpers';
|
||||
import { defer } from './defer-helpers';
|
||||
|
||||
export async function listen(server: http.Server | https.Server | http2.Http2SecureServer) {
|
||||
const hostname = '127.0.0.1';
|
||||
await new Promise<void>((resolve) => server.listen(0, hostname, () => resolve()));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
const protocol = server instanceof http.Server ? 'http' : 'https';
|
||||
return { port, hostname, url: url.format({ protocol, hostname, port }) };
|
||||
}
|
||||
|
||||
// See https://github.com/nodejs/node/issues/40702.
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
|
||||
62
spec/lib/remote-tools.ts
Normal file
62
spec/lib/remote-tools.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Anything an itremote()/remotely() closure references via `import` must come
|
||||
// from here. vite's SSR transform rewrites the spec file's import of this
|
||||
// module to `__vite_ssr_import_N__`, so `path.join(...)` in a closure becomes
|
||||
// `__vite_ssr_import_N__.path.join(...)`. runRemote()/remotely() replace every
|
||||
// `__vite_ssr_import_\d+__` with `__rt`, a renderer-side object whose keys
|
||||
// mirror these export names exactly — so `__rt.path.join(...)` resolves
|
||||
// correctly with no property-name guessing.
|
||||
|
||||
export * as path from 'node:path';
|
||||
export * as fs from 'node:fs';
|
||||
export * as url from 'node:url';
|
||||
export * as util from 'node:util';
|
||||
export * as os from 'node:os';
|
||||
export * as cp from 'node:child_process';
|
||||
export * as http from 'node:http';
|
||||
export { once } from 'node:events';
|
||||
export { setTimeout } from 'node:timers/promises';
|
||||
export { expect } from 'chai';
|
||||
export { BrowserWindow, nativeImage, net, session, webContents } from 'electron/main';
|
||||
// Renderer-only; undefined when this module is loaded in the main process,
|
||||
// but typed correctly for closures stringified into preload scripts.
|
||||
export { contextBridge, ipcRenderer, webFrame } from 'electron/renderer';
|
||||
export { defer } from './defer-helpers';
|
||||
export {
|
||||
collectStreamBody,
|
||||
collectStreamBodyBuffer,
|
||||
getResponse,
|
||||
kOneKiloByte,
|
||||
kOneMegaByte,
|
||||
randomBuffer,
|
||||
randomString,
|
||||
respondNTimes,
|
||||
respondOnce
|
||||
} from './net-helpers';
|
||||
|
||||
// Renderer-side mirror of the exports above. Keep the keys in sync.
|
||||
// Context-specific targets (e.g. the api-net utility-process fixture) can
|
||||
// declare their own __rt with additional keys for helpers that aren't
|
||||
// require()-able from a renderer.
|
||||
export const REMOTE_TOOLS_SHIM = `{
|
||||
path: require('node:path'),
|
||||
fs: require('node:fs'),
|
||||
url: require('node:url'),
|
||||
util: require('node:util'),
|
||||
os: require('node:os'),
|
||||
cp: require('node:child_process'),
|
||||
http: require('node:http'),
|
||||
once: require('node:events').once,
|
||||
setTimeout: require('node:timers/promises').setTimeout,
|
||||
expect: require('chai').expect,
|
||||
BrowserWindow: require('electron').BrowserWindow,
|
||||
nativeImage: require('electron').nativeImage,
|
||||
net: require('electron').net,
|
||||
session: require('electron').session,
|
||||
webContents: require('electron').webContents,
|
||||
}`;
|
||||
|
||||
const SSR_IMPORT_RE = /__vite_ssr_import_\d+__/g;
|
||||
|
||||
export function rewriteForRemoteEval(fn: Function): string {
|
||||
return String(fn).replace(SSR_IMPORT_RE, '__rt');
|
||||
}
|
||||
@@ -1,29 +1,55 @@
|
||||
import { BrowserWindow } from 'electron/main';
|
||||
|
||||
import { AssertionError } from 'chai';
|
||||
import { SuiteFunction, TestFunction } from 'mocha';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
|
||||
import * as childProcess from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import * as http from 'node:http';
|
||||
import * as http2 from 'node:http2';
|
||||
import * as https from 'node:https';
|
||||
import * as net from 'node:net';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import * as url from 'node:url';
|
||||
import * as v8 from 'node:v8';
|
||||
|
||||
const addOnly = <T>(fn: Function): T => {
|
||||
const wrapped = (...args: any[]) => {
|
||||
return fn(...args);
|
||||
};
|
||||
(wrapped as any).only = wrapped;
|
||||
(wrapped as any).skip = wrapped;
|
||||
return wrapped as any;
|
||||
};
|
||||
import { defer } from './defer-helpers';
|
||||
import { REMOTE_TOOLS_SHIM, rewriteForRemoteEval } from './remote-tools';
|
||||
|
||||
export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
|
||||
export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
|
||||
export { defer, runCleanupFunctions } from './defer-helpers';
|
||||
export { listen } from './net-helpers';
|
||||
|
||||
/**
|
||||
* Swallow a loadURL()/loadFile() rejection so it does not surface as an
|
||||
* unhandled rejection when the load is expected to be aborted (e.g. the test
|
||||
* awaits an event and then closes the window). Returns the same promise with
|
||||
* the rejection suppressed.
|
||||
*
|
||||
* Each call site is technical debt: follow-up work should replace these with
|
||||
* an explicit await or a targeted .catch() once the test's actual contract
|
||||
* is understood.
|
||||
*/
|
||||
export function dangerouslyIgnoreWebContentsLoadResult<T>(p: Promise<T>): Promise<T | void> {
|
||||
return p.catch(() => {});
|
||||
}
|
||||
|
||||
export const ifit = (condition: boolean) => it.runIf(condition);
|
||||
export const ifdescribe = (condition: boolean) => describe.runIf(condition);
|
||||
|
||||
type DoneCallback = (err?: unknown) => void;
|
||||
|
||||
/**
|
||||
* Adapts a callback-style test (receiving a `done` function) into a
|
||||
* vitest-compatible test that returns a Promise. `done()` resolves,
|
||||
* `done(err)` rejects.
|
||||
*/
|
||||
export function withDone(fn: (done: DoneCallback) => void): () => Promise<void> {
|
||||
return () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const done: DoneCallback = (err) => {
|
||||
if (err != null) reject(err instanceof Error ? err : new Error(String(err)));
|
||||
else resolve();
|
||||
};
|
||||
fn(done);
|
||||
});
|
||||
}
|
||||
|
||||
export const isWayland =
|
||||
process.platform === 'linux' &&
|
||||
@@ -31,22 +57,6 @@ export const isWayland =
|
||||
!!process.env.WAYLAND_DISPLAY ||
|
||||
process.argv.includes('--ozone-platform=wayland'));
|
||||
|
||||
type CleanupFunction = (() => void) | (() => Promise<void>);
|
||||
const cleanupFunctions: CleanupFunction[] = [];
|
||||
export async function runCleanupFunctions() {
|
||||
for (const cleanup of cleanupFunctions) {
|
||||
const r = cleanup();
|
||||
if (r instanceof Promise) {
|
||||
await r;
|
||||
}
|
||||
}
|
||||
cleanupFunctions.length = 0;
|
||||
}
|
||||
|
||||
export function defer(f: CleanupFunction) {
|
||||
cleanupFunctions.unshift(f);
|
||||
}
|
||||
|
||||
class RemoteControlApp {
|
||||
process: childProcess.ChildProcess;
|
||||
port: number;
|
||||
@@ -85,7 +95,9 @@ class RemoteControlApp {
|
||||
};
|
||||
|
||||
remotely = (script: Function, ...args: any[]): Promise<any> => {
|
||||
return this.remoteEval(`(${script})(...${JSON.stringify(args)})`);
|
||||
return this.remoteEval(
|
||||
`(() => { const __rt = ${REMOTE_TOOLS_SHIM}; return (${rewriteForRemoteEval(script)})(...${JSON.stringify(args)}); })()`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,11 +121,17 @@ export async function startRemoteControlApp(extraArgs: string[] = [], options?:
|
||||
return new RemoteControlApp(appProcess, port);
|
||||
}
|
||||
|
||||
export function waitUntil(callback: () => boolean | Promise<boolean>, opts: { rate?: number; timeout?: number } = {}) {
|
||||
export function waitUntil(
|
||||
callback: () => boolean | Promise<boolean>,
|
||||
signal: AbortSignal,
|
||||
opts: { rate?: number; timeout?: number } = {}
|
||||
) {
|
||||
const { rate = 10, timeout = 10000 } = opts;
|
||||
return (async () => {
|
||||
signal.throwIfAborted();
|
||||
|
||||
const ac = new AbortController();
|
||||
const signal = ac.signal;
|
||||
const combined = AbortSignal.any([signal, ac.signal]);
|
||||
let checkCompleted = false;
|
||||
let timedOut = false;
|
||||
|
||||
@@ -130,19 +148,25 @@ export function waitUntil(callback: () => boolean | Promise<boolean>, opts: { ra
|
||||
return result;
|
||||
};
|
||||
|
||||
setTimeout(timeout, { signal }).then(() => {
|
||||
timedOut = true;
|
||||
checkCompleted = true;
|
||||
});
|
||||
setTimeout(timeout, undefined, { signal: combined })
|
||||
.then(() => {
|
||||
timedOut = true;
|
||||
checkCompleted = true;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
while (checkCompleted === false) {
|
||||
if (signal.aborted) {
|
||||
ac.abort();
|
||||
throw signal.reason ?? new Error('waitUntil aborted');
|
||||
}
|
||||
const checkSatisfied = await check();
|
||||
if (checkSatisfied === true) {
|
||||
ac.abort();
|
||||
checkCompleted = true;
|
||||
return;
|
||||
} else {
|
||||
await setTimeout(rate);
|
||||
await setTimeout(rate, undefined, { signal: combined }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,16 +213,21 @@ export async function getRemoteContext() {
|
||||
}
|
||||
|
||||
export function useRemoteContext(opts?: any) {
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
remoteContext.unshift(await makeRemoteContext(opts));
|
||||
});
|
||||
after(() => {
|
||||
afterAll(async () => {
|
||||
const w = remoteContext.shift();
|
||||
w!.close();
|
||||
if (w && !w.isDestroyed()) {
|
||||
const closed = once(w, 'closed');
|
||||
w.close();
|
||||
await closed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function runRemote(type: 'skip' | 'none' | 'only', name: string, fn: Function, args?: any[]) {
|
||||
const src = rewriteForRemoteEval(fn);
|
||||
const wrapped = async () => {
|
||||
const w = await getRemoteContext();
|
||||
const { ok, message } = await w.webContents.executeJavaScript(`(async () => {
|
||||
@@ -207,7 +236,8 @@ async function runRemote(type: 'skip' | 'none' | 'only', name: string, fn: Funct
|
||||
const promises_1 = require('node:timers/promises')
|
||||
chai_1.use(require('chai-as-promised'))
|
||||
chai_1.use(require('dirty-chai'))
|
||||
await (${fn})(...${JSON.stringify(args ?? [])})
|
||||
const __rt = ${REMOTE_TOOLS_SHIM};
|
||||
await (${src})(...${JSON.stringify(args ?? [])})
|
||||
return {ok: true};
|
||||
} catch (e) {
|
||||
return {ok: false, message: e.message}
|
||||
@@ -243,14 +273,6 @@ export const itremote = Object.assign(
|
||||
}
|
||||
);
|
||||
|
||||
export async function listen(server: http.Server | https.Server | http2.Http2SecureServer) {
|
||||
const hostname = '127.0.0.1';
|
||||
await new Promise<void>((resolve) => server.listen(0, hostname, () => resolve()));
|
||||
const { port } = server.address() as net.AddressInfo;
|
||||
const protocol = server instanceof http.Server ? 'http' : 'https';
|
||||
return { port, hostname, url: url.format({ protocol, hostname, port }) };
|
||||
}
|
||||
|
||||
export function isTestingBindingAvailable() {
|
||||
try {
|
||||
process._linkedBinding('electron_common_testing');
|
||||
|
||||
@@ -4,6 +4,8 @@ import { expect } from 'chai';
|
||||
|
||||
import { once } from 'node:events';
|
||||
|
||||
import { runCleanupFunctions } from './defer-helpers';
|
||||
|
||||
async function ensureWindowIsClosed(window: BaseWindow | null) {
|
||||
if (window && !window.isDestroyed()) {
|
||||
if (window instanceof BrowserWindow && window.webContents && !window.webContents.isDestroyed()) {
|
||||
@@ -24,34 +26,32 @@ async function ensureWindowIsClosed(window: BaseWindow | null) {
|
||||
}
|
||||
}
|
||||
|
||||
export const closeWindow = async (
|
||||
window: BaseWindow | null = null,
|
||||
{ assertNotWindows } = { assertNotWindows: true }
|
||||
) => {
|
||||
export const closeWindow = async (window: BaseWindow | null = null) => {
|
||||
await ensureWindowIsClosed(window);
|
||||
|
||||
if (assertNotWindows) {
|
||||
let windows = BaseWindow.getAllWindows();
|
||||
if (windows.length > 0) {
|
||||
setTimeout(async () => {
|
||||
// Wait until next tick to assert that all windows have been closed.
|
||||
windows = BaseWindow.getAllWindows();
|
||||
try {
|
||||
expect(windows).to.have.lengthOf(0);
|
||||
} finally {
|
||||
for (const win of windows) {
|
||||
await ensureWindowIsClosed(win);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function closeAllWindows(assertNotWindows = false) {
|
||||
export async function assertNoWindowsLeaked() {
|
||||
const windows = BaseWindow.getAllWindows();
|
||||
try {
|
||||
expect(windows).to.have.lengthOf(
|
||||
0,
|
||||
`${windows.length} window(s) leaked across test boundary (ids: ${windows.map((w) => w.id).join(', ')})`
|
||||
);
|
||||
} finally {
|
||||
for (const win of windows) {
|
||||
await ensureWindowIsClosed(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeAllWindows() {
|
||||
// Under vitest, setupFiles-level hooks run after test-file afterEach hooks,
|
||||
// so defer()ed cleanups would see already-destroyed windows. Running them
|
||||
// here (the innermost afterEach in practice) preserves the mocha ordering.
|
||||
await runCleanupFunctions();
|
||||
let windowsClosed = 0;
|
||||
for (const w of BaseWindow.getAllWindows()) {
|
||||
await closeWindow(w, { assertNotWindows });
|
||||
await closeWindow(w);
|
||||
windowsClosed++;
|
||||
}
|
||||
return windowsClosed;
|
||||
@@ -61,6 +61,7 @@ export async function cleanupWebContents() {
|
||||
let webContentsDestroyed = 0;
|
||||
const existingWCS = webContents.getAllWebContents();
|
||||
for (const contents of existingWCS) {
|
||||
if (contents.isDestroyed()) continue;
|
||||
const isDestroyed = once(contents, 'destroyed');
|
||||
contents.destroy();
|
||||
await isDestroyed;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app } from 'electron';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as uuid from 'uuid';
|
||||
import { it } from 'vitest';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as fs from 'node:fs/promises';
|
||||
@@ -154,7 +155,7 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
|
||||
additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
|
||||
}
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
w.loadURL('about:blank').catch(() => {});
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import * as path from 'node:path';
|
||||
|
||||
@@ -108,9 +110,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
|
||||
return foundPrivateAPIs;
|
||||
};
|
||||
|
||||
it('should not use private macOS APIs in main process', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
it('should not use private macOS APIs in main process', { timeout: 60000 }, () => {
|
||||
const binaries = getElectronBinaries();
|
||||
const foundPrivateAPIs = checkBinaryForPrivateAPIs(binaries.mainProcess, 'Electron main process');
|
||||
|
||||
@@ -139,9 +139,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
|
||||
}
|
||||
});
|
||||
|
||||
it('should not use private macOS APIs in Electron Framework', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
it('should not use private macOS APIs in Electron Framework', { timeout: 60000 }, () => {
|
||||
// Check the Electron Framework binary (mentioned in issue #49616)
|
||||
const binaries = getElectronBinaries();
|
||||
const foundAPIs = checkBinaryForPrivateAPIs(binaries.framework, 'Electron Framework');
|
||||
@@ -155,9 +153,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
|
||||
}
|
||||
});
|
||||
|
||||
it('should not use private macOS APIs in helper processes', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
it('should not use private macOS APIs in helper processes', { timeout: 60000 }, () => {
|
||||
const binaries = getElectronBinaries();
|
||||
const allFoundAPIs: Record<string, string[]> = {};
|
||||
|
||||
@@ -180,9 +176,7 @@ ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build',
|
||||
}
|
||||
});
|
||||
|
||||
it('should not reference private Objective-C classes', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
it('should not reference private Objective-C classes', { timeout: 60000 }, () => {
|
||||
// Check for private Objective-C classes (appear as _OBJC_CLASS_$_ClassName)
|
||||
const privateClasses = ['NSAccessibilityRemoteUIElement', 'CAContext'];
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { BrowserWindow, utilityProcess } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterEach, describe, it } from 'vitest';
|
||||
|
||||
import * as childProcess from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||
import { ifdescribe, ifit, withDone, dangerouslyIgnoreWebContentsLoadResult } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
const Module = require('node:module') as NodeJS.ModuleInternal;
|
||||
@@ -25,7 +26,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('@electron-ci/echo'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -56,7 +57,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('@electron-ci/uv-dlopen'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -69,13 +70,16 @@ describe('modules support', () => {
|
||||
|
||||
describe('q', () => {
|
||||
describe('Q.when', () => {
|
||||
it('emits the fulfil callback', (done) => {
|
||||
const Q = require('q');
|
||||
Q(true).then((val: boolean) => {
|
||||
expect(val).to.be.true();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it(
|
||||
'emits the fulfil callback',
|
||||
withDone((done) => {
|
||||
const Q = require('q');
|
||||
Q(true).then((val: boolean) => {
|
||||
expect(val).to.be.true();
|
||||
done();
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +103,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('electron/lol'); null }")).to.eventually.be.rejected();
|
||||
});
|
||||
|
||||
@@ -127,7 +131,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('electron'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -142,7 +146,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('electron/main'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -163,7 +167,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('electron/renderer'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -184,7 +188,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('electron/common'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -205,7 +209,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
await expect(w.webContents.executeJavaScript("{ require('electron/utility'); null }")).to.be.fulfilled();
|
||||
});
|
||||
|
||||
@@ -304,7 +308,7 @@ describe('modules support', () => {
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
w.loadURL('about:blank');
|
||||
dangerouslyIgnoreWebContentsLoadResult(w.loadURL('about:blank'));
|
||||
const result = await w.webContents.executeJavaScript('typeof require("q").when');
|
||||
expect(result).to.equal('function');
|
||||
});
|
||||
@@ -312,14 +316,32 @@ describe('modules support', () => {
|
||||
});
|
||||
|
||||
describe('esm', () => {
|
||||
// These run in a child Electron process because the test runner aliases
|
||||
// 'electron' through a CJS shim, which changes what `import('electron')`
|
||||
// returns.
|
||||
const runFixture = async () => {
|
||||
const child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'electron-esm-vs-cjs.mjs')], {
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
});
|
||||
let out = '';
|
||||
child.stdout.on('data', (d) => {
|
||||
out += d;
|
||||
});
|
||||
const [code] = await once(child, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
return JSON.parse(out) as { esm: string[]; cjs: string[] };
|
||||
};
|
||||
|
||||
it('can load the built-in "electron" module via ESM import', async () => {
|
||||
await expect(import('electron')).to.eventually.be.ok();
|
||||
const { esm } = await runFixture();
|
||||
expect(esm.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('the built-in "electron" module loaded via ESM import has the same exports as the CJS module', async () => {
|
||||
const esmElectron = await import('electron');
|
||||
const cjsElectron = require('electron');
|
||||
expect(Object.keys(esmElectron)).to.deep.equal(Object.keys(cjsElectron));
|
||||
const { esm, cjs } = await runFixture();
|
||||
// Node's CJS→ESM interop adds these wrapper keys; they're not API exports.
|
||||
const interopKeys = new Set(['default', 'module.exports']);
|
||||
expect(esm.filter((k) => !interopKeys.has(k))).to.deep.equal(cjs);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { webContents } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { afterAll, afterEach, beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import * as childProcess from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
spawn
|
||||
} from './lib/codesign-helpers';
|
||||
import { withTempDirectory } from './lib/fs-helpers';
|
||||
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
|
||||
import { expect } from './lib/remote-tools';
|
||||
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext, withDone } from './lib/spec-helpers';
|
||||
|
||||
const mainFixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
@@ -277,31 +278,40 @@ describe('node feature', () => {
|
||||
|
||||
describe('contexts', () => {
|
||||
describe('setTimeout called under Chromium event loop in browser process', () => {
|
||||
it('Can be scheduled in time', (done) => {
|
||||
setTimeout(done, 0);
|
||||
});
|
||||
it(
|
||||
'Can be scheduled in time',
|
||||
withDone((done) => {
|
||||
setTimeout(done, 0);
|
||||
})
|
||||
);
|
||||
|
||||
it('Can be promisified', (done) => {
|
||||
util.promisify(setTimeout)(0).then(done);
|
||||
});
|
||||
it(
|
||||
'Can be promisified',
|
||||
withDone((done) => {
|
||||
util.promisify(setTimeout)(0).then(done);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('setInterval called under Chromium event loop in browser process', () => {
|
||||
it('can be scheduled in time', (done) => {
|
||||
let interval: any = null;
|
||||
let clearing = false;
|
||||
const clear = () => {
|
||||
if (interval === null || clearing) return;
|
||||
it(
|
||||
'can be scheduled in time',
|
||||
withDone((done) => {
|
||||
let interval: any = null;
|
||||
let clearing = false;
|
||||
const clear = () => {
|
||||
if (interval === null || clearing) return;
|
||||
|
||||
// interval might trigger while clearing (remote is slow sometimes)
|
||||
clearing = true;
|
||||
clearInterval(interval);
|
||||
clearing = false;
|
||||
interval = null;
|
||||
done();
|
||||
};
|
||||
interval = setInterval(clear, 10);
|
||||
});
|
||||
// interval might trigger while clearing (remote is slow sometimes)
|
||||
clearing = true;
|
||||
clearInterval(interval);
|
||||
clearing = false;
|
||||
interval = null;
|
||||
done();
|
||||
};
|
||||
interval = setInterval(clear, 10);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const suspendListeners = (emitter: EventEmitter, eventName: string, callback: (...args: any[]) => void) => {
|
||||
@@ -712,72 +722,78 @@ describe('node feature', () => {
|
||||
let child: childProcess.ChildProcessWithoutNullStreams;
|
||||
let exitPromise: Promise<any[]>;
|
||||
|
||||
it('Fails for options disallowed by Node.js itself', (done) => {
|
||||
after(async () => {
|
||||
const [code, signal] = await exitPromise;
|
||||
expect(signal).to.equal(null);
|
||||
it(
|
||||
'Fails for options disallowed by Node.js itself',
|
||||
withDone((done) => {
|
||||
afterAll(async () => {
|
||||
const [code, signal] = await exitPromise;
|
||||
expect(signal).to.equal(null);
|
||||
|
||||
// Exit code 9 indicates cli flag parsing failure
|
||||
expect(code).to.equal(9);
|
||||
child.kill();
|
||||
});
|
||||
// Exit code 9 indicates cli flag parsing failure
|
||||
expect(code).to.equal(9);
|
||||
child.kill();
|
||||
});
|
||||
|
||||
const env = { ...process.env, NODE_OPTIONS: '--v8-options' };
|
||||
child = childProcess.spawn(process.execPath, { env });
|
||||
exitPromise = once(child, 'exit');
|
||||
const env = { ...process.env, NODE_OPTIONS: '--v8-options' };
|
||||
child = childProcess.spawn(process.execPath, { env });
|
||||
exitPromise = once(child, 'exit');
|
||||
|
||||
let output = '';
|
||||
let success = false;
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
let output = '';
|
||||
let success = false;
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/electron: --v8-options is not allowed in NODE_OPTIONS/m.test(output)) {
|
||||
success = true;
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/electron: --v8-options is not allowed in NODE_OPTIONS/m.test(output)) {
|
||||
success = true;
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
child.on('exit', () => {
|
||||
if (!success) {
|
||||
cleanup();
|
||||
done(new Error(`Unexpected output: ${output.toString()}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
child.on('exit', () => {
|
||||
if (!success) {
|
||||
cleanup();
|
||||
done(new Error(`Unexpected output: ${output.toString()}`));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it('Disallows crypto-related options', (done) => {
|
||||
after(() => {
|
||||
child.kill();
|
||||
});
|
||||
it(
|
||||
'Disallows crypto-related options',
|
||||
withDone((done) => {
|
||||
afterAll(() => {
|
||||
child.kill();
|
||||
});
|
||||
|
||||
const appPath = path.join(fixtures, 'module', 'noop.js');
|
||||
const env = { ...process.env, NODE_OPTIONS: '--use-openssl-ca' };
|
||||
child = childProcess.spawn(process.execPath, ['--enable-logging', appPath], { env });
|
||||
const appPath = path.join(fixtures, 'module', 'noop.js');
|
||||
const env = { ...process.env, NODE_OPTIONS: '--use-openssl-ca' };
|
||||
child = childProcess.spawn(process.execPath, ['--enable-logging', appPath], { env });
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/The NODE_OPTION --use-openssl-ca is not supported in Electron/m.test(output)) {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/The NODE_OPTION --use-openssl-ca is not supported in Electron/m.test(output)) {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
});
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
})
|
||||
);
|
||||
|
||||
it('does allow --require in non-packaged apps', async () => {
|
||||
const appPath = path.join(fixtures, 'module', 'noop.js');
|
||||
@@ -829,10 +845,10 @@ describe('node feature', () => {
|
||||
ifdescribe(shouldRunCodesignTests)('NODE_OPTIONS in signed app', function () {
|
||||
let identity = '';
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach((ctx) => {
|
||||
const result = getCodesignIdentity();
|
||||
if (result === null) {
|
||||
this.skip();
|
||||
ctx.skip();
|
||||
} else {
|
||||
identity = result;
|
||||
}
|
||||
@@ -855,14 +871,14 @@ describe('node feature', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('is disabled when invoked by alien binary in app bundle in ELECTRON_RUN_AS_NODE mode', async function () {
|
||||
it('is disabled when invoked by alien binary in app bundle in ELECTRON_RUN_AS_NODE mode', async (ctx) => {
|
||||
await withTempDirectory(async (dir) => {
|
||||
const appPath = await copyMacOSFixtureApp(dir);
|
||||
await signApp(appPath, identity);
|
||||
// Find system node and copy it to app bundle.
|
||||
const nodePath = process.env.PATH?.split(path.delimiter).find((dir) => fs.existsSync(path.join(dir, 'node')));
|
||||
if (!nodePath) {
|
||||
this.skip();
|
||||
ctx.skip();
|
||||
return;
|
||||
}
|
||||
const alienBinary = path.join(appPath, 'Contents/MacOS/node');
|
||||
@@ -891,36 +907,39 @@ describe('node feature', () => {
|
||||
let child: childProcess.ChildProcessWithoutNullStreams;
|
||||
let exitPromise: Promise<any[]>;
|
||||
|
||||
it('Prohibits crypto-related flags in ELECTRON_RUN_AS_NODE mode', (done) => {
|
||||
after(async () => {
|
||||
const [code, signal] = await exitPromise;
|
||||
expect(signal).to.equal(null);
|
||||
expect(code).to.equal(9);
|
||||
child.kill();
|
||||
});
|
||||
it(
|
||||
'Prohibits crypto-related flags in ELECTRON_RUN_AS_NODE mode',
|
||||
withDone((done) => {
|
||||
afterAll(async () => {
|
||||
const [code, signal] = await exitPromise;
|
||||
expect(signal).to.equal(null);
|
||||
expect(code).to.equal(9);
|
||||
child.kill();
|
||||
});
|
||||
|
||||
child = childProcess.spawn(process.execPath, ['--force-fips'], {
|
||||
env: { ELECTRON_RUN_AS_NODE: 'true' }
|
||||
});
|
||||
exitPromise = once(child, 'exit');
|
||||
child = childProcess.spawn(process.execPath, ['--force-fips'], {
|
||||
env: { ELECTRON_RUN_AS_NODE: 'true' }
|
||||
});
|
||||
exitPromise = once(child, 'exit');
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/.*The Node.js cli flag --force-fips is not supported in Electron/m.test(output)) {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/.*The Node.js cli flag --force-fips is not supported in Electron/m.test(output)) {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
});
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('process.stdout', () => {
|
||||
@@ -956,28 +975,35 @@ describe('node feature', () => {
|
||||
exitPromise = null as any;
|
||||
});
|
||||
|
||||
it('Supports starting the v8 inspector with --inspect/--inspect-brk', (done) => {
|
||||
child = childProcess.spawn(process.execPath, ['--inspect-brk', path.join(fixtures, 'module', 'run-as-node.js')], {
|
||||
env: { ELECTRON_RUN_AS_NODE: 'true' }
|
||||
});
|
||||
it(
|
||||
'Supports starting the v8 inspector with --inspect/--inspect-brk',
|
||||
withDone((done) => {
|
||||
child = childProcess.spawn(
|
||||
process.execPath,
|
||||
['--inspect-brk', path.join(fixtures, 'module', 'run-as-node.js')],
|
||||
{
|
||||
env: { ELECTRON_RUN_AS_NODE: 'true' }
|
||||
}
|
||||
);
|
||||
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
let output = '';
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/Debugger listening on ws:/m.test(output)) {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
const listener = (data: Buffer) => {
|
||||
output += data;
|
||||
if (/Debugger listening on ws:/m.test(output)) {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
});
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
})
|
||||
);
|
||||
|
||||
it('Supports starting the v8 inspector with --inspect and a provided port', async () => {
|
||||
child = childProcess.spawn(
|
||||
@@ -1026,53 +1052,56 @@ describe('node feature', () => {
|
||||
});
|
||||
|
||||
// IPC Electron child process not supported on Windows.
|
||||
ifit(process.platform !== 'win32')('does not crash when quitting with the inspector connected', function (done) {
|
||||
child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'delay-exit'), '--inspect=0'], {
|
||||
stdio: ['ipc']
|
||||
}) as childProcess.ChildProcessWithoutNullStreams;
|
||||
exitPromise = once(child, 'exit');
|
||||
ifit(process.platform !== 'win32')(
|
||||
'does not crash when quitting with the inspector connected',
|
||||
withDone((done) => {
|
||||
child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'delay-exit'), '--inspect=0'], {
|
||||
stdio: ['ipc']
|
||||
}) as childProcess.ChildProcessWithoutNullStreams;
|
||||
exitPromise = once(child, 'exit');
|
||||
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
const cleanup = () => {
|
||||
child.stderr.removeListener('data', listener);
|
||||
child.stdout.removeListener('data', listener);
|
||||
};
|
||||
|
||||
let output = '';
|
||||
const success = false;
|
||||
function listener(data: Buffer) {
|
||||
output += data;
|
||||
console.log(data.toString()); // NOTE: temporary debug logging to try to catch flake.
|
||||
const match = /^Debugger listening on (ws:\/\/.+:\d+\/.+)\n/m.exec(output.trim());
|
||||
if (match) {
|
||||
cleanup();
|
||||
// NOTE: temporary debug logging to try to catch flake.
|
||||
child.stderr.on('data', (m) => console.log(m.toString()));
|
||||
child.stdout.on('data', (m) => console.log(m.toString()));
|
||||
const w = (webContents as typeof ElectronInternal.WebContents).create();
|
||||
w.loadURL('about:blank')
|
||||
.then(() =>
|
||||
w.executeJavaScript(`new Promise(resolve => {
|
||||
let output = '';
|
||||
const success = false;
|
||||
function listener(data: Buffer) {
|
||||
output += data;
|
||||
console.log(data.toString()); // NOTE: temporary debug logging to try to catch flake.
|
||||
const match = /^Debugger listening on (ws:\/\/.+:\d+\/.+)\n/m.exec(output.trim());
|
||||
if (match) {
|
||||
cleanup();
|
||||
// NOTE: temporary debug logging to try to catch flake.
|
||||
child.stderr.on('data', (m) => console.log(m.toString()));
|
||||
child.stdout.on('data', (m) => console.log(m.toString()));
|
||||
const w = (webContents as typeof ElectronInternal.WebContents).create();
|
||||
w.loadURL('about:blank')
|
||||
.then(() =>
|
||||
w.executeJavaScript(`new Promise(resolve => {
|
||||
const connection = new WebSocket(${JSON.stringify(match[1])})
|
||||
connection.onopen = () => {
|
||||
connection.onclose = () => resolve()
|
||||
connection.close()
|
||||
}
|
||||
})`)
|
||||
)
|
||||
.then(() => {
|
||||
w.destroy();
|
||||
child.send('plz-quit');
|
||||
done();
|
||||
});
|
||||
)
|
||||
.then(() => {
|
||||
w.destroy();
|
||||
child.send('plz-quit');
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
child.on('exit', () => {
|
||||
if (!success) cleanup();
|
||||
});
|
||||
});
|
||||
child.stderr.on('data', listener);
|
||||
child.stdout.on('data', listener);
|
||||
child.on('exit', () => {
|
||||
if (!success) cleanup();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
it('Supports js binding', async () => {
|
||||
child = childProcess.spawn(
|
||||
@@ -1123,26 +1152,29 @@ describe('node feature', () => {
|
||||
child.kill();
|
||||
});
|
||||
|
||||
it('performs microtask checkpoint correctly', (done) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
const listener = () => {
|
||||
done(new Error('catch block is delayed to next tick'));
|
||||
};
|
||||
it(
|
||||
'performs microtask checkpoint correctly',
|
||||
withDone((done) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
const listener = () => {
|
||||
done(new Error('catch block is delayed to next tick'));
|
||||
};
|
||||
|
||||
const f3 = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
timer = setTimeout(listener);
|
||||
reject(new Error('oops'));
|
||||
});
|
||||
};
|
||||
const f3 = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
timer = setTimeout(listener);
|
||||
reject(new Error('oops'));
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
f3().catch(() => {
|
||||
clearTimeout(timer);
|
||||
done();
|
||||
setTimeout(() => {
|
||||
f3().catch(() => {
|
||||
clearTimeout(timer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
describe('type stripping', () => {
|
||||
it('strips TypeScript types automatically in the main process', async () => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user