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-7972-5fc901727c4582778c14035bfe6e4be6ccd02770
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 9f2102e9b0d83b7b518f37bab3ae497d3a0e9b20
Author: Xinyuan Lin <[email protected]>
AuthorDate: Tue Aug 25 21:36:40 2026 +0000

    test(frontend): close the result-table frame's stat-formatting gaps (#7972)
    
    ### What changes were proposed in this PR?
    
    `result-table-frame.component.spec.ts` goes from 41 tests to 46, closing
    the last reachable gap in the component.
    
    Measured from raw lcov (v8 via vitest), identical include glob on both
    sides, `rm -rf coverage junit.xml` between runs. The before figure came
    from restoring the spec with `git show HEAD:<spec>` — a single exact
    path, never a directory checkout.
    
    | Counter | Before | After |
    |---|---|---|
    | Lines hit | 184/185 = 99.46% | **185/185 = 100%** |
    | Codecov's metric (fully covered) | 180/185 = 97.30% | **184/185 =
    99.46%** |
    | Branch arms | 92/96 | **95/96** |
    | Functions | 37/37 | 37/37 |
    
    **+4 fully-covered lines and +3 branch arms — and this is plainly a
    small PR.** Three short tests, ~40 spec lines. I would rather say that
    than dress it up: 184/185 is this file's permanent ceiling under a
    test-only change.
    
    Functions were already 37/37 with zero `FNDA:0` entries in both runs.
    Worth checking rather than assuming, because three times in this
    campaign a file sat at high line coverage with functions uncovered.
    
    ### What the new tests actually pin
    
    One of them closes a real hole rather than a counter: an existing test's
    *name* claimed it exercised the empty-result guard, while its body
    actually exercised the `operatorId` guard. The two are now separated.
    
    Another covers a genuinely reachable path that the type signature
    denies: `IcebergDocument.getTableStatistics` emits **ISO date strings**
    for `Timestamp` column min/max, contradicting the frontend's
    `Record<string, Record<string, number>>`. Reaching it needs two `as
    unknown as number` casts, which the test carries a comment explaining —
    otherwise a reviewer would reasonably read it as a coverage hack.
    
    ### Verification
    
    16 mutations, **14 killed, 2 survivors**, both stated rather than
    papered over.
    
    - **`BRDA:250,20,1` is structurally dead.** The `: currentStr` arm of
    `previous !== undefined ? previous.toFixed(2) : currentStr` sits inside
    `typeof current === "number" && typeof previous === "number"`, so
    `previous` is provably not undefined. Closing it would need a production
    edit (deleting the vacuous guard), which the test-only constraint
    forbids. This is why the ceiling is 184/185.
    - **A single-sided `previous.toLocaleString()` → `String(previous)` at
    line 253 survives, and is left unkilled on purpose.** It is observable
    only when the *same* column's stat is non-numeric in one snapshot and a
    number ≥ 1000 in the other — a mid-run stat type flip. Pinning that
    would cement a shape the backend does not produce.
    
    Five claims from the first draft were corrected, including a test title
    that claimed to observe `toLocaleString` for string payloads (it cannot
    — `String#toLocaleString` returns the same string), and a "there is no
    other gap for anyone to re-hunt" conclusion drawn from the absence of
    zero-count lcov entries, which is a category error: a fully-hit file can
    still be entirely unpinned.
    
    ### Reported, not pinned
    
    An empty fetched page returns at the line-424 guard **before**
    `isLoadingResult = false` at line 428, so the `nz-table`'s bound spinner
    never clears for an operator that produced zero rows. The test
    deliberately omits that assertion so this PR does not cement it; it
    deserves its own issue.
    
    No production file is touched, and the worktree's `node_modules` is a
    real install rather than a junction.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7971
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false 
--include="**/result-table-frame.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  46 passed (46)
    ```
    
    `yarn format:ci` passes. `frontend/junit.xml` and `frontend/coverage/`
    are regenerated by every run and are not committed.
    
    ### 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]>
---
 .../result-table-frame.component.spec.ts           | 132 ++++++++++++++++++++-
 1 file changed, 129 insertions(+), 3 deletions(-)

diff --git 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
index 0265490ac1..d4c9e85b08 100644
--- 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
@@ -149,10 +149,13 @@ describe("ResultTableFrameComponent", () => {
     expect(component).toBeTruthy();
   });
 
-  it("currentResult should not be modified if setupResultTable is called with 
empty (zero-length) execution result", () => {
+  // NOTE: this exercises the *missing operator* guard, not the empty-result 
guard - the
+  // fixture built in beforeEach has no operatorId, so setupResultTable 
returns before it
+  // ever looks at the row count. The empty-result guard is covered by
+  // "keeps the existing table when the fetched page comes back empty" below.
+  it("currentResult should not be modified if setupResultTable is called 
without a selected operator", () => {
     component.currentResult = [{ test: "property" }];
-    (component as any).setupResultTable([], 0);
-
+    component.setupResultTable([], 0);
     expect(component.currentResult).toEqual([{ test: "property" }]);
   });
 
@@ -236,6 +239,28 @@ describe("ResultTableFrameComponent", () => {
       expect(component.totalNumTuples).toBe(0);
     });
 
+    it("keeps the existing table when the fetched page comes back empty", () 
=> {
+      component.operatorId = "op1";
+      const row: IndexableObject = { media: "https://example.com/clip.mp4"; };
+      // build the "existing table" through the real code path so that 
cellMediaTypes is
+      // populated for real, making a guard that wiped it visible in the 
assertions below
+      component.setupResultTable([row], 5);
+
+      // An empty page whose reported total is NONZERO: the row count shrank 
while the user
+      // sat on a page index past the new end. The two comparands differ, so 
this pins that
+      // the guard tests the length of the fetched page and not the reported 
total.
+      component.setupResultTable([], 7);
+
+      expect(component.currentResult).toEqual([row]);
+      expect(component.currentColumns?.map(c => 
c.columnDef)).toEqual(["media"]);
+      // still the total from the non-empty page: the guard returns before 
totalNumTuples is
+      // reassigned from totalRowCount
+      expect(component.totalNumTuples).toBe(5);
+      // the precomputed media-type map is part of the table state and must 
survive as well,
+      // otherwise every media cell of the still-displayed table degrades to 
plain text
+      expect(component.getCellMediaType(row, 0)).toBe("video");
+    });
+
     it("builds columns from the first row and drops the internal _id column", 
() => {
       component.operatorId = "op1";
       component.isLoadingResult = true;
@@ -402,6 +427,78 @@ describe("ResultTableFrameComponent", () => {
       expect(bypassSpy).toHaveBeenCalledWith(black("3") + black(".") + 
black("0") + black("0"));
     });
 
+    // `tableStats` is declared Record<string, Record<string, number>>, but 
the backend
+    // does put non-numeric values in it: IcebergDocument.getTableStatistics 
seeds a
+    // Timestamp column's min/max with an ISO date *string*, and the template 
feeds those
+    // straight into compare(). The casts below model that real payload, which 
is the only
+    // way to reach the non-numeric formatting branch. Note that 
String#toLocaleString is
+    // the identity, so a string payload cannot observe that call at all; what 
this test
+    // pins is that previousStr is derived from the previous snapshot instead 
of collapsing
+    // onto currentStr. The toLocaleString call itself is pinned by the 
numeric test below.
+    it("highlights only the character that changed when both stats are 
non-numeric strings", () => {
+      const bypassSpy = vi.spyOn(TestBed.inject(DomSanitizer), 
"bypassSecurityTrustHtml");
+      component.isOperatorFinished = false;
+      component.tableStats = { ts: { min: "2024-01-02" as unknown as number } 
};
+      component.prevTableStats = { ts: { min: "2024-01-09" as unknown as 
number } };
+
+      component.compare("ts", "min");
+
+      // the two dates agree up to the last character, which is the only one 
highlighted
+      expect(bypassSpy).toHaveBeenLastCalledWith(
+        "2024-01-0"
+          .split("")
+          .map(char => black(char))
+          .join("") + blue("2")
+      );
+    });
+
+    it("highlights the trailing characters that the shorter previous snapshot 
never reaches", () => {
+      const bypassSpy = vi.spyOn(TestBed.inject(DomSanitizer), 
"bypassSecurityTrustHtml");
+      component.isOperatorFinished = false;
+      component.tableStats = { ts: { min: "2024-01-02" as unknown as number } 
};
+      component.prevTableStats = { ts: { min: "2024" as unknown as number } };
+
+      component.compare("ts", "min");
+
+      // the first four characters match and stay black; for every index past 
the end of
+      // previousStr the lookup yields undefined, which counts as changed
+      expect(bypassSpy).toHaveBeenLastCalledWith(
+        "2024"
+          .split("")
+          .map(char => black(char))
+          .join("") +
+          "-01-02"
+            .split("")
+            .map(char => blue(char))
+            .join("")
+      );
+    });
+
+    // The non-numeric branch is also taken for a *numeric* current stat whose 
previous
+    // snapshot is missing (a column that has only just appeared in the stats 
stream), and
+    // there the difference between toLocaleString and plain String conversion 
is
+    // user-visible: a row count in the millions is rendered with group 
separators. The
+    // expectation is computed rather than hard-coded so it holds under any 
locale, and the
+    // first assertion keeps it from going vacuous on a runtime without number 
grouping.
+    it("formats large numeric stats with locale group separators", () => {
+      const bypassSpy = vi.spyOn(TestBed.inject(DomSanitizer), 
"bypassSecurityTrustHtml");
+      const toLocaleSpy = vi.spyOn(Number.prototype, "toLocaleString");
+      component.isOperatorFinished = false;
+      component.tableStats = { col: { count: 1234567 } };
+      component.prevTableStats = { col: {} };
+
+      component.compare("col", "count");
+
+      expect(toLocaleSpy).toHaveBeenCalled();
+      const grouped = String(toLocaleSpy.mock.results[0]?.value);
+      expect(bypassSpy).toHaveBeenLastCalledWith(
+        grouped
+          .split("")
+          .map(char => black(char))
+          .join("")
+      );
+    });
+
     it("falls back to plain formatting when the previous stat is missing", () 
=> {
       const bypassSpy = vi.spyOn(TestBed.inject(DomSanitizer), 
"bypassSecurityTrustHtml");
       component.isOperatorFinished = false;
@@ -670,6 +767,35 @@ describe("ResultTableFrameComponent", () => {
     expect(isImageUrl("text")).toBe(false);
   });
 
+  describe("getCellMediaType", () => {
+    it("returns the media type precomputed for that exact row and column", () 
=> {
+      component.cellMediaTypes = new Map();
+      expect(component.getCellMediaType({ media: 
"https://example.com/clip.mp4"; }, 0)).toBe("text");
+
+      component.operatorId = "op1";
+      // Two rows x two columns with the media cells on opposite diagonals. A 
1x1 fixture
+      // would leave both of the correspondences this map exists to encode 
unpinned: the
+      // per-row entry (keyed by row identity) and the position of each 
column's type
+      // inside that entry, which the template indexes by the *ngFor index over
+      // currentColumns.
+      const rows: IndexableObject[] = [
+        { pic: "https://example.com/a.png";, clip: "plain" },
+        { pic: "plain", clip: "https://example.com/b.mp4"; },
+      ];
+      component.setupResultTable(rows, 2);
+
+      expect(component.getCellMediaType(rows[0], 0)).toBe("image");
+      expect(component.getCellMediaType(rows[0], 1)).toBe("text");
+      expect(component.getCellMediaType(rows[1], 0)).toBe("text");
+      expect(component.getCellMediaType(rows[1], 1)).toBe("video");
+      // in the map, but past the end of its column list
+      expect(component.getCellMediaType(rows[1], 3)).toBe("text");
+      // an equal-valued row object that never went through setupResultTable: 
the map is
+      // keyed by identity, so this misses and falls back
+      expect(component.getCellMediaType({ ...rows[1] }, 1)).toBe("text");
+    });
+  });
+
   describe("media cell rendering in table", () => {
     beforeEach(() => {
       component.operatorId = "test-op";

Reply via email to