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" } }, + }); + }); + }); });
