Skip to content

차트를 테스트한다

브라우저도, 캔버스도, DOM도 없이 차트를 단언할 수 있다 — createPlotModel 하나로.

다섯 줄

ts
import { createPlotModel, lineSeries } from "@finchart/core";

const model = createPlotModel({
  size: { width: 800, height: 600 },
  series: { series: lineSeries(), data },
  config: { showGrid: false },
});

const line = model.commands().find((c) => c.type === "drawLine");
expect(line?.points).toHaveLength(data.length);

model.commands()는 마지막 프레임이 낸 그리기 명령 목록이다 — drawLine·drawText·drawShape처럼 순수 데이터로 된 값이라 직렬화하고 비교하기에 안전하다. 캔버스를 실제로 그린 적이 없어도 이 목록은 실물과 같다 — 코어가 명령을 만드는 경로 자체가 headless와 브라우저에서 갈라지지 않기 때문이다.

시리즈별 명령 모양

"캔들이 그려졌다"를 단언하려면 캔들이 무엇으로 나오는지 알아야 한다 — "drawCandle" 같은 타입은 없다. 시리즈는 전부 아래 원시 명령의 조합이다 (출처: packages/core/src/series/__tests__/series.test.ts가 실제로 이 모양을 단언한다):

시리즈명령 조합
lineSeriesdrawLine(구멍마다 분절) + 점 표시는 drawShape circle
candleSeries봉마다 drawLine(심지) + drawShape rect(몸통)
barSeries봉마다 drawLine 셋(기둥·시가 틱·종가 틱)
areaSeriesdrawShape(채움 폴리곤) + drawLine(윗선)
baselineSeries기준선 위/아래로 갈린 drawShape 채움 + drawLine
histogramSeries막대마다 drawShape rect

캔들 몸통 개수를 세려면 type === "drawShape" && shape.shape === "rect"를 세면 된다(도형 명령의 payload 필드가 shape다) — 거래량 히스토그램이 같은 pane에 있으면 그것도 rect이므로, pane을 나누거나 색으로 갈라 센다.

DOM이 진짜 없다

ts
const model = createPlotModel({ size: { width: 800, height: 600 } });

expect(model.plot.overlay).toBeNull();

컨테이너도, HTMLElement도 넘기지 않는다. model.plot은 상태·데이터· 장식·플러그인 API를 전부 그대로 가진 **진짜 Plot**이다 — headless 전용으로 축소된 어떤 것이 아니다. 그래서 model.plot.pan(10)을 부르면 다음 프레임의 명령이 실제로 움직인다.

눈금 라벨도 명령이다

서버에서 PNG를 찍어야 하는 경우를 생각하면, 눈금 라벨이 빠진 채로 나오는 게 기본이면 곤란하다. 그래서 라벨도 캔버스 명령(drawText)으로 나온다 — 폰트 렌더러나 DOM 라벨에 기대지 않는다.

ts
const texts = model
  .commands()
  .filter((c) => c.type === "drawText")
  .map((c) => c.params.text);

expect(texts.length).toBeGreaterThan(0);

협력자를 주입해 결정론을 얻는다

측정자(createTextMeasurer)를 안 넣으면 축 폭은 고정 폴백이다 — 브라우저와 픽셀 단위로 똑같은 그림이길 원하는 테스트라면 측정자를 직접 주입한다.

ts
const model = createPlotModel({
  size,
  series: { series: lineSeries(), data },
  config: { axis: { y: { format: () => "####" } } },
  deps: {
    createTextMeasurer: () => ({
      measure: (text) => ({ width: text.length * 10, height: 10 }),
    }),
  },
});

이렇게 넣으면 headless에서도 축 폭이 라벨 길이를 따라간다 — 같은 배선 규칙(어떤 협력자를 넣을지)이 브라우저 쪽 browserDeps()에도 그대로 적용된다.

왜 이게 되는가

Plot의 DOM 의존은 전부 협력자 뒤에 있다 — 컨테이너·오버레이·캔버스 컨텍스트가 없으면, DOM을 실제로 요구하는 구현(캔버스 렌더러·DOM 라벨· 구분자)이 없는 자리에서 만들 때 던진다. 조용히 아무 일도 안 하는 게 아니라 배선 오류로 바로 드러난다. createPlotModel은 이 경계를 공식 진입점으로 만든 것뿐이다 — 별도의 "테스트용 클래스"가 아니라 메모리 레이어·recording 렌더러로 배선된 같은 Plot이다. 자세한 설계 근거는 ADR-0024에 있다.