@grafana/plugin-e2e 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -6,17 +6,113 @@ export type PluginOptions = {
6
6
  selectorRegistration: void;
7
7
  };
8
8
  export type PluginFixture = {
9
+ /**
10
+ * The current Grafana version.
11
+ *
12
+ * If a GRAFANA_VERSION environment variable is set, this will be used. Otherwise,
13
+ * the version will be picked from window.grafanaBootData.settings.buildInfo.version.
14
+ */
9
15
  grafanaVersion: string;
16
+ /**
17
+ * The E2E selectors to use for the current version of Grafana
18
+ */
10
19
  selectors: E2ESelectors;
20
+ /**
21
+ * Isolated {@link DashboardPage} instance for each test.
22
+ *
23
+ * Navigates to a new dashboard page and adds a new panel.
24
+ *
25
+ * Use {@link PanelEditPage.setVisualization} to change the visualization
26
+ * Use {@link PanelEditPage.datasource.set} to change the datasource
27
+ * Use {@link PanelEditPage.getQueryEditorEditorRow} to retrieve the query
28
+ * editor row locator for a given query refId
29
+ */
11
30
  newDashboardPage: DashboardPage;
31
+ /**
32
+ * Isolated {@link PanelEditPage} instance for each test.
33
+ *
34
+ * Navigates to a new dashboard page and adds a new panel.
35
+ *
36
+ * Use {@link PanelEditPage.setVisualization} to change the visualization
37
+ * Use {@link PanelEditPage.datasource.set} to change the datasource
38
+ * Use {@link ExplorePage.getQueryEditorEditorRow} to retrieve the query
39
+ * editor row locator for a given query refId
40
+ */
12
41
  panelEditPage: PanelEditPage;
42
+ /**
43
+ * Isolated {@link VariableEditPage} instance for each test.
44
+ *
45
+ * Navigates to a new dashboard page and adds a new variable.
46
+ *
47
+ * Use {@link VariableEditPage.setVariableType} to change the variable type
48
+ */
13
49
  variableEditPage: VariableEditPage;
50
+ /**
51
+ * Isolated {@link AnnotationEditPage} instance for each test.
52
+ *
53
+ * Navigates to a new dashboard page and adds a new annotation.
54
+ *
55
+ * Use {@link AnnotationEditPage.datasource.set} to change the datasource
56
+ */
14
57
  annotationEditPage: AnnotationEditPage;
58
+ /**
59
+ * Isolated {@link ExplorePage} instance for each test.
60
+ *
61
+ * Navigates to a the explore page.
62
+ *
63
+ * Use {@link ExplorePage.datasource.set} to change the datasource
64
+ * Use {@link ExplorePage.getQueryEditorEditorRow} to retrieve the query editor
65
+ * row locator for a given query refId
66
+ */
15
67
  explorePage: ExplorePage;
68
+ /**
69
+ * Fixture command that will create an isolated DataSourceConfigPage instance for a given data source type.
70
+ *
71
+ * The data source config page cannot be navigated to without a data source uid, so this fixture will create a new
72
+ * data source using the Grafana API, create a new DataSourceConfigPage instance and navigate to the page.
73
+ */
16
74
  createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise<DataSourceConfigPage>;
75
+ /**
76
+ * Fixture command that creates a data source via the Grafana API.
77
+ *
78
+ * If you have tests that depend on the the existance of a data source,
79
+ * you may use this command in a setup project. Read more about setup projects
80
+ * here: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
81
+ */
17
82
  createDataSource: (args: CreateDataSourceArgs) => Promise<DataSource>;
83
+ /**
84
+ * Fixture command that login to Grafana using the Grafana API.
85
+ * If the same credentials should be used in every test,
86
+ * invoke this fixture in a setup project.
87
+ * See https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
88
+ *
89
+ * If no credentials are provided, the default admin/admin credentials will be used.
90
+ *
91
+ * The default credentials can be overridden in the playwright.config.ts file:
92
+ * eg.
93
+ * export default defineConfig({
94
+ use: {
95
+ httpCredentials: {
96
+ username: 'user',
97
+ password: 'pass',
98
+ },
99
+ },
100
+ });
101
+ *
102
+ * To override credentials in a single test:
103
+ * test.use({ httpCredentials: { username: 'admin', password: 'admin' } });
104
+ * To avoid authentication in a single test:
105
+ * test.use({ storageState: { cookies: [], origins: [] } });
106
+ */
18
107
  login: () => Promise<void>;
108
+ /**
109
+ * Fixture command that reads a the yaml file for a provisioned dashboard
110
+ * or data source and returns it as json.
111
+ */
19
112
  readProvision<T = any>(args: ReadProvisionArgs): Promise<T>;
113
+ /**
114
+ * Function that checks if a feature toggle is enabled. Only works for frontend feature toggles.
115
+ */
20
116
  isFeatureToggleEnabled<T = object>(featureToggle: keyof T): Promise<boolean>;
21
117
  };
22
118
  export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & PluginFixture & PluginOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
package/dist/api.js CHANGED
@@ -8,8 +8,18 @@ const test_1 = require("@playwright/test");
8
8
  const fixtures_1 = __importDefault(require("./fixtures"));
9
9
  const matchers_1 = __importDefault(require("./matchers"));
10
10
  const selectorEngine_1 = require("./selectorEngine");
11
+ // extend Playwright with Grafana plugin specific fixtures
11
12
  exports.test = test_1.test.extend(fixtures_1.default);
12
13
  exports.expect = test_1.expect.extend(matchers_1.default);
14
+ /** Register a custom selector engine that resolves locators for Grafana E2E selectors
15
+ *
16
+ * The same functionality is available in the {@link GrafanaPage.getByTestIdOrAriaLabel} method. However,
17
+ * by registering the selector engine, one can resolve locators by Grafana E2E selectors also within a locator.
18
+ *
19
+ * Example:
20
+ * const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); // returns a locator
21
+ * queryEditorRow.locator(`selector=${selectors.components.TimePicker.openButton}`).click();
22
+ * */
13
23
  test_1.selectors.register('selector', selectorEngine_1.grafanaE2ESelectorEngine);
