1import { e2e } from '@grafana/e2e';
2import {
3  addDays,
4  addHours,
5  differenceInCalendarDays,
6  differenceInMinutes,
7  format,
8  isBefore,
9  parseISO,
10  toDate,
11} from 'date-fns';
12
13e2e.scenario({
14  describeName: 'Dashboard time zone support',
15  itName: 'Tests dashboard time zone scenarios',
16  addScenarioDataSource: false,
17  addScenarioDashBoard: false,
18  skipScenario: false,
19  scenario: () => {
20    e2e.flows.openDashboard({ uid: '5SdHCasdf' });
21
22    const fromTimeZone = 'UTC';
23    const toTimeZone = 'America/Chicago';
24    const offset = offsetBetweenTimeZones(toTimeZone, fromTimeZone);
25
26    const panelsToCheck = [
27      'Random walk series',
28      'Millisecond res x-axis and tooltip',
29      '2 yaxis and axis labels',
30      'Stacking value ontop of nulls',
31      'Null between points',
32      'Legend Table No Scroll Visible',
33    ];
34
35    const timesInUtc: Record<string, string> = {};
36
37    for (const title of panelsToCheck) {
38      e2e.components.Panels.Panel.containerByTitle(title)
39        .should('be.visible')
40        .within(() =>
41          e2e.components.Panels.Visualization.Graph.xAxis
42            .labels()
43            .should('be.visible')
44            .last()
45            .should((element) => {
46              timesInUtc[title] = element.text();
47            })
48        );
49    }
50
51    e2e.components.PageToolbar.item('Dashboard settings').click();
52
53    e2e.components.TimeZonePicker.containerV2()
54      .should('be.visible')
55      .within(() => {
56        e2e.components.Select.singleValue().should('be.visible').should('have.text', 'Coordinated Universal Time');
57        e2e.components.Select.input().should('be.visible').click();
58      });
59
60    e2e.components.Select.option().should('be.visible').contains(toTimeZone).click();
61
62    // click to go back to the dashboard.
63    e2e.components.BackButton.backArrow().click({ force: true }).wait(2000);
64
65    for (const title of panelsToCheck) {
66      e2e.components.Panels.Panel.containerByTitle(title)
67        .should('be.visible')
68        .within(() =>
69          e2e.components.Panels.Visualization.Graph.xAxis
70            .labels()
71            .should('be.visible')
72            .last()
73            .should((element) => {
74              const inUtc = timesInUtc[title];
75              const inTz = element.text();
76              const isCorrect = isTimeCorrect(inUtc, inTz, offset);
77              assert.isTrue(isCorrect, `Panel with title: "${title}"`);
78            })
79        );
80    }
81  },
82});
83
84const isTimeCorrect = (inUtc: string, inTz: string, offset: number): boolean => {
85  if (inUtc === inTz) {
86    // we need to catch issues when timezone isn't changed for some reason like https://github.com/grafana/grafana/issues/35504
87    return false;
88  }
89
90  const reference = format(new Date(), 'yyyy-LL-dd');
91
92  const utcDate = toDate(parseISO(`${reference} ${inUtc}`));
93  const utcDateWithOffset = addHours(toDate(parseISO(`${reference} ${inUtc}`)), offset);
94  const dayDifference = differenceInCalendarDays(utcDate, utcDateWithOffset); // if the utcDate +/- offset is the day before/after then we need to adjust reference
95  const dayOffset = isBefore(utcDateWithOffset, utcDate) ? dayDifference * -1 : dayDifference;
96  const tzDate = addDays(toDate(parseISO(`${reference} ${inTz}`)), dayOffset); // adjust tzDate with any dayOffset
97  const diff = Math.abs(differenceInMinutes(utcDate, tzDate)); // use Math.abs if tzDate is in future
98
99  return diff <= Math.abs(offset * 60);
100};
101
102const offsetBetweenTimeZones = (timeZone1: string, timeZone2: string, when: Date = new Date()): number => {
103  const t1 = convertDateToAnotherTimeZone(when, timeZone1);
104  const t2 = convertDateToAnotherTimeZone(when, timeZone2);
105  return (t1.getTime() - t2.getTime()) / (1000 * 60 * 60);
106};
107
108const convertDateToAnotherTimeZone = (date: Date, timeZone: string): Date => {
109  const dateString = date.toLocaleString('en-US', {
110    timeZone: timeZone,
111  });
112  return new Date(dateString);
113};
114