diff --git a/src/components/ai-edition/TransportBar.test.tsx b/src/components/ai-edition/TransportBar.test.tsx
index fa155430d..517f94d97 100644
--- a/src/components/ai-edition/TransportBar.test.tsx
+++ b/src/components/ai-edition/TransportBar.test.tsx
@@ -1,5 +1,5 @@
import "@testing-library/jest-dom";
-import { act, cleanup, render, screen } from "@testing-library/react";
+import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { I18nProvider } from "@/contexts/I18nContext";
import { useProjectStore } from "@/lib/ai-edition/store/projectStore";
@@ -92,3 +92,167 @@ describe("TransportBar reads the playhead from the store", () => {
expect(screen.getByText("0:04.5")).toBeInTheDocument();
});
});
+
+describe("le drag de la barre de progression est coalescé en rAF", () => {
+ beforeEach(() => {
+ useProjectStore.setState({ currentTimeSec: 0 });
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ vi.unstubAllGlobals();
+ });
+
+ /** rAF piloté à la main : les callbacks ne partent que sur `flush()`, ce qui permet de
+ * compter ce qui se passe DANS une frame — impossible avec le vrai rAF en test. */
+ function stubRaf() {
+ const pending: FrameRequestCallback[] = [];
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
+ pending.push(cb);
+ return pending.length;
+ });
+ vi.stubGlobal("cancelAnimationFrame", () => {
+ // rien à annuler : `flush()` ne rejoue que ce qui reste en attente
+ });
+ return () => {
+ const due = pending.splice(0, pending.length);
+ for (const cb of due) cb(0);
+ };
+ }
+
+ // C'est LE défaut que ce chemin avait : `onChange` d'un `` se
+ // déclenche à la cadence du pointeur (jusqu'à 1000 Hz) et appelait `onSeek` à chaque
+ // fois. Or `onSeek` repose un `seekTarget` dans l'état de la racine de l'éditeur, donc
+ // re-rend tout et fait poser `