14
24
  var test_2 = require("@playwright/test");
15
25
  Object.defineProperty(exports, "selectors", { enumerable: true, get: function () { return test_2.selectors; } });
@@ -1,3 +1,10 @@
1
1
  import { E2ESelectors } from './types';
2
2
  import { VersionedSelectors } from './versioned/types';
3
+ /**
4
+ * Resolves selectors based on the Grafana version
5
+ *
6
+ * If the selector has multiple versions, the last version that is less
7
+ * than or equal to the Grafana version will be returned.
8
+ * If the selector doesn't have a version, it will be returned as is.
9
+ */
3
10
  export declare const resolveSelectors: (versionedSelectors: VersionedSelectors, grafanaVersion: string) => E2ESelectors;
@@ -6,12 +6,15 @@ const processSelectors = (selectors, versionedSelectors, grafanaVersion) => {
6
6
  const keys = Object.keys(versionedSelectors);
7
7
  for (let index = 0; index < keys.length; index++) {
8
8
  const key = keys[index];
9
+ // @ts-ignore
9
10
  const value = versionedSelectors[key];
10
11
  if (typeof value === 'object' && Object.keys(value).length > 0 && !semver.valid(Object.keys(value)[0])) {
12
+ // @ts-ignore
11
13
  selectors[key] = processSelectors({}, value, grafanaVersion);
12
14
  }
13
15
  else {
14
16
  if (typeof value === 'object' && Object.keys(value).length > 0 && semver.valid(Object.keys(value)[0])) {
17
+ // @ts-ignore
15
18
  const sorted = Object.keys(value).sort(semver.rcompare);
16
19
  let validVersion = sorted[0];
17
20
  for (let index = 0; index < sorted.length; index++) {
@@ -21,9 +24,11 @@ const processSelectors = (selectors, versionedSelectors, grafanaVersion) => {
21
24
  break;
22
25
  }
23
26
  }
27
+ // @ts-ignore
24
28
  selectors[key] = value[validVersion];
25
29
  }
26
30
  else {
31
+ // @ts-ignore
27
32
  selectors[key] = value;
28
33
  }
29
34
  }
@@ -31,6 +36,13 @@ const processSelectors = (selectors, versionedSelectors, grafanaVersion) => {
31
36
  }
32
37
  return selectors;
33
38
  };
39
+ /**
40
+ * Resolves selectors based on the Grafana version
41
+ *
42
+ * If the selector has multiple versions, the last version that is less
43
+ * than or equal to the Grafana version will be returned.
44
+ * If the selector doesn't have a version, it will be returned as is.
45
+ */
34
46
  const resolveSelectors = (versionedSelectors, grafanaVersion) => {
35
47
  const selectors = {};
36
48
  return processSelectors(selectors, versionedSelectors, grafanaVersion.replace(/\-.*/, ''));
@@ -25,10 +25,13 @@ describe('resolveSelectors', () => {
25
25
  '10.3.0': 'data-testid Code editor container',
26
26
  [constants_1.MIN_GRAFANA_VERSION]: 'Code editor container',
27
27
  };
28
+ // semver great than
28
29
  let selectors = (0, resolver_1.resolveSelectors)(versionedSelectors, '10.4.0');
29
30
  expect(selectors.components.CodeEditor.container).toBe('data-testid Code editor container');
31
+ // semver equals to
30
32
  selectors = (0, resolver_1.resolveSelectors)(versionedSelectors, '10.3.0');
31
33
  expect(selectors.components.CodeEditor.container).toBe('data-testid Code editor container');
34
+ // semver equals to when using pre-release
32
35
  selectors = (0, resolver_1.resolveSelectors)(versionedSelectors, '10.3.0-pre');
33
36
  expect(selectors.components.CodeEditor.container).toBe('data-testid Code editor container');
34
37
  selectors = (0, resolver_1.resolveSelectors)(versionedSelectors, '9.2.0');
@@ -191,6 +191,9 @@ export type Components = {
191
191
  content: string;
192
192
  };
193
193
  Alert: {
194
+ /**
195
+ * @deprecated use alertV2 from Grafana 8.3 instead
196
+ */
194
197
  alert: (severity: string) => string;
195
198
  alertV2: (severity: string) => string;
196
199
  };
@@ -402,6 +405,7 @@ export type Pages = {
402
405
  };
403
406
  AddDataSource: {
404
407
  url: string;
408
+ /** @deprecated Use dataSourcePluginsV2 */
405
409
  dataSourcePlugins: (pluginName: string) => string;
406
410
  dataSourcePluginsV2: (pluginName: string) => string;
407
411
  };
@@ -472,6 +476,9 @@ export type Pages = {
472
476
  };
473
477
  List: {
474
478
  url: (uid: string) => string;
479
+ /**
480
+ * @deprecated use addAnnotationCTAV2 from Grafana 8.3 instead
481
+ */
475
482
  addAnnotationCTA: string;
476
483
  addAnnotationCTAV2: string;
477
484
  };
@@ -96,6 +96,9 @@ export declare const versionedComponents: {
96
96
  };
97
97
  };
98
98
  BarGauge: {
99
+ /**
100
+ * @deprecated use valueV2 from Grafana 8.3 instead
101
+ */
99
102
  value: string;
100
103
  valueV2: string;
101
104
  };
@@ -173,7 +176,13 @@ export declare const versionedComponents: {
173
176
  active: () => string;
174
177
  };
175
178
  RefreshPicker: {
179
+ /**
180
+ * @deprecated use runButtonV2 from Grafana 8.3 instead
181
+ */
176
182
  runButton: string;
183
+ /**
184
+ * @deprecated use intervalButtonV2 from Grafana 8.3 instead
185
+ */
177
186
  intervalButton: string;
178
187
  runButtonV2: string;
179
188
  intervalButtonV2: string;
@@ -199,6 +208,9 @@ export declare const versionedComponents: {
199
208
  content: string;
200
209
  };
201
210
  Alert: {
211
+ /**
212
+ * @deprecated use alertV2 from Grafana 8.3 instead
213
+ */
202
214
  alert: (severity: string) => string;
203
215
  alertV2: (severity: string) => string;
204
216
  };
@@ -293,6 +305,9 @@ export declare const versionedComponents: {
293
305
  content: string;
294
306
  };
295
307
  FolderPicker: {
308
+ /**
309
+ * @deprecated use containerV2 from Grafana 8.3 instead
310
+ */
296
311
  container: string;
297
312
  containerV2: string;
298
313
  input: string;
@@ -305,14 +320,23 @@ export declare const versionedComponents: {
305
320
  '10.0.0': string;
306
321
  '8.3.0': string;
307
322
  };
323
+ /**
324
+ * @deprecated use inputV2 instead
325
+ */
308
326
  input: () => string;
309
327
  inputV2: string;
310
328
  };
311
329
  TimeZonePicker: {
330
+ /**
331
+ * @deprecated use TimeZonePicker.containerV2 from Grafana 8.3 instead
332
+ */
312
333
  container: string;
313
334
  containerV2: string;
314
335
  };
315
336
  WeekStartPicker: {
337
+ /**
338
+ * @deprecated use WeekStartPicker.containerV2 from Grafana 8.3 instead
339
+ */
316
340
  container: string;
317
341
  containerV2: string;
318
342
  placeholder: string;
@@ -334,8 +358,14 @@ export declare const versionedComponents: {
334
358
  select: (name: string) => string;
335
359
  };
336
360
  Search: {
361
+ /**
362
+ * @deprecated use sectionV2 from Grafana 8.3 instead
363
+ */
337
364
  section: string;
338
365
  sectionV2: string;
366
+ /**
367
+ * @deprecated use itemsV2 from Grafana 8.3 instead
368
+ */
339
369
  items: string;
340
370
  itemsV2: string;
341
371
  cards: string;
@@ -355,6 +385,9 @@ export declare const versionedComponents: {
355
385
  icon: string;
356
386
  };
357
387
  CallToActionCard: {
388
+ /**
389
+ * @deprecated use buttonV2 from Grafana 8.3 instead
390
+ */
358
391
  button: (name: string) => string;
359
392
  buttonV2: (name: string) => string;
360
393
  };
@@ -5,6 +5,7 @@ const constants_1 = require("./constants");
5
5
  exports.versionedComponents = {
6
6
  Breadcrumbs: {
7
7
  breadcrumb: {
8
+ // did not exist prior to 9.4.0
8
9
  '9.4.0': (title) => `data-testid ${title} breadcrumb`,
9
10
  },
10
11
  },
@@ -100,6 +101,9 @@ exports.versionedComponents = {
100
101
  },
101
102
  },
102
103
  BarGauge: {
104
+ /**
105
+ * @deprecated use valueV2 from Grafana 8.3 instead
106
+ */
103
107
  value: 'Bar gauge value',
104
108
  valueV2: 'data-testid Bar gauge value',
105
109
  },
@@ -113,6 +117,7 @@ exports.versionedComponents = {
113
117
  header: 'table header',
114
118
  footer: 'table-footer',
115
119
  body: {
120
+ // did not exist prior to 10.2.0
116
121
  '10.2.0': 'data-testid table body',
117
122
  },
118
123
  },
@@ -139,6 +144,7 @@ exports.versionedComponents = {
139
144
  select: 'Panel editor option pane select',
140
145
  fieldLabel: (type) => `${type} field property editor`,
141
146
  },
147
+ // not sure about the naming *DataPane*
142
148
  DataPane: {
143
149
  content: 'Panel editor data pane content',
144
150
  },
@@ -149,6 +155,7 @@ exports.versionedComponents = {
149
155
  },
150
156
  toggleVizOptions: 'data-testid toggle-viz-options',
151
157
  toggleTableView: 'toggle-table-view',
158
+ // [Geomap] Map controls
152
159
  showZoomField: 'Map controls Show zoom control field property editor',
153
160
  showAttributionField: 'Map controls Show attribution field property editor',
154
161
  showScaleField: 'Map controls Show scale field property editor',
@@ -177,7 +184,13 @@ exports.versionedComponents = {
177
184
  active: () => '[class*="-activeTabStyle"]',
178
185
  },
179
186
  RefreshPicker: {
187
+ /**
188
+ * @deprecated use runButtonV2 from Grafana 8.3 instead
189
+ */
180
190
  runButton: 'RefreshPicker run button',
191
+ /**
192
+ * @deprecated use intervalButtonV2 from Grafana 8.3 instead
193
+ */
181
194
  intervalButton: 'RefreshPicker interval button',
182
195
  runButtonV2: 'data-testid RefreshPicker run button',
183
196
  intervalButtonV2: 'data-testid RefreshPicker interval button',
@@ -203,6 +216,9 @@ exports.versionedComponents = {
203
216
  content: 'Alert editor tab content',
204
217
  },
205
218
  Alert: {
219
+ /**
220
+ * @deprecated use alertV2 from Grafana 8.3 instead
221
+ */
206
222
  alert: (severity) => `Alert ${severity}`,
207
223
  alertV2: (severity) => `data-testid Alert ${severity}`,
208
224
  },
@@ -297,6 +313,9 @@ exports.versionedComponents = {
297
313
  content: 'Field overrides editor content',
298
314
  },
299
315
  FolderPicker: {
316
+ /**
317
+ * @deprecated use containerV2 from Grafana 8.3 instead
318
+ */
300
319
  container: 'Folder picker select container',
301
320
  containerV2: 'data-testid Folder picker select container',
302
321
  input: 'Select a folder',
@@ -307,16 +326,26 @@ exports.versionedComponents = {
307
326
  DataSourcePicker: {
308
327
  container: {
309
328
  '10.0.0': 'data-testid Data source picker select container',
329
+ // did not exist prior to 8.3.0
310
330
  '8.3.0': 'Data source picker select container',
311
331
  },
332
+ /**
333
+ * @deprecated use inputV2 instead
334
+ */
312
335
  input: () => 'input[id="data-source-picker"]',
313
336
  inputV2: 'data-testid Select a data source',
314
337
  },
315
338
  TimeZonePicker: {
339
+ /**
340
+ * @deprecated use TimeZonePicker.containerV2 from Grafana 8.3 instead
341
+ */
316
342
  container: 'Time zone picker select container',
317
343
  containerV2: 'data-testid Time zone picker select container',
318
344
  },
319
345
  WeekStartPicker: {
346
+ /**
347
+ * @deprecated use WeekStartPicker.containerV2 from Grafana 8.3 instead
348
+ */
320
349
  container: 'Choose starting day of the week',
321
350
  containerV2: 'data-testid Choose starting day of the week',
322
351
  placeholder: 'Choose starting day of the week',
@@ -336,8 +365,14 @@ exports.versionedComponents = {
336
365
  select: (name) => `Value picker select ${name}`,
337
366
  },
338
367
  Search: {
368
+ /**
369
+ * @deprecated use sectionV2 from Grafana 8.3 instead
370
+ */
339
371
  section: 'Search section',
340
372
  sectionV2: 'data-testid Search section',
373
+ /**
374
+ * @deprecated use itemsV2 from Grafana 8.3 instead
375
+ */
341
376
  items: 'Search items',
342
377
  itemsV2: 'data-testid Search items',
343
378
  cards: 'data-testid Search cards',
@@ -357,6 +392,9 @@ exports.versionedComponents = {
357
392
  icon: 'Loading indicator',
358
393
  },
359
394
  CallToActionCard: {
395
+ /**
396
+ * @deprecated use buttonV2 from Grafana 8.3 instead
397
+ */
360
398
  button: (name) => `Call to action button ${name}`,
361
399
  buttonV2: (name) => `data-testid Call to action button ${name}`,
362
400
  },
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Selectors grouped/defined in Pages
3
+ *
4
+ * @alpha
5
+ */
1
6
  export declare const versionedPages: {
2
7
  Login: {
3
8
  url: string;
@@ -35,6 +40,7 @@ export declare const versionedPages: {
35
40
  '8.5.0': string;
36
41
  '10.1.0': string;
37
42
  };
43
+ /** @deprecated Use dataSourcePluginsV2 */
38
44
  dataSourcePlugins: {
39
45
  '8.5.0': (pluginName: string) => string;
40
46
  '9.5.0': (pluginName: string) => string;
@@ -75,6 +81,9 @@ export declare const versionedPages: {
75
81
  Dashboard: {
76
82
  url: (uid: string) => string;
77
83
  DashNav: {
84
+ /**
85
+ * @deprecated use navV2 from Grafana 8.3 instead
86
+ */
78
87
  nav: string;
79
88
  navV2: string;
80
89
  publicDashboardTag: string;
@@ -101,6 +110,9 @@ export declare const versionedPages: {
101
110
  sectionItems: (item: string) => string;
102
111
  saveDashBoard: string;
103
112
  saveAsDashBoard: string;
113
+ /**
114
+ * @deprecated use components.TimeZonePicker.containerV2 from Grafana 8.3 instead
115
+ */
104
116
  timezone: string;
105
117
  title: string;
106
118
  };
@@ -145,6 +157,9 @@ export declare const versionedPages: {
145
157
  General: {
146
158
  headerLink: string;
147
159
  modeLabelNew: string;
160
+ /**
161
+ * @deprecated
162
+ */
148
163
  modeLabelEdit: string;
149
164
  generalNameInput: string;
150
165
  generalNameInputV2: string;
@@ -207,6 +222,9 @@ export declare const versionedPages: {
207
222
  };
208
223
  Dashboards: {
209
224
  url: string;
225
+ /**
226
+ * @deprecated use components.Search.dashboardItem from Grafana 8.3 instead
227
+ */
210
228
  dashboards: (title: string) => string;
211
229
  };
212
230
  SaveDashboardAsModal: {
@@ -2,6 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.versionedPages = void 0;
4
4
  const constants_1 = require("./constants");
5
+ /**
6
+ * Selectors grouped/defined in Pages
7
+ *
8
+ * @alpha
9
+ */
5
10
  exports.versionedPages = {
6
11
  Login: {
7
12
  url: '/login',
@@ -39,6 +44,7 @@ exports.versionedPages = {
39
44
  '8.5.0': '/datasources/new',
40
45
  '10.1.0': '/connections/datasources/new',
41
46
  },
47
+ /** @deprecated Use dataSourcePluginsV2 */
42
48
  dataSourcePlugins: {
43
49
  '8.5.0': (pluginName) => `Data source plugin item ${pluginName}`,
44
50
  '9.5.0': (pluginName) => `Add new data source ${pluginName}`,
@@ -79,6 +85,9 @@ exports.versionedPages = {
79
85
  Dashboard: {
80
86
  url: (uid) => `/d/${uid}`,
81
87
  DashNav: {
88
+ /**
89
+ * @deprecated use navV2 from Grafana 8.3 instead
90
+ */
82
91
  nav: 'Dashboard navigation',
83
92
  navV2: 'data-testid Dashboard navigation',
84
93
  publicDashboardTag: 'data-testid public dashboard tag',
@@ -105,6 +114,9 @@ exports.versionedPages = {
105
114
  sectionItems: (item) => `Dashboard settings section item ${item}`,
106
115
  saveDashBoard: 'Dashboard settings aside actions Save button',
107
116
  saveAsDashBoard: 'Dashboard settings aside actions Save As button',
117
+ /**
118
+ * @deprecated use components.TimeZonePicker.containerV2 from Grafana 8.3 instead
119
+ */
108
120
  timezone: 'Time zone picker select container',
109
121
  title: 'Tab General',
110
122
  },
@@ -149,6 +161,9 @@ exports.versionedPages = {
149
161
  General: {
150
162
  headerLink: 'Variable editor Header link',
151
163
  modeLabelNew: 'Variable editor Header mode New',
164
+ /**
165
+ * @deprecated
166
+ */
152
167
  modeLabelEdit: 'Variable editor Header mode Edit',
153
168
  generalNameInput: 'Variable editor Form Name field',
154
169
  generalNameInputV2: 'data-testid Variable editor Form Name field',
@@ -211,6 +226,9 @@ exports.versionedPages = {
211
226
  },
212
227
  Dashboards: {
213
228
  url: '/dashboards',
229
+ /**
230
+ * @deprecated use components.Search.dashboardItem from Grafana 8.3 instead
231
+ */
214
232
  dashboards: (title) => `Dashboard search item ${title}`,
215
233
  },
216
234
  SaveDashboardAsModal: {
@@ -7,5 +7,9 @@ export declare class AnnotationEditPage extends GrafanaPage {
7
7
  datasource: DataSourcePicker;
8
8
  constructor(ctx: PluginTestCtx, args: DashboardEditViewArgs<string>);
9
9
  goto(options?: NavigateOptions): Promise<void>;
10
+ /**
11
+ * Executes the annotation query defined in the annotation page and returns the response promise
12
+ * @param options - Optional. RequestOptions to pass to waitForResponse
13
+ */
10
14
  runQuery(options?: RequestOptions): Promise<import("playwright-core").Response>;
11
15
  }
@@ -20,8 +20,13 @@ class AnnotationEditPage extends GrafanaPage_1.GrafanaPage {
20
20
  : AddDashboard.Settings.Annotations.Edit.url(this.args.id);
21
21
  return super.navigate(url, options);
22
22
  }
23
+ /**
24
+ * Executes the annotation query defined in the annotation page and returns the response promise
25
+ * @param options - Optional. RequestOptions to pass to waitForResponse
26
+ */
23
27
  async runQuery(options) {
24
28
  const responsePromise = this.ctx.page.waitForResponse((resp) => resp.url().includes(this.ctx.selectors.apis.DataSource.query), options);
29
+ //TODO: add new selector and use it in grafana/ui
25
30
  await this.ctx.page.getByRole('button', { name: 'TEST' }).click();
26
31
  return responsePromise;
27
32
  }
@@ -45,6 +45,7 @@ class AnnotationPage extends GrafanaPage_1.GrafanaPage {
45
45
  async clickAddNew() {
46
46
  const { Dashboard } = this.ctx.selectors.pages;
47
47
  if (!this.dashboard?.uid) {
48
+ //the dashboard doesn't have any annotations yet (except for the built-in one)
48
49
  if (semver.gte(this.ctx.grafanaVersion, '8.3.0')) {
49
50
  await this.getByTestIdOrAriaLabel(Dashboard.Settings.Annotations.List.addAnnotationCTAV2).click();
50
51
  }
@@ -53,6 +54,8 @@ class AnnotationPage extends GrafanaPage_1.GrafanaPage {
53
54
  }
54
55
  }
55
56
  else {
57
+ //the dashboard already has annotations
58
+ //TODO: add new selector and use it in grafana/ui
56
59
  await this.ctx.page.getByRole('button', { name: 'New query' }).click();
57
60
  }
58
61
  const editIndex = await this.ctx.page.evaluate(() => {
@@ -5,6 +5,16 @@ export declare class DataSourceConfigPage extends GrafanaPage {
5
5
  constructor(ctx: PluginTestCtx, datasource: DataSource);
6
6
  deleteDataSource(): Promise<void>;
7
7
  goto(options?: NavigateOptions): Promise<void>;
8
+ /**
9
+ * Mocks the response of the datasource health check call
10
+ * @param json the json response to return
11
+ * @param status the HTTP status code to return. Defaults to 200
12
+ */
8
13
  mockHealthCheckResponse<T = any>(json: T, status?: number): Promise<void>;
14
+ /**
15
+ * Clicks the save and test button and waits for the response
16
+ *
17
+ * Optionally, you can skip waiting for the response by passing in { skipWaitForResponse: true } as the options parameter
18
+ */
9
19
  saveAndTest(options?: TriggerQueryOptions): Promise<void | import("playwright-core").Response>;
10
20
  }
@@ -14,11 +14,21 @@ class DataSourceConfigPage extends GrafanaPage_1.GrafanaPage {
14
14
  async goto(options) {
15
15
  return super.navigate(this.ctx.selectors.pages.EditDataSource.url(this.datasource.uid), options);
16
16
  }
17
+ /**
18
+ * Mocks the response of the datasource health check call
19
+ * @param json the json response to return
20
+ * @param status the HTTP status code to return. Defaults to 200
21
+ */
17
22
  async mockHealthCheckResponse(json, status = 200) {
18
23
  await this.ctx.page.route(`${this.ctx.selectors.apis.DataSource.health(this.datasource.uid ?? '', this.datasource.id.toString() ?? '')}`, async (route) => {
19
24
  await route.fulfill({ json, status });
20
25
  });
21
26
  }
27
+ /**
28
+ * Clicks the save and test button and waits for the response
29
+ *
30
+ * Optionally, you can skip waiting for the response by passing in { skipWaitForResponse: true } as the options parameter
31
+ */
22
32
  async saveAndTest(options) {
23
33
  if (options?.skipWaitForResponse) {
24
34
  return this.getByTestIdOrAriaLabel(this.ctx.selectors.pages.DataSource.saveAndTest).click();
@@ -10,6 +10,8 @@ class DataSourcePicker extends GrafanaPage_1.GrafanaPage {
10
10
  await this.getByTestIdOrAriaLabel(this.ctx.selectors.components.DataSourcePicker.container)
11
11
  .locator('input')
12
12
  .fill(name);
13
+ // this is a hack to get the selection to work in 10.ish versions of Grafana.
14
+ // TODO: investigate if the select component can somehow be refactored so that its easier to test with playwright
13
15
  await this.ctx.page.keyboard.press('ArrowDown');
14
16
  await this.ctx.page.keyboard.press('ArrowUp');
15
17
  await this.ctx.page.keyboard.press('Enter');
@@ -32,6 +32,7 @@ class ExplorePage extends GrafanaPage_1.GrafanaPage {
32
32
  });
33
33
  }
34
34
  catch (_) {
35
+ // handle the case when the run button is hidden behind the "Show more items" button
35
36
  await this.getByTestIdOrAriaLabel(components.PageToolbar.item(components.PageToolbar.shotMoreItems)).click();
36
37
  await this.getByTestIdOrAriaLabel(components.RefreshPicker.runButtonV2).last().click();
37
38
  }
@@ -1,12 +1,43 @@
1
1
  import { Locator, Request, Response } from '@playwright/test';
2
2
  import { NavigateOptions, PluginTestCtx } from '../types';
3
+ /**
4
+ * Base class for all Grafana pages.
5
+ *
6
+ * Exposes methods for locating Grafana specific elements on the page
7
+ */
3
8
  export declare abstract class GrafanaPage {
4
9
  readonly ctx: PluginTestCtx;
5
10
  constructor(ctx: PluginTestCtx);
6
11
  protected navigate(url: string, options?: NavigateOptions): Promise<void>;
12
+ /**
13
+ * Get a locator for a Grafana element by data-testid or aria-label
14
+ * @param selector the data-testid or aria-label of the element
15
+ * @param root optional root locator to search within. If no locator is provided, the page will be used
16
+ */
7
17
  getByTestIdOrAriaLabel(selector: string, root?: Locator): Locator;
18
+ /**
19
+ * Mocks the response of the datasource query call
20
+ * @param json the json response to return
21
+ * @param status the HTTP status code to return. Defaults to 200
22
+ */
8
23
  mockQueryDataResponse<T = any>(json: T, status?: number): Promise<void>;
24
+ /**
25
+ * Mocks the response of the datasource resource request
26
+ * @param path the path of the resource to mock
27
+ * @param json the json response to return
28
+ * @param status the HTTP status code to return. Defaults to 200
29
+ */
9
30
  mockResourceResponse<T = any>(path: string, json: T, status?: number): Promise<void>;
31
+ /**
32
+ * Waits for a data source query data request to be made.
33
+ *
34
+ * @param cb optional callback to filter the request. Use this to filter by request body or other request properties
35
+ */
10
36
  waitForQueryDataRequest(cb?: (request: Request) => boolean | Promise<boolean>): Promise<Request>;
37
+ /**
38
+ * Waits for a data source query data response
39
+ *
40
+ * @param cb optional callback to filter the response. Use this to filter by response body or other response properties
41
+ */
11
42
  waitForQueryDataResponse(cb?: (request: Response) => boolean | Promise<boolean>): Promise<Response>;
12
43
  }
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GrafanaPage = void 0;
4
+ /**
5
+ * Base class for all Grafana pages.
6
+ *
7
+ * Exposes methods for locating Grafana specific elements on the page
8
+ */
4
9
  class GrafanaPage {
5
10
  ctx;
6
11
  constructor(ctx) {
@@ -15,25 +20,47 @@ class GrafanaPage {
15
20
  ...options,
16
21
  });
17
22
  }
23
+ /**
24
+ * Get a locator for a Grafana element by data-testid or aria-label
25
+ * @param selector the data-testid or aria-label of the element
26
+ * @param root optional root locator to search within. If no locator is provided, the page will be used
27
+ */
18
28
  getByTestIdOrAriaLabel(selector, root) {
19
29
  if (selector.startsWith('data-testid')) {
20
30
  return (root || this.ctx.page).getByTestId(selector);
21
31
  }
22
32
  return (root || this.ctx.page).locator(`[aria-label="${selector}"]`);
23
33
  }
34
+ /**
35
+ * Mocks the response of the datasource query call
36
+ * @param json the json response to return
37
+ * @param status the HTTP status code to return. Defaults to 200
38
+ */
24
39
  async mockQueryDataResponse(json, status = 200) {
25
40
  await this.ctx.page.route(this.ctx.selectors.apis.DataSource.queryPattern, async (route) => {
26
41
  await route.fulfill({ json, status });
27
42
  });
28
43
  }
44
+ /**
45
+ * Mocks the response of the datasource resource request
46
+ * @param path the path of the resource to mock
47
+ * @param json the json response to return
48
+ * @param status the HTTP status code to return. Defaults to 200
49
+ */
29
50
  async mockResourceResponse(path, json, status = 200) {
30
51
  await this.ctx.page.route(`${this.ctx.selectors.apis.DataSource.resourceUIDPattern}/${path}`, async (route) => {
31
52
  await route.fulfill({ json, status });
32
53
  });
54
+ // some data sources use the backendSrv directly, and then the path may be different
33
55
  await this.ctx.page.route(`${this.ctx.selectors.apis.DataSource.resourcePattern}/${path}`, async (route) => {
34
56
  await route.fulfill({ json, status });
35
57
  });
36
58
  }
59
+ /**
60
+ * Waits for a data source query data request to be made.
61
+ *
62
+ * @param cb optional callback to filter the request. Use this to filter by request body or other request properties
63
+ */
37
64
  async waitForQueryDataRequest(cb) {
38
65
  return this.ctx.page.waitForRequest((request) => {
39
66
  if (request.url().includes(this.ctx.selectors.apis.DataSource.query) && request.method() === 'POST') {
@@ -42,6 +69,11 @@ class GrafanaPage {
42
69
  return false;
43
70
  });
44
71
  }
72
+ /**
73
+ * Waits for a data source query data response
74
+ *
75
+ * @param cb optional callback to filter the response. Use this to filter by response body or other response properties
76
+ */
45
77
  async waitForQueryDataResponse(cb) {
46
78
  return this.ctx.page.waitForResponse((response) => {
47
79
  if (response.url().includes(this.ctx.selectors.apis.DataSource.query)) {
@@ -67,6 +67,7 @@ class PanelEditPage extends GrafanaPage_1.GrafanaPage {
67
67
  return locator;
68
68
  }
69
69
  getPanelError() {
70
+ // the selector (not the selector value) used to identify a panel error changed in 9.4.3
70
71
  if (semver.lte(this.ctx.grafanaVersion, '9.4.3')) {
71
72
  return this.getByTestIdOrAriaLabel(this.ctx.selectors.components.Panels.Panel.headerCornerInfo(ERROR_STATUS));
72
73
  }
@@ -74,6 +75,7 @@ class PanelEditPage extends GrafanaPage_1.GrafanaPage {
74
75
  }
75
76
  async refreshPanel(options) {
76
77
  const responsePromise = this.ctx.page.waitForResponse((resp) => resp.url().includes(this.ctx.selectors.apis.DataSource.query), options);
78
+ // in older versions of grafana, the refresh button is rendered twice. this is a workaround to click the correct one
77
79
  await this.getByTestIdOrAriaLabel(this.ctx.selectors.components.PanelEditor.General.content)
78
80
  .locator(`selector=${this.ctx.selectors.components.RefreshPicker.runButtonV2}`)
79
81
  .click();
@@ -11,9 +11,11 @@ class TimeRange extends GrafanaPage_1.GrafanaPage {
11
11
  await this.getByTestIdOrAriaLabel(this.ctx.selectors.components.TimePicker.openButton).click();
12
12
  }
13
13
  catch (e) {
14
+ // seems like in older versions of Grafana the time picker markup is rendered twice
14
15
  await this.ctx.page.locator('[aria-controls="TimePickerContent"]').last().click();
15
16
  }
16
17
  if (zone) {
18
+ //todo: add an e2e selector for the time zone picker and use it in grafana ui
17
19
  await this.ctx.page.getByRole('button', { name: 'Change time settings' }).click();
18
20
  await this.getByTestIdOrAriaLabel(this.ctx.selectors.components.TimeZonePicker.containerV2).fill(zone);
19
21
  }
@@ -9,5 +9,12 @@ export declare class VariableEditPage extends GrafanaPage {
9
9
  constructor(ctx: PluginTestCtx, args: DashboardEditViewArgs<string>);
10
10
  goto(options?: NavigateOptions): Promise<void>;
11
11
  setVariableType(type: VariableType): Promise<void>;
12
+ /**
13
+ * Triggers the variable query to run. Note that unlike {@link PanelEditPage.refreshPanel}, this method doesn't
14
+ * return a request promise. This is because there's no canonical way of querying variables - data sources may
15
+ * call any endpoint or resolve variables in the frontend. If you need to wait for a specific request, you can
16
+ * do that in your test.
17
+ * @example await this.ctx.page.waitForResponse((resp) => resp.url().includes('<url>')
18
+ */
12
19
  runQuery(): Promise<void>;
13
20
  }
@@ -29,11 +29,20 @@ class VariableEditPage extends GrafanaPage_1.GrafanaPage {
29
29
  await this.ctx.page.keyboard.press('Enter');
30
30
  await this.getByTestIdOrAriaLabel(this.ctx.selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2).scrollIntoViewIfNeeded();
31
31
  }
32
+ /**
33
+ * Triggers the variable query to run. Note that unlike {@link PanelEditPage.refreshPanel}, this method doesn't
34
+ * return a request promise. This is because there's no canonical way of querying variables - data sources may
35
+ * call any endpoint or resolve variables in the frontend. If you need to wait for a specific request, you can
36
+ * do that in your test.
37
+ * @example await this.ctx.page.waitForResponse((resp) => resp.url().includes('<url>')
38
+ */
32
39
  async runQuery() {
40
+ // in 9.2.0, the submit button got a new purpose. it no longer submits the form, but instead runs the query
33
41
  if (gte(this.ctx.grafanaVersion, '9.2.0')) {
34
42
  await this.getByTestIdOrAriaLabel(this.ctx.selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton).click();
35
43
  }
36
44
  else {
45
+ // in 9.1.3, the submit button submits the form
37
46
  await this.ctx.page.keyboard.press('Tab');
38
47
  }
39
48
  }
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Returns a selector engine that resolves selectors by data-testid or aria-label
3
+ */
1
4
  export declare const grafanaE2ESelectorEngine: () => {
2
5
  query(root: Element, selector: string): Element;
3
6
  queryAll(root: Element, selector: string): NodeListOf<Element>;
@@ -1,13 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.grafanaE2ESelectorEngine = void 0;
4
+ /**
5
+ * Returns a selector engine that resolves selectors by data-testid or aria-label
6
+ */
4
7
  const grafanaE2ESelectorEngine = () => ({
8
+ // Returns the first element matching given selector in the root's subtree.
5
9
  query(root, selector) {
6
10
  if (selector.startsWith('data-testid')) {
7
11
  return root.querySelector(`[data-testid="${selector}"]`);
8
12
  }
9
13
  return root.querySelector(`[aria-label="${selector}"]`);
10
14
  },
15
+ // Returns all elements matching given selector in the root's subtree.
11
16
  queryAll(root, selector) {
12
17
  if (selector.startsWith('data-testid')) {
13
18
  return root.querySelectorAll(`[data-testid="${selector}"]`);
package/dist/types.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { Locator, PlaywrightTestArgs } from '@playwright/test';
2
2
  import { E2ESelectors } from './e2e-selectors/types';
3
+ /**
4
+ * The context object passed to page object models
5
+ */
3
6
  export type PluginTestCtx = {
4
7
  grafanaVersion: string;
5
8
  selectors: E2ESelectors;
6
9
  } & Pick<PlaywrightTestArgs, 'page' | 'request'>;
10
+ /**
11
+ * The data source object
12
+ */
7
13
  export interface DataSource<T = any> {
8
14
  id?: number;
9
15
  editable?: boolean;
@@ -18,56 +24,167 @@ export interface DataSource<T = any> {
18
24
  jsonData?: T;
19
25
  secureJsonData?: T;
20
26
  }
27
+ /**
28
+ * The dashboard object
29
+ */
21
30
  export interface Dashboard {
22
31
  uid: string;
23
32
  title?: string;
24
33
  }
34
+ /**
35
+ * The YAML provision file parsed to a javascript object
36
+ */
25
37
  export type ProvisionFile<T = DataSource> = {
26
38
  datasources: Array<DataSource<T>>;
27
39
  };
28
40
  export type CreateDataSourceArgs = {
41
+ /**
42
+ * The data source to create
43
+ */
29
44
  datasource: DataSource;
30
45
  };
31
46
  export type CreateDataSourcePageArgs = {
47
+ /**
48
+ * The data source type to create
49
+ */
32
50
  type: string;
51
+ /**
52
+ * The data source name to create
53
+ */
33
54
  name?: string;
55
+ /**
56
+ * Set this to false to delete the data source via Grafana API after the test. Defaults to true.
57
+ */
34
58
  deleteDataSourceAfterTest?: boolean;
35
59
  };
36
60
  export type RequestOptions = {
61
+ /**
62
+ * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can
63
+ * be changed by using the
64
+ * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
65
+ * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
66
+ */
37
67
  timeout?: number;
38
68
  };
39
69
  export interface TimeRangeArgs {
70
+ /**
71
+ * The from time
72
+ * @example 'now-6h'
73
+ * @example '2020-01-01 00:00:00'
74
+ */
40
75
  from: string;
76
+ /**
77
+ * The to time
78
+ * @example 'now'
79
+ * @example '2020-01-01 00:00:00'
80
+ */
41
81
  to: string;
82
+ /**
83
+ * The time zone
84
+ * @example 'utc'
85
+ * @example 'browser'
86
+ */
42
87
  zone?: string;
43
88
  }
44
89
  export type DashboardPageArgs = {
90
+ /**
91
+ * The uid of the dashboard to go to
92
+ */
45
93
  uid?: string;
94
+ /**
95
+ * The time range to set
96
+ */
46
97
  timeRange?: TimeRangeArgs;
98
+ /**
99
+ * Query parameters to add to the url
100
+ */
47
101
  queryParams?: URLSearchParams;
48
102
  };
103
+ /**
104
+ * DashboardEditViewArgs is used to pass arguments to the page object models that represent a dashboard edit view,
105
+ * such as {@link PanelEditPage}, {@link VariableEditPage} and {@link AnnotationEditPage}.
106
+ *
107
+ * If dashboard is not specified, it's assumed that it's a new dashboard. Otherwise, the dashboard uid is used to
108
+ * navigate to an already existing dashboard.
109
+ */
49
110
  export type DashboardEditViewArgs<T> = {
50
111
  dashboard?: DashboardPageArgs;
51
112
  id: T;
52
113
  };
53
114
  export type ReadProvisionArgs = {
115
+ /**
116
+ * The path, relative to the provisioning folder, to the dashboard json file
117
+ */
54
118
  filePath: string;
55
119
  };
56
120
  export type NavigateOptions = {
121
+ /**
122
+ * Referer header value.
123
+ */
57
124
  referer?: string;
125
+ /**
126
+ * Maximum operation time in milliseconds. Defaults to `0` - no timeout.
127
+ */
58
128
  timeout?: number;
129
+ /**
130
+ * When to consider operation succeeded, defaults to `load`. Events can be either:
131
+ * - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired.
132
+ * - `'load'` - consider operation to be finished when the `load` event is fired.
133
+ * - `'networkidle'` - **DISCOURAGED** consider operation to be finished when there are no network connections for
134
+ * at least `500` ms. Don't use this method for testing, rely on web assertions to assess readiness instead.
135
+ * - `'commit'` - consider operation to be finished when network response is received and the document started
136
+ * loading.
137
+ */
59
138
  waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
139
+ /**
140
+ * Query parameters to add to the url. Optional
141
+ */
60
142
  queryParams?: URLSearchParams;
61
143
  };
62
144
  export type TriggerQueryOptions = {
145
+ /**
146
+ * Set this to true to skip waiting for the response. Defaults to false.
147
+ */
63
148
  skipWaitForResponse: boolean;
64
149
  };
150
+ /**
151
+ * Panel visualization types
152
+ */
65
153
  export type Visualization = 'Alert list' | 'Bar gauge' | 'Clock' | 'Dashboard list' | 'Gauge' | 'Graph' | 'Heatmap' | 'Logs' | 'News' | 'Pie Chart' | 'Plugin list' | 'Polystat' | 'Stat' | 'Table' | 'Text' | 'Time series' | 'Worldmap Panel';
66
154
  export type AlertVariant = 'success' | 'warning' | 'error' | 'info';
67
155
  export interface AlertPageOptions {
156
+ /**
157
+ * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can
158
+ * be changed by using the
159
+ * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
160
+ * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
161
+ */
68
162
  timeout?: number;
163
+ /**
164
+ * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer
165
+ * one. For example, `article` that has `text=Playwright` matches `<article><div>Playwright</div></article>`.
166
+ *
167
+ * Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@link
168
+ * FrameLocator}s.
169
+ */
69
170
  has?: Locator;
171
+ /**
172
+ * Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
173
+ * outer one. For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
174
+ *
175
+ * Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@link
176
+ * FrameLocator}s.
177
+ */
70
178
  hasNot?: Locator;
179
+ /**
180
+ * Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
181
+ * When passed a [string], matching is case-insensitive and searches for a substring.
182
+ */
71
183
  hasNotText?: string | RegExp;
184
+ /**
185
+ * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
186
+ * passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
187
+ * `<article><div>Playwright</div></article>`.
188
+ */
72
189
  hasText?: string | RegExp;
73
190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/plugin-e2e",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -44,5 +44,5 @@
44
44
  "semver": "^7.5.4",
45
45
  "uuid": "^9.0.1"
46
46
  },
47
- "gitHead": "cd4548fcbe0a49b97effddfdc44cb21b659afccf"
47
+ "gitHead": "b907d536ea7d9d4ad920b10789663e99288cac58"
48
48
  }