|
@@ -75,67 +75,72 @@ function areColorsSimilar (
|
|
|
return distance <= distanceThreshold;
|
|
|
}
|
|
|
|
|
|
-function imageCenter (image: NativeImage): Electron.Point {
|
|
|
- const size = image.getSize();
|
|
|
+function displayCenter (display: Electron.Display): Electron.Point {
|
|
|
return {
|
|
|
- x: size.width / 2,
|
|
|
- y: size.height / 2
|
|
|
+ x: display.size.width / 2,
|
|
|
+ y: display.size.height / 2
|
|
|
};
|
|
|
}
|
|
|
+
|
|
|
+/** Resolve when approx. one frame has passed (30FPS) */
|
|
|
+export async function nextFrameTime (): Promise<void> {
|
|
|
+ return await new Promise((resolve) => {
|
|
|
+ setTimeout(resolve, 1000 / 30);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Utilities for creating and inspecting a screen capture.
|
|
|
*
|
|
|
+ * Set `PAUSE_CAPTURE_TESTS` env var to briefly pause during screen
|
|
|
+ * capture for easier inspection.
|
|
|
+ *
|
|
|
* 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<ScreenCapture> {
|
|
|
- const display = screen.getPrimaryDisplay();
|
|
|
- return ScreenCapture._createImpl(display);
|
|
|
- }
|
|
|
+ /** Timeout to wait for expected color to match. */
|
|
|
+ static TIMEOUT = 3000;
|
|
|
|
|
|
- public static async createForDisplay (
|
|
|
- display: Electron.Display
|
|
|
- ): Promise<ScreenCapture> {
|
|
|
- return ScreenCapture._createImpl(display);
|
|
|
+ constructor (display?: Electron.Display) {
|
|
|
+ this.display = display || screen.getPrimaryDisplay();
|
|
|
}
|
|
|
|
|
|
public async expectColorAtCenterMatches (hexColor: string) {
|
|
|
- return this._expectImpl(imageCenter(this.image), hexColor, true);
|
|
|
+ return this._expectImpl(displayCenter(this.display), hexColor, true);
|
|
|
}
|
|
|
|
|
|
public async expectColorAtCenterDoesNotMatch (hexColor: string) {
|
|
|
- return this._expectImpl(imageCenter(this.image), hexColor, false);
|
|
|
+ return this._expectImpl(displayCenter(this.display), hexColor, false);
|
|
|
}
|
|
|
|
|
|
public async expectColorAtPointOnDisplayMatches (
|
|
|
hexColor: string,
|
|
|
findPoint: (displaySize: Electron.Size) => Electron.Point
|
|
|
) {
|
|
|
- return this._expectImpl(findPoint(this.image.getSize()), hexColor, true);
|
|
|
+ return this._expectImpl(findPoint(this.display.size), hexColor, true);
|
|
|
}
|
|
|
|
|
|
- private static async _createImpl (display: Electron.Display) {
|
|
|
+ private async captureFrame (): Promise<NativeImage> {
|
|
|
const sources = await desktopCapturer.getSources({
|
|
|
types: ['screen'],
|
|
|
- thumbnailSize: display.size
|
|
|
+ thumbnailSize: this.display.size
|
|
|
});
|
|
|
|
|
|
const captureSource = sources.find(
|
|
|
- (source) => source.display_id === display.id.toString()
|
|
|
+ (source) => source.display_id === this.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}`
|
|
|
+ `Unable to find screen capture for display '${this.display.id}'\n\tAvailable displays: ${displayIds}`
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- return new ScreenCapture(captureSource.thumbnail);
|
|
|
+ if (process.env.PAUSE_CAPTURE_TESTS) {
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
|
+ }
|
|
|
+
|
|
|
+ return captureSource.thumbnail;
|
|
|
}
|
|
|
|
|
|
private async _expectImpl (
|
|
@@ -143,15 +148,29 @@ export class ScreenCapture {
|
|
|
expectedColor: string,
|
|
|
matchIsExpected: boolean
|
|
|
) {
|
|
|
- const actualColor = getPixelColor(this.image, point);
|
|
|
- const colorsMatch = areColorsSimilar(expectedColor, actualColor);
|
|
|
- const gotExpectedResult = matchIsExpected ? colorsMatch : !colorsMatch;
|
|
|
+ let frame: Electron.NativeImage;
|
|
|
+ let actualColor: string;
|
|
|
+ let gotExpectedResult: boolean = false;
|
|
|
+ const expiration = Date.now() + ScreenCapture.TIMEOUT;
|
|
|
+
|
|
|
+ // Continuously capture frames until we either see the expected result or
|
|
|
+ // reach a timeout. This helps avoid flaky tests in which a short waiting
|
|
|
+ // period is required for the expected result.
|
|
|
+ do {
|
|
|
+ frame = await this.captureFrame();
|
|
|
+ actualColor = getPixelColor(frame, point);
|
|
|
+ const colorsMatch = areColorsSimilar(expectedColor, actualColor);
|
|
|
+ gotExpectedResult = matchIsExpected ? colorsMatch : !colorsMatch;
|
|
|
+ if (gotExpectedResult) break;
|
|
|
+
|
|
|
+ await nextFrameTime(); // limit framerate
|
|
|
+ } while (Date.now() < expiration);
|
|
|
|
|
|
if (!gotExpectedResult) {
|
|
|
// Save the image as an artifact for better debugging
|
|
|
const artifactName = await createArtifactWithRandomId(
|
|
|
(id) => `color-mismatch-${id}.png`,
|
|
|
- this.image.toPNG()
|
|
|
+ frame.toPNG()
|
|
|
);
|
|
|
throw new AssertionError(
|
|
|
`Expected color at (${point.x}, ${point.y}) to ${
|
|
@@ -161,7 +180,7 @@ export class ScreenCapture {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private image: NativeImage;
|
|
|
+ private display: Electron.Display;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -174,5 +193,5 @@ export class ScreenCapture {
|
|
|
* - Win32 x64: virtual screen display is 0x0
|
|
|
*/
|
|
|
export const hasCapturableScreen = () => {
|
|
|
- return process.platform === 'darwin';
|
|
|
+ return process.env.CI ? process.platform === 'darwin' : true;
|
|
|
};
|