mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
test: support for multimonitor tests (#47911)
* test: support for multimonitor tests * fix: update yarn.lock file * test: support any resolution for new displays * test: support display positioning * docs: multi-monitor tests * test: remove dummy test
This commit is contained in:
committed by
Keeley Hammond
parent
96c28c3325
commit
a6093b1575
79
docs/development/multi-monitor-testing.md
Normal file
79
docs/development/multi-monitor-testing.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Multi-Monitor Testing
|
||||
|
||||
The `virtualDisplay` addon leverages macOS CoreGraphics APIs to create virtual displays, allowing you to write and run multi-monitor tests without the need for physical monitors.
|
||||
|
||||
## Methods
|
||||
|
||||
#### `virtualDisplay.create([options])`
|
||||
|
||||
Creates a virtual display and returns a display ID.
|
||||
|
||||
```js @ts-nocheck
|
||||
const virtualDisplay = require('@electron-ci/virtual-display')
|
||||
// Default: 1920×1080 at origin (0, 0)
|
||||
const displayId = virtualDisplay.create()
|
||||
```
|
||||
|
||||
```js @ts-nocheck
|
||||
const virtualDisplay = require('@electron-ci/virtual-display')
|
||||
// Custom options (all parameters optional and have default values)
|
||||
const displayId = virtualDisplay.create({
|
||||
width: 2560, // Display width in pixels
|
||||
height: 1440, // Display height in pixels
|
||||
x: 1920, // X position (top-left corner)
|
||||
y: 0 // Y position (top-left corner)
|
||||
})
|
||||
```
|
||||
|
||||
**Returns:** `number` - Unique display ID used to identify the display. Returns `0` on failure to create display.
|
||||
|
||||
#### `virtualDisplay.destroy(displayId)`
|
||||
|
||||
Removes the virtual display.
|
||||
|
||||
```js @ts-nocheck
|
||||
const success = virtualDisplay.destroy(displayId)
|
||||
```
|
||||
|
||||
**Returns:** `boolean` - Success status
|
||||
|
||||
## Display Constraints
|
||||
|
||||
### Size Limits
|
||||
|
||||
Virtual displays are constrained to 720×720 pixels minimum and 8192×8192 pixels maximum. Actual limits may vary depending on your Mac's graphics capabilities, so sizes outside this range (like 9000×6000) may fail on some systems.
|
||||
|
||||
```js @ts-nocheck
|
||||
// Safe sizes for testing
|
||||
virtualDisplay.create({ width: 1920, height: 1080 }) // Full HD
|
||||
virtualDisplay.create({ width: 3840, height: 2160 }) // 4K
|
||||
```
|
||||
|
||||
### Positioning Behavior
|
||||
|
||||
macOS maintains a contiguous desktop space by automatically adjusting display positions if there are any overlaps or gaps. In case of either, the placement of the new origin is as close as possible to the requested location, without overlapping or leaving a gap between displays.
|
||||
|
||||
**Overlap:**
|
||||
|
||||
```js @ts-nocheck
|
||||
// Requested positions
|
||||
const display1 = virtualDisplay.create({ x: 0, y: 0, width: 1920, height: 1080 })
|
||||
const display2 = virtualDisplay.create({ x: 500, y: 0, width: 1920, height: 1080 })
|
||||
|
||||
// macOS automatically repositions display2 to x: 1920 to prevent overlap
|
||||
const actualBounds = screen.getAllDisplays().map(d => d.bounds)
|
||||
// Result: [{ x: 0, y: 0, width: 1920, height: 1080 },
|
||||
// { x: 1920, y: 0, width: 1920, height: 1080 }]
|
||||
```
|
||||
|
||||
**Gap:**
|
||||
|
||||
```js @ts-nocheck
|
||||
// Requested: gap between displays
|
||||
const display1 = virtualDisplay.create({ width: 1920, height: 1080, x: 0, y: 0 })
|
||||
const display2 = virtualDisplay.create({ width: 1920, height: 1080, x: 2000, y: 0 })
|
||||
// macOS snaps display2 to x: 1920 (eliminates 80px gap)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Always verify actual positions with `screen.getAllDisplays()` after creation, as macOS may adjust coordinates from the set values.
|
||||
@@ -95,3 +95,11 @@ To configure display scaling:
|
||||
|
||||
1. Push the Windows key and search for _Display settings_.
|
||||
2. Under _Scale and layout_, make sure that the device is set to 100%.
|
||||
|
||||
## Multi-Monitor Tests
|
||||
|
||||
Some Electron APIs require testing across multiple displays, such as screen detection, window positioning, and display-related events. For contributors working on these features, the `virtualDisplay` native addon enables you to create and position virtual displays programmatically, making it possible to test multi-monitor scenarios without any physical hardware.
|
||||
|
||||
For detailed information on using virtual displays in your tests, see [Multi-Monitor Testing](multi-monitor-testing.md).
|
||||
|
||||
**Platform support:** macOS only
|
||||
|
||||
90
spec/fixtures/native-addon/virtual-display/binding.gyp
vendored
Normal file
90
spec/fixtures/native-addon/virtual-display/binding.gyp
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"targets": [{
|
||||
"target_name": "virtual_display",
|
||||
"conditions": [
|
||||
['OS=="mac"', {
|
||||
"sources": [
|
||||
"src/addon.mm",
|
||||
"src/VirtualDisplayBridge.m"
|
||||
],
|
||||
"include_dirs": [
|
||||
"<!@(node -p \"require('node-addon-api').include\")",
|
||||
"include",
|
||||
"build_swift"
|
||||
],
|
||||
"dependencies": [
|
||||
"<!(node -p \"require('node-addon-api').gyp\")"
|
||||
],
|
||||
"libraries": [
|
||||
"<(PRODUCT_DIR)/libVirtualDisplay.dylib"
|
||||
],
|
||||
"defines": [
|
||||
"NODE_ADDON_API_CPP_EXCEPTIONS"
|
||||
],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ],
|
||||
"xcode_settings": {
|
||||
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
||||
"CLANG_ENABLE_OBJC_ARC": "YES",
|
||||
"CLANG_CXX_LIBRARY": "libc++",
|
||||
"SWIFT_OBJC_BRIDGING_HEADER": "include/VirtualDisplayBridge.h",
|
||||
"SWIFT_VERSION": "5.0",
|
||||
"SWIFT_OBJC_INTERFACE_HEADER_NAME": "virtual_display-Swift.h",
|
||||
"MACOSX_DEPLOYMENT_TARGET": "11.0",
|
||||
"OTHER_CFLAGS": [
|
||||
"-ObjC++",
|
||||
"-fobjc-arc"
|
||||
],
|
||||
"OTHER_LDFLAGS": [
|
||||
"-lswiftCore",
|
||||
"-lswiftFoundation",
|
||||
"-lswiftObjectiveC",
|
||||
"-lswiftDarwin",
|
||||
"-lswiftDispatch",
|
||||
"-L/usr/lib/swift",
|
||||
"-Wl,-rpath,/usr/lib/swift",
|
||||
"-Wl,-rpath,@loader_path"
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"action_name": "build_swift",
|
||||
"inputs": [
|
||||
"src/VirtualDisplay.swift",
|
||||
"src/Dummy.swift",
|
||||
"include/VirtualDisplayBridge.h"
|
||||
],
|
||||
"outputs": [
|
||||
"build_swift/libVirtualDisplay.dylib",
|
||||
"build_swift/virtual_display-Swift.h"
|
||||
],
|
||||
"action": [
|
||||
"swiftc",
|
||||
"src/VirtualDisplay.swift",
|
||||
"src/Dummy.swift",
|
||||
"-import-objc-header", "include/VirtualDisplayBridge.h",
|
||||
"-emit-objc-header-path", "./build_swift/virtual_display-Swift.h",
|
||||
"-emit-library", "-o", "./build_swift/libVirtualDisplay.dylib",
|
||||
"-emit-module", "-module-name", "virtual_display",
|
||||
"-module-link-name", "VirtualDisplay"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action_name": "copy_swift_lib",
|
||||
"inputs": [
|
||||
"<(module_root_dir)/build_swift/libVirtualDisplay.dylib"
|
||||
],
|
||||
"outputs": [
|
||||
"<(PRODUCT_DIR)/libVirtualDisplay.dylib"
|
||||
],
|
||||
"action": [
|
||||
"sh",
|
||||
"-c",
|
||||
"cp -f <(module_root_dir)/build_swift/libVirtualDisplay.dylib <(PRODUCT_DIR)/libVirtualDisplay.dylib && install_name_tool -id @rpath/libVirtualDisplay.dylib <(PRODUCT_DIR)/libVirtualDisplay.dylib"
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
]
|
||||
}]
|
||||
}
|
||||
120
spec/fixtures/native-addon/virtual-display/include/VirtualDisplayBridge.h
vendored
Normal file
120
spec/fixtures/native-addon/virtual-display/include/VirtualDisplayBridge.h
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
#ifndef VirtualDisplayBridge_h
|
||||
#define VirtualDisplayBridge_h
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface VirtualDisplayBridge : NSObject
|
||||
|
||||
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y;
|
||||
+ (BOOL)destroy:(NSInteger)displayId;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplay : NSObject {
|
||||
unsigned int _vendorID;
|
||||
unsigned int _productID;
|
||||
unsigned int _serialNum;
|
||||
NSString* _name;
|
||||
struct CGSize _sizeInMillimeters;
|
||||
unsigned int _maxPixelsWide;
|
||||
unsigned int _maxPixelsHigh;
|
||||
struct CGPoint _redPrimary;
|
||||
struct CGPoint _greenPrimary;
|
||||
struct CGPoint _bluePrimary;
|
||||
struct CGPoint _whitePoint;
|
||||
id _queue;
|
||||
id _terminationHandler;
|
||||
void* _client;
|
||||
unsigned int _displayID;
|
||||
unsigned int _hiDPI;
|
||||
NSArray* _modes;
|
||||
unsigned int _serverRPC_port;
|
||||
unsigned int _proxyRPC_port;
|
||||
unsigned int _clientHandler_port;
|
||||
}
|
||||
|
||||
@property(readonly, nonatomic) NSArray* modes;
|
||||
@property(readonly, nonatomic) unsigned int hiDPI;
|
||||
@property(readonly, nonatomic) unsigned int displayID;
|
||||
@property(readonly, nonatomic) id terminationHandler;
|
||||
@property(readonly, nonatomic) id queue;
|
||||
@property(readonly, nonatomic) struct CGPoint whitePoint;
|
||||
@property(readonly, nonatomic) struct CGPoint bluePrimary;
|
||||
@property(readonly, nonatomic) struct CGPoint greenPrimary;
|
||||
@property(readonly, nonatomic) struct CGPoint redPrimary;
|
||||
@property(readonly, nonatomic) unsigned int maxPixelsHigh;
|
||||
@property(readonly, nonatomic) unsigned int maxPixelsWide;
|
||||
@property(readonly, nonatomic) struct CGSize sizeInMillimeters;
|
||||
@property(readonly, nonatomic) NSString* name;
|
||||
@property(readonly, nonatomic) unsigned int serialNum;
|
||||
@property(readonly, nonatomic) unsigned int productID;
|
||||
@property(readonly, nonatomic) unsigned int vendorID;
|
||||
- (BOOL)applySettings:(id)arg1;
|
||||
- (void)dealloc;
|
||||
- (id)initWithDescriptor:(id)arg1;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplayDescriptor : NSObject {
|
||||
unsigned int _vendorID;
|
||||
unsigned int _productID;
|
||||
unsigned int _serialNum;
|
||||
NSString* _name;
|
||||
struct CGSize _sizeInMillimeters;
|
||||
unsigned int _maxPixelsWide;
|
||||
unsigned int _maxPixelsHigh;
|
||||
struct CGPoint _redPrimary;
|
||||
struct CGPoint _greenPrimary;
|
||||
struct CGPoint _bluePrimary;
|
||||
struct CGPoint _whitePoint;
|
||||
id _queue;
|
||||
id _terminationHandler;
|
||||
}
|
||||
|
||||
@property(retain, nonatomic) id queue;
|
||||
@property(retain, nonatomic) NSString* name;
|
||||
@property(nonatomic) struct CGPoint whitePoint;
|
||||
@property(nonatomic) struct CGPoint bluePrimary;
|
||||
@property(nonatomic) struct CGPoint greenPrimary;
|
||||
@property(nonatomic) struct CGPoint redPrimary;
|
||||
@property(nonatomic) unsigned int maxPixelsHigh;
|
||||
@property(nonatomic) unsigned int maxPixelsWide;
|
||||
@property(nonatomic) struct CGSize sizeInMillimeters;
|
||||
@property(nonatomic) unsigned int serialNum;
|
||||
@property(nonatomic) unsigned int productID;
|
||||
@property(nonatomic) unsigned int vendorID;
|
||||
- (void)dealloc;
|
||||
- (id)init;
|
||||
@property(copy, nonatomic) id terminationHandler;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplayMode : NSObject {
|
||||
unsigned int _width;
|
||||
unsigned int _height;
|
||||
double _refreshRate;
|
||||
}
|
||||
|
||||
@property(readonly, nonatomic) double refreshRate;
|
||||
@property(readonly, nonatomic) unsigned int height;
|
||||
@property(readonly, nonatomic) unsigned int width;
|
||||
- (id)initWithWidth:(unsigned int)arg1
|
||||
height:(unsigned int)arg2
|
||||
refreshRate:(double)arg3;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplaySettings : NSObject {
|
||||
NSArray* _modes;
|
||||
unsigned int _hiDPI;
|
||||
}
|
||||
|
||||
@property(nonatomic) unsigned int hiDPI;
|
||||
- (void)dealloc;
|
||||
- (id)init;
|
||||
@property(retain, nonatomic) NSArray* modes;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
6
spec/fixtures/native-addon/virtual-display/lib/virtual-display.js
vendored
Normal file
6
spec/fixtures/native-addon/virtual-display/lib/virtual-display.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = process.platform === 'darwin'
|
||||
? require('../build/Release/virtual_display.node')
|
||||
: {
|
||||
create: () => { throw new Error('Virtual displays only supported on macOS'); },
|
||||
destroy: () => { throw new Error('Virtual displays only supported on macOS'); }
|
||||
};
|
||||
20
spec/fixtures/native-addon/virtual-display/package.json
vendored
Normal file
20
spec/fixtures/native-addon/virtual-display/package.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@electron-ci/virtual-display",
|
||||
"version": "1.0.0",
|
||||
"description": "Virtual display for multi-monitor testing",
|
||||
"main": "./lib/virtual-display.js",
|
||||
"scripts": {
|
||||
"clean": "rm -rf build",
|
||||
"build-electron": "electron-rebuild",
|
||||
"build": "node-gyp configure && node-gyp build"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"node-addon-api": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
"node-gyp": "^11.1.0"
|
||||
}
|
||||
}
|
||||
151
spec/fixtures/native-addon/virtual-display/src/Dummy.swift
vendored
Normal file
151
spec/fixtures/native-addon/virtual-display/src/Dummy.swift
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import os.log
|
||||
|
||||
class DummyManager {
|
||||
struct DefinedDummy {
|
||||
var dummy: Dummy
|
||||
}
|
||||
|
||||
static var definedDummies: [Int: DefinedDummy] = [:]
|
||||
static var dummyCounter: Int = 0
|
||||
|
||||
static func createDummy(_ dummyDefinition: DummyDefinition, isPortrait _: Bool = false, serialNum: UInt32 = 0, doConnect: Bool = true) -> Int? {
|
||||
let dummy = Dummy(dummyDefinition: dummyDefinition, serialNum: serialNum, doConnect: doConnect)
|
||||
self.dummyCounter += 1
|
||||
self.definedDummies[self.dummyCounter] = DefinedDummy(dummy: dummy)
|
||||
return self.dummyCounter
|
||||
}
|
||||
|
||||
static func discardDummyByNumber(_ number: Int) {
|
||||
if let definedDummy = self.definedDummies[number] {
|
||||
if definedDummy.dummy.isConnected {
|
||||
definedDummy.dummy.disconnect()
|
||||
}
|
||||
}
|
||||
self.definedDummies[number] = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct DummyDefinition {
|
||||
let aspectWidth, aspectHeight, multiplierStep, minMultiplier, maxMultiplier: Int
|
||||
let refreshRates: [Double]
|
||||
let description: String
|
||||
let addSeparatorAfter: Bool
|
||||
|
||||
init(_ aspectWidth: Int, _ aspectHeight: Int, _ step: Int, _ refreshRates: [Double], _ description: String, _ addSeparatorAfter: Bool = false) {
|
||||
let minX: Int = 720
|
||||
let minY: Int = 720
|
||||
let maxX: Int = 8192
|
||||
let maxY: Int = 8192
|
||||
let minMultiplier = max(Int(ceil(Float(minX) / (Float(aspectWidth) * Float(step)))), Int(ceil(Float(minY) / (Float(aspectHeight) * Float(step)))))
|
||||
let maxMultiplier = min(Int(floor(Float(maxX) / (Float(aspectWidth) * Float(step)))), Int(floor(Float(maxY) / (Float(aspectHeight) * Float(step)))))
|
||||
|
||||
self.aspectWidth = aspectWidth
|
||||
self.aspectHeight = aspectHeight
|
||||
self.minMultiplier = minMultiplier
|
||||
self.maxMultiplier = maxMultiplier
|
||||
self.multiplierStep = step
|
||||
self.refreshRates = refreshRates
|
||||
self.description = description
|
||||
self.addSeparatorAfter = addSeparatorAfter
|
||||
}
|
||||
}
|
||||
|
||||
class Dummy: Equatable {
|
||||
var virtualDisplay: CGVirtualDisplay?
|
||||
var dummyDefinition: DummyDefinition
|
||||
let serialNum: UInt32
|
||||
var isConnected: Bool = false
|
||||
var displayIdentifier: CGDirectDisplayID = 0
|
||||
|
||||
static func == (lhs: Dummy, rhs: Dummy) -> Bool {
|
||||
lhs.serialNum == rhs.serialNum
|
||||
}
|
||||
|
||||
init(dummyDefinition: DummyDefinition, serialNum: UInt32 = 0, doConnect: Bool = true) {
|
||||
var storedSerialNum: UInt32 = serialNum
|
||||
if storedSerialNum == 0 {
|
||||
storedSerialNum = UInt32.random(in: 0 ... UInt32.max)
|
||||
}
|
||||
self.dummyDefinition = dummyDefinition
|
||||
self.serialNum = storedSerialNum
|
||||
if doConnect {
|
||||
_ = self.connect()
|
||||
}
|
||||
}
|
||||
|
||||
func getName() -> String {
|
||||
"Dummy \(self.dummyDefinition.description.components(separatedBy: " ").first ?? self.dummyDefinition.description)"
|
||||
}
|
||||
|
||||
func connect() -> Bool {
|
||||
if self.virtualDisplay != nil || self.isConnected {
|
||||
self.disconnect()
|
||||
}
|
||||
let name: String = self.getName()
|
||||
if let virtualDisplay = Dummy.createVirtualDisplay(self.dummyDefinition, name: name, serialNum: self.serialNum) {
|
||||
self.virtualDisplay = virtualDisplay
|
||||
self.displayIdentifier = virtualDisplay.displayID
|
||||
self.isConnected = true
|
||||
os_log("Display %{public}@ successfully connected", type: .info, "\(name)")
|
||||
return true
|
||||
} else {
|
||||
os_log("Failed to connect display %{public}@", type: .info, "\(name)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
self.virtualDisplay = nil
|
||||
self.isConnected = false
|
||||
os_log("Disconnected virtual display: %{public}@", type: .info, "\(self.getName())")
|
||||
}
|
||||
|
||||
private static func waitForDisplayRegistration(_ displayId: CGDirectDisplayID) -> Bool {
|
||||
for _ in 0..<20 {
|
||||
var count: UInt32 = 0, displays = [CGDirectDisplayID](repeating: 0, count: 32)
|
||||
if CGGetActiveDisplayList(32, &displays, &count) == .success && displays[0..<Int(count)].contains(displayId) {
|
||||
return true
|
||||
}
|
||||
usleep(100000)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func createVirtualDisplay(_ definition: DummyDefinition, name: String, serialNum: UInt32, hiDPI: Bool = false) -> CGVirtualDisplay? {
|
||||
if let descriptor = CGVirtualDisplayDescriptor() {
|
||||
descriptor.queue = DispatchQueue.global(qos: .userInteractive)
|
||||
descriptor.name = name
|
||||
descriptor.whitePoint = CGPoint(x: 0.950, y: 1.000)
|
||||
descriptor.redPrimary = CGPoint(x: 0.454, y: 0.242)
|
||||
descriptor.greenPrimary = CGPoint(x: 0.353, y: 0.674)
|
||||
descriptor.bluePrimary = CGPoint(x: 0.157, y: 0.084)
|
||||
descriptor.maxPixelsWide = UInt32(definition.aspectWidth * definition.multiplierStep * definition.maxMultiplier)
|
||||
descriptor.maxPixelsHigh = UInt32(definition.aspectHeight * definition.multiplierStep * definition.maxMultiplier)
|
||||
let diagonalSizeRatio: Double = (24 * 25.4) / sqrt(Double(definition.aspectWidth * definition.aspectWidth + definition.aspectHeight * definition.aspectHeight))
|
||||
descriptor.sizeInMillimeters = CGSize(width: Double(definition.aspectWidth) * diagonalSizeRatio, height: Double(definition.aspectHeight) * diagonalSizeRatio)
|
||||
descriptor.serialNum = serialNum
|
||||
descriptor.productID = UInt32(min(definition.aspectWidth - 1, 255) * 256 + min(definition.aspectHeight - 1, 255))
|
||||
descriptor.vendorID = UInt32(0xF0F0)
|
||||
if let display = CGVirtualDisplay(descriptor: descriptor) {
|
||||
var modes = [CGVirtualDisplayMode?](repeating: nil, count: definition.maxMultiplier - definition.minMultiplier + 1)
|
||||
for multiplier in definition.minMultiplier ... definition.maxMultiplier {
|
||||
for refreshRate in definition.refreshRates {
|
||||
let width = UInt32(definition.aspectWidth * multiplier * definition.multiplierStep)
|
||||
let height = UInt32(definition.aspectHeight * multiplier * definition.multiplierStep)
|
||||
modes[multiplier - definition.minMultiplier] = CGVirtualDisplayMode(width: width, height: height, refreshRate: refreshRate)!
|
||||
}
|
||||
}
|
||||
if let settings = CGVirtualDisplaySettings() {
|
||||
settings.hiDPI = hiDPI ? 1 : 0
|
||||
settings.modes = modes as [Any]
|
||||
if display.applySettings(settings) {
|
||||
return waitForDisplayRegistration(display.displayID) ? display : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
54
spec/fixtures/native-addon/virtual-display/src/VirtualDisplay.swift
vendored
Normal file
54
spec/fixtures/native-addon/virtual-display/src/VirtualDisplay.swift
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import os.log
|
||||
|
||||
@objc public class VirtualDisplay: NSObject {
|
||||
@objc public static func create(width: Int, height: Int, x: Int, y: Int) -> Int {
|
||||
let refreshRates: [Double] = [60.0] // Always 60Hz default
|
||||
let description = "\(width)x\(height) Display"
|
||||
let definition = DummyDefinition(width, height, 1, refreshRates, description, false)
|
||||
let displayId = DummyManager.createDummy(definition) ?? 0
|
||||
positionDisplay(displayId: displayId, x: x, y: y)
|
||||
|
||||
return displayId
|
||||
}
|
||||
|
||||
@objc public static func destroy(id: Int) -> Bool {
|
||||
DummyManager.discardDummyByNumber(id)
|
||||
return true
|
||||
}
|
||||
|
||||
private static func positionDisplay(displayId: Int, x: Int, y: Int) {
|
||||
guard let definedDummy = DummyManager.definedDummies[displayId],
|
||||
definedDummy.dummy.isConnected else {
|
||||
os_log("VirtualDisplay: Cannot position display %{public}@: display not found or not connected", type: .error, "\(displayId)")
|
||||
return
|
||||
}
|
||||
|
||||
let cgDisplayId = definedDummy.dummy.displayIdentifier
|
||||
|
||||
var config: CGDisplayConfigRef? = nil
|
||||
let beginResult = CGBeginDisplayConfiguration(&config)
|
||||
|
||||
if beginResult != .success {
|
||||
os_log("VirtualDisplay: Cannot position display, failed to begin display configuration via CGBeginDisplayConfiguration: error %{public}@", type: .error, "\(beginResult.rawValue)")
|
||||
return
|
||||
}
|
||||
|
||||
let configResult = CGConfigureDisplayOrigin(config, cgDisplayId, Int32(x), Int32(y))
|
||||
|
||||
if configResult != .success {
|
||||
os_log("VirtualDisplay: Cannot position display, failed to configure display origin via CGConfigureDisplayOrigin: error %{public}@", type: .error, "\(configResult.rawValue)")
|
||||
CGCancelDisplayConfiguration(config)
|
||||
return
|
||||
}
|
||||
|
||||
let completeResult = CGCompleteDisplayConfiguration(config, .permanently)
|
||||
|
||||
if completeResult == .success {
|
||||
os_log("VirtualDisplay: Successfully positioned display %{public}@ at (%{public}@, %{public}@)", type: .info, "\(displayId)", "\(x)", "\(y)")
|
||||
} else {
|
||||
os_log("VirtualDisplay: Cannot position display, failed to complete display configuration via CGCompleteDisplayConfiguration: error %{public}@", type: .error, "\(completeResult.rawValue)")
|
||||
}
|
||||
}
|
||||
}
|
||||
14
spec/fixtures/native-addon/virtual-display/src/VirtualDisplayBridge.m
vendored
Normal file
14
spec/fixtures/native-addon/virtual-display/src/VirtualDisplayBridge.m
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
#import "VirtualDisplayBridge.h"
|
||||
#import "../build_swift/virtual_display-Swift.h"
|
||||
|
||||
@implementation VirtualDisplayBridge
|
||||
|
||||
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y {
|
||||
return [VirtualDisplay createWithWidth:width height:height x:x y:y];
|
||||
}
|
||||
|
||||
+ (BOOL)destroy:(NSInteger)displayId {
|
||||
return [VirtualDisplay destroyWithId:(int)displayId];
|
||||
}
|
||||
|
||||
@end
|
||||
183
spec/fixtures/native-addon/virtual-display/src/addon.mm
vendored
Normal file
183
spec/fixtures/native-addon/virtual-display/src/addon.mm
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
#include <js_native_api.h>
|
||||
#include <node_api.h>
|
||||
#include "VirtualDisplayBridge.h"
|
||||
|
||||
namespace {
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
int default_val;
|
||||
int* ptr;
|
||||
} PropertySpec;
|
||||
|
||||
// Helper function to get an integer property from an object
|
||||
bool GetIntProperty(napi_env env,
|
||||
napi_value object,
|
||||
const char* prop_name,
|
||||
int* result,
|
||||
int default_value) {
|
||||
*result = default_value;
|
||||
|
||||
bool has_prop;
|
||||
if (napi_has_named_property(env, object, prop_name, &has_prop) != napi_ok ||
|
||||
!has_prop) {
|
||||
return true;
|
||||
}
|
||||
|
||||
napi_value prop_value;
|
||||
if (napi_get_named_property(env, object, prop_name, &prop_value) != napi_ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (napi_get_value_int32(env, prop_value, result) != napi_ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper function to validate and parse object properties
|
||||
bool ParseObjectProperties(napi_env env,
|
||||
napi_value object,
|
||||
PropertySpec props[],
|
||||
size_t prop_count) {
|
||||
// Process all properties
|
||||
for (size_t i = 0; i < prop_count; i++) {
|
||||
if (!GetIntProperty(env, object, props[i].name, props[i].ptr,
|
||||
props[i].default_val)) {
|
||||
char error_msg[50];
|
||||
snprintf(error_msg, sizeof(error_msg), "%s must be a number",
|
||||
props[i].name);
|
||||
napi_throw_error(env, NULL, error_msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unknown properties
|
||||
napi_value prop_names;
|
||||
uint32_t count;
|
||||
napi_get_property_names(env, object, &prop_names);
|
||||
napi_get_array_length(env, prop_names, &count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
napi_value prop_name;
|
||||
napi_get_element(env, prop_names, i, &prop_name);
|
||||
size_t len;
|
||||
char name[20];
|
||||
napi_get_value_string_utf8(env, prop_name, name, sizeof(name), &len);
|
||||
|
||||
bool found = false;
|
||||
for (size_t j = 0; j < prop_count; j++) {
|
||||
if (strcmp(name, props[j].name) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
napi_throw_error(env, NULL, "Object contains unknown properties");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// virtualDisplay.create()
|
||||
napi_value create(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 1;
|
||||
napi_value args[1];
|
||||
|
||||
if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int width = 1920, height = 1080, x = 0, y = 0;
|
||||
|
||||
PropertySpec props[] = {{"width", 1920, &width},
|
||||
{"height", 1080, &height},
|
||||
{"x", 0, &x},
|
||||
{"y", 0, &y}};
|
||||
|
||||
if (argc >= 1) {
|
||||
napi_valuetype valuetype;
|
||||
if (napi_typeof(env, args[0], &valuetype) != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Failed to get argument type");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (valuetype == napi_object) {
|
||||
if (!ParseObjectProperties(env, args[0], props,
|
||||
sizeof(props) / sizeof(props[0]))) {
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
napi_throw_error(env, NULL, "Expected an object as the argument");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger displayId = [VirtualDisplayBridge create:width
|
||||
height:height
|
||||
x:x
|
||||
y:y];
|
||||
|
||||
if (displayId == 0) {
|
||||
napi_throw_error(env, NULL, "Failed to create virtual display");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
napi_value result;
|
||||
if (napi_create_int64(env, displayId, &result) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// virtualDisplay.destroy()
|
||||
napi_value destroy(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 1;
|
||||
napi_value args[1];
|
||||
|
||||
if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (argc < 1) {
|
||||
napi_throw_error(env, NULL, "Expected number argument");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int64_t displayId;
|
||||
if (napi_get_value_int64(env, args[0], &displayId) != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Expected number argument");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
BOOL result = [VirtualDisplayBridge destroy:(NSInteger)displayId];
|
||||
|
||||
napi_value js_result;
|
||||
if (napi_get_boolean(env, result, &js_result) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return js_result;
|
||||
}
|
||||
|
||||
napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_property_descriptor descriptors[] = {
|
||||
{"create", NULL, create, NULL, NULL, NULL, napi_default, NULL},
|
||||
{"destroy", NULL, destroy, NULL, NULL, NULL, napi_default, NULL}};
|
||||
|
||||
if (napi_define_properties(env, exports,
|
||||
sizeof(descriptors) / sizeof(*descriptors),
|
||||
descriptors) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
||||
@@ -24,6 +24,7 @@
|
||||
"@electron-ci/uv-dlopen": "file:./fixtures/native-addon/uv-dlopen/",
|
||||
"@electron-ci/osr-gpu": "file:./fixtures/native-addon/osr-gpu/",
|
||||
"@electron-ci/external-ab": "file:./fixtures/native-addon/external-ab/",
|
||||
"@electron-ci/virtual-display": "file:./fixtures/native-addon/virtual-display/",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@electron/packager": "^18.3.2",
|
||||
"@types/sinon": "^9.0.4",
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
"@electron-ci/uv-dlopen@file:./fixtures/native-addon/uv-dlopen":
|
||||
version "0.0.1"
|
||||
|
||||
"@electron-ci/virtual-display@file:./fixtures/native-addon/virtual-display":
|
||||
version "1.0.0"
|
||||
dependencies:
|
||||
bindings "^1.5.0"
|
||||
node-addon-api "^8.3.0"
|
||||
|
||||
"@electron/asar@^3.2.1", "@electron/asar@^3.2.7":
|
||||
version "3.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.10.tgz#615cf346b734b23cafa4e0603551010bd0e50aa8"
|
||||
@@ -517,7 +523,7 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||
|
||||
bindings@^1.2.1:
|
||||
bindings@^1.2.1, bindings@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
||||
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
|
||||
@@ -1892,6 +1898,11 @@ node-addon-api@8.0.0:
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.0.0.tgz#5453b7ad59dd040d12e0f1a97a6fa1c765c5c9d2"
|
||||
integrity sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==
|
||||
|
||||
node-addon-api@^8.3.0:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.5.0.tgz#c91b2d7682fa457d2e1c388150f0dff9aafb8f3f"
|
||||
integrity sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==
|
||||
|
||||
node-fetch@^2.6.7:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
|
||||
Reference in New Issue
Block a user