Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions integration_tests/pages/ProjectListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export class ProjectListPage extends BasePage {
name: "Image comparison library",
});

// the rendering-noise filter: part of each library's config section
noiseFilter = this.dialog.getByLabel("Ignore rendering noise");
noiseThreshold = this.dialog.locator("#blurAmplitudeThreshold");

// the engine's own fields, below the library select
vlmProvider = this.dialog.getByRole("combobox", { name: "VLM Provider" });
ollamaModel = this.dialog.getByRole("combobox", { name: "Model" });
Expand Down
179 changes: 177 additions & 2 deletions integration_tests/test/projectList.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { expect } from "@playwright/test";
import { expect, Page, Request } from "@playwright/test";
import { test } from "fixtures";
import { TEST_PROJECT } from "~client/_test/test.data.helper";
import { ODIFF_DEFAULT_CONFIG } from "~client/constants/project";
import {
LOOKSSAME_DEFAULT_CONFIG,
ODIFF_DEFAULT_CONFIG,
PIXELMATCH_DEFAULT_CONFIG,
} from "~client/constants/project";
import { ImageComparison } from "~client/types/imageComparison";
import {
API_URL,
Expand Down Expand Up @@ -152,3 +156,174 @@ test("edits a project", async ({ projectListPage, page }) => {
await expect(projectListPage.project("Renamed project")).toBeVisible();
await expect(projectListPage.project(project.name)).toHaveCount(0);
});

// The rendering-noise filter is switched on in the library's config, where a
// library that supports it offers it; the backend applies it to every library.
const savedConfig = (request: Request) =>
JSON.parse(request.postDataJSON().imageComparisonConfig);

const waitForSave = (page: Page, method: "POST" | "PUT") =>
page.waitForRequest(
(request) =>
request.url() === `${API_URL}/projects` && request.method() === method,
);

test("creates a project with the rendering-noise filter on", async ({
projectListPage,
page,
}) => {
await mockSaveProject(page);

await projectListPage.addBtn.click();
await projectListPage.name.fill("New project");
await projectListPage.mainBranch.fill("main");
await projectListPage.selectImageComparison(ImageComparison.lookSame);
await projectListPage.noiseFilter.check();
await projectListPage.noiseThreshold.fill("60");

const [request] = await Promise.all([
waitForSave(page, "POST"),
projectListPage.modal.confirmBtn.click(),
]);

expect(savedConfig(request)).toEqual({
...JSON.parse(LOOKSSAME_DEFAULT_CONFIG),
blurAmplitudeFilter: true,
blurAmplitudeThreshold: 60,
});
});

// the threshold means nothing with the filter off, so it is only offered on
test("offers the threshold only while the filter is on", async ({
projectListPage,
}) => {
await projectListPage.addBtn.click();
await expect(projectListPage.noiseThreshold).toHaveCount(0);

await projectListPage.noiseFilter.check();
// the backend's default, until changed
await expect(projectListPage.noiseThreshold).toHaveValue("80");

await projectListPage.noiseFilter.uncheck();
await expect(projectListPage.noiseThreshold).toHaveCount(0);
});

for (const [threshold, message] of [
["129", "Enter at most 128"],
["-1", "Enter at least 0"],
["", "Enter a threshold"],
]) {
test(`does not save a rendering-noise threshold of "${threshold}"`, async ({
projectListPage,
page,
}) => {
const creates: string[] = [];
page.on("request", (request) => {
if (
request.url() === `${API_URL}/projects` &&
request.method() === "POST"
) {
creates.push(request.url());
}
});

await projectListPage.addBtn.click();
await projectListPage.name.fill("New project");
await projectListPage.mainBranch.fill("main");
await projectListPage.noiseFilter.check();
await projectListPage.noiseThreshold.fill(threshold);
await projectListPage.modal.confirmBtn.click();

await expect(projectListPage.dialog.getByText(message)).toBeVisible();
await expect(projectListPage.dialog).toBeVisible();
expect(creates).toEqual([]);
});
}

// switching swaps in the new library's defaults; the filter is not one of the
// library's own options and would otherwise be silently switched off
test("keeps the rendering-noise filter when the library changes", async ({
projectListPage,
page,
}) => {
await mockSaveProject(page);

await projectListPage.addBtn.click();
await projectListPage.name.fill("New project");
await projectListPage.mainBranch.fill("main");
await projectListPage.noiseFilter.check();
await projectListPage.noiseThreshold.fill("50");
await projectListPage.selectImageComparison(ImageComparison.odiff);

await expect(projectListPage.noiseFilter).toBeChecked();
await expect(projectListPage.noiseThreshold).toHaveValue("50");
const [request] = await Promise.all([
waitForSave(page, "POST"),
projectListPage.modal.confirmBtn.click(),
]);
expect(savedConfig(request)).toEqual({
...JSON.parse(ODIFF_DEFAULT_CONFIG),
blurAmplitudeFilter: true,
blurAmplitudeThreshold: 50,
});
});

// switching provider rebuilds the VLM config from scratch
test("keeps the rendering-noise filter when the VLM provider changes", async ({
projectListPage,
}) => {
await projectListPage.addBtn.click();
await projectListPage.selectImageComparison(ImageComparison.vlm);
await projectListPage.noiseFilter.check();
await projectListPage.noiseThreshold.fill("65");

await projectListPage.selectVlmProvider("Google Gemini");

await expect(projectListPage.noiseFilter).toBeChecked();
await expect(projectListPage.noiseThreshold).toHaveValue("65");
});

test("turns the rendering-noise filter on for an existing project", async ({
projectListPage,
page,
}) => {
await mockSaveProject(page);

await projectListPage.editBtn.click();
await expect(projectListPage.noiseFilter).not.toBeChecked();
await projectListPage.noiseFilter.check();
await projectListPage.noiseThreshold.fill("70.5");

const [request] = await Promise.all([
waitForSave(page, "PUT"),
projectListPage.modal.confirmBtn.click(),
]);

expect(savedConfig(request)).toEqual({
...JSON.parse(PIXELMATCH_DEFAULT_CONFIG),
blurAmplitudeFilter: true,
blurAmplitudeThreshold: 70.5,
});
});

test("opens a project with the filter on as it was saved", async ({
projectListPage,
page,
}) => {
await mockGetProjects(page, [
{
...project,
imageComparisonConfig: JSON.stringify({
...JSON.parse(PIXELMATCH_DEFAULT_CONFIG),
blurAmplitudeFilter: true,
blurAmplitudeThreshold: 42,
}),
},
]);
await page.reload();

await projectListPage.editBtn.click();

await expect(projectListPage.noiseFilter).toBeChecked();
await expect(projectListPage.noiseThreshold).toHaveValue("42");
});
16 changes: 16 additions & 0 deletions integration_tests/test/screens.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,22 @@ const VIEWS: [name: string, body: Body][] = [
},
],

