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 => { 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); }); }); } });