Skip to content
Merged
111 changes: 107 additions & 4 deletions packages/scenes/src/core/SceneTimeRange.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -212,6 +208,113 @@ 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', () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/scenes/src/core/SceneTimeRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,13 @@ export class SceneTimeRange extends SceneObjectBase<SceneTimeRangeState> 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']) {
Expand Down