a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 292 lines 11 kB view raw
1import { 2 namedViewTransition, 3 startViewTransition, 4 supportsViewTransitions, 5 withViewTransition, 6} from "$core/view-transitions"; 7import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 8 9describe("View Transitions API", () => { 10 let mockStartViewTransition: ReturnType<typeof vi.fn>; 11 let originalStartViewTransition: unknown; 12 let originalMatchMedia: unknown; 13 14 beforeEach(() => { 15 mockStartViewTransition = vi.fn((callback: () => void | Promise<void>) => { 16 const result = callback(); 17 return { 18 finished: Promise.resolve(result).then(() => {}), 19 ready: Promise.resolve(), 20 updateCallbackDone: Promise.resolve(result).then(() => {}), 21 skipTransition: vi.fn(), 22 }; 23 }); 24 25 originalStartViewTransition = (document as Document & { startViewTransition?: unknown }).startViewTransition; 26 originalMatchMedia = globalThis.matchMedia; 27 28 (document as Document & { startViewTransition: typeof mockStartViewTransition }).startViewTransition = 29 mockStartViewTransition; 30 31 globalThis.matchMedia = vi.fn((query: string) => ({ 32 matches: query === "(prefers-reduced-motion: reduce)" ? false : false, 33 media: query, 34 onchange: null, 35 addListener: vi.fn(), 36 removeListener: vi.fn(), 37 addEventListener: vi.fn(), 38 removeEventListener: vi.fn(), 39 dispatchEvent: vi.fn(), 40 })) as typeof globalThis.matchMedia; 41 }); 42 43 afterEach(() => { 44 if (originalStartViewTransition === undefined) { 45 // @ts-expect-error mocking browser without view transitions API 46 delete (document as Document & { startViewTransition?: unknown }).startViewTransition; 47 } else { 48 // @ts-expect-error mocking view transitions API 49 (document as Document & { startViewTransition: unknown }).startViewTransition = originalStartViewTransition; 50 } 51 52 globalThis.matchMedia = originalMatchMedia as typeof globalThis.matchMedia; 53 54 vi.restoreAllMocks(); 55 }); 56 57 describe("supportsViewTransitions", () => { 58 it("should return true when View Transitions API is supported", () => { 59 expect(supportsViewTransitions()).toBe(true); 60 }); 61 62 it("should return false when View Transitions API is not supported", () => { 63 // @ts-expect-error mocking browser without view transitions API 64 delete (document as Document & { startViewTransition?: unknown }).startViewTransition; 65 expect(supportsViewTransitions()).toBe(false); 66 }); 67 }); 68 69 describe("startViewTransition", () => { 70 it("should use View Transitions API when supported", async () => { 71 const callback = vi.fn(); 72 73 await startViewTransition(callback); 74 75 expect(mockStartViewTransition).toHaveBeenCalledWith(callback); 76 expect(callback).toHaveBeenCalled(); 77 }); 78 79 it("should fallback to direct execution when API is not supported", async () => { 80 // @ts-expect-error mocking browser without view transitions API 81 delete (document as Document & { startViewTransition?: unknown }).startViewTransition; 82 83 const callback = vi.fn(); 84 await startViewTransition(callback); 85 86 expect(callback).toHaveBeenCalled(); 87 }); 88 89 it("should skip transition when prefers-reduced-motion is enabled", async () => { 90 globalThis.matchMedia = vi.fn((query: string) => ({ 91 matches: query === "(prefers-reduced-motion: reduce)" ? true : false, 92 media: query, 93 onchange: null, 94 addListener: vi.fn(), 95 removeListener: vi.fn(), 96 addEventListener: vi.fn(), 97 removeEventListener: vi.fn(), 98 dispatchEvent: vi.fn(), 99 })) as typeof globalThis.matchMedia; 100 101 const callback = vi.fn(); 102 await startViewTransition(callback); 103 104 expect(mockStartViewTransition).not.toHaveBeenCalled(); 105 expect(callback).toHaveBeenCalled(); 106 }); 107 108 it("should force fallback when forceFallback option is true", async () => { 109 const callback = vi.fn(); 110 await startViewTransition(callback, { forceFallback: true }); 111 112 expect(mockStartViewTransition).not.toHaveBeenCalled(); 113 expect(callback).toHaveBeenCalled(); 114 }); 115 116 it("should respect reduced motion preference when respectReducedMotion is true", async () => { 117 globalThis.matchMedia = vi.fn((query: string) => ({ 118 matches: query === "(prefers-reduced-motion: reduce)" ? true : false, 119 media: query, 120 onchange: null, 121 addListener: vi.fn(), 122 removeListener: vi.fn(), 123 addEventListener: vi.fn(), 124 removeEventListener: vi.fn(), 125 dispatchEvent: vi.fn(), 126 })) as typeof globalThis.matchMedia; 127 128 const callback = vi.fn(); 129 await startViewTransition(callback, { respectReducedMotion: true }); 130 131 expect(mockStartViewTransition).not.toHaveBeenCalled(); 132 expect(callback).toHaveBeenCalled(); 133 }); 134 135 it("should use View Transitions API when respectReducedMotion is false even with reduced motion", async () => { 136 globalThis.matchMedia = vi.fn((query: string) => ({ 137 matches: query === "(prefers-reduced-motion: reduce)" ? true : false, 138 media: query, 139 onchange: null, 140 addListener: vi.fn(), 141 removeListener: vi.fn(), 142 addEventListener: vi.fn(), 143 removeEventListener: vi.fn(), 144 dispatchEvent: vi.fn(), 145 })) as typeof globalThis.matchMedia; 146 147 const callback = vi.fn(); 148 await startViewTransition(callback, { respectReducedMotion: false }); 149 150 expect(mockStartViewTransition).toHaveBeenCalledWith(callback); 151 expect(callback).toHaveBeenCalled(); 152 }); 153 154 it("should apply and remove view-transition-name for named transitions", async () => { 155 const element = document.createElement("div"); 156 const callback = vi.fn(); 157 158 await startViewTransition(callback, { name: "test-transition", elements: [element] }); 159 160 expect(callback).toHaveBeenCalled(); 161 expect(element.style.viewTransitionName).toBe(""); 162 }); 163 164 it("should apply unique names for multiple elements", async () => { 165 const element1 = document.createElement("div"); 166 const element2 = document.createElement("div"); 167 const callback = vi.fn(() => { 168 expect(element1.style.viewTransitionName).toBe("test-transition-0"); 169 expect(element2.style.viewTransitionName).toBe("test-transition-1"); 170 }); 171 172 await startViewTransition(callback, { name: "test-transition", elements: [element1, element2] }); 173 174 expect(callback).toHaveBeenCalled(); 175 expect(element1.style.viewTransitionName).toBe(""); 176 expect(element2.style.viewTransitionName).toBe(""); 177 }); 178 179 it("should restore original view-transition-name values", async () => { 180 const element = document.createElement("div"); 181 element.style.viewTransitionName = "original-name"; 182 183 const callback = vi.fn(() => { 184 expect(element.style.viewTransitionName).toBe("test-transition"); 185 }); 186 187 await startViewTransition(callback, { name: "test-transition", elements: [element] }); 188 189 expect(callback).toHaveBeenCalled(); 190 expect(element.style.viewTransitionName).toBe("original-name"); 191 }); 192 193 it("should handle async callbacks", async () => { 194 const callback = vi.fn(async () => { 195 await new Promise((resolve) => setTimeout(resolve, 10)); 196 }); 197 198 await startViewTransition(callback); 199 200 expect(mockStartViewTransition).toHaveBeenCalledWith(callback); 201 expect(callback).toHaveBeenCalled(); 202 }); 203 }); 204 205 describe("namedViewTransition", () => { 206 it("should apply named transition to single element", async () => { 207 const element = document.createElement("div"); 208 const callback = vi.fn(() => { 209 expect(element.style.viewTransitionName).toBe("card-flip"); 210 }); 211 212 await namedViewTransition("card-flip", [element], callback); 213 214 expect(callback).toHaveBeenCalled(); 215 expect(element.style.viewTransitionName).toBe(""); 216 }); 217 218 it("should apply named transition to multiple elements", async () => { 219 const element1 = document.createElement("div"); 220 const element2 = document.createElement("div"); 221 const callback = vi.fn(() => { 222 expect(element1.style.viewTransitionName).toBe("card-flip-0"); 223 expect(element2.style.viewTransitionName).toBe("card-flip-1"); 224 }); 225 226 await namedViewTransition("card-flip", [element1, element2], callback); 227 228 expect(callback).toHaveBeenCalled(); 229 expect(element1.style.viewTransitionName).toBe(""); 230 expect(element2.style.viewTransitionName).toBe(""); 231 }); 232 }); 233 234 describe("withViewTransition", () => { 235 it("should use View Transitions API when supported", () => { 236 const callback = vi.fn(); 237 238 withViewTransition(callback); 239 240 expect(mockStartViewTransition).toHaveBeenCalledWith(callback); 241 expect(callback).toHaveBeenCalled(); 242 }); 243 244 it("should fallback to direct execution when API is not supported", () => { 245 // @ts-expect-error mocking browser without view transitions API 246 delete (document as Document & { startViewTransition?: unknown }).startViewTransition; 247 248 const callback = vi.fn(); 249 withViewTransition(callback); 250 251 expect(callback).toHaveBeenCalled(); 252 }); 253 254 it("should skip transition when prefers-reduced-motion is enabled and respectReducedMotion is true", () => { 255 globalThis.matchMedia = vi.fn((query: string) => ({ 256 matches: query === "(prefers-reduced-motion: reduce)" ? true : false, 257 media: query, 258 onchange: null, 259 addListener: vi.fn(), 260 removeListener: vi.fn(), 261 addEventListener: vi.fn(), 262 removeEventListener: vi.fn(), 263 dispatchEvent: vi.fn(), 264 })) as typeof globalThis.matchMedia; 265 266 const callback = vi.fn(); 267 withViewTransition(callback, true); 268 269 expect(mockStartViewTransition).not.toHaveBeenCalled(); 270 expect(callback).toHaveBeenCalled(); 271 }); 272 273 it("should use View Transitions API when respectReducedMotion is false", () => { 274 globalThis.matchMedia = vi.fn((query: string) => ({ 275 matches: query === "(prefers-reduced-motion: reduce)" ? true : false, 276 media: query, 277 onchange: null, 278 addListener: vi.fn(), 279 removeListener: vi.fn(), 280 addEventListener: vi.fn(), 281 removeEventListener: vi.fn(), 282 dispatchEvent: vi.fn(), 283 })) as typeof globalThis.matchMedia; 284 285 const callback = vi.fn(); 286 withViewTransition(callback, false); 287 288 expect(mockStartViewTransition).toHaveBeenCalledWith(callback); 289 expect(callback).toHaveBeenCalled(); 290 }); 291 }); 292});