Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import { SVGTransformUtils } from "../utils/SVGTransformUtils";
import { SVGPathUtils } from "../utils/SVGPathUtils";
import { type BoundingBox, BoundingBoxUtils } from "../utils/BoundingBoxUtils";
import { TWO_FRAMES_TIMEOUT } from "libs/editor/tests/integration/e2e/utils/constants";
 
class TimeSeriesHelper {
  private get _baseRootSelector() {
    return ".htx-timeseries";
  }
 
  private _rootSelector: string;
 
  constructor(rootSelector) {
    this._rootSelector = rootSelector.replace(/^\&/, this._baseRootSelector);
  }
 
  get root() {
    return cy.get(this._rootSelector);
  }
 
  get channelContainer() {
    return this.root.find(".htx-timeseries-channel");
  }
 
  get multiChannelContainer() {
    return this.root.find(".htx-timeseries-multichannel");
  }
 
  get overview() {
    return this.root.find(".htx-timeseries-overview");
  }
 
  get overviewOverlay() {
    return this.overview.find(".overlay");
  }
 
  get channelOverlay() {
    return this.channelContainer.find(".overlay");
  }
 
  get channelSvg() {
    return this.channelContainer.find("svg");
  }
 
  get paths() {
    return this.channelSvg.find("[clip-path] path");
  }
 
  get overviewSvg() {
    return this.overview.find("svg");
  }
 
  get legend() {
    return this.root.find(".htx-timeseries-channel-legend-container");
  }
 
  get legendItems() {
    return this.legend.find(".channel-name");
  }
 
  // Hover over legend item by index
  hoverLegendItem(index: number) {
    cy.log(`Hovering over legend item ${index}`);
    this.legendItems.eq(index).trigger("mouseover");
    return this;
  }
 
  // Remove hover from legend
  unhoverLegend() {
    cy.log("Removing hover from legend");
    this.legendItems.first().trigger("mouseout");
    return this;
  }
 
  // Click legend item by index to toggle channel visibility
  clickLegendItem(index: number) {
    cy.log(`Clicking legend item ${index} to toggle channel visibility`);
    this.legendItems.eq(index).click();
    return this;
  }
 
  // Wait for TimeSeries to be ready
  waitForReady() {
    cy.log("Waiting for TimeSeries to be ready");
    this.root.should("be.visible");
    this.channelSvg.should("be.visible");
    this.paths.should("have.length.greaterThan", 0);
    return this;
  }
 
  // Verify no positioning errors (NaN/Infinity) in SVG elements
  verifyNoPositioningErrors() {
    cy.log("Verifying no NaN or Infinity values in SVG elements");
    this.paths.each(($el) => {
      const element = $el[0];
 
      const value = element.getAttribute("d");
      if (value) {
        expect(value).to.not.include("NaN");
        expect(value).to.not.include("Infinity");
      }
    });
    return this;
  }
 
  getViewBox(channel) {
    return channel.invoke("attr", "viewBox");
  }
 
  getPaths(channel) {
    return channel.find("[clip-path] path");
  }
 
  // Simple click-based navigation instead of drag
  clickOverviewAt(percent: number) {
    cy.log(`Clicking overview at ${percent}%`);
 
    this.overviewOverlay
      .scrollIntoView()
      .should("be.visible")
      .then(($overlay) => {
        const rect = $overlay[0].getBoundingClientRect();
        const x = rect.left + rect.width * (percent / 100);
        const y = rect.top + rect.height * 0.5;
 
        cy.wrap($overlay).click(x - rect.left, y - rect.top, { force: true });
      });
 
    return this;
  }
 
  verifyDataVisibleInViewport() {
    cy.log("Verifying chart data is visible in SVG viewport");
 
    this.channelSvg.should("be.visible").then(($svgs) => {
      for (const $svg of $svgs) {
        const svgElement = $svg as unknown as SVGSVGElement;
        const viewBox = svgElement.viewBox.baseVal;
 
        const visibleArea = {
          x: viewBox.x,
          y: viewBox.y,
          width: viewBox.width,
          height: viewBox.height,
        };
 
        cy.log(`SVG viewBox: ${visibleArea.x}, ${visibleArea.y}, ${visibleArea.width}, ${visibleArea.height}`);
 
        this.getPaths(cy.wrap($svg)).each(($path) => {
          const pathElement = $path[0] as unknown as SVGPathElement;
          const pathData = pathElement.getAttribute("d");
 
          // Get transform attributes from path element and its parents
          const transformMatrix = SVGTransformUtils.getTransformMatrix(pathElement, $svg);
 
          this.verifyPathDataInViewport(pathData, visibleArea, transformMatrix);
        });
      }
    });
 
    return this;
  }
 
