1import { scopeThreadRef } from "@t3tools/client-runtime/environment";
2import { type EnvironmentId, ThreadId } from "@t3tools/contracts";
3import { beforeEach, describe, expect, it } from "vite-plus/test";
4
5import {
6 browserMiniPlayerSource,
7 type PreviewMiniPlayerSource,
8 selectThreadPreviewMiniPlayer,
9 selectThreadPreviewMiniPlayerTabId,
10 usePreviewMiniPlayerStore,
11} from "./previewMiniPlayerStore";
12
13const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A"));
14const refB = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-B"));
15const tabA = browserMiniPlayerSource("tab-a");
16const tabB = browserMiniPlayerSource("tab-b");
17const pixel: PreviewMiniPlayerSource = {
18 kind: "device",
19 hostId: "nucbox",
20 deviceId: "emulator-5580",
21 platform: "android",
22 name: "Pixel",
23};
24
25beforeEach(() => {
26 usePreviewMiniPlayerStore.setState({ byThreadKey: {} });
27});
28
29describe("previewMiniPlayerStore", () => {
30 it("keeps floating previews scoped to their thread", () => {
31 usePreviewMiniPlayerStore.getState().open(refA, tabA);
32 usePreviewMiniPlayerStore.getState().open(refB, tabB);
33
34 expect(
35 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
36 ).toMatchObject({ source: tabA });
37 expect(
38 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refB),
39 ).toMatchObject({ source: tabB });
40 });
41
42 it("preserves position when switching the floating tab within one thread", () => {
43 usePreviewMiniPlayerStore.getState().open(refA, tabA);
44 usePreviewMiniPlayerStore.getState().move(refA, "browser:tab-a", { x: 24, y: 48 });
45 usePreviewMiniPlayerStore.getState().open(refA, tabB);
46
47 expect(
48 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
49 ).toEqual({
50 source: tabB,
51 position: { x: 24, y: 48 },
52 width: null,
53 lastInteraction: "drag",
54 });
55 });
56
57 it("ignores stale drag updates after the floating tab changes", () => {
58 usePreviewMiniPlayerStore.getState().open(refA, tabA);
59 usePreviewMiniPlayerStore.getState().open(refA, tabB);
60 usePreviewMiniPlayerStore.getState().move(refA, "browser:tab-a", { x: 100, y: 100 });
61
62 expect(
63 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
64 ).toEqual({
65 source: tabB,
66 position: null,
67 width: null,
68 lastInteraction: "drag",
69 });
70 });
71
72 it("preserves a thread-bound width while switching tabs", () => {
73 usePreviewMiniPlayerStore.getState().open(refA, tabA);
74 usePreviewMiniPlayerStore.getState().resize(refA, "browser:tab-a", 480);
75 usePreviewMiniPlayerStore.getState().open(refA, tabB);
76
77 expect(
78 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
79 ).toMatchObject({ source: tabB, width: 480 });
80 });
81
82 it("keeps a resize placement after release and clears it when dragging", () => {
83 const store = usePreviewMiniPlayerStore.getState();
84 store.open(refA, tabA);
85 store.resize(refA, "browser:tab-a", 460, { x: 1112, y: 275 });
86 expect(
87 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
88 ).toMatchObject({ width: 460, position: { x: 1112, y: 275 }, lastInteraction: "resize" });
89 store.move(refA, "browser:tab-a", { x: 1112, y: 275 });
90 expect(
91 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
92 ).toMatchObject({ width: 460, position: { x: 1112, y: 275 }, lastInteraction: "drag" });
93 });
94
95 it("floats one source per thread, so a device replaces the browser tab", () => {
96 usePreviewMiniPlayerStore.getState().open(refA, tabA);
97 usePreviewMiniPlayerStore.getState().open(refA, pixel);
98 const floating = selectThreadPreviewMiniPlayer(
99 usePreviewMiniPlayerStore.getState().byThreadKey,
100 refA,
101 );
102
103 expect(floating).toMatchObject({ source: pixel });
104 expect(
105 selectThreadPreviewMiniPlayerTabId(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
106 ).toBeNull();
107 // The same device under a new label is still the same floating source.
108 usePreviewMiniPlayerStore.getState().open(refA, { ...pixel, name: "Renamed" });
109 expect(
110 selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, refA),
111 ).toBe(floating);
112 });
113});