@@ -13,13 +13,18 @@ const {
1313 getRoms,
1414 getPlatform,
1515 push,
16+ routeGuards,
1617 snackbarError,
1718 snackbarInfo,
1819} = vi . hoisted ( ( ) => ( {
1920 getRandomRom : vi . fn ( ) ,
2021 getRoms : vi . fn ( ) ,
2122 getPlatform : vi . fn ( ) ,
2223 push : vi . fn ( ) ,
24+ routeGuards : [ ] as ( ( to : {
25+ name : string ;
26+ params : Record < string , string > ;
27+ } ) => unknown ) [ ] ,
2328 snackbarError : vi . fn ( ) ,
2429 snackbarInfo : vi . fn ( ) ,
2530} ) ) ;
@@ -41,7 +46,9 @@ vi.mock("vue-router", async (importOriginal) => ({
4146 ...( await importOriginal < typeof import ( "vue-router" ) > ( ) ) ,
4247 useRoute : ( ) => routeState ,
4348 useRouter : ( ) => ( { push, replace : vi . fn ( ) } ) ,
44- onBeforeRouteUpdate : vi . fn ( ) ,
49+ // Captured rather than dropped: calling the guard is how a test moves
50+ // the view to another platform, which is what the stale check watches.
51+ onBeforeRouteUpdate : vi . fn ( ( guard ) => routeGuards . push ( guard ) ) ,
4552} ) ) ;
4653
4754vi . mock ( "@/plugins/router" , ( ) => ( {
@@ -104,13 +111,13 @@ vi.mock("@/v2/composables/useSnackbar", () => ({
104111 useSnackbar : ( ) => ( { error : snackbarError , info : snackbarInfo } ) ,
105112} ) ) ;
106113
107- function platform ( id : number ) : Platform {
114+ function platform ( id : number , name = "Super Nintendo" ) : Platform {
108115 return {
109116 id,
110- name : "Super Nintendo" ,
111- display_name : "Super Nintendo" ,
112- slug : "snes" ,
113- fs_slug : "snes" ,
117+ name,
118+ display_name : name ,
119+ slug : `platform- ${ id } ` ,
120+ fs_slug : `platform- ${ id } ` ,
114121 rom_count : 83000 ,
115122 } as Platform ;
116123}
@@ -119,9 +126,41 @@ function rom(id: number): SimpleRom {
119126 return { id, name : "Chrono Trigger" } as SimpleRom ;
120127}
121128
129+ /** Resolves the promise the next `getRandomRom` call returns, on demand. */
130+ function deferRandomRom ( ) {
131+ let settle : ( value : { data : SimpleRom | null } ) => void = ( ) => { } ;
132+ getRandomRom . mockReturnValueOnce (
133+ new Promise ( ( resolve ) => {
134+ settle = resolve ;
135+ } ) ,
136+ ) ;
137+ return ( pick : SimpleRom | null ) => settle ( { data : pick } ) ;
138+ }
139+
140+ /** Rejects the promise the next `getRandomRom` call returns, on demand. */
141+ function deferRandomRomFailure ( ) {
142+ let fail : ( reason : Error ) => void = ( ) => { } ;
143+ getRandomRom . mockReturnValueOnce (
144+ new Promise ( ( _resolve , reject ) => {
145+ fail = reject ;
146+ } ) ,
147+ ) ;
148+ return ( ) => fail ( new Error ( "boom" ) ) ;
149+ }
150+
151+ /** Moves the view to another platform the way the router would. */
152+ async function navigateTo ( platformId : number ) {
153+ routeState . params = { platform : String ( platformId ) } ;
154+ routeState . path = `/platform/${ platformId } ` ;
155+ routeGuards . forEach ( ( guard ) =>
156+ guard ( { name : "platform" , params : { platform : String ( platformId ) } } ) ,
157+ ) ;
158+ await flushPromises ( ) ;
159+ }
160+
122161async function mountView ( ) {
123162 const platforms = storePlatforms ( ) ;
124- platforms . set ( [ platform ( 1 ) ] ) ;
163+ platforms . set ( [ platform ( 1 ) , platform ( 2 , "Mega Drive" ) ] ) ;
125164 const galleryRoms = storeGalleryRoms ( ) ;
126165 vi . spyOn ( galleryRoms , "fetchInitialMetadata" ) . mockResolvedValue ( ) ;
127166
@@ -136,7 +175,13 @@ describe("Platform view random rom", () => {
136175 beforeEach ( ( ) => {
137176 setActivePinia ( createPinia ( ) ) ;
138177 vi . clearAllMocks ( ) ;
139- getPlatform . mockResolvedValue ( { data : platform ( 1 ) } ) ;
178+ routeState . name = "platform" ;
179+ routeState . path = "/platform/1" ;
180+ routeState . params = { platform : "1" } ;
181+ routeGuards . length = 0 ;
182+ getPlatform . mockImplementation ( ( id : number ) =>
183+ Promise . resolve ( { data : platform ( id ) } ) ,
184+ ) ;
140185 getRoms . mockResolvedValue ( { data : { items : [ ] , total : 0 } } ) ;
141186 } ) ;
142187
@@ -178,6 +223,61 @@ describe("Platform view random rom", () => {
178223 expect ( push ) . not . toHaveBeenCalled ( ) ;
179224 } ) ;
180225
226+ // Issue #4104: the pick is scoped to the platform that was on screen when
227+ // the button was clicked, so following it after the user moved on drops
228+ // them into a game from a gallery they already left.
229+ it ( "drops a pick that lands after the view moved to another platform" , async ( ) => {
230+ const resolvePick = deferRandomRom ( ) ;
231+
232+ const wrapper = await mountView ( ) ;
233+ await wrapper . get ( "button.random" ) . trigger ( "click" ) ;
234+
235+ await navigateTo ( 2 ) ;
236+
237+ resolvePick ( rom ( 42 ) ) ;
238+ await flushPromises ( ) ;
239+
240+ expect ( push ) . not . toHaveBeenCalled ( ) ;
241+ } ) ;
242+
243+ it ( "stays quiet when a pick fails after the view moved to another platform" , async ( ) => {
244+ const failPick = deferRandomRomFailure ( ) ;
245+
246+ const wrapper = await mountView ( ) ;
247+ await wrapper . get ( "button.random" ) . trigger ( "click" ) ;
248+
249+ await navigateTo ( 2 ) ;
250+
251+ failPick ( ) ;
252+ await flushPromises ( ) ;
253+
254+ expect ( snackbarError ) . not . toHaveBeenCalled ( ) ;
255+ expect ( push ) . not . toHaveBeenCalled ( ) ;
256+
257+ // The button still works on the platform the user landed on.
258+ getRandomRom . mockResolvedValue ( { data : rom ( 7 ) } ) ;
259+ await wrapper . get ( "button.random" ) . trigger ( "click" ) ;
260+ await flushPromises ( ) ;
261+
262+ expect ( push ) . toHaveBeenCalledWith ( { name : "rom" , params : { rom : 7 } } ) ;
263+ } ) ;
264+
265+ // A same-platform reload swaps in a fresh `Platform` record, so the check
266+ // has to compare ids rather than object identity.
267+ it ( "still navigates when the view reloaded the same platform" , async ( ) => {
268+ const resolvePick = deferRandomRom ( ) ;
269+
270+ const wrapper = await mountView ( ) ;
271+ await wrapper . get ( "button.random" ) . trigger ( "click" ) ;
272+
273+ await navigateTo ( 1 ) ;
274+
275+ resolvePick ( rom ( 42 ) ) ;
276+ await flushPromises ( ) ;
277+
278+ expect ( push ) . toHaveBeenCalledWith ( { name : "rom" , params : { rom : 42 } } ) ;
279+ } ) ;
280+
181281 it ( "ignores a click while a pick is in flight" , async ( ) => {
182282 let resolvePick : ( value : { data : SimpleRom } ) => void = ( ) => { } ;
183283 getRandomRom . mockReturnValue (
0 commit comments