[
"edit project dialog with the rendering-noise filter on",
async ({ projectListPage, shot }) => {
await projectListPage.editBtn.click();
await projectListPage.noiseFilter.check();
// the switch keeps its tooltip open while it has the focus: take it off,
// then bring the threshold the switch revealed into the dialog's view
await projectListPage.dialog.getByRole("heading").first().click();
await projectListPage.noiseThreshold.scrollIntoViewIfNeeded();

await expect(projectListPage.noiseThreshold).toHaveValue("80");
await expect(projectListPage.page.getByRole("tooltip")).toHaveCount(0);
await shot("edit-project-noise-filter");
},
],

[
"delete project confirmation",
async ({ projectListPage, shot }) => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions src/components/ProjectForm/BlurAmplitudeFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { FormControlLabel, Switch } from "@mui/material";
import React from "react";
import { TextValidator } from "react-material-ui-form-validator";
import { DEFAULT_BLUR_AMPLITUDE_THRESHOLD } from "../../constants";
import { BlurAmplitudeConfig } from "../../types/imageComparison";
import { Tooltip } from "../Tooltip";
import { useConfigHook } from "./useConfigHook";

// Rendered by the config form of every library that supports the filter.
export const BlurAmplitudeFields: React.FunctionComponent = () => {
const [config, updateConfig] = useConfigHook<BlurAmplitudeConfig>();

// absent: the backend default applies; null: the field was cleared, which
// the required validator then refuses
const threshold =
config.blurAmplitudeThreshold === undefined
? DEFAULT_BLUR_AMPLITUDE_THRESHOLD
: (config.blurAmplitudeThreshold ?? "");

return (
<React.Fragment>
<Tooltip title="Pass a failing comparison when its change, blurred by about a pixel, is no stronger than anti-aliasing or sub-pixel rendering noise.">
<FormControlLabel
label="Ignore rendering noise"
control={
<Switch
checked={config.blurAmplitudeFilter === true}
onChange={(event, checked) =>
updateConfig("blurAmplitudeFilter", checked)
}
color="primary"
name="blurAmplitudeFilter"
/>
}
/>
</Tooltip>
{config.blurAmplitudeFilter === true && (
<TextValidator
name="blurAmplitudeThreshold"
validators={["required", "minNumber:0", "maxNumber:128"]}
errorMessages={[
"Enter a threshold",
"Enter at least 0",
"Enter at most 128",
]}
slotProps={{
htmlInput: {
min: 0,
// the backend's ceiling: near 255 almost any change would pass
max: 128,
// the backend takes fractions; a whole step would make the
// browser refuse them
step: "any",
},
}}
margin="dense"
id="blurAmplitudeThreshold"
label="Rendering noise threshold"
helperText="Strongest blurred difference (0-128) still treated as noise. Lower is stricter."
type="number"
fullWidth
value={threshold}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(event.target.value);

Check warning on line 64 in src/components/ProjectForm/BlurAmplitudeFields.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=Visual-Regression-Tracker_frontend&issues=AaFuIUlRdA8JAur1ejeS&open=AaFuIUlRdA8JAur1ejeS&pullRequest=440
updateConfig(
"blurAmplitudeThreshold",
Number.isNaN(value) ? null : value,
);
}}
/>
)}
</React.Fragment>
);
};
2 changes: 2 additions & 0 deletions src/components/ProjectForm/LooksSameConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import { TextValidator } from "react-material-ui-form-validator";
import { LooksSameConfig } from "../../types/imageComparison";
import { useConfigHook } from "./useConfigHook";
import { BlurAmplitudeFields } from "./BlurAmplitudeFields";

