Miscellaneous fixes (#24172)
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s

* fix frigate+ submission state bleeding onto the next tracked object

* add Korean

* fix tests
This commit is contained in:
Josh Hawkins
2026-09-03 06:44:18 -05:00
committed by GitHub
parent b4d5035b79
commit 287fc42404
6 changed files with 102 additions and 12 deletions
+4 -1
View File
@@ -54,8 +54,11 @@ export class FrigateApp {
});
await this.ws.install(this.page);
await this.media.install();
await this.api.install(overrides);
// media goes last so its per-event routes win over the broader
// `**/api/events**` list route, which otherwise answers thumbnail and
// snapshot requests with the events JSON
await this.media.install();
}
/** Navigate to a page. Always call installDefaults() first. */
+3
View File
@@ -52,6 +52,9 @@ function deepMerge<T extends Record<string, unknown>>(
export const BASE_CONFIG = {
...configSnapshot,
version: "0.15.0-test",
// injected by the /config endpoint rather than the Pydantic model, so it
// is absent from the snapshot
plus: { enabled: false },
cameras: {
...configSnapshot.cameras,
front_door: {
+3 -2
View File
@@ -249,8 +249,9 @@ export class MediaMocker {
}),
);
// Event thumbnails
await this.page.route("**/api/events/*/thumbnail.jpg**", (route) =>
// Event thumbnails. The explore grid and detail dialog request .webp,
// everything else requests .jpg.
await this.page.route("**/api/events/*/thumbnail.{jpg,webp}**", (route) =>
route.fulfill({
contentType: "image/png",
body: PLACEHOLDER_PNG,
+68
View File
@@ -263,3 +263,71 @@ test.describe("Explore — mobile @high @mobile", () => {
await expect(searchInput).toBeFocused();
});
});
// ---------------------------------------------------------------------------
// Frigate+ submission — desktop only
// The detail dialog's previous/next arrows only render on desktop.
// ---------------------------------------------------------------------------
test.describe("Explore — Frigate+ submission (desktop) @high", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Detail dialog navigation arrows are desktop-only",
);
test("in-flight submission does not mark the next tracked object as submitted", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({ config: { plus: { enabled: true } } });
const page = frigateApp.page;
// Hold the submission open so it is still in flight while the user moves
// on to the next tracked object.
let releaseSubmission: () => void = () => {};
const submissionHeld = new Promise<void>((resolve) => {
releaseSubmission = resolve;
});
let submissions = 0;
await page.route("**/api/events/*/plus", async (route) => {
submissions += 1;
await submissionHeld;
await route.fulfill({ json: { success: true } });
});
await frigateApp.goto("/explore?labels=person");
const firstResult = page.locator("[data-start]").first();
await expect(firstResult).toBeVisible({ timeout: 10_000 });
await firstResult.click();
// The label being confirmed is rendered in a <code> tag inside the
// "Is this object a <label>?" question.
const dialog = page.getByRole("dialog");
await expect(dialog.locator("code")).toHaveText("person");
await dialog.getByRole("button", { name: "Yes", exact: true }).click();
await expect.poll(() => submissions, { timeout: 5_000 }).toBe(1);
await page.getByRole("button", { name: "Next tracked object" }).click();
await expect(dialog.locator("code")).toHaveText("car");
const submissionLanded = page.waitForResponse(/\/api\/events\/.*\/plus/);
releaseSubmission();
await submissionLanded;
// two frames is enough for React to flush the response handler's state
// updates, so the assertions below can't pass by racing ahead of them
await page.evaluate(
() =>
new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
),
);
// The car was never submitted, so its question must be untouched.
expect(submissions).toBe(1);
await expect(dialog.getByText("Submitted")).toHaveCount(0);
await expect(
dialog.getByRole("button", { name: "Yes", exact: true }),
).toBeVisible();
});
});
@@ -1240,10 +1240,14 @@ function ObjectDetailsTab({
search?.plus_id ? "submitted" : "reviewing",
);
useEffect(
() => setState(search?.plus_id ? "submitted" : "reviewing"),
[search],
);
// a submission request outlives the object it was made for, so the
// response handler needs to know which object is on screen now
const displayedIdRef = useRef(search?.id);
useEffect(() => {
displayedIdRef.current = search?.id;
setState(search?.plus_id ? "submitted" : "reviewing");
}, [search]);
const onSubmitToPlus = useCallback(
async (falsePositive: boolean) => {
@@ -1251,10 +1255,12 @@ function ObjectDetailsTab({
return;
}
const eventId = search.id;
try {
const resp = falsePositive
? await axios.put(`events/${search.id}/false_positive`)
: await axios.post(`events/${search.id}/plus`, {
? await axios.put(`events/${eventId}/false_positive`)
: await axios.post(`events/${eventId}/plus`, {
include_annotation: 1,
});
@@ -1262,12 +1268,15 @@ function ObjectDetailsTab({
throw new Error();
}
setState("submitted");
if (displayedIdRef.current === eventId) {
setState("submitted");
}
mutate(
(key) => isEventsKey(key),
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
mapSearchResults(currentData, (event) =>
event.id === search.id
event.id === eventId
? { ...event, plus_id: "new_upload" }
: event,
),
@@ -1278,7 +1287,12 @@ function ObjectDetailsTab({
},
);
} catch {
setState("reviewing");
if (displayedIdRef.current === eventId) {
setState("reviewing");
}
// the toast is not object specific, so it is always shown to avoid
// silently dropping a failed submission
toast.error(
t("explore.plus.review.toast.error", { ns: "components/dialog" }),
{ position: "top-center" },
+1
View File
@@ -31,6 +31,7 @@ export const supportedLanguageKeys = [
"zh-CN",
"zh-Hant",
"yue-Hant",
"ko",
"ja",
"vi",
"th",