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:
Nilay Arya
2025-08-11 03:43:16 -04:00
committed by Keeley Hammond
parent 96c28c3325
commit a6093b1575
12 changed files with 738 additions and 1 deletions

View 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.

View File

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

View 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"
]
}
]
}]
]
}]
}

View 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

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

View 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"
}
}

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

View 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)")
}
}
}

View 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

View 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)

View File

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

View File

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