This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 90eabda0d24a772b241ad97773c84aac9adaa680
Author: Meng Wang <[email protected]>
AuthorDate: Sat Jul 18 16:53:37 2026 -0700

    test(frontend): extend UserQuotaComponent coverage with data and chart 
tests (#6524)
    
    ### What changes were proposed in this PR?
    
    Extends the existing `UserQuotaComponent` spec (which only asserted
    `should create`) to cover the previously untested data-transform and
    chart-building methods
    
    
(`frontend/src/app/dashboard/component/user/user-quota/user-quota.component.ts`,
    ~52% coverage). `plotly.js-basic-dist-min` is mocked at the module level
    so the
    chart methods can be asserted without a real render.
    
    10 new tests cover:
    
    - `aggregateByMonth` — sums values sharing a `YYYY-MM` prefix.
    - `filterOutdatedData` — keeps entries within the last year, drops older
    ones.
    - `aggregateData` — returns the data unchanged below the 8-point
    threshold; delegates to monthly aggregation across ≥3 months; and
    day-groups otherwise, preserving the total in every branch.
    - `refreshData` — loads datasets and executions, sets the totals/counts,
    and groups executions by workflow.
    - `deleteCollection` — removes an execution and subtracts its bytes;
    drops a workflow once its last execution is gone.
    - `generatePieChart` / `generateLineChart` — pass the expected
    series/labels and sizing/axis layout to `Plotly.newPlot`.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6519
    
    ### How was this PR tested?
    
    Extended unit tests, run locally in `frontend/` (all green; the failure
    path was
    verified by deliberately breaking a chart-config assertion to confirm
    the suite
    goes red):
    
    ```
    ng test --watch=false --include 
src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts
    # Test Files 1 passed (1) | Tests 11 passed (11)
    prettier --write <spec>   # formatted
    eslint  <spec>            # clean
    ```
    
    The quota service is stubbed and Plotly is mocked, so the suite makes no
    network
    calls and renders no real charts (see `frontend/TESTING.md`).
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
    
    ---------
    
    Signed-off-by: Meng Wang <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../user/user-quota/user-quota.component.spec.ts   | 206 +++++++++++++++++++++
 1 file changed, 206 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts
index 1190a562e9..400e2fcdca 100644
--- 
a/frontend/src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts
@@ -24,6 +24,44 @@ import { HttpClientTestingModule } from 
"@angular/common/http/testing";
 import { commonTestProviders } from "../../../../common/testing/test-utils";
 import { of } from "rxjs";
 import type { Mocked } from "vitest";
+import * as Plotly from "plotly.js-basic-dist-min";
+import { ExecutionQuota } from "../../../../common/type/user";
+import { DatasetQuota } from "../../../type/quota-statistic.interface";
+
+// Plotly is a read-only ESM namespace (can't be spied on), so mock the whole 
module.
+vi.mock("plotly.js-basic-dist-min", () => ({ newPlot: vi.fn() }));
+
+// ISO 'YYYY-MM-DD' for a date `days` before now (kept by the 1-year filter 
for small values).
+function isoDaysAgo(days: number): string {
+  const d = new Date();
+  d.setUTCHours(0, 0, 0, 0);
+  d.setUTCDate(d.getUTCDate() - days);
+  return d.toISOString().slice(0, 10);
+}
+
+// ISO 'YYYY-MM-DD' on `day` of the month `monthsAgo` months before now.
+function isoInMonthsAgo(monthsAgo: number, day: number): string {
+  const d = new Date();
+  d.setUTCHours(0, 0, 0, 0);
+  d.setUTCDate(1); // avoid month-length overflow before shifting the month
+  d.setUTCMonth(d.getUTCMonth() - monthsAgo);
+  d.setUTCDate(day);
+  return d.toISOString().slice(0, 10);
+}
+
+function execution(eid: number, workflowId: number, result: number, runtime: 
number, log: number): ExecutionQuota {
+  return {
+    eid,
+    workflowId,
+    workflowName: `wf-${workflowId}`,
+    resultBytes: result,
+    runTimeStatsBytes: runtime,
+    logBytes: log,
+  };
+}
+
+const sumValues = (data: Array<[string, number]>): number => data.reduce((acc, 
[, v]) => acc + v, 0);
+
 describe("UserQuotaComponent", () => {
   let component: UserQuotaComponent;
   let fixture: ComponentFixture<UserQuotaComponent>;
@@ -51,8 +89,176 @@ describe("UserQuotaComponent", () => {
     component = fixture.componentInstance;
   });
 
+  afterEach(() => vi.restoreAllMocks());
+
   it("should create", () => {
     fixture.detectChanges();
     expect(component).toBeTruthy();
   });
+
+  describe("aggregateByMonth", () => {
+    it("sums the values that share a 'YYYY-MM' prefix", () => {
+      const result = component.aggregateByMonth([
+        ["2024-01-05", 2],
+        ["2024-01-20", 3],
+        ["2024-02-10", 5],
+      ]);
+      expect(result).toEqual([
+        ["2024-01", 5],
+        ["2024-02", 5],
+      ]);
+    });
+  });
+
+  describe("filterOutdatedData", () => {
+    it("keeps entries within the last year and drops older ones", () => {
+      const recent = isoDaysAgo(30);
+      const old = isoDaysAgo(400);
+      expect(
+        component.filterOutdatedData([
+          [recent, 1],
+          [old, 2],
+        ])
+      ).toEqual([[recent, 1]]);
+    });
+  });
+
+  describe("aggregateData", () => {
+    it("returns the (filtered) data unchanged when there are fewer than 8 
points", () => {
+      const data: Array<[string, number]> = [
+        [isoDaysAgo(10), 1],
+        [isoDaysAgo(20), 2],
+        [isoDaysAgo(30), 3],
+      ];
+      expect(component.aggregateData(data, 5)).toEqual(data);
+    });
+
+    it("aggregates by month when the data spans at least three months", () => {
+      const data: Array<[string, number]> = [
+        [isoInMonthsAgo(2, 5), 1],
+        [isoInMonthsAgo(2, 15), 1],
+        [isoInMonthsAgo(1, 5), 1],
+        [isoInMonthsAgo(1, 15), 1],
+        [isoInMonthsAgo(1, 25), 1],
+        [isoInMonthsAgo(0, 3), 1],
+        [isoInMonthsAgo(0, 6), 1],
+        [isoInMonthsAgo(0, 9), 1],
+      ];
+      const result = component.aggregateData(data, 5) as Array<[string, 
number]>;
+      expect(result.length).toBe(3); // one bucket per month
+      expect(sumValues(result)).toBe(sumValues(data)); // aggregation 
preserves the total
+    });
+
+    it("aggregates by day-group when there are 8+ points within fewer than 
three months", () => {
+      const data: Array<[string, number]> = Array.from({ length: 8 }, (_, i) 
=> [isoInMonthsAgo(0, i + 1), i + 1]);
+      const result = component.aggregateData(data, 5) as Array<[string, 
number]>;
+      expect(result.length).toBeGreaterThan(0);
+      expect(result.length).toBeLessThan(data.length); // grouping collapses 
points
+      expect(sumValues(result)).toBe(sumValues(data)); // total preserved
+    });
+  });
+
+  describe("refreshData", () => {
+    it("loads datasets and executions, and groups executions by workflow", () 
=> {
+      const datasets: DatasetQuota[] = [
+        { did: 1, name: "d1", creationTime: Date.now(), size: 100 },
+        { did: 2, name: "d2", creationTime: Date.now(), size: 200 },
+      ];
+      mockUserQuotaService.getCreatedDatasets.mockReturnValue(of(datasets));
+      mockUserQuotaService.getExecutionQuota.mockReturnValue(
+        of([execution(10, 1, 100, 5, 10), execution(11, 1, 50, 5, 5), 
execution(12, 2, 20, 2, 3)])
+      );
+      // Chart rendering is exercised separately; stub it here to isolate the 
data wiring.
+      vi.spyOn(component, "generatePieChart").mockImplementation(() => {});
+      vi.spyOn(component, "generateLineChart").mockImplementation(() => {});
+
+      component.refreshData();
+
+      expect(component.datasetList).toEqual(datasets);
+      expect(component.totalUploadedDatasetCount).toBe(2);
+      expect(component.totalUploadedDatasetSize).toBe(300);
+      expect(component.totalQuotaSize).toBe(200); // 115 + 60 + 25
+      expect(component.workflows.map(w => w.workflowId)).toEqual([1, 2]);
+      expect(component.workflows[0].executions.map(e => e.eid)).toEqual([10, 
11]);
+      expect(component.workflows[1].executions.map(e => e.eid)).toEqual([12]);
+    });
+  });
+
+  describe("deleteCollection", () => {
+    it("removes the execution and subtracts its bytes from the total", () => {
+      component.workflows = [
+        { workflowId: 1, workflowName: "wf-1", executions: [execution(10, 1, 
100, 5, 10), execution(11, 1, 50, 5, 5)] },
+      ];
+      component.totalQuotaSize = 175; // 115 + 60
+      
mockUserQuotaService.deleteExecutionCollection.mockReturnValue(of(undefined));
+
+      component.deleteCollection(10);
+
+      
expect(mockUserQuotaService.deleteExecutionCollection).toHaveBeenCalledWith(10);
+      expect(component.totalQuotaSize).toBe(60);
+      expect(component.workflows[0].executions.map(e => e.eid)).toEqual([11]);
+    });
+
+    it("drops the workflow when its last execution is removed", () => {
+      component.workflows = [{ workflowId: 1, workflowName: "wf-1", 
executions: [execution(10, 1, 100, 5, 10)] }];
+      component.totalQuotaSize = 115;
+      
mockUserQuotaService.deleteExecutionCollection.mockReturnValue(of(undefined));
+
+      component.deleteCollection(10);
+
+      expect(component.workflows).toEqual([]);
+    });
+  });
+
+  describe("chart generation", () => {
+    it("generatePieChart passes labels/values and the sizing layout to 
Plotly", () => {
+      const newPlot = vi.mocked(Plotly.newPlot);
+      newPlot.mockClear();
+
+      component.generatePieChart(
+        [
+          ["a", 1],
+          ["b", 2],
+        ],
+        "Pie Title",
+        "pieDiv"
+      );
+
+      expect(newPlot).toHaveBeenCalledTimes(1);
+      const [chartId, data, layout] = newPlot.mock.calls[0];
+      expect(chartId).toBe("pieDiv");
+      expect(data).toEqual([{ values: [1, 2], labels: ["a", "b"], type: "pie" 
}]);
+      expect(layout).toMatchObject({
+        height: component.DEFAULT_PIE_CHART_HEIGHT,
+        width: component.DEFAULT_PIE_CHART_WIDTH,
+        title: { text: "Pie Title" },
+      });
+    });
+
+    it("generateLineChart passes x/y series and the axis labels to Plotly", () 
=> {
+      const newPlot = vi.mocked(Plotly.newPlot);
+      newPlot.mockClear();
+
+      component.generateLineChart(
+        [
+          ["2024-01-01", 1],
+          ["2024-01-02", 3],
+        ],
+        "X Label",
+        "Y Label",
+        "Line Title",
+        "lineDiv"
+      );
+
+      expect(newPlot).toHaveBeenCalledTimes(1);
+      const [chartId, data, layout] = newPlot.mock.calls[0];
+      expect(chartId).toBe("lineDiv");
+      expect(data).toEqual([{ x: ["2024-01-01", "2024-01-02"], y: [1, 3], 
type: "scatter" }]);
+      expect(layout).toMatchObject({
+        title: { text: "Line Title" },
+        xaxis: { title: { text: "X Label" } },
+        yaxis: { title: { text: "Y Label" } },
+      });
+    });
+  });
 });

Reply via email to