Skip to content
56 changes: 52 additions & 4 deletions apps/azure/src/components/editor/FrameView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
/**
* FrameView (Azure) — FRAME workspace (ADR-070).
*
* Azure-app equivalent of the PWA FrameView. Renders `LayeredProcessView`
* Azure-app equivalent of the PWA FrameView. Renders `LayeredProcessViewWithCapability`
* wired to `projectStore.processContext.processMap` with live gap detection from
* `@variscout/core/frame`.
*
* V1 is deterministic-only: no CoScout, no templates. Pre-data hunches
* persist as draft SuspectedCause hubs through the projectStore +
* investigationStore; the full integration lands in follow-up.
*
* Plan C2: ProductionLineGlanceDashboard is wired into the Operations band
* via a synthetic preview rollup (empty rows — authoring surface has no
* investigation data). Live-data wiring lands in C3 (right-hand drawer).
*/
import React from 'react';
import { LayeredProcessView } from '@variscout/ui';
import { LayeredProcessViewWithCapability } from '@variscout/ui';
import {
useProductionLineGlanceData,
useProductionLineGlanceFilter,
useProductionLineGlanceOpsToggle,
} from '@variscout/hooks';
import { useProjectStore } from '@variscout/stores';
import type { ProcessContext } from '@variscout/core';
import type { ProcessContext, ProcessHub, ProcessHubInvestigation } from '@variscout/core';
import type { DataRow } from '@variscout/core';
import { createEmptyMap, detectGaps, type ProcessMap } from '@variscout/core/frame';

