feat: customize border radius of Views (#42320)

* feat: add View#setBorderRadius

test: initial setBorderRadius tests

fix: robustly set border radius

chore: add PAUSE_CAPTURE_TESTS for easier screencap dev

feat: add view border radius support

test: view border radius

refactor: cleanup view code

* maybe delay capture to fix tests?

* refactor: retry screen captures in an attempt to fix flakiness

* refactor: ScreenCapture constructor no longer async

* increase screen capture timeout, feels a little short

* refactor: move rounded rect util into chromium_src

* skip some capture tests on mas
This commit is contained in:
Sam Maddock 2024-07-16 20:16:25 -04:00 committed by GitHub
parent cbd11bb605
commit 778d3098a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 261 additions and 90 deletions

View file

@ -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;
}
/** Timeout to wait for expected color to match. */
static TIMEOUT = 3000;
public static async create (): Promise<ScreenCapture> {
const display = screen.getPrimaryDisplay();
return ScreenCapture._createImpl(display);
}
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;
};