Skip to content

Add unit tests for web/data-visualizer's Tree and OriginalDrawer - #80

Merged
Shrey5132 merged 3 commits into
source-academy:mainfrom
Shrey5132:test/data-visualizer-web-coverage
Jul 29, 2026
Merged

Add unit tests for web/data-visualizer's Tree and OriginalDrawer#80
Shrey5132 merged 3 commits into
source-academy:mainfrom
Shrey5132:test/data-visualizer-web-coverage

Conversation

@Shrey5132

Copy link
Copy Markdown
Contributor

Summary

Fixes #72. common-data-visualizer and runner-data-visualizer already had solid coverage (11 tests, per #71's description), but web/data-visualizer — the package containing the actually intricate logic (Tree.fromSerializedNode's cycle handling, and OriginalDrawer's pixel-math in getNodeWidth/getNodeHeight and backward-arrow routing) — had none. As #72 points out, that's exactly the kind of code that already produced a silent bug once (the margin-clipping fix in #71).

What's covered

  • Tree.fromSerializedNode: a plain non-shared array, a self-referential array (the wire form of x = []; x.append(x)), a shared-but-acyclic structure (two refs to the same non-cyclic array), and a malformed unresolved-ref falling back to empty instead of throwing.
  • OriginalDrawer pixel math, via the same trees drawn end-to-end:
    • A plain pair, as a width/height baseline.
    • The self-referential case — proves getNodeWidth/getNodeHeight terminate rather than recursing forever through the cycle (draw() doesn't throw/hang), and that a self-loop is correctly classified as a backward arrow (checked via the resulting minX/minY offset baked into the final size).
    • The shared-but-acyclic case, as a contrasting proof that sharing alone does not trip the backward-arrow path (the reference is drawn forward/downward, no offset).
    • Basic monotonic sanity: width grows with sibling count, height grows with nesting depth.

Every expected width/height in these tests is hand-traced against Config constants and the real getNodeWidth/getNodeHeight/drawNode logic, not just echoed back from the same formula.

Note on konva mocking

Importing the real konva/react-konva in a plain Node test environment makes Konva switch to its server-side build, which unconditionally requires the optional canvas native package (not a dependency anywhere in this workspace). Since these tests only exercise layout arithmetic — never actual canvas rendering — I mocked both modules rather than pulling in jsdom+canvas for a logic-only test file.

Test plan

  • yarn vitest run src/web/data-visualizer — 8/8 new tests pass.
  • yarn build, yarn format:ci, yarn lint (0 errors, only pre-existing unrelated warnings), yarn vitest run --coverage (126/126) all pass locally.

common and runner already had solid coverage, but web/data-visualizer
- the package with the actually intricate logic - had none: cycle
handling in Tree.fromSerializedNode, and OriginalDrawer's pixel math
(getNodeWidth/getNodeHeight, backward-arrow routing). That gap is how
the margin-clipping bug shipped silently earlier.

Covers: plain non-shared arrays, a self-referential array (proves
getNodeWidth/getNodeHeight terminate instead of recursing through the
cycle, and that the self-loop correctly routes as a backward arrow),
a shared-but-acyclic structure (proves sharing alone does NOT trip the
backward-arrow path), a malformed unresolved-ref fallback, and basic
width/height growth invariants. Every pixel value is hand-traced
against the real formulas, not just re-derived from them.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 83a1296

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package or glob expression "@sourceacademy/web-stepper" is specified in the `ignore` option but it is not found in the project. You may have misspelled the package name or provided an invalid glob expression. Note that glob expressions must be defined according to https://www.npmjs.com/package/micromatch
The package or glob expression "@sourceacademy/web-data-visualizer" is specified in the `ignore` option but it is not found in the project. You may have misspelled the package name or provided an invalid glob expression. Note that glob expressions must be defined according to https://www.npmjs.com/package/micromatch

@Shrey5132

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds Vitest coverage for data visualizer tree parsing and OriginalDrawer layout calculations, including cyclic and shared references, malformed references, arrow behavior, and dimension growth.

Changes

Tree and drawer test coverage

Layer / File(s) Summary
Serialized tree parsing tests
src/web/data-visualizer/src/__tests__/tree.test.ts
Mocks Konva dependencies and tests array construction, cyclic references, shared references, and unresolved references.
Drawer pixel math tests
src/web/data-visualizer/src/__tests__/tree.test.ts
Verifies drawer width and height for leaf, cyclic, shared, sibling-growth, and nesting-depth layouts.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Poem

I’m a bunny with tests in my burrow,
Trees link and loop without sorrow.
Konva stays still,
Dimensions fit the bill,
And shared leaves hop forth tomorrow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding unit tests for Tree and OriginalDrawer in web/data-visualizer.
Description check ✅ Passed The description is directly related to the test additions and explains the covered logic and test plan.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/web/data-visualizer/src/__tests__/tree.test.ts`:
- Around line 123-125: Update the height assertion in the tree drawing test to
use Config.ArrowMarginTop, matching the vertical margin used by minY, while
leaving the width assertion based on Config.ArrowMarginHorizontal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b6e4bf7-0102-4d85-9e73-e5eb12548927

📥 Commits

Reviewing files that changed from the base of the PR and between 135ed62 and c0e8756.

📒 Files selected for processing (1)
  • src/web/data-visualizer/src/__tests__/tree.test.ts

Comment on lines +123 to +125
const minShift = Config.ArrowMarginHorizontal + Config.StrokeWidth / 2;
expect(drawer.width).toBe(nodeWidth + minShift);
expect(drawer.height).toBe(nodeHeight + minShift + Config.StrokeWidth);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the vertical arrow margin for the height expectation.

minY is calculated with Config.ArrowMarginTop, but Line 125 reuses the horizontal margin. This encodes the wrong height contract when the two margins differ.

Proposed fix
-    const minShift = Config.ArrowMarginHorizontal + Config.StrokeWidth / 2;
-    expect(drawer.width).toBe(nodeWidth + minShift);
-    expect(drawer.height).toBe(nodeHeight + minShift + Config.StrokeWidth);
+    const minShiftX = Config.ArrowMarginHorizontal + Config.StrokeWidth / 2;
+    const minShiftY = Config.ArrowMarginTop + Config.StrokeWidth / 2;
+    expect(drawer.width).toBe(nodeWidth + minShiftX);
+    expect(drawer.height).toBe(nodeHeight + minShiftY + Config.StrokeWidth);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const minShift = Config.ArrowMarginHorizontal + Config.StrokeWidth / 2;
expect(drawer.width).toBe(nodeWidth + minShift);
expect(drawer.height).toBe(nodeHeight + minShift + Config.StrokeWidth);
const minShiftX = Config.ArrowMarginHorizontal + Config.StrokeWidth / 2;
const minShiftY = Config.ArrowMarginTop + Config.StrokeWidth / 2;
expect(drawer.width).toBe(nodeWidth + minShiftX);
expect(drawer.height).toBe(nodeHeight + minShiftY + Config.StrokeWidth);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/web/data-visualizer/src/__tests__/tree.test.ts` around lines 123 - 125,
Update the height assertion in the tree drawing test to use
Config.ArrowMarginTop, matching the vertical margin used by minY, while leaving
the width assertion based on Config.ArrowMarginHorizontal.

Follow-up to the earlier tree/OriginalDrawer tests, closing gaps that
those tests inadvertently exposed rather than covered (importing
Tree.ts pulls in the whole package - including 5 previously-untracked
drawable components - for the first time, so istanbul started scoring
their unexecuted render() bodies against the aggregate).

Added: a "function" wire-node case (construction, sharing, and drawing
- FunctionTreeNode.createDrawable was completely untested), the real
Tree.draw() entry point DataVisualizerView.tsx actually calls (only
OriginalDrawer directly was tested before), OriginalDrawer's other
top-level branch (a bare leaf with no pair/array), formatLeaf's
truncation/quoting logic, and ArrayDrawable's render() - the one
drawable with real branching (empty-vs-leaf slots, separator line
count, the BoxMinWidth clamp for a real zero-element list case).

Left uncovered on purpose: ArrowDrawable, BackwardArrowDrawable,
FunctionDrawable, NullDrawable - static prop-to-JSX mapping with
no real branching (their arrow-routing *decisions* are already
covered via OriginalDrawer; only the leaf visual components' own
render bodies aren't, and treating those isn't worth the friction of
reaching into memo()-wrapped internals).
@Shrey5132

Copy link
Copy Markdown
Contributor Author

Why the Coveralls status shows a decrease

web/data-visualizer had zero tests before this PR, so none of its files were ever imported during a test run — istanbul never saw them, so they weren't counted in the coverage denominator at all. Importing Tree.ts here pulls in its whole transitive graph (OriginalDrawer, all 5 drawable components, TreeNode's subclasses) for the first time. Most of that is now genuinely exercised, but the 8 initial tests mocked react-konva (needed to avoid Konva's Node build requiring the native canvas package) and never actually rendered the drawable components, so their render() bodies got newly tracked but 0%-covered — a net decrease even though real coverage was added.

Pushed a follow-up commit closing most of that gap with tests that are genuine regression coverage, not padding for the metric:

  • A "function" wire-node case end-to-end (construction, sharing, and drawing — FunctionTreeNode.createDrawable was completely untested).
  • Tree.draw() itself, the actual entry point DataVisualizerView.tsx calls (the first round only tested OriginalDrawer directly).
  • OriginalDrawer's other top-level branch (a bare leaf with no pair/array — e.g. draw_data(1, 42)).
  • formatLeaf's truncation/quoting logic.
  • ArrayDrawable.render() — the one drawable with real branching (empty-vs-leaf slots, separator line count, and the BoxMinWidth clamp for a real zero-element-list case, e.g. draw_data([])).

Result: -15.9% → -4.6% (82.536% → 93.763%).

What's still deliberately uncovered: ArrowDrawable, BackwardArrowDrawable, FunctionDrawable, NullDrawable. These are static prop-to-JSX mapping with no real branching — the arrow-routing decisions (forward vs. backward, when to draw one at all) are already covered via the OriginalDrawer tests; only the leaf visual components' own render bodies aren't, and reaching into memo()-wrapped internals to cover them didn't seem worth it for what's left.

This is a non-blocking status check (separate from the required build job, which passes), so this should be fine to merge as-is.

CodeRabbit caught this: OriginalDrawer's own minY update uses
Config.ArrowMarginTop, distinct from minX's ArrowMarginHorizontal. The
test only passed because both happen to be 15 right now - it was
asserting the wrong formula, not a coincidentally-right one.
@Shrey5132
Shrey5132 merged commit b884153 into source-academy:main Jul 29, 2026
1 of 2 checks passed
@Shrey5132
Shrey5132 deleted the test/data-visualizer-web-coverage branch July 29, 2026 08:31
martin-henz pushed a commit that referenced this pull request Jul 29, 2026
…81)