const FrameView: React.FC = () => {
Expand Down Expand Up @@ -50,6 +60,35 @@ const FrameView: React.FC = () => {
setSpecs({ ...(specs ?? {}), ...next });
};

// Plan C2: URL-backed filter + ops-mode state.
const filter = useProductionLineGlanceFilter();
const ops = useProductionLineGlanceOpsToggle();

// Synthetic preview rollup — FrameView is a canonical-map authoring surface;
// investigation rows are not loaded here. The dashboard renders empty-state
// gracefully. Live data wiring lands in C3 (right-hand drawer).
const previewRollup = React.useMemo(() => {
const previewHub: ProcessHub = {
id: 'frame-preview',
name: 'Frame preview',
canonicalProcessMap: map,
canonicalMapVersion: 'preview',
contextColumns: [],
} as unknown as ProcessHub;
return {
hub: previewHub,
members: [] as ProcessHubInvestigation[],
rowsByInvestigation: new Map<string, ReadonlyArray<DataRow>>(),
};
}, [map]);

const data = useProductionLineGlanceData({
hub: previewRollup.hub,
members: previewRollup.members,
rowsByInvestigation: previewRollup.rowsByInvestigation,
contextFilter: filter.value,
});

return (
<div className="flex-1 overflow-auto" data-testid="frame-view">
<div className="mx-auto max-w-6xl">
Expand All @@ -61,7 +100,7 @@ const FrameView: React.FC = () => {
least one rational-subgroup axis.
</p>
</header>
<LayeredProcessView
<LayeredProcessViewWithCapability
map={map}
availableColumns={availableColumns}
onChange={handleChange}
Expand All @@ -70,6 +109,15 @@ const FrameView: React.FC = () => {
lsl={specs?.lsl}
usl={specs?.usl}
onSpecsChange={handleSpecsChange}
data={data}
filter={{
availableContext: data.availableContext,
contextValueOptions: data.contextValueOptions,
value: filter.value,
onChange: filter.onChange,
}}
mode={ops.mode}
onModeChange={ops.setMode}
/>
</div>
</div>
Expand Down
51 changes: 47 additions & 4 deletions apps/azure/src/components/editor/__tests__/FrameView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';

// vi.mock MUST come before component imports (per writing-tests skill / testing.md rule)

// vi.mock MUST come before component imports (per writing-tests skill)
vi.mock('@variscout/stores', () => {
const setProcessContext = vi.fn();
const setSpecs = vi.fn();
Expand All @@ -19,15 +20,57 @@ vi.mock('@variscout/stores', () => {
};
});

vi.mock('@variscout/hooks', async () => {
const actual = await import('@variscout/hooks');
return {
...actual,
useProductionLineGlanceFilter: vi.fn(() => ({
value: {},
onChange: vi.fn(),
})),
useProductionLineGlanceOpsToggle: vi.fn(() => ({
mode: 'spatial' as const,
setMode: vi.fn(),
toggle: vi.fn(),
})),
useProductionLineGlanceData: vi.fn(() => ({
cpkTrend: { data: [], stats: null, specs: {} },
cpkGapTrend: { series: [], stats: null },
capabilityNodes: [],
errorSteps: [],
availableContext: { hubColumns: [], tributaryGroups: [] },
contextValueOptions: {},
})),
};
});

vi.mock('@variscout/charts', async importOriginal => {
const actual = await importOriginal<typeof import('@variscout/charts')>();
const React = await import('react');
return {
...actual,
IChart: () => React.createElement('div', { 'data-testid': 'mock-cpk-trend' }),
CapabilityGapTrendChart: () => React.createElement('div', { 'data-testid': 'mock-gap-trend' }),
CapabilityBoxplot: () =>
React.createElement('div', { 'data-testid': 'mock-capability-boxplot' }),
StepErrorPareto: () => React.createElement('div', { 'data-testid': 'mock-step-pareto' }),
};
});

import FrameView from '../FrameView';

describe('FrameView (Azure)', () => {
it('renders LayeredProcessView with three bands', () => {
describe('FrameView (Plan C2 wiring)', () => {
beforeEach(() => {
window.history.replaceState(null, '', '/test');
});

it('renders LayeredProcessViewWithCapability composition (three bands + ops dashboard)', () => {
render(<FrameView />);

expect(screen.getByTestId('layered-process-view')).toBeInTheDocument();
expect(screen.getByTestId('band-outcome')).toBeInTheDocument();
expect(screen.getByTestId('band-process-flow')).toBeInTheDocument();
expect(screen.getByTestId('band-operations')).toBeInTheDocument();
expect(screen.getByTestId('ops-band-dashboard')).toBeInTheDocument();
});
});
56 changes: 52 additions & 4 deletions apps/pwa/src/components/views/FrameView.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
/**
* FrameView — PWA FRAME workspace (ADR-070).
*
* Renders the `LayeredProcessView` component wired to `projectStore.processContext.processMap`,
* Renders `LayeredProcessViewWithCapability` wired to `projectStore.processContext.processMap`,
* with live gap detection from `@variscout/core/frame`. The user builds the process map
* (SIPOC spine + tributaries + ocean/CTS + hunches). V2+ will add CoScout drafting,
* template libraries, and data-seeded skeletons — V1 is deterministic-only.
*
* Plan C2: ProductionLineGlanceDashboard is wired into the Operations band
* via a synthetic preview rollup (empty rows — authoring surface has no
* investigation data). Live-data wiring lands in C3 (right-hand drawer).
*/
import React from 'react';
import { LayeredProcessView } from '@variscout/ui';
import { LayeredProcessViewWithCapability } from '@variscout/ui';
import {
useProductionLineGlanceData,
useProductionLineGlanceFilter,
useProductionLineGlanceOpsToggle,
} from '@variscout/hooks';
import { useProjectStore } from '@variscout/stores';
import type { ProcessContext } from '@variscout/core';
import type { ProcessContext, ProcessHub, ProcessHubInvestigation } from '@variscout/core';
import type { DataRow } from '@variscout/core';
import { createEmptyMap, detectGaps, type ProcessMap } from '@variscout/core/frame';

const FrameView: React.FC = () => {
Expand Down Expand Up @@ -47,6 +57,35 @@ const FrameView: React.FC = () => {
setSpecs({ ...(specs ?? {}), ...next });
};

// Plan C2: URL-backed filter + ops-mode state.
const filter = useProductionLineGlanceFilter();
const ops = useProductionLineGlanceOpsToggle();

// Synthetic preview rollup — FrameView is a canonical-map authoring surface;
// investigation rows are not loaded here. The dashboard renders empty-state
// gracefully. Live data wiring lands in C3 (right-hand drawer).
const previewRollup = React.useMemo(() => {
const previewHub: ProcessHub = {
id: 'frame-preview',
name: 'Frame preview',
canonicalProcessMap: map,
canonicalMapVersion: 'preview',
contextColumns: [],
} as unknown as ProcessHub;
return {
hub: previewHub,
members: [] as ProcessHubInvestigation[],
rowsByInvestigation: new Map<string, ReadonlyArray<DataRow>>(),
};
}, [map]);

const data = useProductionLineGlanceData({
hub: previewRollup.hub,
members: previewRollup.members,
rowsByInvestigation: previewRollup.rowsByInvestigation,
contextFilter: filter.value,
});

return (
<div className="flex-1 overflow-auto" data-testid="frame-view">
<div className="mx-auto max-w-6xl">
Expand All @@ -58,7 +97,7 @@ const FrameView: React.FC = () => {
least one rational-subgroup axis.
</p>
</header>
<LayeredProcessView
<LayeredProcessViewWithCapability
map={map}
availableColumns={availableColumns}
onChange={handleChange}
Expand All @@ -67,6 +106,15 @@ const FrameView: React.FC = () => {
lsl={specs?.lsl}
usl={specs?.usl}
onSpecsChange={handleSpecsChange}
data={data}
filter={{
availableContext: data.availableContext,
contextValueOptions: data.contextValueOptions,
value: filter.value,
onChange: filter.onChange,
}}
mode={ops.mode}
onModeChange={ops.setMode}
/>
</div>
</div>
Expand Down
49 changes: 46 additions & 3 deletions apps/pwa/src/components/views/__tests__/FrameView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';

// vi.mock MUST come before component imports (per writing-tests skill / testing.md rule)

// vi.mock MUST come before component imports (per writing-tests skill)
vi.mock('@variscout/stores', () => {
const setProcessContext = vi.fn();
const setSpecs = vi.fn();
Expand All @@ -19,15 +20,57 @@ vi.mock('@variscout/stores', () => {
};
});

vi.mock('@variscout/hooks', async () => {
const actual = await import('@variscout/hooks');
return {
...actual,
useProductionLineGlanceFilter: vi.fn(() => ({
value: {},
onChange: vi.fn(),
})),
useProductionLineGlanceOpsToggle: vi.fn(() => ({
mode: 'spatial' as const,
setMode: vi.fn(),
toggle: vi.fn(),
})),
useProductionLineGlanceData: vi.fn(() => ({
cpkTrend: { data: [], stats: null, specs: {} },
cpkGapTrend: { series: [], stats: null },
capabilityNodes: [],
errorSteps: [],
availableContext: { hubColumns: [], tributaryGroups: [] },
contextValueOptions: {},
})),
};
});

vi.mock('@variscout/charts', async importOriginal => {
const actual = await importOriginal<typeof import('@variscout/charts')>();
const React = await import('react');
return {
...actual,
IChart: () => React.createElement('div', { 'data-testid': 'mock-cpk-trend' }),
CapabilityGapTrendChart: () => React.createElement('div', { 'data-testid': 'mock-gap-trend' }),
CapabilityBoxplot: () =>
React.createElement('div', { 'data-testid': 'mock-capability-boxplot' }),
StepErrorPareto: () => React.createElement('div', { 'data-testid': 'mock-step-pareto' }),
};
});

import FrameView from '../FrameView';

describe('FrameView (PWA)', () => {
it('renders LayeredProcessView with three bands', () => {
beforeEach(() => {
window.history.replaceState(null, '', '/test');
});

it('renders LayeredProcessViewWithCapability composition (three bands + ops dashboard)', () => {
render(<FrameView />);

expect(screen.getByTestId('layered-process-view')).toBeInTheDocument();
expect(screen.getByTestId('band-outcome')).toBeInTheDocument();
expect(screen.getByTestId('band-process-flow')).toBeInTheDocument();
expect(screen.getByTestId('band-operations')).toBeInTheDocument();
expect(screen.getByTestId('ops-band-dashboard')).toBeInTheDocument();
});
});
Loading