Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Latency optimizations in the flag-gated (VINPIN_ENABLED) Vinpin decode driver. Every change keeps the existing fixed-wait value as a fallback cap, so worst-case behaviour and robustness are unchanged — only the common case is faster. No live seat session was run (prod holds the single Horizon seat). #2 Renault→Fiat symmetric swap: ensureReady no longer tears the whole browser down on a Renault→Fiat flow switch (was a ~60-80s cold restart). It now keeps the authenticated Horizon/VinPower session, returns to the brand grid via the existing ensureBrandGrid (which closes a resumed foreign catalog window) and reopens Fiat via the normal tile flow — symmetric with the Fiat→Renault direction. Guarded fallback: if the in-session swap can't reach the grid / throws, it falls back to close()+cold re-establish. #3 OCR-poll-until-ready: replaced big fixed post-action sleeps whose completion is OCR-detectable with a capped poll (screenshot→ocrRegion→regex every ~700ms, return on match, cap == old fixed wait). Converted: afterFiatOpen(12s), afterVinSubmit(7s, Fiat modal), afterRpartstoreSubmit(8s), afterRenaultCatalogOpen(12s, ×2), plus cold-path afterLogin(20s) and afterVinPowerLogin(18s). Left afterVinpinLaunch(14s) fixed — the VinPower login dialog has no reliable OCR marker (detected via DOM), and it doubles as a generic connecting-screen settle. Left afterDialogysSubmit fixed (out of scope). #4 Micro-trims: field-clear Backspace burst 40→5 (VINPIN_FIELD_CLEAR_BACKSPACES), key-type delay 45-50ms→20ms (VINPIN_TYPE_DELAY_MS, 17-char VIN ~850→~340ms), afterAlertDismiss 700→250ms. New pure/injectable pollForText helper in vinpin.ocr.ts (unit-tested: early-return, cap-never-exceeded, first-read match). typecheck + biome clean; 31 vinpin unit tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { pollForText } from "./vinpin.ocr";
|
|
|
|
/** Deterministic fake clock: `sleep` advances virtual time; `now` reads it. */
|
|
function makeClock() {
|
|
let t = 0;
|
|
const slept: number[] = [];
|
|
return {
|
|
now: () => t,
|
|
sleep: async (ms: number) => {
|
|
slept.push(ms);
|
|
t += ms;
|
|
},
|
|
slept,
|
|
total: () => slept.reduce((a, b) => a + b, 0),
|
|
};
|
|
}
|
|
|
|
describe("pollForText", () => {
|
|
it("returns early as soon as the predicate matches (fast path)", async () => {
|
|
const clock = makeClock();
|
|
const reads = ["no", "no", "hit"];
|
|
let i = 0;
|
|
const res = await pollForText({
|
|
read: async () => reads[i++] ?? "",
|
|
predicate: (t) => t === "hit",
|
|
capMs: 10_000,
|
|
intervalMs: 700,
|
|
sleep: clock.sleep,
|
|
now: clock.now,
|
|
});
|
|
expect(res.matched).toBe(true);
|
|
expect(res.text).toBe("hit");
|
|
// Stopped at the 3rd read — well under the cap.
|
|
expect(i).toBe(3);
|
|
expect(clock.total()).toBe(2100);
|
|
expect(clock.total()).toBeLessThan(10_000);
|
|
});
|
|
|
|
it("caps total sleep at capMs when the predicate never matches (fallback == old fixed wait)", async () => {
|
|
const clock = makeClock();
|
|
let reads = 0;
|
|
const res = await pollForText({
|
|
read: async () => {
|
|
reads++;
|
|
return "no";
|
|
},
|
|
predicate: (t) => t === "hit",
|
|
capMs: 2_000,
|
|
intervalMs: 700,
|
|
sleep: clock.sleep,
|
|
now: clock.now,
|
|
});
|
|
expect(res.matched).toBe(false);
|
|
expect(res.text).toBe("no");
|
|
// Total sleep is exactly the cap — never longer than the old fixed wait.
|
|
expect(clock.total()).toBe(2_000);
|
|
// The final interval was clamped to the remaining budget (no overshoot).
|
|
expect(Math.max(...clock.slept)).toBeLessThanOrEqual(700);
|
|
expect(reads).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("matches on the very first read without over-sleeping", async () => {
|
|
const clock = makeClock();
|
|
const res = await pollForText({
|
|
read: async () => "ready",
|
|
predicate: (t) => t.includes("ready"),
|
|
capMs: 12_000,
|
|
intervalMs: 750,
|
|
sleep: clock.sleep,
|
|
now: clock.now,
|
|
});
|
|
expect(res.matched).toBe(true);
|
|
expect(clock.total()).toBe(750); // one interval, then matched
|
|
});
|
|
});
|