Skip to content
Source
Sign inSign up
Open History to see the last commit

phoneViewer.test.ts

154 lines4.6 KB

1import type { PerspectiveCamera, Scene, Object3D } from "three";
2import { afterEach, expect, it, vi } from "vite-plus/test";
3​
4const gpu = vi.hoisted(() => ({
5 instances: [] as {
6 blank: boolean;
7 allocations: number;
8 disposed: boolean;
9 size: { width: number; height: number; pixelRatio: number };
10 frames: {
11 scene: Scene;
12 phone: Object3D | undefined;
13 rotation: Quaternion | undefined;
14 displayAngle: number | undefined;
15 yaw: number | undefined;
16 cameraZ: number;
17 }[];
18 }[],
19}));
20​
21vi.mock("three", async () => {
22 const actual = await vi.importActual<typeof import("three")>("three");
23 return {
24 ...actual,
25 WebGLRenderer: class {
26 outputColorSpace = "";
27 state: (typeof gpu.instances)[number] = {
28 blank: true,
29 allocations: 0,
30 disposed: false,
31 size: { width: 0, height: 0, pixelRatio: 1 },
32 frames: [],
33 };
34 constructor() {
35 gpu.instances.push(this.state);
36 }
37 setDrawingBufferSize(width: number, height: number, pixelRatio: number) {
38 this.state.size = { width, height, pixelRatio };
39 this.state.allocations++;
40 this.state.blank = true;
41 }
42 setSize(width: number, height: number) {
43 this.setDrawingBufferSize(width, height, this.state.size.pixelRatio);
44 }
45 setPixelRatio(pixelRatio: number) {
46 this.setDrawingBufferSize(this.state.size.width, this.state.size.height, pixelRatio);
47 }
48 render(scene: Scene, camera: PerspectiveCamera) {
49 const phone = scene.children.find((child) => child.type === "Group");
50 this.state.frames.push({
51 scene,
52 phone,
53 rotation: phone?.quaternion.clone(),
54 displayAngle: phone?.children[0]?.rotation.z,
55 yaw: phone?.rotation.y,
56 cameraZ: camera.position.z,
57 });
58 this.state.blank = false;
59 }
60 dispose() {
61 this.state.disposed = true;
62 }
63 forceContextLoss() {}
64 },
65 };
66});
67​
68const models = vi.hoisted(() => ({
69 pending: [] as {
70 signal: AbortSignal;
71 resolve: (model: { asset: import("three").Group; dispose: () => void }) => void;
72 }[],
73}));
74vi.mock("./modelScene.ts", async () => {
75 const actual = await vi.importActual<typeof import("./modelScene.ts")>("./modelScene.ts");
76 return {
77 ...actual,
78 loadDeviceModel: (_source: unknown, signal: AbortSignal) =>
79 new Promise((resolve) => models.pending.push({ signal, resolve })),
80 };
81});
82​
83import { Group, Quaternion } from "three";
84import { createPhoneViewer } from "./phoneViewer.ts";
85import { type DeviceShapeProfile } from "./shapeProfile.ts";
86​
87afterEach(() => {
88 vi.restoreAllMocks();
89 vi.unstubAllGlobals();
90 gpu.instances.length = 0;
91 models.pending.length = 0;
92});
93​
94function fixture(profile?: DeviceShapeProfile) {
95 const pending = new Map<number, FrameRequestCallback>();
96 let id = 0;
97 let now = 0;
98 vi.spyOn(performance, "now").mockImplementation(() => now);
99 vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
100 pending.set(++id, callback);
101 return id;
102 });
103 vi.stubGlobal("cancelAnimationFrame", (id: number) => pending.delete(id));
104 const canvas = Object.assign(new EventTarget(), { width: 0, height: 0 }) as HTMLCanvasElement;
105 const source = { width: 1206, height: 2622 } as HTMLCanvasElement;
106 const onUnavailable = vi.fn();
107 const onFramingAspect = vi.fn();
108 const viewer = createPhoneViewer({
109 canvas,
110 source,
111 onUnavailable,
112 onFramingAspect,
113 ...(profile ? { profile } : {}),
114 });
115 const draw = (time = now) => {
116 now = time;
117 const callbacks = [...pending.values()];
118 pending.clear();
119 callbacks.forEach((callback) => callback(0));
120 };
121 viewer.resize(400, 700, 2);
122 draw();
123 return {
124 viewer,
125 draw,
126 pending,
127 source,
128 onUnavailable,
129 onFramingAspect,
130 state: gpu.instances[0]!,
131 };
133​
134it("rejects incompatible accessories and releases an accessory that finishes after detaching", async () => {
135 const { viewer } = fixture();
136 const keyboard = {
137 id: "ipad-pro-13-m5-magic-keyboard",
138 modelId: "ipad-pro-13-m5",
139 url: "/keyboard.glb",
140 } as const;
141 viewer.setAccessory(keyboard);
142 expect(models.pending).toHaveLength(0);
143 viewer.setModel({ id: "ipad-pro-13-m5", url: "/ipad.glb" });
144 viewer.setAccessory(keyboard);
145 viewer.setAccessory(null);
146 expect(models.pending[1]!.signal.aborted).toBe(true);
147 const dispose = vi.fn();
148 const asset = new Group();
149 models.pending[1]!.resolve({ asset, dispose });
150 await Promise.resolve();
151 expect(asset.parent).toBeNull();
152 expect(dispose).toHaveBeenCalledOnce();
153 viewer.dispose();
154});