This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-7437-ca41bdd0cd1c99a164868afe6b0dce2d4c502105 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 97cb8cf699e676e7d9ee1ca643da58d0b3bc8b84 Author: Xinyuan Lin <[email protected]> AuthorDate: Sun Aug 9 18:16:43 2026 -0700 test(frontend): render the quota page's result cache tab (#7437) ### What changes were proposed in this PR? The quota page's Result Cache tab is never rendered by its spec, which drives the quota data and its charts only. Adds 5 tests. The reported cache size is the centrepiece: ```html {{ formatSize(execution.resultBytes + execution.logBytes + execution.runTimeStatsBytes) }} ``` Three separate byte counts added together. A dropped or double-counted term still produces a plausible-looking size and nothing else in the suite would notice, so the fixture uses distinct powers of two — any such slip lands on a different total rather than coincidentally matching. Also covered: one collapse panel per workflow headed by its name, the executions listed under the opened workflow, the delete button carrying the row's **execution** id rather than the workflow id, and the table paging rather than listing everything. **Verified by mutation**, all reverted (template diff empty): | Mutation | Result | |---|---| | cache size drops `logBytes` | red | | cache size drops runtime statistics | red | | cache size double-counts `resultBytes` | red | | delete passes the workflow id | red | | panel header shows the id | red | | execution-id column shows the name | red | | pagination widened | red | | rows read the raw list instead of the page | red | `ngOnInit` resets `workflows`, so the first change-detection cycle runs before the fixture data is assigned; that is commented in the spec. No production file is touched. ### Any related issues, documentation, discussions? Closes #7434 ### How was this PR tested? ``` npx ng test --watch=false --include="**/user-quota.component.spec.ts" ``` ``` Test Files 1 passed (1) Tests 16 passed (16) ``` 5 new on top of the existing 11. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --------- Signed-off-by: Xinyuan Lin <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> --- .../user/user-quota/user-quota.component.spec.ts | 96 +++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) 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 a659317792..e1e3dc92f4 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 @@ -23,8 +23,9 @@ import { UserQuotaService } from "../../../service/user/quota/user-quota.service import { HttpClientTestingModule } from "@angular/common/http/testing"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { of } from "rxjs"; +import { By } from "@angular/platform-browser"; import type { Mocked } from "vitest"; -import { ExecutionQuota } from "../../../../common/type/user"; +import { ExecutionQuota, WorkflowQuota } from "../../../../common/type/user"; import { DatasetQuota } from "../../../type/quota-statistic.interface"; // Real Plotly renders into a DOM element by id; create one and assert on the @@ -258,4 +259,97 @@ describe("UserQuotaComponent", () => { expect(gd.layout.yaxis.title).toMatchObject({ text: "Y Label" }); }); }); + /** + * The Result Cache tab is template-only: the suite above drives the component's data and charts + * and never renders this tab, so the per-execution row — including the size it reports and the id + * its delete button carries — was unexercised. + */ + describe("result cache tab", () => { + /** Selects a tab by its title and returns the host element. */ + function openTab(title: string): HTMLElement { + const host = fixture.nativeElement as HTMLElement; + const tab = Array.from(host.querySelectorAll<HTMLElement>(".ant-tabs-tab")).find(t => + (t.textContent || "").includes(title) + ); + if (!tab) { + throw new Error(`Result Cache spec: tab not found: ${title}`); + } + tab.click(); + fixture.detectChanges(); + return host; + } + + /** Expands the collapse panel whose header contains the given text. */ + function openPanel(host: HTMLElement, header: string): void { + const panel = Array.from(host.querySelectorAll<HTMLElement>(".ant-collapse-header")).find(h => + (h.textContent || "").includes(header) + ); + panel!.click(); + fixture.detectChanges(); + } + + /** Renders the Result Cache tab with the given workflows and opens the first panel. */ + function renderCache(workflows: any[], openHeader = "wf-1"): HTMLElement { + // ngOnInit loads the quota and resets `workflows`, so the first cycle has to run before the + // fixture data is put in place, or it is wiped before anything renders. + fixture.detectChanges(); + component.workflows = workflows; + fixture.detectChanges(); + const host = openTab("Result Cache"); + openPanel(host, openHeader); + return host; + } + + const workflow = (workflowId: number, executions: any[]) => ({ + workflowId, + workflowName: `wf-${workflowId}`, + executions, + }); + + it("groups the executions under one panel per workflow", () => { + const host = renderCache([workflow(1, [execution(10, 1, 1, 1, 1)]), workflow(2, [execution(20, 2, 1, 1, 1)])]); + + const headers = Array.from(host.querySelectorAll(".ant-collapse-header")).map(h => h.textContent?.trim()); + expect(headers).toEqual(["wf-1", "wf-2"]); + }); + + it("lists every execution of the opened workflow", () => { + const host = renderCache([workflow(1, [execution(10, 1, 1, 1, 1), execution(11, 1, 1, 1, 1)])]); + + const eids = Array.from(host.querySelectorAll("tbody tr")).map(r => + r.querySelectorAll("td")[1]?.textContent?.trim() + ); + expect(eids).toEqual(["10", "11"]); + }); + + it("reports the cache size as result plus log plus runtime statistics", () => { + // Three separate byte counts are added together. Distinct powers of two, so dropping or + // double-counting any one of them lands on a different total rather than coincidentally + // matching — which is exactly how a missing term would otherwise go unnoticed. + const host = renderCache([workflow(1, [execution(10, 1, 1024, 4096, 2048)])]); + + const sizeCell = host.querySelectorAll("tbody tr td")[2]; + expect(sizeCell.textContent?.trim()).toBe(component.formatSize(1024 + 4096 + 2048)); + }); + + it("deletes the collection of the row it was pressed on", () => { + // The row carries the execution id; passing the workflow id would delete the wrong cache. + const spy = vi.spyOn(component, "deleteCollection").mockImplementation(() => {}); + const host = renderCache([workflow(1, [execution(10, 1, 1, 1, 1), execution(11, 1, 1, 1, 1)])]); + + const secondRowButton = host.querySelectorAll("tbody tr")[1].querySelector("button")!; + secondRowButton.click(); + // nz-popconfirm defers the action to its confirmation, so trigger that directly. + fixture.debugElement.queryAll(By.css("button[nz-popconfirm]"))[1].triggerEventHandler("nzOnConfirm", null); + + expect(spy).toHaveBeenCalledWith(11); + }); + + it("pages the executions rather than listing all of them", () => { + const executions = Array.from({ length: 5 }, (_, i) => execution(100 + i, 1, 1, 1, 1)); + const host = renderCache([workflow(1, executions)]); + + expect(host.querySelectorAll("tbody tr").length).toBe(3); + }); + }); });
