fix(vinpin): bail a DOWN Rpartstore on iteration 1, don't re-click the flyout 6×
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
When Rpartstore is DOWN, its hard launch-error modal ("Ошибка запуска каталога",
title "Renault Rpartstore") renders OVER the brand grid. The grid tiles stay
OCR-visible behind the small centered modal, so the full-frame read matches both
`brandGrid` and `renaultSubmenu` — making ensureRpartstore's flyout branch fire on
EVERY iteration, re-clicking renaultRpartstore(584,779) + langOk + a 12s full-frame
poll for all ~6 iterations (~72s) before finally returning false. That wastes ~60s on
the first cold DOWN decode AND repeatedly actuates coordinates on a wedged desktop.
Detect the launch-error modal (upscaled crop via the existing
`rpartstoreLaunchErrorPresent`) at the top of the per-iteration loop, BEFORE the
flyout branch: if present, return false on iteration 1 so the acquire loop's
`!present` path dismisses it, sets the down-cooldown, and routes straight to Dialogys.
Depends on the crop OCR being legible (ffmpeg upscale, added in 281c54a); when
illegible it's false and behaviour is exactly as before.
Adds two robustness tests: (1) modal-over-grid → false on the first iteration with no
flyout re-clicks; (2) illegible crop → flyout path still runs (unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,56 @@ describe("VinpinDriverService — ensureRpartstore flyout false-positive (change
|
||||
VINPIN_COORDS.renaultRpartstore.y,
|
||||
);
|
||||
});
|
||||
|
||||
it("bails on the FIRST iteration when the DOWN launch-error modal is over the grid (no repeated flyout clicks)", async () => {
|
||||
const driver = new VinpinDriverService();
|
||||
const page = fakePage();
|
||||
// DOWN Rpartstore: the launch-error modal sits OVER the brand grid. The FULL-frame
|
||||
// read (no clip) sees the grid tiles + the modal TITLE "Renault Rpartstore" → it
|
||||
// matches brandGrid AND renaultSubmenu, which — without the fix — makes the flyout
|
||||
// branch re-click renaultRpartstore(584,779) on all ~6 iterations. The UPSCALED
|
||||
// modal crop (clip passed) reads the Cyrillic launch error, so the launch-error
|
||||
// short-circuit must fire and return false on iteration 1.
|
||||
mockOcr.mockImplementation(async (_page: unknown, clip?: unknown) =>
|
||||
clip
|
||||
? "Owwu6ka 3anycka KaTanora" // upscaled modal crop → matches rpartstoreLaunchError
|
||||
: "Volkswagen Mitsubishi Renault Rpartstore Dialogys",
|
||||
);
|
||||
|
||||
const ok = await ensure(driver, page);
|
||||
|
||||
expect(ok).toBe(false);
|
||||
// Never re-clicked the flyout Rpartstore entry (would be up to 6× without the fix).
|
||||
expect(page.mouse.click).not.toHaveBeenCalledWith(
|
||||
VINPIN_COORDS.renaultRpartstore.x,
|
||||
VINPIN_COORDS.renaultRpartstore.y,
|
||||
);
|
||||
// Exactly ONE full-frame OCR read (no clip) proves it bailed on the first iteration
|
||||
// rather than looping the 6-try budget.
|
||||
const fullFrameReads = mockOcr.mock.calls.filter(([, clip]) => clip === undefined);
|
||||
expect(fullFrameReads).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("still enters the flyout branch when the modal crop is ILLEGIBLE (no ffmpeg) — behaviour unchanged", async () => {
|
||||
const driver = new VinpinDriverService();
|
||||
const page = fakePage();
|
||||
// Same grid+flyout-title screen, but the crop OCR yields no launch-error text (the
|
||||
// no-ffmpeg / illegible case). The launch-error short-circuit must NOT fire, so the
|
||||
// proven flyout path still runs and clicks the Rpartstore entry.
|
||||
mockOcr.mockImplementation(async (_page: unknown, clip?: unknown) =>
|
||||
clip
|
||||
? "" // illegible crop → rpartstoreLaunchErrorPresent === false
|
||||
: "Volkswagen Mitsubishi Renault Rpartstore Dialogys",
|
||||
);
|
||||
|
||||
// Bounded loop; we only assert the flyout entry was clicked (grid+submenu branch).
|
||||
await ensure(driver, page);
|
||||
|
||||
expect(page.mouse.click).toHaveBeenCalledWith(
|
||||
VINPIN_COORDS.renaultRpartstore.x,
|
||||
VINPIN_COORDS.renaultRpartstore.y,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VinpinDriverService — closeRpartstoreTab never hits the language selector (change 3)", () => {
|
||||
|
||||
@@ -1752,6 +1752,20 @@ export class VinpinDriverService implements OnModuleDestroy {
|
||||
// loop then saw no content, misread it as a spinner, and burned reopens.
|
||||
if (VINPIN_OCR.rpartstoreLoaded.test(t)) return true;
|
||||
|
||||
// DOWN Rpartstore short-circuit — checked BEFORE the flyout branch. A DOWN
|
||||
// Rpartstore renders the HARD launch-error modal ("Ошибка запуска каталога",
|
||||
// title "Renault Rpartstore") OVER the brand grid: the grid tiles stay
|
||||
// OCR-visible behind the small centered modal, so `brandGrid` matches AND the
|
||||
// modal title matches `renaultSubmenu` — which would make the flyout branch below
|
||||
// re-click the Rpartstore entry + langOk + a full-frame poll on EVERY iteration
|
||||
// (~72s of flailing, re-actuating coords on a wedged desktop) before finally
|
||||
// returning false. Detect the launch error on the UPSCALED modal crop first and
|
||||
// bail on iteration 1 so the acquire loop's `!present` path dismisses it, sets the
|
||||
// down-cooldown, and routes straight to Dialogys. Depends on the crop OCR being
|
||||
// legible (ffmpeg upscale); when it isn't, `rpartstoreLaunchErrorPresent` is false
|
||||
// and this is a no-op → behaviour is exactly as before.
|
||||
if (await this.rpartstoreLaunchErrorPresent(page)) return false;
|
||||
|
||||
// FLYOUT is open OVER the brand grid (the grid tiles are still visible behind
|
||||
// the Renault submenu) → click its Rpartstore entry. Gating on `brandGrid` here
|
||||
// is what disambiguates a genuine flyout from an already-open window whose TITLE
|
||||
|
||||
Reference in New Issue
Block a user