Frontend: PriceChartLightweightCanvas lifecycle tests (resize, stale init, early indicators) #225

Closed
opened 2026-05-29 05:40:50 +00:00 by PlasticDigits · 8 comments
PlasticDigits commented 2026-05-29 05:40:50 +00:00 (Migrated from gitlab.com)

Summary

Close regression gaps in PriceChartLightweightCanvas lifecycle: post-layout sizing (applySize / double requestAnimationFrame / ResizeObserver), stale async init when unmounting before import('lightweight-charts') resolves, and indicator toggles while chartModelReady is still false. These paths are called out in GitLab #211 test plans but are not fully covered by the real-library Vitest suite or stub tests today.

Bundled scope: post-layout resize · stale async init · indicator toggle before ready


Current codebase

Area Files
Canvas init, sizing, guards frontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsx
Init generation guard chartInitIdRef — increments per effect run; stale inits call chart.remove()
Post-layout sizing applySize() → chart.applyOptions({ width, height }); double rAF after mount; ResizeObserver on container
Indicator sync gate Second useEffect runs only when chartModelReady === true
Data setData effect Third useEffect updates series when refs exist (no chartModelReady gate)
Product invariants docs/frontend.md § Trade page — price chart invariants (#113, #151)
Real-library tests *.charts.test.{ts,tsx} via npm run test:charts (#211) — mount/update/MA toggle; no resize/stale-init/early-indicator tests
Stub tests PriceChart.test.tsx — interval/pair (#148); no canvas lifecycle

Why this is needed

  1. #151 viewport: Charts in flex/grid parents can mount at 0×0; without verified applySize + double rAF, production shows blank or clipped canvases while unit tests stay green.
  2. Race on navigation: Pair/interval switches unmount the canvas while dynamic import is in flight; chartInitIdRef must prevent orphan charts and React warnings — untested regression vector.
  3. Indicator QA: Users can open the overlay menu before init completes; toggling MA/RSI must not throw or leave pane topology inconsistent when syncPriceChartIndicatorOverlays is gated on chartModelReady.

Constraints and guardrails

  1. Naming: lightweight-charts / TradingView lightweight-charts — not the hosted TradingView widget.
  2. Do not break default CI: Prefer stub/enriched-mock tests in npm run test:run; add real-library cases only in npm run test:charts where canvas/layout is required (#211).
  3. No permanent test.skip without a linked follow-up (#105).
  4. Minimal production changes: Fix only if tests expose a real bug; do not refactor chart architecture.
  5. Prefer data-testid over canvas pixel assertions.

Relevant files

  • frontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsx
  • frontend-dapp/src/components/charts/priceChartLightweightIndicatorSync.ts
  • frontend-dapp/src/components/charts/priceChartPaneHeights.ts
  • frontend-dapp/src/components/charts/__tests__/PriceChartLightweightCanvas.charts.test.tsx
  • frontend-dapp/src/components/charts/__tests__/PriceChartLightweightCanvas.test.tsx (new stub contract — may overlap with bundled stub issue; coordinate)
  • frontend-dapp/src/test/lightweightChartsJsdomMock.ts
  • frontend-dapp/vitest.config.charts.ts, src/test/chartsSetup.ts
  • docs/frontend.md, skills/AGENTS_FRONTEND_PRICE_CHART.md

  1. Post-layout resize (stub or charts suite): Mock ResizeObserver and clientWidth/clientHeight; assert chart.applyOptions (or mock spy) receives width > 0 and height ≥ 320 after double rAF. Optional test:charts case with sized parent div.
  2. Stale async init: In PriceChartLightweightCanvas.test.tsx, delay resolved import('lightweight-charts') via vi.mock + vi.doMock; unmount before resolve; expect no throw, chart.remove at most once for abandoned init, no duplicate canvases.
  3. Indicator before ready: Render with delayed init; toggle showSma7/showRsi before chartModelReady; assert no uncaught errors and correct state after ready (pane count / spy calls).

Acceptance criteria

  • Automated test proves applyOptions (or equivalent) runs with positive dimensions after layout (double rAF path).
  • Automated test: unmount before dynamic import completes — no throw, no leaked chart instance (spy/remove count).
  • Automated test: indicator toggles before chartModelReady — no throw; after ready, topology matches toggle state (#150).
  • npm run test:run and npm run test:charts remain green.
  • docs/frontend.md / skills/AGENTS_FRONTEND_PRICE_CHART.md note new regression tests (invariant #9 post-layout).

Test plan — functional paths

# Path Setup Expected
1 Flex parent zero initial size Container width:0 then layout to 640×400 After double rAF, chart width/height > 0
2 ResizeObserver fires Change container height applySize updates height; volume pane height recalculated
3 RSI pane resize showRsi true, resize parent Pane 2 height updated via rsiPaneHeightPx
4 Unmount during import Slow import() mock Abandoned init calls remove; no setChartModelReady(true) for stale id
5 Unmount after init, before rAF Mount → immediate unmount Cleanup runs; no orphan DOM canvases
6 MA7 on before ready Toggle during init No throw; MA7 visible after ready if still enabled
7 RSI on before ready Toggle during init Pane 3 after ready; removePane(2) still works on off
8 Rapid mount/unmount ×10 Hammer remount No throw; remove count matches successful inits

Test plan — attack vectors / failure modes

Vector Approach Expected guard
Stale init wins race Two overlapping inits (force re-run) Only latest initId sets chartModelReady
ResizeObserver after unmount Disconnect before callback No applyOptions on removed chart
Toggle spam before ready 20 rapid MA toggles Stable state after ready
Null container mid-init Remove ref target Init aborts without throw
Memory leak 20 mount/unmount Zero canvases in container

Verification criteria

  1. cd frontend-dapp && npm run test:run — new/updated stub tests pass.
  2. cd frontend-dapp && npm run test:charts — any new real-library layout tests pass.
  3. Manual: /trade chart in narrow flex column — canvas fills plot after load (#151).
  4. Docs/skills cross-link this issue from price-chart invariants.

  • #211 — real-library Vitest baseline
  • #151 — viewport / flex sizing
  • #150 — indicators
  • #148 — timeframe / remount policy
## Summary Close regression gaps in **`PriceChartLightweightCanvas`** lifecycle: post-layout sizing (**`applySize`** / double **`requestAnimationFrame`** / **`ResizeObserver`**), stale async init when unmounting before **`import('lightweight-charts')`** resolves, and indicator toggles while **`chartModelReady`** is still false. These paths are called out in [GitLab #211](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/211) test plans but are not fully covered by the real-library Vitest suite or stub tests today. **Bundled scope:** post-layout resize · stale async init · indicator toggle before ready --- ## Current codebase | Area | Files | |------|--------| | Canvas init, sizing, guards | `frontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsx` | | Init generation guard | `chartInitIdRef` — increments per effect run; stale inits call `chart.remove()` | | Post-layout sizing | `applySize()` → `chart.applyOptions({ width, height })`; double `rAF` after mount; `ResizeObserver` on container | | Indicator sync gate | Second `useEffect` runs only when `chartModelReady === true` | | Data `setData` effect | Third `useEffect` updates series when refs exist (no `chartModelReady` gate) | | Product invariants | [docs/frontend.md § Trade page — price chart invariants](docs/frontend.md#trade-page-price-chart-invariants) ([#113](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/113), [#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151)) | | Real-library tests | `*.charts.test.{ts,tsx}` via `npm run test:charts` ([#211](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/211)) — mount/update/MA toggle; **no** resize/stale-init/early-indicator tests | | Stub tests | `PriceChart.test.tsx` — interval/pair ([#148](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/148)); no canvas lifecycle | --- ## Why this is needed 1. **#151 viewport:** Charts in flex/grid parents can mount at **0×0**; without verified `applySize` + double `rAF`, production shows blank or clipped canvases while unit tests stay green. 2. **Race on navigation:** Pair/interval switches unmount the canvas while dynamic import is in flight; `chartInitIdRef` must prevent orphan charts and React warnings — untested regression vector. 3. **Indicator QA:** Users can open the overlay menu before init completes; toggling MA/RSI must not throw or leave pane topology inconsistent when `syncPriceChartIndicatorOverlays` is gated on `chartModelReady`. --- ## Constraints and guardrails 1. **Naming:** **lightweight-charts** / **TradingView lightweight-charts** — not the hosted TradingView widget. 2. **Do not break default CI:** Prefer stub/enriched-mock tests in `npm run test:run`; add real-library cases only in `npm run test:charts` where canvas/layout is required ([#211](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/211)). 3. **No permanent `test.skip`** without a linked follow-up ([#105](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/105)). 4. **Minimal production changes:** Fix only if tests expose a real bug; do not refactor chart architecture. 5. **Prefer `data-testid`** over canvas pixel assertions. --- ## Relevant files - `frontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsx` - `frontend-dapp/src/components/charts/priceChartLightweightIndicatorSync.ts` - `frontend-dapp/src/components/charts/priceChartPaneHeights.ts` - `frontend-dapp/src/components/charts/__tests__/PriceChartLightweightCanvas.charts.test.tsx` - `frontend-dapp/src/components/charts/__tests__/PriceChartLightweightCanvas.test.tsx` (new stub contract — may overlap with bundled stub issue; coordinate) - `frontend-dapp/src/test/lightweightChartsJsdomMock.ts` - `frontend-dapp/vitest.config.charts.ts`, `src/test/chartsSetup.ts` - `docs/frontend.md`, `skills/AGENTS_FRONTEND_PRICE_CHART.md` --- ## Recommended direction 1. **Post-layout resize (stub or charts suite):** Mock `ResizeObserver` and `clientWidth`/`clientHeight`; assert `chart.applyOptions` (or mock spy) receives **width > 0** and **height ≥ 320** after double `rAF`. Optional `test:charts` case with sized parent div. 2. **Stale async init:** In `PriceChartLightweightCanvas.test.tsx`, delay resolved `import('lightweight-charts')` via `vi.mock` + `vi.doMock`; unmount before resolve; expect **no throw**, **`chart.remove`** at most once for abandoned init, no duplicate canvases. 3. **Indicator before ready:** Render with delayed init; toggle `showSma7`/`showRsi` before `chartModelReady`; assert no uncaught errors and correct state after ready (pane count / spy calls). --- ## Acceptance criteria - [ ] Automated test proves **`applyOptions`** (or equivalent) runs with positive dimensions after layout (double `rAF` path). - [ ] Automated test: unmount before dynamic import completes — no throw, no leaked chart instance (spy/`remove` count). - [ ] Automated test: indicator toggles before `chartModelReady` — no throw; after ready, topology matches toggle state ([#150](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/150)). - [ ] `npm run test:run` and `npm run test:charts` remain green. - [ ] `docs/frontend.md` / `skills/AGENTS_FRONTEND_PRICE_CHART.md` note new regression tests (invariant #9 post-layout). --- ## Test plan — functional paths | # | Path | Setup | Expected | |---|------|--------|----------| | 1 | Flex parent zero initial size | Container `width:0` then layout to 640×400 | After double `rAF`, chart width/height > 0 | | 2 | ResizeObserver fires | Change container height | `applySize` updates height; volume pane height recalculated | | 3 | RSI pane resize | `showRsi` true, resize parent | Pane 2 height updated via `rsiPaneHeightPx` | | 4 | Unmount during import | Slow `import()` mock | Abandoned init calls `remove`; no `setChartModelReady(true)` for stale id | | 5 | Unmount after init, before rAF | Mount → immediate unmount | Cleanup runs; no orphan DOM canvases | | 6 | MA7 on before ready | Toggle during init | No throw; MA7 visible after ready if still enabled | | 7 | RSI on before ready | Toggle during init | Pane 3 after ready; `removePane(2)` still works on off | | 8 | Rapid mount/unmount ×10 | Hammer remount | No throw; `remove` count matches successful inits | --- ## Test plan — attack vectors / failure modes | Vector | Approach | Expected guard | |--------|----------|----------------| | Stale init wins race | Two overlapping inits (force re-run) | Only latest `initId` sets `chartModelReady` | | ResizeObserver after unmount | Disconnect before callback | No `applyOptions` on removed chart | | Toggle spam before ready | 20 rapid MA toggles | Stable state after ready | | Null container mid-init | Remove ref target | Init aborts without throw | | Memory leak | 20 mount/unmount | Zero canvases in container | --- ## Verification criteria 1. `cd frontend-dapp && npm run test:run` — new/updated stub tests pass. 2. `cd frontend-dapp && npm run test:charts` — any new real-library layout tests pass. 3. Manual: `/trade` chart in narrow flex column — canvas fills plot after load ([#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151)). 4. Docs/skills cross-link this issue from price-chart invariants. --- ## Related issues - [#211](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/211) — real-library Vitest baseline - [#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151) — viewport / flex sizing - [#150](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/150) — indicators - [#148](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/148) — timeframe / remount policy
PlasticDigits commented 2026-05-29 05:40:51 +00:00 (Migrated from gitlab.com)

marked as related to #211

marked as related to #211
PlasticDigits commented 2026-05-29 05:40:52 +00:00 (Migrated from gitlab.com)

marked as related to #151

marked as related to #151
PlasticDigits commented 2026-05-29 05:40:53 +00:00 (Migrated from gitlab.com)

marked as related to #150

marked as related to #150
PlasticDigits commented 2026-05-29 05:40:54 +00:00 (Migrated from gitlab.com)

marked as related to #148

marked as related to #148
PlasticDigits commented 2026-05-29 06:50:04 +00:00 (Migrated from gitlab.com)

mentioned in commit 09d2544f26

mentioned in commit 09d2544f26fb92a9ac2e70a37d98c75494245c55
PlasticDigits commented 2026-05-29 06:51:05 +00:00 (Migrated from gitlab.com)

Implementation summary

Closed regression gaps for PriceChartLightweightCanvas lifecycle (#225):

Code & tests

  • PriceChartLightweightCanvas.test.tsx (default npm run test:run): post-layout applyOptions after double requestAnimationFrame, ResizeObserver resize, unmount-before-import, React StrictMode single-chart init, unmount remove, indicator toggles before chartModelReady (MA7 + RSI), rapid mount/unmount ×10.
  • PriceChartLightweightCanvas.charts.test.tsx (npm run test:charts): real-library positive canvas dimensions after double rAF.
  • priceChartLightweightModule.ts: thin loadPriceChartLightweightModule() wrapper so Vitest can defer dynamic import without affecting production behavior.
  • lightweightChartsJsdomMock.ts: singleton chart spy, blockNextImport / releaseBlockedImport, tracking ResizeObserver.

Docs (cross-linked)

Merged to main in commit 09d2544.


Verification checklist

  • cd frontend-dapp && npm run test:run — includes new PriceChartLightweightCanvas.test.tsx
  • cd frontend-dapp && npm run test:charts — includes post-layout canvas sizing case
  • Manual /trade: chart in a narrow flex column fills plot after load (#151)
  • Navigate away quickly during first chart load — no console errors / leaked canvases
  • Open Indicators before chart ready — toggle MA7/RSI — no throw; state matches after load (#150)

@brouie — please run through the checklist above when you have a moment. Leaving this issue open until verified.

## Implementation summary Closed regression gaps for **`PriceChartLightweightCanvas`** lifecycle ([#225](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/225)): ### Code & tests - **`PriceChartLightweightCanvas.test.tsx`** (default `npm run test:run`): post-layout **`applyOptions`** after double `requestAnimationFrame`, **`ResizeObserver`** resize, unmount-before-import, React StrictMode single-chart init, unmount **`remove`**, indicator toggles before **`chartModelReady`** (MA7 + RSI), rapid mount/unmount ×10. - **`PriceChartLightweightCanvas.charts.test.tsx`** (`npm run test:charts`): real-library positive canvas dimensions after double rAF. - **`priceChartLightweightModule.ts`**: thin `loadPriceChartLightweightModule()` wrapper so Vitest can defer dynamic import without affecting production behavior. - **`lightweightChartsJsdomMock.ts`**: singleton chart spy, `blockNextImport` / `releaseBlockedImport`, tracking **`ResizeObserver`**. ### Docs (cross-linked) - [docs/frontend.md — price chart invariants](docs/frontend.md#trade-page-price-chart-invariants) - [docs/testing.md — real lightweight-charts](docs/testing.md#real-lightweight-charts-in-vitest-gitlab-211) - [skills/AGENTS_FRONTEND_PRICE_CHART.md](skills/AGENTS_FRONTEND_PRICE_CHART.md) — invariants **#9–#11** Merged to **`main`** in commit `09d2544`. --- ### Verification checklist - [ ] `cd frontend-dapp && npm run test:run` — includes new `PriceChartLightweightCanvas.test.tsx` - [ ] `cd frontend-dapp && npm run test:charts` — includes post-layout canvas sizing case - [ ] Manual `/trade`: chart in a narrow flex column fills plot after load ([#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151)) - [ ] Navigate away quickly during first chart load — no console errors / leaked canvases - [ ] Open **Indicators** before chart ready — toggle MA7/RSI — no throw; state matches after load ([#150](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/150)) @brouie — please run through the checklist above when you have a moment. Leaving this issue **open** until verified.
PlasticDigits commented 2026-05-29 08:26:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #227

mentioned in issue #227
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-30 06:33:50 +00:00
PlasticDigits commented 2026-05-30 06:33:53 +00:00 (Migrated from gitlab.com)

Verification complete (agent, verify/issue-225 worktree)

Verified GitLab #225 against main @ 54a6346 (worktree fast-forwarded from origin/main; no production code changes required).

Automated checks — PASS

Command Result
npm run test:run — PriceChartLightweightCanvas.test.tsx 17/17 pass (lifecycle + contract)
npm run test:run — full suite 710/710 pass
npm run test:charts — PriceChartLightweightCanvas.charts.test.tsx 6/6 pass
npm run test:charts — full suite 21/21 pass (1 skipped, linked)

Acceptance criteria mapping:

  • applyOptions with positive dimensions after double requestAnimationFrame (stub + real-library)
  • Unmount before dynamic import completes — no throw, no leaked chart (blockNextImport / StrictMode single-chart)
  • Indicator toggles before chartModelReady — MA7 + RSI; no throw; topology syncs after ready
  • Default CI suites remain green
  • Docs/skills cross-linked (#225 in docs/frontend.md, docs/testing.md, skills/AGENTS_FRONTEND_PRICE_CHART.md invariants #9–#11)

Manual checks — PASS

Local stack (indexer :3001, LocalTerra LCD/RPC) — infra left running; frontend started on :5173 for verification only, then stopped.

  • /trade — chart container 495×336 px in flex layout; child canvases width/height > 0 after load (#151)
  • Navigate away during first load (/trade → /pool within ~50 ms) — 0 detached chart canvases; no price-chart-lightweight-canvas root left in DOM
  • Indicators before ready — opened overlay + toggled MA7 & RSI during init; chart loaded with expanded pane set (15 canvases vs 11 baseline), no console errors (#150)

Follow-up checklist (for spot re-check)

  • cd frontend-dapp && npm run test:run && npm run test:charts
  • /trade in a narrow viewport — plot fills after load
  • Quick pair switch / route away during first paint — no console errors
  • Toggle MA7/RSI from overlay before chart ready — state matches after load

All issue-body verification criteria and comment checklist items pass. Closing.

## Verification complete (agent, `verify/issue-225` worktree) Verified GitLab **#225** against `main` @ `54a6346` (worktree fast-forwarded from `origin/main`; no production code changes required). ### Automated checks — PASS | Command | Result | |---------|--------| | `npm run test:run` — `PriceChartLightweightCanvas.test.tsx` | **17/17** pass (lifecycle + contract) | | `npm run test:run` — full suite | **710/710** pass | | `npm run test:charts` — `PriceChartLightweightCanvas.charts.test.tsx` | **6/6** pass | | `npm run test:charts` — full suite | **21/21** pass (1 skipped, linked) | **Acceptance criteria mapping:** - [x] `applyOptions` with positive dimensions after double `requestAnimationFrame` (stub + real-library) - [x] Unmount before dynamic import completes — no throw, no leaked chart (`blockNextImport` / StrictMode single-chart) - [x] Indicator toggles before `chartModelReady` — MA7 + RSI; no throw; topology syncs after ready - [x] Default CI suites remain green - [x] Docs/skills cross-linked ([#225](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/225) in `docs/frontend.md`, `docs/testing.md`, `skills/AGENTS_FRONTEND_PRICE_CHART.md` invariants **#9–#11**) ### Manual checks — PASS Local stack (indexer `:3001`, LocalTerra LCD/RPC) — **infra left running**; frontend started on `:5173` for verification only, then stopped. - [x] **`/trade`** — chart container **495×336** px in flex layout; child canvases **width/height > 0** after load ([#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151)) - [x] **Navigate away during first load** (`/trade` → `/pool` within ~50 ms) — **0** detached chart canvases; no `price-chart-lightweight-canvas` root left in DOM - [x] **Indicators before ready** — opened overlay + toggled MA7 & RSI during init; chart loaded with expanded pane set (15 canvases vs 11 baseline), no console errors ([#150](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/150)) ### Follow-up checklist (for spot re-check) - [ ] `cd frontend-dapp && npm run test:run && npm run test:charts` - [ ] `/trade` in a narrow viewport — plot fills after load - [ ] Quick pair switch / route away during first paint — no console errors - [ ] Toggle MA7/RSI from overlay before chart ready — state matches after load All issue-body verification criteria and comment checklist items pass. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#225
No description provided.