Skip to content

Commit

Permalink
update code after review
Browse files Browse the repository at this point in the history
  • Loading branch information
tzvielwix committed Jan 30, 2025
1 parent c0c22a9 commit 9c06a3a
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 61 deletions.
2 changes: 2 additions & 0 deletions src/Copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PilotPromptCreator } from "./utils/PilotPromptCreator";
import { ScreenCapturer } from "./utils/ScreenCapturer";
import { CopilotAPISearchPromptCreator } from "./utils/CopilotAPISearchPromptCreator";
import { ViewAnalysisPromptCreator } from "./utils/ViewAnalysisPromptCreator";
import downscaleImage from "./utils/downscaleImage";

/**
* The main Copilot class that provides AI-assisted testing capabilities for a given underlying testing framework.
Expand Down Expand Up @@ -48,6 +49,7 @@ export class Copilot {
this.snapshotManager = new SnapshotManager(
config.frameworkDriver,
snapshotComparator,
downscaleImage,
);
this.pilotPromptCreator = new PilotPromptCreator();
this.cacheHandler = new CacheHandler();
Expand Down
5 changes: 0 additions & 5 deletions src/integration tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ import { SnapshotComparator } from "@/utils/SnapshotComparator";

jest.mock("crypto");
jest.mock("fs");
jest.mock("../utils/ImageScaler", () => ({
downscaleImage: jest
.fn()
.mockImplementation((path: string) => Promise.resolve(path)),
}));

describe("Copilot Integration Tests", () => {
let mockedCachedSnapshotHash: SnapshotHashObject;
Expand Down
70 changes: 28 additions & 42 deletions src/utils/SnapshotManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,20 @@ import { SnapshotManager } from "./SnapshotManager";
import { TestingFrameworkDriver } from "@/types";
import { SnapshotComparator } from "@/utils/SnapshotComparator";
import crypto from "crypto";
import { downscaleImage } from "./ImageScaler";

// Mock the crypto module
jest.mock("crypto", () => ({
createHash: jest.fn().mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: jest.fn(),
}),
}));

jest.mock("./ImageScaler", () => ({
downscaleImage: jest
.fn()
.mockImplementation((path: string) => Promise.resolve(path)),
}));

const downscaleImageMock = downscaleImage as jest.MockedFunction<
typeof downscaleImage
>;

describe("SnapshotManager", () => {
let mockDriver: jest.Mocked<TestingFrameworkDriver>;
let mockSnapshotComparator: jest.Mocked<SnapshotComparator>;
let snapshotManager: SnapshotManager;
let mockDownscaleImage: jest.Mock;

beforeEach(() => {
mockDriver = {
Expand All @@ -37,19 +28,19 @@ describe("SnapshotManager", () => {
compareSnapshot: jest.fn(),
} as any;

snapshotManager = new SnapshotManager(mockDriver, mockSnapshotComparator);

// Mock timers
jest.useFakeTimers();
// Create a mock for downscaleImage
mockDownscaleImage = jest.fn();

snapshotManager = new SnapshotManager(
mockDriver,
mockSnapshotComparator,
mockDownscaleImage,
);
mockDownscaleImage.mockImplementation(async (imagePath) => imagePath);
// Clear mocks
jest.clearAllMocks();
});

afterEach(() => {
jest.useRealTimers();
});

describe("captureSnapshotImage", () => {
it("should return stable snapshot when UI becomes stable", async () => {
const snapshots = [
Expand Down Expand Up @@ -83,28 +74,28 @@ describe("SnapshotManager", () => {
},
);

const capturePromise = snapshotManager.captureSnapshotImage(100);
const resultPromise = snapshotManager.captureSnapshotImage(100);

await jest.advanceTimersByTimeAsync(200);
await jest.advanceTimersByTimeAsync(100);
// Wait for the snapshots to be captured and compared
await new Promise((resolve) => setTimeout(resolve, 300));

const result = await capturePromise;
const result = await resultPromise;

expect(result).toBe("/path/to/snapshot2.png");
expect(mockDriver.captureSnapshotImage).toHaveBeenCalledTimes(3);
expect(mockSnapshotComparator.generateHashes).toHaveBeenCalledTimes(4);
expect(mockSnapshotComparator.compareSnapshot).toHaveBeenCalledTimes(2);

expect(downscaleImageMock).toHaveBeenCalledTimes(3);
expect(downscaleImageMock).toHaveBeenNthCalledWith(
expect(mockDownscaleImage).toHaveBeenCalledTimes(3);
expect(mockDownscaleImage).toHaveBeenNthCalledWith(
1,
"/path/to/snapshot1.png",
);
expect(downscaleImageMock).toHaveBeenNthCalledWith(
expect(mockDownscaleImage).toHaveBeenNthCalledWith(
2,
"/path/to/snapshot2.png",
);
expect(downscaleImageMock).toHaveBeenNthCalledWith(
expect(mockDownscaleImage).toHaveBeenNthCalledWith(
3,
"/path/to/snapshot2.png",
);
Expand Down Expand Up @@ -141,18 +132,16 @@ describe("SnapshotManager", () => {

mockSnapshotComparator.compareSnapshot.mockReturnValue(false);

const capturePromise = snapshotManager.captureSnapshotImage(100, 250);
const resultPromise = snapshotManager.captureSnapshotImage(100, 250);

// Fast-forward past the timeout
await jest.advanceTimersByTimeAsync(300);
// Wait for the timeout to be reached
await new Promise((resolve) => setTimeout(resolve, 300));

const result = await capturePromise;
const result = await resultPromise;

expect(result).toBe("/path/to/snapshot3.png");
expect(mockDriver.captureSnapshotImage).toHaveBeenCalledTimes(3);

// Verify that downscaleImage was called
expect(downscaleImageMock).toHaveBeenCalledTimes(3);
expect(mockDownscaleImage).toHaveBeenCalledTimes(3);
});

it("should handle undefined snapshots", async () => {
Expand All @@ -163,7 +152,7 @@ describe("SnapshotManager", () => {
expect(result).toBeUndefined();
expect(mockDriver.captureSnapshotImage).toHaveBeenCalledTimes(1);
expect(mockSnapshotComparator.generateHashes).not.toHaveBeenCalled();
expect(downscaleImageMock).not.toHaveBeenCalled();
expect(mockDownscaleImage).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -192,15 +181,12 @@ describe("SnapshotManager", () => {
digest: mockDigest,
});

const capturePromise = snapshotManager.captureViewHierarchyString(100);

// Fast-forward time to trigger the first two different hierarchies
await jest.advanceTimersByTimeAsync(200);
const resultPromise = snapshotManager.captureViewHierarchyString(100);

// Fast-forward to get the stable hierarchy
await jest.advanceTimersByTimeAsync(100);
// Wait for the view hierarchies to be captured and compared
await new Promise((resolve) => setTimeout(resolve, 300));

const result = await capturePromise;
const result = await resultPromise;

expect(result).toBe("<view>2</view>");
expect(mockDriver.captureViewHierarchyString).toHaveBeenCalledTimes(3);
Expand Down
25 changes: 16 additions & 9 deletions src/utils/SnapshotManager.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { TestingFrameworkDriver } from "@/types";
import { SnapshotComparator } from "@/utils/SnapshotComparator";
import crypto from "crypto";
import { downscaleImage } from "./ImageScaler";

const DEFAULT_POLL_INTERVAL = 500; // ms
const DEFAULT_TIMEOUT = 5000; // ms
const DEFAULT_STABILITY_THRESHOLD = 0.05;

export class SnapshotManager {
private downscaleImage: (imagePath: string) => Promise<string>;
constructor(
private driver: TestingFrameworkDriver,
private snapshotComparator: SnapshotComparator,
) {}
downscaleImage: (imagePath: string) => Promise<string>,
) {
this.downscaleImage = downscaleImage;
}

private async waitForStableState<T>(
captureFunc: () => Promise<T | undefined>,
Expand Down Expand Up @@ -65,19 +68,23 @@ export class SnapshotManager {
return currentHash === lastHash;
}

private async captureSnapshotAndDownScaleImage(): Promise<
string | undefined
> {
const imagePath = await this.driver.captureSnapshotImage();
if (imagePath) {
const downscaledImagePath = await this.downscaleImage(imagePath);
return downscaledImagePath;
}
}

async captureSnapshotImage(
pollInterval?: number,
timeout?: number,
stabilityThreshold: number = DEFAULT_STABILITY_THRESHOLD,
): Promise<string | undefined> {
return this.waitForStableState(
async () => {
const imagePath = await this.driver.captureSnapshotImage();
if (imagePath) {
const downscaledImagePath = await downscaleImage(imagePath);
return downscaledImagePath;
}
},
async () => this.captureSnapshotAndDownScaleImage(),
(current, last) =>
this.compareSnapshots(current, last, stabilityThreshold),
pollInterval,
Expand Down
29 changes: 24 additions & 5 deletions src/utils/ImageScaler.ts → src/utils/downscaleImage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import gm from "gm";
import logger from "./logger";
import path from "path";
import os from "os";

const MAX_PIXELS = 2000000;

export async function downscaleImage(imagePath: string): Promise<string> {
async function downscaleImage(imagePath: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const parsedPath = path.parse(imagePath);
parsedPath.dir = os.tmpdir();
parsedPath.name += "_downscaled";
parsedPath.base = parsedPath.name + parsedPath.ext;
const downscaledPath = path.format(parsedPath);
gm(imagePath).size(function (
err: any,
size: { width: number; height: number },
) {
if (err) {
console.error("Error getting image size:", err);
logger.error({
message: `Error getting image size: ${err}`,
isBold: false,
color: "gray",
});
reject(err);
} else {
const originalWidth = size.width;
Expand All @@ -26,16 +39,22 @@ export async function downscaleImage(imagePath: string): Promise<string> {

gm(imagePath)
.resize(roundedWidth, roundedHeight)
.write(imagePath, (err: any) => {
.write(downscaledPath, (err: any) => {
if (err) {
console.error("Error downscaling image:", err);
logger.error({
message: `Error writing downscaled image: ${err}`,
isBold: false,
color: "gray",
});
reject(err);
} else {
resolve(imagePath);
resolve(downscaledPath);
}
});
}
}
});
});
}

export default downscaleImage;

0 comments on commit 9c06a3a

Please sign in to comment.