diff --git a/.circleci/config/base.yml b/.circleci/config/base.yml
index 8042719220..fc60394dc8 100644
--- a/.circleci/config/base.yml
+++ b/.circleci/config/base.yml
@@ -1642,6 +1642,8 @@ commands:
fi
- store_test_results:
path: src/junit
+ - store_artifacts:
+ path: src/electron/spec/artifacts
- *step-verify-mksnapshot
- *step-verify-chromedriver
diff --git a/spec/api-browser-view-spec.ts b/spec/api-browser-view-spec.ts
index 1f8edc2393..24551bfffb 100644
--- a/spec/api-browser-view-spec.ts
+++ b/spec/api-browser-view-spec.ts
@@ -3,7 +3,7 @@ import * as path from 'node:path';
import { BrowserView, BrowserWindow, screen, webContents } from 'electron/main';
import { closeWindow } from './lib/window-helpers';
import { defer, ifit, startRemoteControlApp } from './lib/spec-helpers';
-import { areColorsSimilar, captureScreen, getPixelColor } from './lib/screen-helpers';
+import { ScreenCapture } from './lib/screen-helpers';
import { once } from 'node:events';
describe('BrowserView module', () => {
@@ -88,13 +88,8 @@ describe('BrowserView module', () => {
w.setBrowserView(view);
await view.webContents.loadURL('data:text/html,hello there');
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
-
- expect(areColorsSimilar(centerColor, WINDOW_BACKGROUND_COLOR)).to.be.true();
+ const screenCapture = await ScreenCapture.createForDisplay(display);
+ await screenCapture.expectColorAtCenterMatches(WINDOW_BACKGROUND_COLOR);
});
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
@@ -114,13 +109,8 @@ describe('BrowserView module', () => {
w.setBackgroundColor(VIEW_BACKGROUND_COLOR);
await view.webContents.loadURL('data:text/html,hello there');
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
-
- expect(areColorsSimilar(centerColor, VIEW_BACKGROUND_COLOR)).to.be.true();
+ const screenCapture = await ScreenCapture.createForDisplay(display);
+ await screenCapture.expectColorAtCenterMatches(VIEW_BACKGROUND_COLOR);
});
});
diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts
index 0b4b241266..2a893041ca 100644
--- a/spec/api-browser-window-spec.ts
+++ b/spec/api-browser-window-spec.ts
@@ -11,7 +11,7 @@ import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersLi
import { emittedUntil, emittedNTimes } from './lib/events-helpers';
import { ifit, ifdescribe, defer, listen } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
-import { areColorsSimilar, captureScreen, HexColors, getPixelColor, hasCapturableScreen } from './lib/screen-helpers';
+import { HexColors, hasCapturableScreen, ScreenCapture } from './lib/screen-helpers';
import { once } from 'node:events';
import { setTimeout } from 'node:timers/promises';
import { setTimeout as syncSetTimeout } from 'node:timers';
@@ -1246,6 +1246,7 @@ describe('BrowserWindow module', () => {
}
});
+ // FIXME: disabled in `disabled-tests.json`
ifit(process.platform === 'darwin')('it does not activate the app if focusing an inactive panel', async () => {
// Show to focus app, then remove existing window
w.show();
@@ -6496,18 +6497,22 @@ describe('BrowserWindow module', () => {
await foregroundWindow.loadFile(colorFile);
await setTimeout(1000);
- const screenCapture = await captureScreen();
- const leftHalfColor = getPixelColor(screenCapture, {
- x: display.size.width / 4,
- y: display.size.height / 2
- });
- const rightHalfColor = getPixelColor(screenCapture, {
- x: display.size.width - (display.size.width / 4),
- y: display.size.height / 2
- });
- expect(areColorsSimilar(leftHalfColor, HexColors.GREEN)).to.be.true();
- expect(areColorsSimilar(rightHalfColor, HexColors.RED)).to.be.true();
+ const screenCapture = await ScreenCapture.createForDisplay(display);
+ await screenCapture.expectColorAtPointOnDisplayMatches(
+ HexColors.GREEN,
+ (size) => ({
+ x: size.width / 4,
+ y: size.height / 2
+ })
+ );
+ await screenCapture.expectColorAtPointOnDisplayMatches(
+ HexColors.RED,
+ (size) => ({
+ x: size.width * 3 / 4,
+ y: size.height / 2
+ })
+ );
});
ifit(process.platform === 'darwin')('Allows setting a transparent window via CSS', async () => {
@@ -6537,13 +6542,9 @@ describe('BrowserWindow module', () => {
await once(ipcMain, 'set-transparent');
await setTimeout(1000);
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
- expect(areColorsSimilar(centerColor, HexColors.PURPLE)).to.be.true();
+ const screenCapture = await ScreenCapture.createForDisplay(display);
+ await screenCapture.expectColorAtCenterMatches(HexColors.PURPLE);
});
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
@@ -6560,15 +6561,11 @@ describe('BrowserWindow module', () => {
await window.webContents.loadURL('data:text/html,
');
await setTimeout(1000);
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
- window.close();
-
+ const screenCapture = await ScreenCapture.createForDisplay(display);
// color-scheme is set to dark so background should not be white
- expect(areColorsSimilar(centerColor, HexColors.WHITE)).to.be.false();
+ await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
+
+ window.close();
}
});
});
@@ -6590,13 +6587,9 @@ describe('BrowserWindow module', () => {
await once(w, 'ready-to-show');
await setTimeout(1000);
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
- expect(areColorsSimilar(centerColor, HexColors.BLUE)).to.be.true();
+ const screenCapture = await ScreenCapture.createForDisplay(display);
+ await screenCapture.expectColorAtCenterMatches(HexColors.BLUE);
});
});
diff --git a/spec/disabled-tests.json b/spec/disabled-tests.json
index 873ca2415c..7419c28138 100644
--- a/spec/disabled-tests.json
+++ b/spec/disabled-tests.json
@@ -7,5 +7,6 @@
"session module ses.cookies should set cookie for standard scheme",
"webFrameMain module WebFrame.visibilityState should match window state",
"reporting api sends a report for a deprecation",
- "chromium features SpeechSynthesis should emit lifecycle events"
+ "chromium features SpeechSynthesis should emit lifecycle events",
+ "BrowserWindow module focus and visibility BrowserWindow.focus() it does not activate the app if focusing an inactive panel"
]
\ No newline at end of file
diff --git a/spec/guest-window-manager-spec.ts b/spec/guest-window-manager-spec.ts
index 21011a32c4..eff675b03a 100644
--- a/spec/guest-window-manager-spec.ts
+++ b/spec/guest-window-manager-spec.ts
@@ -1,6 +1,6 @@
import { BrowserWindow, screen } from 'electron';
import { expect, assert } from 'chai';
-import { areColorsSimilar, captureScreen, HexColors, getPixelColor } from './lib/screen-helpers';
+import { HexColors, ScreenCapture } from './lib/screen-helpers';
import { ifit } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
import { once } from 'node:events';
@@ -209,12 +209,8 @@ describe('webContents.setWindowOpenHandler', () => {
childWindow.setBounds(display.bounds);
await childWindow.webContents.executeJavaScript("const meta = document.createElement('meta'); meta.name = 'color-scheme'; meta.content = 'dark'; document.head.appendChild(meta); true;");
await setTimeoutAsync(1000);
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
+ const screenCapture = await ScreenCapture.createForDisplay(display);
// color-scheme is set to dark so background should not be white
- expect(areColorsSimilar(centerColor, HexColors.WHITE)).to.be.false();
+ await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
});
});
diff --git a/spec/lib/artifacts.ts b/spec/lib/artifacts.ts
new file mode 100644
index 0000000000..5b1b66aba6
--- /dev/null
+++ b/spec/lib/artifacts.ts
@@ -0,0 +1,36 @@
+import path = require('node:path');
+import fs = require('node:fs/promises');
+import { randomBytes } from 'node:crypto';
+
+const IS_CI = !!process.env.CI;
+const ARTIFACT_DIR = path.join(__dirname, '..', 'artifacts');
+
+async function ensureArtifactDir (): Promise {
+ if (!IS_CI) {
+ return;
+ }
+
+ await fs.mkdir(ARTIFACT_DIR, { recursive: true });
+}
+
+export async function createArtifact (
+ fileName: string,
+ data: Buffer
+): Promise {
+ if (!IS_CI) {
+ return;
+ }
+
+ await ensureArtifactDir();
+ await fs.writeFile(path.join(ARTIFACT_DIR, fileName), data);
+}
+
+export async function createArtifactWithRandomId (
+ makeFileName: (id: string) => string,
+ data: Buffer
+): Promise {
+ const randomId = randomBytes(12).toString('hex');
+ const fileName = makeFileName(randomId);
+ await createArtifact(fileName, data);
+ return fileName;
+}
diff --git a/spec/lib/screen-helpers.ts b/spec/lib/screen-helpers.ts
index 3c6f855f8d..3c8762c11c 100644
--- a/spec/lib/screen-helpers.ts
+++ b/spec/lib/screen-helpers.ts
@@ -1,60 +1,18 @@
-import * as path from 'node:path';
-import * as fs from 'node:fs';
import { screen, desktopCapturer, NativeImage } from 'electron';
-
-const fixtures = path.resolve(__dirname, '..', 'fixtures');
+import { createArtifactWithRandomId } from './artifacts';
+import { AssertionError } from 'chai';
export enum HexColors {
GREEN = '#00b140',
PURPLE = '#6a0dad',
RED = '#ff0000',
BLUE = '#0000ff',
- WHITE = '#ffffff'
-};
+ WHITE = '#ffffff',
+}
-/**
- * Capture the screen at the given point.
- *
- * NOTE: Not yet supported on Linux in CI due to empty sources list.
- */
-export const captureScreen = async (point: Electron.Point = { x: 0, y: 0 }): Promise => {
- const display = screen.getDisplayNearestPoint(point);
- const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: display.size });
- // Toggle to save screen captures for debugging.
- const DEBUG_CAPTURE = process.env.DEBUG_CAPTURE || false;
- if (DEBUG_CAPTURE) {
- for (const source of sources) {
- await fs.promises.writeFile(path.join(fixtures, `screenshot_${source.display_id}_${Date.now()}.png`), source.thumbnail.toPNG());
- }
- }
- const screenCapture = sources.find(source => source.display_id === `${display.id}`);
- // Fails when HDR is enabled on Windows.
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1247730
- if (!screenCapture) {
- const displayIds = sources.map(source => source.display_id);
- throw new Error(`Unable to find screen capture for display '${display.id}'\n\tAvailable displays: ${displayIds.join(', ')}`);
- }
- return screenCapture.thumbnail;
-};
-
-const formatHexByte = (val: number): string => {
- const str = val.toString(16);
- return str.length === 2 ? str : `0${str}`;
-};
-
-/**
- * Get the hex color at the given pixel coordinate in an image.
- */
-export const getPixelColor = (image: Electron.NativeImage, point: Electron.Point): string => {
- // image.crop crashes if point is fractional, so round to prevent that crash
- const pixel = image.crop({ x: Math.round(point.x), y: Math.round(point.y), width: 1, height: 1 });
- // TODO(samuelmaddock): NativeImage.toBitmap() should return the raw pixel
- // color, but it sometimes differs. Why is that?
- const [b, g, r] = pixel.toBitmap();
- return `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
-};
-
-const hexToRgba = (hexColor: string) => {
+function hexToRgba (
+ hexColor: string
+): [number, number, number, number] | undefined {
const match = hexColor.match(/^#([0-9a-fA-F]{6,8})$/);
if (!match) return;
@@ -63,34 +21,148 @@ const hexToRgba = (hexColor: string) => {
parseInt(colorStr.substring(0, 2), 16),
parseInt(colorStr.substring(2, 4), 16),
parseInt(colorStr.substring(4, 6), 16),
- parseInt(colorStr.substring(6, 8), 16) || 0xFF
+ parseInt(colorStr.substring(6, 8), 16) || 0xff
];
-};
+}
+
+function formatHexByte (val: number): string {
+ const str = val.toString(16);
+ return str.length === 2 ? str : `0${str}`;
+}
+
+/**
+ * Get the hex color at the given pixel coordinate in an image.
+ */
+function getPixelColor (
+ image: Electron.NativeImage,
+ point: Electron.Point
+): string {
+ // image.crop crashes if point is fractional, so round to prevent that crash
+ const pixel = image.crop({
+ x: Math.round(point.x),
+ y: Math.round(point.y),
+ width: 1,
+ height: 1
+ });
+ // TODO(samuelmaddock): NativeImage.toBitmap() should return the raw pixel
+ // color, but it sometimes differs. Why is that?
+ const [b, g, r] = pixel.toBitmap();
+ return `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
+}
/** Calculate euclidian distance between colors. */
-const colorDistance = (hexColorA: string, hexColorB: string) => {
+function colorDistance (hexColorA: string, hexColorB: string): number {
const colorA = hexToRgba(hexColorA);
const colorB = hexToRgba(hexColorB);
if (!colorA || !colorB) return -1;
return Math.sqrt(
Math.pow(colorB[0] - colorA[0], 2) +
- Math.pow(colorB[1] - colorA[1], 2) +
- Math.pow(colorB[2] - colorA[2], 2)
+ Math.pow(colorB[1] - colorA[1], 2) +
+ Math.pow(colorB[2] - colorA[2], 2)
);
-};
+}
/**
* Determine if colors are similar based on distance. This can be useful when
* comparing colors which may differ based on lossy compression.
*/
-export const areColorsSimilar = (
+function areColorsSimilar (
hexColorA: string,
hexColorB: string,
distanceThreshold = 90
-): boolean => {
+): boolean {
const distance = colorDistance(hexColorA, hexColorB);
return distance <= distanceThreshold;
-};
+}
+
+function imageCenter (image: NativeImage): Electron.Point {
+ const size = image.getSize();
+ return {
+ x: size.width / 2,
+ y: size.height / 2
+ };
+}
+/**
+ * Utilities for creating and inspecting a screen capture.
+ *
+ * NOTE: Not yet supported on Linux in CI due to empty sources list.
+ */
+export class ScreenCapture {
+ /** Use the async constructor `ScreenCapture.create()` instead. */
+ private constructor (image: NativeImage) {
+ this.image = image;
+ }
+
+ public static async create (): Promise {
+ const display = screen.getPrimaryDisplay();
+ return ScreenCapture._createImpl(display);
+ }
+
+ public static async createForDisplay (
+ display: Electron.Display
+ ): Promise {
+ return ScreenCapture._createImpl(display);
+ }
+
+ public async expectColorAtCenterMatches (hexColor: string) {
+ return this._expectImpl(imageCenter(this.image), hexColor, true);
+ }
+
+ public async expectColorAtCenterDoesNotMatch (hexColor: string) {
+ return this._expectImpl(imageCenter(this.image), hexColor, false);
+ }
+
+ public async expectColorAtPointOnDisplayMatches (
+ hexColor: string,
+ findPoint: (displaySize: Electron.Size) => Electron.Point
+ ) {
+ return this._expectImpl(findPoint(this.image.getSize()), hexColor, true);
+ }
+
+ private static async _createImpl (display: Electron.Display) {
+ const sources = await desktopCapturer.getSources({
+ types: ['screen'],
+ thumbnailSize: display.size
+ });
+
+ const captureSource = sources.find(
+ (source) => source.display_id === display.id.toString()
+ );
+ if (captureSource === undefined) {
+ const displayIds = sources.map((source) => source.display_id).join(', ');
+ throw new Error(
+ `Unable to find screen capture for display '${display.id}'\n\tAvailable displays: ${displayIds}`
+ );
+ }
+
+ return new ScreenCapture(captureSource.thumbnail);
+ }
+
+ private async _expectImpl (
+ point: Electron.Point,
+ expectedColor: string,
+ matchIsExpected: boolean
+ ) {
+ const actualColor = getPixelColor(this.image, point);
+ const colorsMatch = areColorsSimilar(expectedColor, actualColor);
+ const gotExpectedResult = matchIsExpected ? colorsMatch : !colorsMatch;
+
+ if (!gotExpectedResult) {
+ // Save the image as an artifact for better debugging
+ const artifactName = await createArtifactWithRandomId(
+ (id) => `color-mismatch-${id}.png`,
+ this.image.toPNG()
+ );
+ throw new AssertionError(
+ `Expected color at (${point.x}, ${point.y}) to ${
+ matchIsExpected ? 'match' : '*not* match'
+ } '${expectedColor}', but got '${actualColor}'. See the artifact '${artifactName}' for more information.`
+ );
+ }
+ }
+
+ private image: NativeImage;
+}
/**
* Whether the current VM has a valid screen which can be used to capture.
@@ -101,6 +173,8 @@ export const areColorsSimilar = (
* - Win32 ia32: skipped
*/
export const hasCapturableScreen = () => {
- return process.platform === 'darwin' ||
- (process.platform === 'win32' && process.arch === 'x64');
+ return (
+ process.platform === 'darwin' ||
+ (process.platform === 'win32' && process.arch === 'x64')
+ );
};
diff --git a/spec/webview-spec.ts b/spec/webview-spec.ts
index 3e4dca739f..d4aeddfcd7 100644
--- a/spec/webview-spec.ts
+++ b/spec/webview-spec.ts
@@ -1,6 +1,6 @@
import * as path from 'node:path';
import * as url from 'node:url';
-import { BrowserWindow, session, ipcMain, app, WebContents, screen } from 'electron/main';
+import { BrowserWindow, session, ipcMain, app, WebContents } from 'electron/main';
import { closeAllWindows } from './lib/window-helpers';
import { emittedUntil } from './lib/events-helpers';
import { ifit, ifdescribe, defer, itremote, useRemoteContext, listen } from './lib/spec-helpers';
@@ -9,7 +9,7 @@ import * as http from 'node:http';
import * as auth from 'basic-auth';
import { once } from 'node:events';
import { setTimeout } from 'node:timers/promises';
-import { areColorsSimilar, captureScreen, HexColors, getPixelColor } from './lib/screen-helpers';
+import { HexColors, ScreenCapture } from './lib/screen-helpers';
declare let WebView: any;
const features = process._linkedBinding('electron_common_features');
@@ -804,14 +804,8 @@ describe(' tag', function () {
await setTimeout(1000);
- const display = screen.getPrimaryDisplay();
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
-
- expect(areColorsSimilar(centerColor, WINDOW_BACKGROUND_COLOR)).to.be.true();
+ const screenCapture = await ScreenCapture.create();
+ await screenCapture.expectColorAtCenterMatches(WINDOW_BACKGROUND_COLOR);
});
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
@@ -823,14 +817,8 @@ describe(' tag', function () {
await setTimeout(1000);
- const display = screen.getPrimaryDisplay();
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
-
- expect(areColorsSimilar(centerColor, WINDOW_BACKGROUND_COLOR)).to.be.true();
+ const screenCapture = await ScreenCapture.create();
+ await screenCapture.expectColorAtCenterMatches(WINDOW_BACKGROUND_COLOR);
});
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
@@ -842,14 +830,8 @@ describe(' tag', function () {
await setTimeout(1000);
- const display = screen.getPrimaryDisplay();
- const screenCapture = await captureScreen();
- const centerColor = getPixelColor(screenCapture, {
- x: display.size.width / 2,
- y: display.size.height / 2
- });
-
- expect(areColorsSimilar(centerColor, HexColors.WHITE)).to.be.true();
+ const screenCapture = await ScreenCapture.create();
+ await screenCapture.expectColorAtCenterMatches(HexColors.WHITE);
});
});