mirror of
https://github.com/electron/electron.git
synced 2026-01-06 22:24:03 -05:00
test: drag region tests
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"@electron/github-app-auth": "^3.2.0",
|
||||
"@electron/lint-roller": "^3.1.2",
|
||||
"@electron/typescript-definitions": "^9.1.5",
|
||||
"@hurdlegroup/robotjs": "^0.12.3",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
"@primer/octicons": "^10.0.0",
|
||||
"@types/minimist": "^1.2.5",
|
||||
|
||||
@@ -6979,119 +6979,4 @@ describe('BrowserWindow module', () => {
|
||||
await screenCapture.expectColorAtCenterMatches(HexColors.BLUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('draggable regions', () => {
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
ifit(hasCapturableScreen())('should allow the window to be dragged when enabled', async () => {
|
||||
// FIXME: nut-js has been removed from npm; we need to find a replacement
|
||||
// WOA fails to load libnut so we're using require to defer loading only
|
||||
// on supported platforms.
|
||||
// "@nut-tree\libnut-win32\build\Release\libnut.node is not a valid Win32 application."
|
||||
// @ts-ignore: nut-js is an optional dependency so it may not be installed
|
||||
const { mouse, straightTo, centerOf, Region, Button } = require('@nut-tree/nut-js') as typeof import('@nut-tree/nut-js');
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
|
||||
const w = new BrowserWindow({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: display.bounds.width / 2,
|
||||
height: display.bounds.height / 2,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden'
|
||||
});
|
||||
|
||||
const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html');
|
||||
w.loadFile(overlayHTML);
|
||||
await once(w, 'ready-to-show');
|
||||
|
||||
const winBounds = w.getBounds();
|
||||
const titleBarHeight = 30;
|
||||
const titleBarRegion = new Region(winBounds.x, winBounds.y, winBounds.width, titleBarHeight);
|
||||
const screenRegion = new Region(display.bounds.x, display.bounds.y, display.bounds.width, display.bounds.height);
|
||||
|
||||
const startPos = w.getPosition();
|
||||
|
||||
await mouse.setPosition(await centerOf(titleBarRegion));
|
||||
await mouse.pressButton(Button.LEFT);
|
||||
await mouse.drag(straightTo(centerOf(screenRegion)));
|
||||
|
||||
// Wait for move to complete
|
||||
await Promise.race([
|
||||
once(w, 'move'),
|
||||
setTimeout(100) // fallback for possible race condition
|
||||
]);
|
||||
|
||||
const endPos = w.getPosition();
|
||||
|
||||
expect(startPos).to.not.deep.equal(endPos);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should allow the window to be dragged when no WCO and --webkit-app-region: drag enabled', async () => {
|
||||
// FIXME: nut-js has been removed from npm; we need to find a replacement
|
||||
// @ts-ignore: nut-js is an optional dependency so it may not be installed
|
||||
const { mouse, straightTo, centerOf, Region, Button } = require('@nut-tree/nut-js') as typeof import('@nut-tree/nut-js');
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const w = new BrowserWindow({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: display.bounds.width / 2,
|
||||
height: display.bounds.height / 2,
|
||||
frame: false
|
||||
});
|
||||
|
||||
const basePageHTML = path.join(__dirname, 'fixtures', 'pages', 'base-page.html');
|
||||
w.loadFile(basePageHTML);
|
||||
await once(w, 'ready-to-show');
|
||||
|
||||
await w.webContents.executeJavaScript(`
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = \`
|
||||
#titlebar {
|
||||
|
||||
background-color: red;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000000000000;
|
||||
}
|
||||
\`;
|
||||
|
||||
const titleBar = document.createElement('title-bar');
|
||||
titleBar.id = 'titlebar';
|
||||
titleBar.textContent = 'test-titlebar';
|
||||
|
||||
document.body.append(style);
|
||||
document.body.append(titleBar);
|
||||
`);
|
||||
// allow time for titlebar to finish loading
|
||||
await setTimeout(2000);
|
||||
|
||||
const winBounds = w.getBounds();
|
||||
const titleBarHeight = 30;
|
||||
const titleBarRegion = new Region(winBounds.x, winBounds.y, winBounds.width, titleBarHeight);
|
||||
const screenRegion = new Region(display.bounds.x, display.bounds.y, display.bounds.width, display.bounds.height);
|
||||
|
||||
const startPos = w.getPosition();
|
||||
await mouse.setPosition(await centerOf(titleBarRegion));
|
||||
await mouse.pressButton(Button.LEFT);
|
||||
await mouse.drag(straightTo(centerOf(screenRegion)));
|
||||
|
||||
// Wait for move to complete
|
||||
await Promise.race([
|
||||
once(w, 'move'),
|
||||
setTimeout(1000) // fallback for possible race condition
|
||||
]);
|
||||
|
||||
const endPos = w.getPosition();
|
||||
|
||||
expect(startPos).to.not.deep.equal(endPos);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
267
spec/drag-region-spec.ts
Normal file
267
spec/drag-region-spec.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { BrowserWindow, screen } from 'electron/main';
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { hasCapturableScreen } from './lib/screen-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
|
||||
const fixtures = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
// Try to load robotjs
|
||||
let robot: typeof import('@hurdlegroup/robotjs');
|
||||
try {
|
||||
robot = require('@hurdlegroup/robotjs');
|
||||
} catch {
|
||||
// ignore. tests are skipped below if this is undefined.
|
||||
}
|
||||
|
||||
const draggablePageURL = pathToFileURL(
|
||||
path.join(fixtures, 'pages', 'draggable-page.html')
|
||||
);
|
||||
const iframePageURL = pathToFileURL(
|
||||
path.join(fixtures, 'pages', 'iframe.html')
|
||||
);
|
||||
const webviewPageURL = pathToFileURL(
|
||||
path.join(fixtures, 'pages', 'webview.html')
|
||||
);
|
||||
|
||||
const testWindowOpts: Electron.BrowserWindowConstructorOptions = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: Math.round(display.bounds.width / 2),
|
||||
height: Math.round(display.bounds.height / 2),
|
||||
frame: false
|
||||
};
|
||||
|
||||
const center = (rect: Electron.Rectangle): Electron.Point => ({
|
||||
x: Math.round(rect.x + rect.width / 2),
|
||||
y: Math.round(rect.y + rect.height / 2)
|
||||
});
|
||||
|
||||
const performDrag = async (
|
||||
w: BrowserWindow
|
||||
): Promise<{
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
}> => {
|
||||
const winBounds = w.getBounds();
|
||||
const winCenter = center(winBounds);
|
||||
const screenCenter = center(display.bounds);
|
||||
|
||||
const start = w.getPosition() as [number, number];
|
||||
const moved = once(w, 'move');
|
||||
|
||||
// Extra events based on research from https://github.com/octalmage/robotjs/issues/389
|
||||
robot.moveMouse(winCenter.x, winCenter.y);
|
||||
robot.mouseToggle('down', 'left');
|
||||
robot.moveMouse(winCenter.x + 2, winCenter.y + 2); // extra
|
||||
await setTimeout(200); // extra
|
||||
robot.dragMouse(screenCenter.x, screenCenter.y);
|
||||
robot.mouseToggle('up', 'left');
|
||||
|
||||
await Promise.race([moved, setTimeout(1000)]);
|
||||
|
||||
const end = w.getPosition() as [number, number];
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
const loadDraggableSubframe = async (w: BrowserWindow): Promise<void> => {
|
||||
let selector: string;
|
||||
let eventName: string;
|
||||
if (w.getURL() === iframePageURL.href) {
|
||||
selector = 'iframe';
|
||||
eventName = 'load';
|
||||
} else if (w.getURL() === webviewPageURL.href) {
|
||||
selector = 'webview';
|
||||
eventName = 'did-finish-load';
|
||||
} else {
|
||||
throw new Error('Unexpected page loaded');
|
||||
}
|
||||
|
||||
await w.webContents.executeJavaScript(`
|
||||
new Promise((resolve) => {
|
||||
const frame = document.querySelector('${selector}');
|
||||
frame.addEventListener(
|
||||
'${eventName}',
|
||||
() => resolve(),
|
||||
{ once: true }
|
||||
);
|
||||
frame.src = '${draggablePageURL.href}';
|
||||
})
|
||||
`);
|
||||
};
|
||||
|
||||
describe('draggable regions', function () {
|
||||
before(async function () {
|
||||
if (!robot || !robot.moveMouse || !hasCapturableScreen()) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
// The first window may not properly receive events due to UI transitions or
|
||||
// focus management. To mitigate this, warm up with a test run.
|
||||
const w = new BrowserWindow(testWindowOpts);
|
||||
await w.loadURL(draggablePageURL.href);
|
||||
await performDrag(w);
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
describe('main window', () => {
|
||||
let w: BrowserWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow(testWindowOpts);
|
||||
});
|
||||
|
||||
it('drags with app-region: drag', async () => {
|
||||
await w.loadURL(draggablePageURL.href);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
|
||||
it('does not drag when app-region: no-drag overlaps drag region', async () => {
|
||||
const noDragURL = new URL(draggablePageURL.href);
|
||||
noDragURL.searchParams.set('no-drag', '1');
|
||||
await w.loadURL(noDragURL.href);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.deep.equal(end);
|
||||
});
|
||||
|
||||
it('drags after navigation', async () => {
|
||||
await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'));
|
||||
await w.loadURL(draggablePageURL.href);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
|
||||
it('drags after in-page navigation', async () => {
|
||||
await w.loadURL(draggablePageURL.href);
|
||||
|
||||
const didNavigate = once(w.webContents, 'did-navigate-in-page');
|
||||
await w.webContents.executeJavaScript(`
|
||||
window.history.pushState({}, '', '/new-path');
|
||||
`);
|
||||
await didNavigate;
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
});
|
||||
|
||||
describe('child windows (window.open)', () => {
|
||||
let childWindow: BrowserWindow;
|
||||
|
||||
beforeEach(async () => {
|
||||
const parentWindow = new BrowserWindow({ show: false });
|
||||
await parentWindow.loadFile(
|
||||
path.join(fixtures, 'pages', 'base-page.html')
|
||||
);
|
||||
|
||||
parentWindow.webContents.setWindowOpenHandler(() => ({
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: testWindowOpts
|
||||
}));
|
||||
|
||||
const newBrowserWindow = once(parentWindow.webContents, 'did-create-window');
|
||||
|
||||
await parentWindow.webContents.executeJavaScript(
|
||||
`void window.open('${draggablePageURL.href}', '_blank');`
|
||||
);
|
||||
|
||||
[childWindow] = await newBrowserWindow;
|
||||
await once(childWindow, 'ready-to-show');
|
||||
});
|
||||
|
||||
it('drags with app-region: drag', async () => {
|
||||
const { start, end } = await performDrag(childWindow);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
|
||||
it('drags after navigation', async () => {
|
||||
await childWindow.loadURL(draggablePageURL.href);
|
||||
|
||||
const { start, end } = await performDrag(childWindow);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
});
|
||||
|
||||
for (const frameType of ['webview', 'iframe'] as const) {
|
||||
// FIXME: this behavior is broken before the tests were added
|
||||
// See: https://github.com/electron/electron/issues/49256
|
||||
describe.skip(`child frames (${frameType})`, () => {
|
||||
const subframePageURL = frameType === 'webview' ? webviewPageURL : iframePageURL;
|
||||
let w: BrowserWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow({
|
||||
...testWindowOpts,
|
||||
webPreferences: frameType === 'webview'
|
||||
? {
|
||||
webviewTag: true
|
||||
}
|
||||
: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('drags in subframe with app-region: drag', async () => {
|
||||
await w.loadURL(subframePageURL.href);
|
||||
await loadDraggableSubframe(w);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
|
||||
it('drags after subframe navigation', async () => {
|
||||
await w.loadURL(subframePageURL.href);
|
||||
await loadDraggableSubframe(w);
|
||||
await loadDraggableSubframe(w);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
|
||||
it('does not drag after host page navigation without draggable region', async () => {
|
||||
await w.loadURL(subframePageURL.href);
|
||||
await loadDraggableSubframe(w);
|
||||
|
||||
await w.loadFile(
|
||||
path.join(fixtures, 'pages', 'base-page.html')
|
||||
);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.deep.equal(end);
|
||||
});
|
||||
|
||||
it('drags after host page navigation', async () => {
|
||||
await w.loadURL(subframePageURL.href);
|
||||
await loadDraggableSubframe(w);
|
||||
await w.loadURL(subframePageURL.href);
|
||||
await loadDraggableSubframe(w);
|
||||
|
||||
const { start, end } = await performDrag(w);
|
||||
|
||||
expect(start).to.not.deep.equal(end);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
52
spec/fixtures/pages/draggable-page.html
vendored
52
spec/fixtures/pages/draggable-page.html
vendored
@@ -1,22 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Draggable Page</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
app-region: drag;
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Draggable Page</title>
|
||||
<style>
|
||||
.draggable {
|
||||
-webkit-app-region: drag;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="draggable"></div>
|
||||
</body>
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#no-drag {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: orangered;
|
||||
app-region: no-drag;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.location.search.includes('no-drag')) {
|
||||
const noDragDiv = document.createElement('div');
|
||||
noDragDiv.id = 'no-drag';
|
||||
document.body.appendChild(noDragDiv);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
27
spec/fixtures/pages/iframe.html
vendored
Normal file
27
spec/fixtures/pages/iframe.html
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>iframe Host</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe></iframe>
|
||||
</body>
|
||||
</html>
|
||||
26
spec/fixtures/pages/webview.html
vendored
Normal file
26
spec/fixtures/pages/webview.html
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>webview Host</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
webview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<webview></webview>
|
||||
</body>
|
||||
</html>
|
||||
32
yarn.lock
32
yarn.lock
@@ -594,6 +594,7 @@ __metadata:
|
||||
"@electron/github-app-auth": "npm:^3.2.0"
|
||||
"@electron/lint-roller": "npm:^3.1.2"
|
||||
"@electron/typescript-definitions": "npm:^9.1.5"
|
||||
"@hurdlegroup/robotjs": "npm:^0.12.3"
|
||||
"@octokit/rest": "npm:^20.1.2"
|
||||
"@primer/octicons": "npm:^10.0.0"
|
||||
"@types/minimist": "npm:^1.2.5"
|
||||
@@ -1090,6 +1091,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@hurdlegroup/robotjs@npm:^0.12.3":
|
||||
version: 0.12.3
|
||||
resolution: "@hurdlegroup/robotjs@npm:0.12.3"
|
||||
dependencies:
|
||||
node-addon-api: "npm:*"
|
||||
node-gyp: "npm:latest"
|
||||
node-gyp-build: "npm:^4.8.1"
|
||||
checksum: 10c0/6d5310320187bacfccaa7f636315b8d5d5a4ecdc586b7d3467b1def22ee31428d6b39f06dadb240e72d0ac4548068be1f896f2501dd7115d0100820a0640c562
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/external-editor@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "@inquirer/external-editor@npm:1.0.2"
|
||||
@@ -10019,6 +10031,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:*":
|
||||
version: 8.5.0
|
||||
resolution: "node-addon-api@npm:8.5.0"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/e4de0b4e70998fed7ef41933946f60565fc3a17cb83b7d626a0c0bb1f734cf7852e0e596f12681e7c8ed424163ee3cdbb4f0abaa9cc269d03f48834c263ba162
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "node-addon-api@npm:8.0.0"
|
||||
@@ -10056,6 +10077,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp-build@npm:^4.8.1":
|
||||
version: 4.8.4
|
||||
resolution: "node-gyp-build@npm:4.8.4"
|
||||
bin:
|
||||
node-gyp-build: bin.js
|
||||
node-gyp-build-optional: optional.js
|
||||
node-gyp-build-test: build-test.js
|
||||
checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:^11.4.2, node-gyp@npm:latest":
|
||||
version: 11.5.0
|
||||
resolution: "node-gyp@npm:11.5.0"
|
||||
|
||||
Reference in New Issue
Block a user