1- import { renderHook , act , cleanup , waitFor } from '@testing-library/react' ;
1+ import { render , renderHook , act , cleanup , waitFor } from '@testing-library/react' ;
22
33import * as React from 'react' ;
44import '@testing-library/jest-dom/extend-expect' ;
@@ -17,12 +17,16 @@ import { baseConfig } from './appConfig';
1717import { randomString } from './test-utils' ;
1818
1919import { addDoc , collection , doc , getFirestore , query , setDoc , connectFirestoreEmulator , where } from 'firebase/firestore' ;
20- import type { DocumentReference } from 'firebase/firestore' ;
20+ import type { DocumentReference , FirestoreDataConverter } from 'firebase/firestore' ;
2121
2222describe ( 'Firestore' , ( ) => {
2323 const app = initializeApp ( baseConfig , 'firestore-test-suite' ) ;
2424 const db = getFirestore ( app ) ;
2525 connectFirestoreEmulator ( db , 'localhost' , 8085 ) ;
26+ const secondDb = getFirestore ( app , 'second-db' ) ;
27+ connectFirestoreEmulator ( secondDb , 'localhost' , 8085 ) ;
28+ const thirdDb = getFirestore ( app , 'third-db' ) ;
29+ connectFirestoreEmulator ( thirdDb , 'localhost' , 8085 ) ;
2630
2731 const Provider = ( { children } : { children : React . ReactNode } ) => (
2832 < FirebaseAppProvider firebaseApp = { app } >
@@ -51,7 +55,9 @@ describe('Firestore', () => {
5155
5256 // clear all Firestore emulator data
5357 // do this AFTER cleaning up hooks, otherwise they'll re-emit values
54- await fetch ( `http://localhost:8085/emulator/v1/projects/rxfire-525a3/databases/(default)/documents` , { method : 'DELETE' } ) ;
58+ for ( const database of [ '(default)' , 'second-db' , 'third-db' ] ) {
59+ await fetch ( `http://localhost:8085/emulator/v1/projects/rxfire-525a3/databases/${ database } /documents` , { method : 'DELETE' } ) ;
60+ }
5561 } ) ;
5662
5763 test ( 'double check - emulator is running' , async ( ) => {
@@ -352,5 +358,173 @@ describe('Firestore', () => {
352358
353359 expect ( result . current . data ) . toEqual ( [ mockData ] ) ;
354360 } ) ;
361+
362+ it ( 'separates cache entries for the same document path with and without a converter' , async ( ) => {
363+ const colRef = collection ( db , randomString ( ) ) ;
364+ const rawRef = doc ( colRef , 'shared' ) ;
365+ await setDoc ( rawRef , { v : 7 } ) ;
366+
367+ const converter : FirestoreDataConverter < { converted : boolean ; v : number } > = {
368+ toFirestore : ( d ) => ( { v : d . v } ) ,
369+ fromFirestore : ( snap ) => ( { converted : true , v : ( snap . data ( ) as { v : number } ) . v } ) ,
370+ } ;
371+ const convertedRef = rawRef . withConverter ( converter ) ;
372+
373+ const RawDoc = ( ) => {
374+ const { data } = useFirestoreDocData ( rawRef , { suspense : true } ) ;
375+ return < div data-testid = "raw" > { JSON . stringify ( data ) } </ div > ;
376+ } ;
377+ const ConvertedDoc = ( ) => {
378+ const { data } = useFirestoreDocData ( convertedRef , { suspense : true } ) ;
379+ return < div data-testid = "conv" > { JSON . stringify ( data ) } </ div > ;
380+ } ;
381+
382+ const { getByTestId } = render (
383+ < Provider >
384+ < React . Suspense fallback = { < div data-testid = "fb" > loading</ div > } >
385+ < RawDoc />
386+ < ConvertedDoc />
387+ </ React . Suspense >
388+ </ Provider >
389+ ) ;
390+
391+ await waitFor ( ( ) => expect ( getByTestId ( 'conv' ) ) . toBeInTheDocument ( ) ) ;
392+ expect ( JSON . parse ( getByTestId ( 'conv' ) . textContent || '{}' ) . converted ) . toBe ( true ) ;
393+ } ) ;
394+
395+ it ( 'resolves when a converter object is created inline during render' , async ( ) => {
396+ const colRef = collection ( db , randomString ( ) ) ;
397+ await setDoc ( doc ( colRef , 'inline' ) , { name : 'alice' } ) ;
398+
399+ const makeConverter = ( ) : FirestoreDataConverter < { name : string } > => ( {
400+ toFirestore : ( p ) => p ,
401+ fromFirestore : ( snap ) => ( { name : String ( snap . get ( 'name' ) ) } ) ,
402+ } ) ;
403+
404+ let statusRenders = 0 ;
405+ const StatusProfile = ( ) => {
406+ statusRenders ++ ;
407+ const { status, data } = useFirestoreDocData ( doc ( colRef , 'inline' ) . withConverter ( makeConverter ( ) ) ) ;
408+ return < div data-testid = "status-out" > { status === 'success' ? data ?. name : 'loading' } </ div > ;
409+ } ;
410+
411+ let suspenseRenders = 0 ;
412+ const SuspenseProfile = ( ) => {
413+ suspenseRenders ++ ;
414+ const { data } = useFirestoreDocData ( doc ( colRef , 'inline' ) . withConverter ( makeConverter ( ) ) , { suspense : true } ) ;
415+ return < div data-testid = "suspense-out" > { data ?. name } </ div > ;
416+ } ;
417+
418+ const { getByTestId } = render (
419+ < Provider >
420+ < StatusProfile />
421+ < React . Suspense fallback = { < div data-testid = "suspense-fb" > fallback</ div > } >
422+ < SuspenseProfile />
423+ </ React . Suspense >
424+ </ Provider >
425+ ) ;
426+
427+ await waitFor ( ( ) => expect ( getByTestId ( 'status-out' ) ) . toHaveTextContent ( 'alice' ) ) ;
428+ await waitFor ( ( ) => expect ( getByTestId ( 'suspense-out' ) ) . toHaveTextContent ( 'alice' ) ) ;
429+ expect ( statusRenders ) . toBeLessThan ( 10 ) ;
430+ expect ( suspenseRenders ) . toBeLessThan ( 10 ) ;
431+ } ) ;
432+
433+ it ( 'never shares a cache entry for different documents across two module instances' , async ( ) => {
434+ const colRef = collection ( db , randomString ( ) ) ;
435+ const x = doc ( colRef , 'x' ) ;
436+ const y = doc ( colRef , 'y' ) ;
437+ await setDoc ( x , { v : 'x' } ) ;
438+ await setDoc ( y , { v : 'y' } ) ;
439+
440+ const copyA = await import ( '../src/index' ) ;
441+ vi . resetModules ( ) ;
442+ const copyB = await import ( '../src/index' ) ;
443+ expect ( copyA . useFirestoreDocData ) . not . toBe ( copyB . useFirestoreDocData ) ;
444+
445+ const A = ( ) => {
446+ const { data } = copyA . useFirestoreDocData ( x ) ;
447+ return < span data-testid = "dual-a" > { String ( ( data as { v ?: string } ) ?. v ) } </ span > ;
448+ } ;
449+ const B = ( ) => {
450+ const { data } = copyB . useFirestoreDocData ( y ) ;
451+ return < span data-testid = "dual-b" > { String ( ( data as { v ?: string } ) ?. v ) } </ span > ;
452+ } ;
453+
454+ const { getByTestId } = render (
455+ < copyA . FirebaseAppProvider firebaseApp = { app } >
456+ < copyA . FirestoreProvider sdk = { db } >
457+ < A />
458+ </ copyA . FirestoreProvider >
459+ < copyB . FirebaseAppProvider firebaseApp = { app } >
460+ < copyB . FirestoreProvider sdk = { db } >
461+ < B />
462+ </ copyB . FirestoreProvider >
463+ </ copyB . FirebaseAppProvider >
464+ </ copyA . FirebaseAppProvider >
465+ ) ;
466+
467+ await waitFor ( ( ) => expect ( getByTestId ( 'dual-a' ) ) . toHaveTextContent ( 'x' ) ) ;
468+ await waitFor ( ( ) => expect ( getByTestId ( 'dual-b' ) ) . toHaveTextContent ( 'y' ) ) ;
469+ } ) ;
470+
471+ it ( 'separates cache entries for the same document path in two databases on one app' , async ( ) => {
472+ const path = `${ randomString ( ) } /shared` ;
473+ await setDoc ( doc ( db , path ) , { v : 'default' } ) ;
474+ await setDoc ( doc ( secondDb , path ) , { v : 'second' } ) ;
475+
476+ const DefaultDoc = ( ) => {
477+ const { data } = useFirestoreDocData ( doc ( db , path ) ) ;
478+ return < span data-testid = "db-default" > { String ( ( data as { v ?: string } ) ?. v ) } </ span > ;
479+ } ;
480+ const SecondDoc = ( ) => {
481+ const { data } = useFirestoreDocData ( doc ( secondDb , path ) ) ;
482+ return < span data-testid = "db-second" > { String ( ( data as { v ?: string } ) ?. v ) } </ span > ;
483+ } ;
484+
485+ const { getByTestId } = render (
486+ < Provider >
487+ < DefaultDoc />
488+ < SecondDoc />
489+ </ Provider >
490+ ) ;
491+
492+ await waitFor ( ( ) => expect ( getByTestId ( 'db-default' ) ) . toHaveTextContent ( 'default' ) ) ;
493+ await waitFor ( ( ) => expect ( getByTestId ( 'db-second' ) ) . toHaveTextContent ( 'second' ) ) ;
494+ } ) ;
495+
496+ it ( 'separates cache entries for the same path in two databases across two module instances' , async ( ) => {
497+ // Both copies are freshly imported, so if the id table were module-local each would give its first database
498+ // id 0 and the two keys would collide.
499+ const path = `${ randomString ( ) } /shared` ;
500+ await setDoc ( doc ( db , path ) , { v : 'default' } ) ;
501+ await setDoc ( doc ( thirdDb , path ) , { v : 'third' } ) ;
502+
503+ vi . resetModules ( ) ;
504+ const copyA = await import ( '../src/index' ) ;
505+ vi . resetModules ( ) ;
506+ const copyB = await import ( '../src/index' ) ;
507+
508+ const A = ( ) => {
509+ const { data } = copyA . useFirestoreDocData ( doc ( db , path ) ) ;
510+ return < span data-testid = "copies-default" > { String ( ( data as { v ?: string } ) ?. v ) } </ span > ;
511+ } ;
512+ const B = ( ) => {
513+ const { data } = copyB . useFirestoreDocData ( doc ( thirdDb , path ) ) ;
514+ return < span data-testid = "copies-third" > { String ( ( data as { v ?: string } ) ?. v ) } </ span > ;
515+ } ;
516+
517+ const { getByTestId } = render (
518+ < copyA . FirebaseAppProvider firebaseApp = { app } >
519+ < A />
520+ < copyB . FirebaseAppProvider firebaseApp = { app } >
521+ < B />
522+ </ copyB . FirebaseAppProvider >
523+ </ copyA . FirebaseAppProvider >
524+ ) ;
525+
526+ await waitFor ( ( ) => expect ( getByTestId ( 'copies-default' ) ) . toHaveTextContent ( 'default' ) ) ;
527+ await waitFor ( ( ) => expect ( getByTestId ( 'copies-third' ) ) . toHaveTextContent ( 'third' ) ) ;
528+ } ) ;
355529 } ) ;
356530} ) ;
0 commit comments