* Restore Binary Tree / General Tree view modes

Prof Martin flagged that dropping these during the Conductor port
(deleted outright, "now fully unused") was a mistake — they're an
important feature, not dead weight. Ports the old pre-Conductor
frontend's classification (isBinaryTree/isGeneralTree) and layout
(initializeTreeMetaData) algorithms from dataVisualizer.tsx/Tree.tsx,
and the two drawers, onto the new wire format:

- classify.ts: re-derives cycle/shared-structure detection and
  binary/general-tree classification against SerializedDataVisualizerNode
  (a tagged N-ary "array" node) instead of the old code's raw nested-pair
  walking (`structures[0]`/`structures[1]` on live objects), generalized
  from a fixed 2-element-pair representation since a native list isn't
  fixed at length 2. Keyed by RefId instead of WeakMap<object, _> — no
  live object identity crosses the wire.
- BinaryTreeDrawer.tsx/GeneralTreeDrawer.tsx: the drawing algorithms
  themselves are an unchanged, verified line-for-line port (every
  leftCOUNTER/rightCOUNTER/EY1/EY2/scalerV/originIndex calculation) —
  only the state-access pattern changed, from the old code's global
  DataVisualizer statics (DataVisualizer.isBinTree, .TreeDepth, ...) to
  an injected per-call ClassificationResult.
