From b7fc3cfe47272d4b614702a5cb9f0497c27e72fc Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Thu, 16 Oct 2025 15:11:39 +0200 Subject: [PATCH 1/8] SceneTimeRange: Preserve explicit timezone values in URL state Fixes dashboard links not preserving timezone settings like "browser". Previously, getUrlState() always called getTimeZone() which resolves "browser" to the actual browser timezone (e.g., "Europe/Madrid"). The issue: "browser" is a semantic value meaning "use my current browser timezone". Resolving it makes the timezone fixed - if the user travels or switches devices, the timezone won't update as expected. Solution: Preserve state.timeZone when explicitly set, only falling back to getTimeZone() when undefined. This ensures: - timezone="browser" stays as "browser" in URLs (preserves semantics) - timezone="UTC" stays as "UTC" in URLs - undefined timezone still resolves to actual timezone (backward compatible) Changed in getUrlState() (line 198): Before: timezone: this.getTimeZone() After: timezone: this.state.timeZone !== undefined ? this.state.timeZone : this.getTimeZone() This maintains backward compatibility while fixing dashboard link navigation. Internal timezone resolution for calculations still uses getTimeZone(). Related: - Customer issue: Dashboard links with scenes enabled resolve "browser" timezone - Works correctly with scenes disabled (old dashboard implementation) - Requires corresponding Grafana PR to preserve timezone parameter Fixes: Dashboard links not preserving timezone setting with scenes enabled --- packages/scenes/src/core/SceneTimeRange.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/scenes/src/core/SceneTimeRange.tsx b/packages/scenes/src/core/SceneTimeRange.tsx index 9b6aea1ef..bebe456f7 100644 --- a/packages/scenes/src/core/SceneTimeRange.tsx +++ b/packages/scenes/src/core/SceneTimeRange.tsx @@ -195,7 +195,13 @@ export class SceneTimeRange extends SceneObjectBase impleme public getUrlState() { const params = locationService.getSearchObject(); - const urlValues: SceneObjectUrlValues = { from: this.state.from, to: this.state.to, timezone: this.getTimeZone() }; + // Use state.timeZone if explicitly set, otherwise use getTimeZone() to resolve from parent or default + // This preserves explicit timezone values like "browser" instead of resolving them + const urlValues: SceneObjectUrlValues = { + from: this.state.from, + to: this.state.to, + timezone: this.state.timeZone !== undefined ? this.state.timeZone : this.getTimeZone(), + }; // Clear time and time.window once they are converted to from and to if (params.time && params['time.window']) { From 2d695acbfeb1b65e9a56281d866adec541578d7f Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Thu, 16 Oct 2025 23:13:12 +0200 Subject: [PATCH 2/8] Add tests --- .../scenes/src/core/SceneTimeRange.test.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/scenes/src/core/SceneTimeRange.test.tsx b/packages/scenes/src/core/SceneTimeRange.test.tsx index 31fbaea98..cfed7be4c 100644 --- a/packages/scenes/src/core/SceneTimeRange.test.tsx +++ b/packages/scenes/src/core/SceneTimeRange.test.tsx @@ -212,6 +212,92 @@ describe('SceneTimeRange', () => { expect(timeRange.getTimeZone()).toBe(browserTimeZone); }); }); + + describe('getUrlState timezone preservation', () => { + it('should preserve explicit "browser" timezone in URL state', () => { + const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); + const urlState = timeRange.urlSync?.getUrlState(); + expect(urlState?.timezone).toBe('browser'); + }); + + it('should preserve explicit "UTC" timezone in URL state', () => { + const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'UTC' }); + const urlState = timeRange.urlSync?.getUrlState(); + expect(urlState?.timezone).toBe('UTC'); + }); + + it('should preserve explicit IANA timezone in URL state', () => { + const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'America/New_York' }); + const urlState = timeRange.urlSync?.getUrlState(); + expect(urlState?.timezone).toBe('America/New_York'); + }); + + it('should use resolved timezone when state.timeZone is undefined', () => { + const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now' }); + const urlState = timeRange.urlSync?.getUrlState(); + // When no explicit timezone is set, should resolve to browser timezone + expect(urlState?.timezone).toBe(browserTimeZone); + }); + + it('should preserve explicit timezone even when it matches resolved timezone', () => { + // Set explicit timezone to the same value as browser timezone + const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: browserTimeZone }); + const urlState = timeRange.urlSync?.getUrlState(); + // Should still preserve the explicit value + expect(urlState?.timezone).toBe(browserTimeZone); + }); + + it('should resolve timezone from parent when state.timeZone is undefined', () => { + const outerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'America/New_York' }); + const innerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now' }); + const scene = new SceneFlexLayout({ + $timeRange: outerTimeRange, + children: [ + new SceneFlexItem({ + $timeRange: innerTimeRange, + body: PanelBuilders.text().build(), + }), + ], + }); + scene.activate(); + + const urlState = innerTimeRange.urlSync?.getUrlState(); + // Should resolve to parent's timezone + expect(urlState?.timezone).toBe('America/New_York'); + }); + + it('should preserve explicit timezone over parent timezone', () => { + const outerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'America/New_York' }); + const innerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'Europe/Berlin' }); + const scene = new SceneFlexLayout({ + $timeRange: outerTimeRange, + children: [ + new SceneFlexItem({ + $timeRange: innerTimeRange, + body: PanelBuilders.text().build(), + }), + ], + }); + scene.activate(); + + const urlState = innerTimeRange.urlSync?.getUrlState(); + // Should preserve own explicit timezone, not parent's + expect(urlState?.timezone).toBe('Europe/Berlin'); + }); + + it('should preserve "browser" timezone even when browser timezone is different', () => { + // This is the key test: "browser" should stay as "browser", not resolve to actual timezone + const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); + const urlState = timeRange.urlSync?.getUrlState(); + + // Verify getTimeZone() would resolve it + expect(timeRange.getTimeZone()).toBe(browserTimeZone); + + // But URL state should preserve "browser" + expect(urlState?.timezone).toBe('browser'); + expect(urlState?.timezone).not.toBe(browserTimeZone); + }); + }); }); describe('delay now', () => { From 383f2c6033dd4c20a01fd363e3871f3870293d8c Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Thu, 16 Oct 2025 23:27:32 +0200 Subject: [PATCH 3/8] linter --- packages/scenes/src/core/SceneTimeRange.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scenes/src/core/SceneTimeRange.test.tsx b/packages/scenes/src/core/SceneTimeRange.test.tsx index cfed7be4c..1af91ba5e 100644 --- a/packages/scenes/src/core/SceneTimeRange.test.tsx +++ b/packages/scenes/src/core/SceneTimeRange.test.tsx @@ -289,10 +289,10 @@ describe('SceneTimeRange', () => { // This is the key test: "browser" should stay as "browser", not resolve to actual timezone const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); const urlState = timeRange.urlSync?.getUrlState(); - + // Verify getTimeZone() would resolve it expect(timeRange.getTimeZone()).toBe(browserTimeZone); - + // But URL state should preserve "browser" expect(urlState?.timezone).toBe('browser'); expect(urlState?.timezone).not.toBe(browserTimeZone); From 9a557737e720f255298917b3fc78c097a73ec04f Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Fri, 17 Oct 2025 00:01:02 +0200 Subject: [PATCH 4/8] Update tests --- .../scenes/src/core/SceneTimeRange.test.tsx | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/scenes/src/core/SceneTimeRange.test.tsx b/packages/scenes/src/core/SceneTimeRange.test.tsx index 1af91ba5e..ba83c0a56 100644 --- a/packages/scenes/src/core/SceneTimeRange.test.tsx +++ b/packages/scenes/src/core/SceneTimeRange.test.tsx @@ -7,10 +7,6 @@ import { EmbeddedScene } from '../components/EmbeddedScene'; import { SceneReactObject } from '../components/SceneReactObject'; import { defaultTimeZone as browserTimeZone } from '@grafana/schema'; -jest.mock('@grafana/data', () => ({ - ...jest.requireActual('@grafana/data'), -})); - function simulateDelay(newDateString: string, scene: EmbeddedScene) { jest.setSystemTime(new Date(newDateString)); scene.activate(); @@ -285,17 +281,38 @@ describe('SceneTimeRange', () => { expect(urlState?.timezone).toBe('Europe/Berlin'); }); - it('should preserve "browser" timezone even when browser timezone is different', () => { - // This is the key test: "browser" should stay as "browser", not resolve to actual timezone - const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); - const urlState = timeRange.urlSync?.getUrlState(); + it('should preserve "browser" timezone even when getTimeZone returns different value', () => { + // This is the critical test: explicit state.timeZone should be preserved in URL + // Create a scene with an inner time range that has explicit "browser" timezone + const innerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); - // Verify getTimeZone() would resolve it - expect(timeRange.getTimeZone()).toBe(browserTimeZone); + // Create parent scene with a different timezone (Australia/Sydney from config) + const outerTimeRange = new SceneTimeRange({ + from: 'now-6h', + to: 'now', + timeZone: USER_PROFILE_DEFAULT_TIME_ZONE, + }); - // But URL state should preserve "browser" + const scene = new EmbeddedScene({ + $timeRange: outerTimeRange, + body: new SceneFlexLayout({ + children: [ + new SceneFlexItem({ + $timeRange: innerTimeRange, + body: PanelBuilders.timeseries().build(), + }), + ], + }), + }); + + scene.activate(); + + // The inner time range should preserve its explicit "browser" timezone in URL + const urlState = innerTimeRange.urlSync?.getUrlState(); expect(urlState?.timezone).toBe('browser'); - expect(urlState?.timezone).not.toBe(browserTimeZone); + + // Even though parent has different timezone + expect(outerTimeRange.state.timeZone).toBe(USER_PROFILE_DEFAULT_TIME_ZONE); }); }); }); From 0cb596f52f9fa65f8ae52593c55a3f7181434d40 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Fri, 17 Oct 2025 16:04:04 +0200 Subject: [PATCH 5/8] test --- .../src/variables/macros/timeMacros.test.ts | 20 +++++++++++++++---- .../scenes/src/variables/macros/timeMacros.ts | 5 ++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/scenes/src/variables/macros/timeMacros.test.ts b/packages/scenes/src/variables/macros/timeMacros.test.ts index be3f61c9c..12a4fd01a 100644 --- a/packages/scenes/src/variables/macros/timeMacros.test.ts +++ b/packages/scenes/src/variables/macros/timeMacros.test.ts @@ -8,14 +8,26 @@ import { LoadingState } from '@grafana/schema'; import { DataQueryRequest, getDefaultTimeRange } from '@grafana/data'; describe('timeMacros', () => { - it('Can use use $__url_time_range with browser timeZone', () => { + it('Can use use $__url_time_range with explicit browser timeZone', () => { + const scene = new TestScene({ + $timeRange: new SceneTimeRange({ from: 'now-5m', to: 'now', timeZone: 'browser' }), + }); + + // Browser timezone should be preserved as "browser", not resolved to actual timezone + expect(sceneInterpolator(scene, '$__url_time_range')).toBe('from=now-5m&to=now&timezone=browser'); + }); + + it('Can use use $__url_time_range when timezone is undefined', () => { const scene = new TestScene({ $timeRange: new SceneTimeRange({ from: 'now-5m', to: 'now' }), }); - expect(sceneInterpolator(scene, '$__url_time_range')).toBe( - `from=now-5m&to=now&timezone=${encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone)}` - ); + // When timezone is undefined, getUrlState() will use getTimeZone() which resolves to browser default + // This should include the resolved timezone in the URL + const result = sceneInterpolator(scene, '$__url_time_range'); + expect(result).toContain('from=now-5m'); + expect(result).toContain('to=now'); + expect(result).toContain('timezone='); }); it('Can use use $__url_time_range with custom timeZone', () => { diff --git a/packages/scenes/src/variables/macros/timeMacros.ts b/packages/scenes/src/variables/macros/timeMacros.ts index 8c43a1787..75472a201 100644 --- a/packages/scenes/src/variables/macros/timeMacros.ts +++ b/packages/scenes/src/variables/macros/timeMacros.ts @@ -20,9 +20,8 @@ export class UrlTimeRangeMacro implements FormatVariable { public getValue(): SkipFormattingValue { const timeRange = getTimeRange(this._sceneObject); const urlState = timeRange.urlSync?.getUrlState(); - if (urlState?.timezone === 'browser') { - urlState.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - } + // Preserve timezone as-is, including semantic values like "browser" + // This ensures dashboard links maintain the original timezone setting return new SkipFormattingValue(urlUtil.toUrlParams(urlState)); } From e9855b72cd5816b122ad17cbb6fd3495e33f541f Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Thu, 23 Oct 2025 13:44:31 +0200 Subject: [PATCH 6/8] UrlTimeRangeMacro: Preserve browser timezone in dashboard links The __url_time_range macro was incorrectly resolving 'browser' timezone to the actual IANA timezone when generating dashboard link URLs. This caused dashboard links to lose the browser timezone setting. Now the macro preserves timezone values as-is, including semantic values like 'browser', ensuring dashboard links maintain the original timezone. Fixes dashboard links not preserving browser timezone setting. --- packages/scenes/src/variables/macros/timeMacros.test.ts | 7 +++---- packages/scenes/src/variables/macros/timeMacros.ts | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/scenes/src/variables/macros/timeMacros.test.ts b/packages/scenes/src/variables/macros/timeMacros.test.ts index be3f61c9c..74bdbd5d7 100644 --- a/packages/scenes/src/variables/macros/timeMacros.test.ts +++ b/packages/scenes/src/variables/macros/timeMacros.test.ts @@ -10,12 +10,11 @@ import { DataQueryRequest, getDefaultTimeRange } from '@grafana/data'; describe('timeMacros', () => { it('Can use use $__url_time_range with browser timeZone', () => { const scene = new TestScene({ - $timeRange: new SceneTimeRange({ from: 'now-5m', to: 'now' }), + $timeRange: new SceneTimeRange({ from: 'now-5m', to: 'now', timeZone: 'browser' }), }); - expect(sceneInterpolator(scene, '$__url_time_range')).toBe( - `from=now-5m&to=now&timezone=${encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone)}` - ); + // Browser timezone should be preserved as "browser", not resolved to actual timezone + expect(sceneInterpolator(scene, '$__url_time_range')).toBe('from=now-5m&to=now&timezone=browser'); }); it('Can use use $__url_time_range with custom timeZone', () => { diff --git a/packages/scenes/src/variables/macros/timeMacros.ts b/packages/scenes/src/variables/macros/timeMacros.ts index 8c43a1787..9c737cdbf 100644 --- a/packages/scenes/src/variables/macros/timeMacros.ts +++ b/packages/scenes/src/variables/macros/timeMacros.ts @@ -20,9 +20,7 @@ export class UrlTimeRangeMacro implements FormatVariable { public getValue(): SkipFormattingValue { const timeRange = getTimeRange(this._sceneObject); const urlState = timeRange.urlSync?.getUrlState(); - if (urlState?.timezone === 'browser') { - urlState.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - } + return new SkipFormattingValue(urlUtil.toUrlParams(urlState)); } From 026690eb5927b7eb6b8f3908146117a39be7afc3 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Thu, 23 Oct 2025 13:52:31 +0200 Subject: [PATCH 7/8] undo unnecessary changes --- .../scenes/src/core/SceneTimeRange.test.tsx | 107 ------------------ packages/scenes/src/core/SceneTimeRange.tsx | 8 +- 2 files changed, 1 insertion(+), 114 deletions(-) diff --git a/packages/scenes/src/core/SceneTimeRange.test.tsx b/packages/scenes/src/core/SceneTimeRange.test.tsx index ba83c0a56..68077ebc5 100644 --- a/packages/scenes/src/core/SceneTimeRange.test.tsx +++ b/packages/scenes/src/core/SceneTimeRange.test.tsx @@ -208,113 +208,6 @@ describe('SceneTimeRange', () => { expect(timeRange.getTimeZone()).toBe(browserTimeZone); }); }); - - describe('getUrlState timezone preservation', () => { - it('should preserve explicit "browser" timezone in URL state', () => { - const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); - const urlState = timeRange.urlSync?.getUrlState(); - expect(urlState?.timezone).toBe('browser'); - }); - - it('should preserve explicit "UTC" timezone in URL state', () => { - const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'UTC' }); - const urlState = timeRange.urlSync?.getUrlState(); - expect(urlState?.timezone).toBe('UTC'); - }); - - it('should preserve explicit IANA timezone in URL state', () => { - const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'America/New_York' }); - const urlState = timeRange.urlSync?.getUrlState(); - expect(urlState?.timezone).toBe('America/New_York'); - }); - - it('should use resolved timezone when state.timeZone is undefined', () => { - const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now' }); - const urlState = timeRange.urlSync?.getUrlState(); - // When no explicit timezone is set, should resolve to browser timezone - expect(urlState?.timezone).toBe(browserTimeZone); - }); - - it('should preserve explicit timezone even when it matches resolved timezone', () => { - // Set explicit timezone to the same value as browser timezone - const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: browserTimeZone }); - const urlState = timeRange.urlSync?.getUrlState(); - // Should still preserve the explicit value - expect(urlState?.timezone).toBe(browserTimeZone); - }); - - it('should resolve timezone from parent when state.timeZone is undefined', () => { - const outerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'America/New_York' }); - const innerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now' }); - const scene = new SceneFlexLayout({ - $timeRange: outerTimeRange, - children: [ - new SceneFlexItem({ - $timeRange: innerTimeRange, - body: PanelBuilders.text().build(), - }), - ], - }); - scene.activate(); - - const urlState = innerTimeRange.urlSync?.getUrlState(); - // Should resolve to parent's timezone - expect(urlState?.timezone).toBe('America/New_York'); - }); - - it('should preserve explicit timezone over parent timezone', () => { - const outerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'America/New_York' }); - const innerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'Europe/Berlin' }); - const scene = new SceneFlexLayout({ - $timeRange: outerTimeRange, - children: [ - new SceneFlexItem({ - $timeRange: innerTimeRange, - body: PanelBuilders.text().build(), - }), - ], - }); - scene.activate(); - - const urlState = innerTimeRange.urlSync?.getUrlState(); - // Should preserve own explicit timezone, not parent's - expect(urlState?.timezone).toBe('Europe/Berlin'); - }); - - it('should preserve "browser" timezone even when getTimeZone returns different value', () => { - // This is the critical test: explicit state.timeZone should be preserved in URL - // Create a scene with an inner time range that has explicit "browser" timezone - const innerTimeRange = new SceneTimeRange({ from: 'now-1h', to: 'now', timeZone: 'browser' }); - - // Create parent scene with a different timezone (Australia/Sydney from config) - const outerTimeRange = new SceneTimeRange({ - from: 'now-6h', - to: 'now', - timeZone: USER_PROFILE_DEFAULT_TIME_ZONE, - }); - - const scene = new EmbeddedScene({ - $timeRange: outerTimeRange, - body: new SceneFlexLayout({ - children: [ - new SceneFlexItem({ - $timeRange: innerTimeRange, - body: PanelBuilders.timeseries().build(), - }), - ], - }), - }); - - scene.activate(); - - // The inner time range should preserve its explicit "browser" timezone in URL - const urlState = innerTimeRange.urlSync?.getUrlState(); - expect(urlState?.timezone).toBe('browser'); - - // Even though parent has different timezone - expect(outerTimeRange.state.timeZone).toBe(USER_PROFILE_DEFAULT_TIME_ZONE); - }); - }); }); describe('delay now', () => { diff --git a/packages/scenes/src/core/SceneTimeRange.tsx b/packages/scenes/src/core/SceneTimeRange.tsx index bebe456f7..9b6aea1ef 100644 --- a/packages/scenes/src/core/SceneTimeRange.tsx +++ b/packages/scenes/src/core/SceneTimeRange.tsx @@ -195,13 +195,7 @@ export class SceneTimeRange extends SceneObjectBase impleme public getUrlState() { const params = locationService.getSearchObject(); - // Use state.timeZone if explicitly set, otherwise use getTimeZone() to resolve from parent or default - // This preserves explicit timezone values like "browser" instead of resolving them - const urlValues: SceneObjectUrlValues = { - from: this.state.from, - to: this.state.to, - timezone: this.state.timeZone !== undefined ? this.state.timeZone : this.getTimeZone(), - }; + const urlValues: SceneObjectUrlValues = { from: this.state.from, to: this.state.to, timezone: this.getTimeZone() }; // Clear time and time.window once they are converted to from and to if (params.time && params['time.window']) { From de883799447497e82413595b26aa8ab5844b0e93 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Thu, 23 Oct 2025 13:56:12 +0200 Subject: [PATCH 8/8] Update sceneInterpolator test for timezone preservation The test was expecting the old behavior where browser timezone gets resolved to actual IANA timezone. Updated to expect the new behavior where browser timezone is preserved as-is in dashboard links. --- .../interpolation/sceneInterpolator.test.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/scenes/src/variables/interpolation/sceneInterpolator.test.ts b/packages/scenes/src/variables/interpolation/sceneInterpolator.test.ts index 603d7cb42..896c6d3bc 100644 --- a/packages/scenes/src/variables/interpolation/sceneInterpolator.test.ts +++ b/packages/scenes/src/variables/interpolation/sceneInterpolator.test.ts @@ -221,14 +221,26 @@ describe('sceneInterpolator', () => { expect(sceneInterpolator(scene, '$__all_variables', {}, VariableFormatID.PercentEncode)).toBe('var-cluster=A'); }); - it('Can use use $__url_time_range with browser timezone', () => { + it('Can use use $__url_time_range with explicit browser timezone', () => { + const scene = new TestScene({ + $timeRange: new SceneTimeRange({ from: 'now-5m', to: 'now', timeZone: 'browser' }), + }); + + // Browser timezone should be preserved as "browser", not resolved to actual timezone + expect(sceneInterpolator(scene, '$__url_time_range')).toBe('from=now-5m&to=now&timezone=browser'); + }); + + it('Can use use $__url_time_range when timezone is undefined', () => { const scene = new TestScene({ $timeRange: new SceneTimeRange({ from: 'now-5m', to: 'now' }), }); - expect(sceneInterpolator(scene, '$__url_time_range')).toBe( - `from=now-5m&to=now&timezone=${encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone)}` - ); + // When timezone is undefined, getUrlState() will use getTimeZone() which resolves to browser default + // This should include the resolved timezone in the URL + const result = sceneInterpolator(scene, '$__url_time_range'); + expect(result).toContain('from=now-5m'); + expect(result).toContain('to=now'); + expect(result).toContain('timezone='); }); it('Can use use $__url_time_range with custom timezone', () => {