Files
Jocelyn LinandClaude Opus 4.7 7591ddd594 feat(observe): ExecutionDetail with tree view, headers, and resizable drawer (#6325)
* feat(observe): ExecutionDetail with tree view, headers, and resizable drawer

## Summary

Ports the legacy `ExecutionDetails.jsx` (~984 lines) into the `@flowiseai/observe` SDK as a composable `ExecutionDetail` orchestrator.

- **`ExecutionDetail`**: split-pane (sidebar tree + node detail) with sidebar header that holds the agentflow chip, Copy-ID chip, formatted updated date, and refresh button. Auto-selects the first STOPPED node (or first top-level otherwise) on initial tree build.
- **`ExecutionTreeSidebar`**: `@mui/x-tree-view` `RichTreeView` with a custom `TreeItem2` slot — per-branch border colored from `AGENTFLOW_ICONS`, status-icon variants on each row, virtual `Iteration #N` containers, controlled expand/collapse defaulting to all-expanded.
- **`useExecutionTree`** rewritten to mirror the legacy three-pass `buildTreeData`: `previousNodeIds` parent→child with most-recent disambiguation, iteration grouping under the latest parent instance, sub-tree linking inside virtual containers, iterations-first sibling sort, and recursive `FLOWISE_CREDENTIAL_ID` scrub. Iteration container status is the legacy `ERROR > INPROGRESS > all-FINISHED > UNKNOWN` rollup.
- **`useDrawerWidths`** + `useResizableSidebar({ inverted })` extension drive a resizable `ExecutionsViewer` Drawer (parity with the legacy left-edge drag handle). Adds a public `ExecutionsViewerProps.drawer?: { defaultWidth?, minWidth?, maxWidth? }` override.
- **`AGENTFLOW_ICONS`** lifted from `atoms/NodeIcon.tsx` to `core/primitives/agentflowIcons.ts` so the tree slot can consume it without re-importing through `atoms`.
- **`ExecutionDetailProps.agentflow?: AgentflowRef`** seeds the header chip while the detail endpoint loads (server-side `getExecutionById` doesn't perform the agentflow join — only `getAllExecutions` does).
- Adds `@mui/x-tree-view` as a runtime dep.

## Test plan

- [x] `pnpm -C packages/observe test:coverage` — 331 tests pass, all `coverageThreshold` entries met (85.37% statements / 82.98% branches overall; `useExecutionTree` 96.5% / 85.18%, new `useDrawerWidths` 100% / 90.9%, `ExecutionDetail` 89.18% / 92.5%).
- [x] Manually verify in `packages/observe/examples` (`pnpm dev` after copying `.env.example` to `.env`):
  - List → row click opens the resizable Drawer; left-edge handle widens/narrows.
  - Tree shows status icons for each row plus rolled-up status on virtual iteration containers; `IconLoader` spins for INPROGRESS rows.
  - Agentflow chip is clickable and opens the canvas in a new tab when `VITE_AGENTFLOW_CANVAS_URL` is set; non-clickable otherwise.
  - Copy-ID chip flips inline to `Copied!` for 2s. **Behavior change from legacy**: legacy dispatched a Redux/notistack snackbar toast ("ID copied to clipboard") via the host app; the SDK can't assume a snackbar provider exists, so the confirmation is rendered on the chip itself.
  - Refresh button re-fetches; date formatted as `MMM d, yyyy h:mm a`.
  - Standalone `<ExecutionDetail>` demo by execution id renders the same surface without the list.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* address gemini review feedback

* fix flaky test and console warning in test

* fix drawer min/max edge case bug

* fix Copy ID chip falsely showing 'Copied!' on clipboard reject

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(observe): iteration tree parity with legacy

Iteration metadata (parentNodeId/iterationIndex) is nested under `data`
in the runtime payload per IAgentflowExecutedData; the SDK was reading
from the top level so virtual Iteration #N containers never materialized
and iteration children dangled at root. Same shape fix applies to
humanInputAgentflow detection in useNodeData. Iteration parent input
now shows "No data" matching legacy's `data.input.question || '*No
data*'` fallback, and virtual Iteration #N containers are selectable
with empty Input/Output panels.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-04 10:57:33 -07:00

6.8 KiB

@flowiseai/observe — Testing Guide

Running Tests

pnpm test              # Fast run (no coverage)
pnpm test:coverage     # With coverage enforcement
pnpm test:watch        # Watch mode during development

Test Strategy

Tests are prioritized by impact. When modifying a file, add or update tests in the same PR.

Tier 1 — Core Logic (must test)

Pure business logic in infrastructure/ and critical hooks. These carry the highest risk — a bug here affects every consumer of the SDK. Always test in the same PR when modifying.

What belongs here: API client methods, context/store, data-transformation hooks (useExecutionTree).

Tier 2 — Feature Hooks (test when changing)

Feature-level hooks that orchestrate polling and UI state. Test when adding features or fixing bugs.

What belongs here: useExecutionPoll, any future pagination or filter hooks.

Tier 3 — UI Components (test if logic exists)

Presentational components that are mostly JSX. Only add tests if the component contains meaningful business logic. Pure layout/display components do not need tests.

Currently classified as Tier 3 (no coverageThreshold entry):

  • ExecutionsListTable.tsx, ExecutionsViewer.tsx — data-table + outer composition shells.

ExecutionDetail.tsx was promoted to Tier 2 once it grew real branching (initial-selection rule, copy-id flow, error/loading/empty states). Drag-resize lives in useResizableSidebar and the recursive tree renderer in <ExecutionTreeSidebar>, both tested in isolation; the orchestrator's own tests cover composition + the auto-select branches.

If any of these grows real branching logic (filter predicates, sort comparators, debounced handlers), promote it to Tier 2 by adding a coverageThreshold entry in jest.config.js and writing the corresponding tests.

Writing Tests

File Extension Convention

The Jest config uses file extensions to select the test environment:

Extension Environment When to use
.test.ts node (no DOM) Pure logic — utilities, API clients, data transformations
.test.tsx jsdom (browser DOM) Anything that renders React — renderHook with providers, component tests

Source files use .tsx only when they contain JSX syntax. A hook that has no JSX stays .ts even if its test is .test.tsx (because the test uses renderHook with a JSX wrapper).

Mocking Patterns

Mocking axios:

import axios from 'axios'

jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios>

const mockGet = jest.fn()
mockedAxios.create.mockReturnValue({ get: mockGet, post: jest.fn(), delete: jest.fn(), put: jest.fn() } as any)

Mocking context hooks:

jest.mock('@/infrastructure/store', () => ({
    useObserveApi: () => mockApiClient,
    useObserveConfig: () => ({ pollInterval: 3000 })
}))

Module Mocks

Axios (src/__mocks__/axios.ts): Prevents network errors by mocking all HTTP methods. Returns empty arrays/objects by default.

CSS/SVG (src/__mocks__/styleMock.js): Empty object export for CSS and SVG imports.

Canvas (src/__mocks__/canvas.js): Empty object export — also intercepted at the Node.js level by the custom jsdom environment (see below).

Custom Jest Environment

src/__test_utils__/jest-environment-jsdom.js intercepts require('canvas') and returns a mock before jsdom tries to load the native binary. This prevents build failures in environments without native canvas compilation.

Best Practices

The same content lives in the dev-implement-jira skill so it's discoverable cross-package. Keep both copies in sync when editing.

Test behavior, not implementation

Assert on what a consumer of the unit observes — return values, rendered output, calls made to dependencies — not on internal state or which private helper ran. A refactor that preserves behavior should not break tests.

Use Arrange-Act-Assert structure

Group each test into three blocks: set up inputs and mocks, invoke the unit under test, then assert. Keeping the steps visually distinct makes failures easier to diagnose.

One concept per test

Each it(...) should verify a single behavior. Multiple low-level expect calls are fine if they describe one concept (e.g. one rendered tree shape), but unrelated assertions belong in separate tests so a failure points to a single cause.

Write descriptive test names

Name tests by scenario and expected outcome: it('returns empty tree when execution has no nodes'), not it('works'). Prefer describe blocks per function or component, with it names that read as full sentences.

Keep tests independent and deterministic

Reset state between tests — call jest.clearAllMocks() in beforeEach and avoid module-level mutable variables. No test should depend on execution order. Avoid real timers, real network, or Date.now() without jest.useFakeTimers().

Mock at boundaries, not internals

Mock external dependencies (axios, the API client, browser globals) and let internal modules run as written. Over-mocking internal collaborators couples tests to structure and hides integration bugs.

Cover meaningful edge cases

For data transforms and hooks, add tests for empty input, single-element input, error/loading states, and any branch in the code. Coverage percentage is a floor — a 100%-covered function with only happy-path tests still has gaps.

Prefer userEvent over fireEvent

When testing components, @testing-library/user-event simulates real interaction sequences (focus, keystrokes, click order). Use fireEvent only for events userEvent cannot synthesize.

Query by accessible role and text

getByRole, getByLabelText, and getByText produce tests that double as accessibility checks. Reach for getByTestId only when no semantic query works.

Keep test data minimal

Build fixtures with the smallest fields needed to exercise the behavior. Use factory helpers (or inline object literals) and override only the fields the test cares about — extraneous data hides what the test is actually asserting.

Configuration

  • Jest config: jest.config.js — two projects: unit (node env, .test.ts) and components (custom jsdom env, .test.tsx)
  • Import aliases: @test-utils maps to src/__test_utils__, @/ maps to src/
  • Coverage thresholds: 80% floor for branches, functions, lines, statements — see coverageThreshold in jest.config.js for per-path entries
  • Coverage exclusions: src/__test_utils__/**, src/__mocks__/**, src/**/index.ts
  • Reports: coverage/lcov-report/index.html for detailed HTML report