export const LooksSameConfigForm: React.FunctionComponent = () => {
const [config, updateConfig] = useConfigHook<LooksSameConfig>();
Expand Down Expand Up @@ -101,6 +102,7 @@ export const LooksSameConfigForm: React.FunctionComponent = () => {
/>
}
/>
<BlurAmplitudeFields />
</React.Fragment>
);
};
2 changes: 2 additions & 0 deletions src/components/ProjectForm/OdiffConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import { TextValidator } from "react-material-ui-form-validator";
import { OdiffConfig } from "../../types/imageComparison";
import { useConfigHook } from "./useConfigHook";
import { BlurAmplitudeFields } from "./BlurAmplitudeFields";

export const OdiffConfigForm: React.FunctionComponent = () => {
const [config, updateConfig] = useConfigHook<OdiffConfig>();
Expand Down Expand Up @@ -69,6 +70,7 @@ export const OdiffConfigForm: React.FunctionComponent = () => {
updateConfig("threshold", parseFloat(value));
}}
/>
<BlurAmplitudeFields />
</React.Fragment>
);
};
2 changes: 2 additions & 0 deletions src/components/ProjectForm/PixelmatchConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TextValidator } from "react-material-ui-form-validator";
import { PixelmatchConfig } from "../../types/imageComparison";
import { Tooltip } from "../Tooltip";
import { useConfigHook } from "./useConfigHook";
import { BlurAmplitudeFields } from "./BlurAmplitudeFields";

export const PixelmatchConfigForm: React.FunctionComponent = () => {
const [config, updateConfig] = useConfigHook<PixelmatchConfig>();
Expand Down Expand Up @@ -64,6 +65,7 @@ export const PixelmatchConfigForm: React.FunctionComponent = () => {
updateConfig("threshold", parseFloat(value));
}}
/>
<BlurAmplitudeFields />
</React.Fragment>
);
};
Loading
Loading