diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index 70d57977af6a..8c9c45e60282 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -12,6 +12,7 @@ import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersLi import { emittedOnce, emittedUntil, emittedNTimes } from './events-helpers'; import { ifit, ifdescribe, defer, delay } from './spec-helpers'; import { closeWindow, closeAllWindows } from './window-helpers'; +import { areColorsSimilar, captureScreen, CHROMA_COLOR_HEX, getPixelColor } from './screen-helpers'; const features = process._linkedBinding('electron_common_features'); const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures'); @@ -4872,5 +4873,69 @@ describe('BrowserWindow module', () => { w.setBounds(newBounds); expect(w.getBounds()).to.deep.equal(newBounds); }); + + // Linux doesn't return any capture sources. + ifit(process.platform !== 'linux')('should not display a visible background', async () => { + const display = screen.getPrimaryDisplay(); + + const backgroundWindow = new BrowserWindow({ + ...display.bounds, + frame: false, + backgroundColor: CHROMA_COLOR_HEX, + hasShadow: false + }); + + await backgroundWindow.loadURL('about:blank'); + + const foregroundWindow = new BrowserWindow({ + ...display.bounds, + show: true, + transparent: true, + frame: false, + hasShadow: false + }); + + foregroundWindow.loadFile(path.join(__dirname, 'fixtures', 'pages', 'half-background-color.html')); + await emittedOnce(foregroundWindow, 'ready-to-show'); + + 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, CHROMA_COLOR_HEX)).to.be.true(); + expect(areColorsSimilar(rightHalfColor, '#ff0000')).to.be.true(); + }); + }); + + describe('"backgroundColor" option', () => { + afterEach(closeAllWindows); + + // Linux doesn't return any capture sources. + ifit(process.platform !== 'linux')('should display the set color', async () => { + const display = screen.getPrimaryDisplay(); + + const w = new BrowserWindow({ + ...display.bounds, + show: true, + backgroundColor: CHROMA_COLOR_HEX + }); + + w.loadURL('about:blank'); + await emittedOnce(w, 'ready-to-show'); + + const screenCapture = await captureScreen(); + const centerColor = getPixelColor(screenCapture, { + x: display.size.width / 2, + y: display.size.height / 2 + }); + + expect(areColorsSimilar(centerColor, CHROMA_COLOR_HEX)).to.be.true(); + }); }); }); diff --git a/spec-main/fixtures/pages/half-background-color.html b/spec-main/fixtures/pages/half-background-color.html new file mode 100644 index 000000000000..07e5ccdcadf9 --- /dev/null +++ b/spec-main/fixtures/pages/half-background-color.html @@ -0,0 +1,20 @@ + + + + + + + + +
+ + diff --git a/spec-main/screen-helpers.ts b/spec-main/screen-helpers.ts new file mode 100644 index 000000000000..26fbcb8ea2b0 --- /dev/null +++ b/spec-main/screen-helpers.ts @@ -0,0 +1,87 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { screen, desktopCapturer, NativeImage } from 'electron'; + +const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures'); + +/** Chroma key green. */ +export const CHROMA_COLOR_HEX = '#00b140'; + +/** + * 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 = false; + if (DEBUG_CAPTURE) { + for (const source of sources) { + await fs.promises.writeFile(path.join(fixtures, `screenshot_${source.display_id}.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 => { + const pixel = image.crop({ ...point, 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) => { + const match = hexColor.match(/^#([0-9a-fA-F]{6,8})$/); + if (!match) return; + + const colorStr = match[1]; + return [ + 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 + ]; +}; + +/** Calculate euclidian distance between colors. */ +const colorDistance = (hexColorA: string, hexColorB: string) => { + 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) + ); +}; + +/** + * 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 = ( + hexColorA: string, + hexColorB: string, + distanceThreshold = 90 +): boolean => { + const distance = colorDistance(hexColorA, hexColorB); + return distance <= distanceThreshold; +};