  private verifyPathDataInViewport(pathData: string, visibleArea: any, transformMatrix?: DOMMatrix) {
    const coords = SVGPathUtils.extractPathCoordinates(pathData);
 
    // Apply transforms to coordinates if matrix is provided
    const transformedCoords = SVGTransformUtils.applyTransformToCoordinates(coords, transformMatrix);
 
    // Check that all points that should be visible are within the viewport
    const visibleRangePoint = transformedCoords.filter(
      (coord) => coord.x >= visibleArea.x && coord.x <= visibleArea.x + visibleArea.width,
    );
    const visiblePoints = visibleRangePoint.filter(
      (coord) =>
        // Coordinates in visible area should be within the viewport in terms of y
        coord.y >= visibleArea.y && coord.y <= visibleArea.y + visibleArea.height,
    );
 
    expect(visiblePoints.length).to.be.eq(
      visibleRangePoint.length,
      `Not all chart points in displaying range are visible in viewport. ${visiblePoints.length} out of ${visibleRangePoint.length} are visible.`,
    );
  }
 
  // Simple region creation using dragging
  drawRegionRelative(x1: number, x2: number) {
    this.channelContainer
      .eq(0)
      .find(".new_brush .overlay")
      .should("be.visible")
      .then(($overlay) => {
        const rect = $overlay[0].getBoundingClientRect();
        // Calculate relative coordinates (relative to the element, not the viewport)
        const startX = rect.width * x1;
        const endX = rect.width * x2;
        const centerY = rect.height * 0.5;
 
        const eventOptions = {
          eventConstructor: "MouseEvent",
          buttons: 1,
          force: true,
        };
        cy.wrap($overlay)
          .trigger("mousedown", startX, centerY, eventOptions)
          .trigger("mousemove", endX, centerY, eventOptions)
          .trigger("mouseup", endX, centerY, eventOptions);
      });
 
    return this;
  }
 
  // Zoom to maximum level using mouse wheel with platform detection
  zoomToMaximum() {
    cy.log("Zooming to maximum level using mouse wheel");
 
    this.channelOverlay.should("be.visible").then(($overlay) => {
      const rect = $overlay[0].getBoundingClientRect();
      const centerX = rect.width / 2;
      const centerY = rect.height / 2;
 
      // Detect platform and use appropriate deltaY value
      // macOS with "Natural scrolling" typically needs positive deltaY for zoom in
      // Windows/Linux and macOS without "Natural scrolling" need negative deltaY
      cy.window().then((win) => {
        // Hold Ctrl/Cmd and scroll to zoom in multiple times
        cy.wrap($overlay).trigger("keydown", { key: "Control", ctrlKey: true });
 
        // Perform multiple zoom-in operations to reach maximum zoom
        for (let i = 0; i < 25; i++) {
          cy.wrap($overlay).trigger("wheel", {
            deltaY: 100,
            clientX: rect.left + centerX,
            clientY: rect.top + centerY,
            ctrlKey: true,
          });
        }
 
        cy.wrap($overlay).trigger("keyup", { key: "Control", ctrlKey: false });
        cy.wait(TWO_FRAMES_TIMEOUT);
      });
    });
 
    return this;
  }
 
  // Verify that chart client bounding boxes align with their clip-path containers
  // Only checks odd-indexed paths (1st, 3rd, 5th, etc.) in each clip-path container group
  verifyChartBoundingBoxAlignment() {
    cy.log("Verifying chart client bounding boxes align with clip-path containers (odd-indexed paths only)");
 
    // Get all clip-path containers and check odd-indexed paths in each
    this.channelSvg.find("[clip-path]").each(($clipContainer) => {
      const clipPathParent = $clipContainer[0] as unknown as SVGElement;
 
      // Find all path elements within this clip-path container
      const paths = clipPathParent.querySelectorAll("path");
 
      if (paths.length === 0) {
        cy.log("No paths found in clip-path container, skipping");
        return;
      }
 
      // Check only odd-indexed paths (0-based: 0, 2, 4, etc. which are 1st, 3rd, 5th, etc.)
      for (let i = 0; i < paths.length; i += 2) {
        const pathElement = paths[i] as SVGPathElement;
 
        // Get client bounding rectangles (rendered coordinates in viewport)
        const pathClientRect = pathElement.getBoundingClientRect() as BoundingBox;
        const clipParentClientRect = clipPathParent.getBoundingClientRect() as BoundingBox;
 
        cy.log(
          `Path ${i + 1} client rect: x=${pathClientRect.x.toFixed(2)}, y=${pathClientRect.y.toFixed(2)}, width=${pathClientRect.width.toFixed(2)}, height=${pathClientRect.height.toFixed(2)}`,
        );
        cy.log(
          `Clip parent client rect: x=${clipParentClientRect.x.toFixed(2)}, y=${clipParentClientRect.y.toFixed(2)}, width=${clipParentClientRect.width.toFixed(2)}, height=${clipParentClientRect.height.toFixed(2)}`,
        );
 
        // Check that client bounding boxes are equal (path should fill its clip-path container)
        const isAligned = BoundingBoxUtils.isEqual(pathClientRect, clipParentClientRect, 2);
        expect(isAligned, `Path ${i + 1} client bounding box should align with its clip-path container`).to.be.true;
      }
    });
 
    return this;
  }
}
 
const TimeSeries = new TimeSeriesHelper("&:eq(0)");
const useTimeSeries = (rootSelector: string) => {
  return new TimeSeriesHelper(rootSelector);
};
 
export { TimeSeries, useTimeSeries };