@@ -19,6 +19,25 @@ jest.mock('@hubble.gl/react', () => ({
1919 KeplerUIContext : MockKeplerUIContext
2020} ) ) ;
2121
22+ const mockComputeDeckEffects = jest . fn ( ( ) => [ ] ) ;
23+
24+ jest . mock ( '@kepler.gl/utils' , ( ) => {
25+ const actual = jest . requireActual ( '@kepler.gl/utils' ) ;
26+ return {
27+ ...actual ,
28+ computeDeckEffects : ( ...args ) => mockComputeDeckEffects ( ...args ) ,
29+ patchDeckRendererForPostProcessing : jest . fn ( )
30+ } ;
31+ } ) ;
32+
33+ jest . mock ( '@kepler.gl/effects' , ( ) => ( {
34+ DeckShadowCompositingEffect : jest . fn ( ) . mockImplementation ( ( ) => ( {
35+ id : 'shadow-compositing-effect' ,
36+ postRender : jest . fn ( ) ,
37+ cleanup : jest . fn ( )
38+ } ) )
39+ } ) ) ;
40+
2241jest . mock ( './hubble-utils' , ( ) => ( {
2342 getStaticMapProps : jest . fn ( ( _state , _onChange , _token ) => ( {
2443 latitude : 37.7 ,
@@ -39,6 +58,7 @@ jest.mock('./hubble-utils', () => ({
3958} ) ) ;
4059
4160import ExportVideoModalFactory from './export-video-modal' ;
61+ import { DeckShadowCompositingEffect } from '@kepler.gl/effects' ;
4262
4363function renderWithThemeLocal ( component ) {
4464 return render (
@@ -97,6 +117,7 @@ async function renderAndWaitForPanel(props = DEFAULT_PROPS) {
97117describe ( 'ExportVideoModal' , ( ) => {
98118 beforeEach ( ( ) => {
99119 jest . clearAllMocks ( ) ;
120+ mockComputeDeckEffects . mockReturnValue ( [ ] ) ;
100121 } ) ;
101122
102123 test ( 'renders without crashing' , async ( ) => {
@@ -186,4 +207,131 @@ describe('ExportVideoModal', () => {
186207 panelProps . onTripFrameUpdate ( 42 ) ;
187208 expect ( DEFAULT_PROPS . visStateActions . setLayerAnimationTime ) . toHaveBeenCalledWith ( 42 ) ;
188209 } ) ;
210+
211+ describe ( 'NO_MAP_ID handling' , ( ) => {
212+ const NO_MAP_PROPS = {
213+ ...DEFAULT_PROPS ,
214+ mapStyle : {
215+ styleType : 'no_map' ,
216+ mapStyles : {
217+ no_map : {
218+ id : 'no_map' ,
219+ label : 'No Basemap' ,
220+ // url is intentionally missing to exercise the data-URI patch
221+ icon : '' ,
222+ layerGroups : [ ] ,
223+ colorMode : 'NONE' ,
224+ style : { version : 8 , sources : { } , layers : [ ] }
225+ }
226+ } ,
227+ bottomMapStyle : null ,
228+ topMapStyle : null
229+ }
230+ } ;
231+
232+ test ( 'renders without crashing when styleType is NO_MAP_ID' , async ( ) => {
233+ const { container} = await renderAndWaitForPanel ( NO_MAP_PROPS ) ;
234+ expect ( container . querySelector ( '.export-video-modal' ) ) . toBeInTheDocument ( ) ;
235+ } ) ;
236+
237+ test ( 'injects data-URI empty style URL into mapData when NO_MAP_ID entry has no url' , async ( ) => {
238+ await renderAndWaitForPanel ( NO_MAP_PROPS ) ;
239+
240+ const panelProps = mockExportVideoPanelContainer . mock . calls [ 0 ] [ 0 ] ;
241+ const noMapEntry = panelProps . mapData . mapStyle . mapStyles . no_map ;
242+
243+ expect ( noMapEntry . url ) . toBeDefined ( ) ;
244+ expect ( noMapEntry . url ) . toMatch ( / ^ d a t a : a p p l i c a t i o n \/ j s o n , / ) ;
245+
246+ const encoded = noMapEntry . url . replace ( 'data:application/json,' , '' ) ;
247+ const decoded = JSON . parse ( decodeURIComponent ( encoded ) ) ;
248+ expect ( decoded ) . toEqual ( { version : 8 , sources : { } , layers : [ ] } ) ;
249+ } ) ;
250+
251+ test ( 'preserves original mapStyle when NO_MAP_ID entry already has a url' , async ( ) => {
252+ const propsWithUrl = {
253+ ...DEFAULT_PROPS ,
254+ mapStyle : {
255+ styleType : 'no_map' ,
256+ mapStyles : {
257+ no_map : {
258+ id : 'no_map' ,
259+ url : 'https://existing-style.json'
260+ }
261+ } ,
262+ bottomMapStyle : null ,
263+ topMapStyle : null
264+ }
265+ } ;
266+
267+ await renderAndWaitForPanel ( propsWithUrl ) ;
268+
269+ const panelProps = mockExportVideoPanelContainer . mock . calls [ 0 ] [ 0 ] ;
270+ expect ( panelProps . mapData . mapStyle ) . toBe ( propsWithUrl . mapStyle ) ;
271+ } ) ;
272+ } ) ;
273+
274+ describe ( 'shadow compositing fallback' , ( ) => {
275+ const mockEffect = {
276+ id : 'light-shadow-1' ,
277+ type : 'light-and-shadow' ,
278+ isEnabled : true ,
279+ clone : jest . fn ( ( ) => ( { ...mockEffect , clone : mockEffect . clone } ) )
280+ } ;
281+
282+ function propsWithEffects ( effects ) {
283+ return {
284+ ...DEFAULT_PROPS ,
285+ visState : {
286+ ...DEFAULT_PROPS . visState ,
287+ effects,
288+ effectOrder : effects . map ( e => e . id )
289+ }
290+ } ;
291+ }
292+
293+ test ( 'adds DeckShadowCompositingEffect when shadows exist but no postRender effect' , async ( ) => {
294+ const shadowDeckEffect = { id : 'lighting' , shadow : true } ;
295+ mockComputeDeckEffects . mockReturnValue ( [ shadowDeckEffect ] ) ;
296+
297+ await renderAndWaitForPanel ( propsWithEffects ( [ mockEffect ] ) ) ;
298+
299+ const panelProps = mockExportVideoPanelContainer . mock . calls [ 0 ] [ 0 ] ;
300+ const effects = panelProps . deckProps . effects ;
301+
302+ expect ( effects . length ) . toBe ( 2 ) ;
303+ expect ( effects [ 0 ] ) . toBe ( shadowDeckEffect ) ;
304+ expect ( effects [ 1 ] . id ) . toBe ( 'shadow-compositing-effect' ) ;
305+ expect ( DeckShadowCompositingEffect ) . toHaveBeenCalled ( ) ;
306+ } ) ;
307+
308+ test ( 'does not add DeckShadowCompositingEffect when a postRender effect already exists' , async ( ) => {
309+ const shadowDeckEffect = { id : 'lighting' , shadow : true } ;
310+ const postProcessEffect = { id : 'fog' , postRender : jest . fn ( ) } ;
311+ mockComputeDeckEffects . mockReturnValue ( [ shadowDeckEffect , postProcessEffect ] ) ;
312+
313+ await renderAndWaitForPanel ( propsWithEffects ( [ mockEffect ] ) ) ;
314+
315+ const panelProps = mockExportVideoPanelContainer . mock . calls [ 0 ] [ 0 ] ;
316+ const effects = panelProps . deckProps . effects ;
317+
318+ expect ( effects . length ) . toBe ( 2 ) ;
319+ expect ( effects [ 0 ] ) . toBe ( shadowDeckEffect ) ;
320+ expect ( effects [ 1 ] ) . toBe ( postProcessEffect ) ;
321+ expect ( effects . every ( e => e . id !== 'shadow-compositing-effect' ) ) . toBe ( true ) ;
322+ } ) ;
323+
324+ test ( 'does not add DeckShadowCompositingEffect when no shadow effect exists' , async ( ) => {
325+ const nonShadowEffect = { id : 'lighting' , shadow : false } ;
326+ mockComputeDeckEffects . mockReturnValue ( [ nonShadowEffect ] ) ;
327+
328+ await renderAndWaitForPanel ( propsWithEffects ( [ mockEffect ] ) ) ;
329+
330+ const panelProps = mockExportVideoPanelContainer . mock . calls [ 0 ] [ 0 ] ;
331+ const effects = panelProps . deckProps . effects ;
332+
333+ expect ( effects . length ) . toBe ( 1 ) ;
334+ expect ( effects [ 0 ] ) . toBe ( nonShadowEffect ) ;
335+ } ) ;
336+ } ) ;
189337} ) ;
0 commit comments