- BaseTreeNode.ts: restores nodePos/nodeColor, populated in
  Tree.fromSerializedNode from the classification's computed layout.
- Tree.ts: draw() now takes a ViewMode and returns the matching drawer
  instance (mirrors the old Tree.tsx's draw()) instead of drawing
  directly — callers now do `.draw(viewMode).draw(x, y, key)`.
- DataVisualizerView.tsx: a global Original/Binary Tree/General Tree
  toggle, applying across every call and argument (matching the old
  tool's single static toggle). Doesn't hide anything — a structure
  that isn't actually the selected shape still renders, via the
  drawers' own "not a binary/general tree" warning box.

OriginalDrawer.tsx's Tree import switches to `import type` — needed to
break a real circular import (Tree.ts -> BinaryTreeDrawer.tsx ->
OriginalDrawer.tsx -> Tree.ts) introduced by Tree.ts now importing the
drawer subclasses as values.

* Add tests for classify, BinaryTreeDrawer, GeneralTreeDrawer

classify.test.ts: cyclic and shared-but-acyclic structures classify
as neither tree kind; a proper binary tree and a proper general tree
each classify correctly with a populated layout; a flat list and a
bare pair are correctly rejected as binary trees.

treeViews.test.ts: both drawers' non-matching-shape warning box
(exact size and message), a valid tree drawing without throwing to a
positive finite size, and width/height growing with more nodes —
mirrors the rigor from the original OriginalDrawer/Tree tests (#80)
rather than just smoke-testing the new files.

tree.test.ts: updates the one existing test that called
Tree.draw(x, y, key) directly for the API change (Tree.draw(viewMode)
now returns a drawer instance).

* fix: correct isGeneralTreeNode to match the old tool's actual (permissive) semantics

My port required an exact `[data, [childrenList, <end>]]` shape (label, then one
nested branches-list) — but hand-tracing the old isGeneralTree against several
worked examples shows it actually walks the *whole* proper list uniformly: any
element that's itself compound is recursively re-checked, with no slot singled
out as "the children list". That accepts both SICP-textbook `list(label,
branches)` *and* a flat `llist(label, child1, child2, ...)`, since nothing
distinguishes the second slot from the third.

Concretely, draw_data(llist(1, llist(2, None, None), llist(3, None, None)))
was rejected by General Tree View - a flat llist chain, not the nested-list
encoding my port required.

One consequence worth calling out: since a binary tree is structurally just a
proper list whose compound elements happen to recurse the same way, a valid
binary tree is now also always a valid general tree. Confirmed (by tracing the
old algorithm by hand, not just this port) this isn't a new bug - General Tree
View was always meant to be a strict superset of Binary Tree View, not a
disjoint shape. Updated the one test that encoded the old (incorrect)
assumption, added coverage for the flat-llist and binary-tree-is-general-tree
cases.

Live-verified in-browser against the reported failing case.

* Improve test coverage for tree-view restoration

classify.ts, BinaryTreeDrawer.tsx, GeneralTreeDrawer.tsx, OriginalDrawer.tsx
and Tree.ts (this PR's own files) went from 87.76%/86.26% stmt/branch
coverage to 97.13%/94.42%. Closes:

- classify.ts: the data-slot-is-empty short-circuit in isBinaryTreeNode,
  mismatched sibling data-type labels, and an improper compound head
  rejecting a general tree.
- BinaryTreeDrawer/GeneralTreeDrawer: drawNode's cycle-arrow (forward and
  backward) and function-node branches, via direct Tree/classification
  construction — unreachable through the real Tree.fromSerializedNode path
  since any AlreadyParsedTreeNode forces isSharedStructure, which always
  forces isBinaryTree/isGeneralTree false, so draw()'s warning-box check
  short-circuits before drawNode ever runs. Still real, hand-ported logic
  worth protecting from regression. Also added GeneralTreeDrawer's missing
  counterpart to BinaryTreeDrawer's existing bare-empty-terminator test.
- Tree.ts: getNodeById, previously uncalled by any test.
- OriginalDrawer.tsx: the unrecognized-TreeNode-subtype and
  null-children ArrayTreeNode fallback branches in getNodeWidth/getNodeHeight.

The one remaining line in classify.ts (133) and one in BinaryTreeDrawer.tsx
(78) are left uncovered: the former is provably unreachable per the
preceding code's own logic (see its comment), the latter is a narrow
leftCOUNTER/rightCOUNTER-both-zero arithmetic edge in the ported layout
math not worth a contrived test for one line. DataVisualizerView.tsx
remains untested — it needs real component-rendering infra (no
react-test-renderer/jsdom precedent exists anywhere in this monorepo
today), a separate decision from adding more unit tests.

* Add DataVisualizerView component tests

The view-mode toggle component had zero coverage - not even partial,
since nothing in the package imported it before now. Covers: the
no-rows placeholder, view-mode button state, multi-argument "Structure
N" labeling, Previous/Next pagination clamped at both ends, ArrowLeft/
ArrowRight keyboard nav (and its suppression while focus is in an
editable element), and the currentStep reset on a fresh set of rows.

Uses react-test-renderer + a few hand-written window/document/
HTMLElement stubs rather than jsdom, matching this package's existing
practice of precise hand-mocking (konva/react-konva) over pulling in
heavier environment dependencies - no jsdom precedent exists anywhere
in this monorepo. The one real snag: React 19 requires the initial
TestRenderer.create() itself to run inside act(), not just later
interactions, or effects (including the window.addEventListener call
these tests depend on) aren't guaranteed to have run yet.

DataVisualizerView.tsx: 0% (untested) -> 100% stmt/line, 95.45% branch.
Package total: 97.13%/94.42% -> 97.36%/94.5% stmt/branch.

* Address CodeRabbit review comments on PR #81

Fixed:
- BinaryTreeDrawer.drawNode: rename unused originIndex/originX params to
  _originIndex/_originX (unlike GeneralTreeDrawer, Binary's own algorithm
  never reads them - confirmed via the pre-existing lint warning).
- classify.test.ts: derive the shared-node test's sharedRef.refId from the
  refId the pair() helper actually assigned, instead of a hardcoded 1 that
  only happened to be correct because this was the file's first pair() call.
- tree.test.ts: the Tree.draw('original') smoke test called drawer.draw()
  twice on the same stateful drawer instance; now calls once and reuses
  the result.
- DataVisualizerView.test.tsx: the view-mode-click test only asserted the
  clicked Button's own `active` prop, which can't distinguish a correctly
  wired toggle from a bug that updates state but passes the wrong value
  into Tree.draw. Now spies on Tree.prototype.draw and asserts it's called
  with "original" before the click and "binaryTree" after.

Skipped, with reasons:
- GeneralTreeDrawer.tsx: the suggestion to hoist runningX2/downCOUNTER out
  of the per-node ArrayTreeNode visit into draw() would change behavior,
  not just refactor it. Checked against the old frontend's
  GeneralTreeDrawer.tsx (frontend/src/features/dataVisualizer/tree/
  GeneralTreeDrawer.tsx:144-146) - it has the exact same per-node reset,
  just reading from DataVisualizer.nodeCount[0]/DataVisualizer.TreeDepth
  statics instead of this.classification.layout. This is a faithful
  line-for-line port of the old tool's actual behavior, not a regression
  introduced here - "fixing" it would be an unreviewed behavior change
  beyond this PR's restore-the-old-behavior scope.
- DataVisualizerView.test.tsx migration to React Testing Library + jsdom:
  the suggestion asserts an "existing JSDOM test setup" that doesn't exist
  - grepped the whole monorepo, no jsdom/@testing-library reference
  anywhere. The user explicitly chose react-test-renderer + hand-rolled
  stubs over jsdom for this exact file in this same session, specifically
  because there's no jsdom precedent to build on; reversing that call on
  a mistaken premise isn't a "still-valid issue" to silently act on.

Verification: npx tsc --noEmit, yarn eslint, yarn prettier --list-different,
yarn vitest run all pass (63/63 in this package, no regressions elsewhere).

---------

Co-authored-by: Shrey Jain <“shreyjain5132@email.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dataviz: test cases needed

1 participant