Skip to content

Commit 04ee897

Browse files
os-warrenclaude
andcommitted
test(console): pin the four object-metric query members (objectui#8071 slice 8)
Slice 8 of objectui#8071 takes the first PARTIAL bite of a multi-key block. The batch is a rule, not a pick: every `object-metric` member that shapes the aggregate query behind the NUMBER the tile paints — the four that reach `ObjectMetricWidget`'s `fetchMetric`/`computeOne` (`dataSource` gates the effect; `aggregateKey`, `resolvedFilterKey`, `compareToKey` are its other dependencies). `trend` and `drillDown` reach neither: they shape what is drawn around the number once it exists, and stay exempt for the next slice. MEMBER_PIN_EXEMPTIONS 37 -> 33, MEMBER_PIN_EXEMPTION_CEILING 37 -> 33 and MEMBER_PINS 53 -> 57, in this one commit. NEWLY_JUDGED_UNPINNED_MEMBERS is unchanged — no block it names was touched. Two pins promote pre-existing files, each read end to end and each given the one row it was missing: `ObjectMetric.elementDataSource.test.tsx` gains the negative direction (the view's `columns`/`sort`/`pagination` asserted OUT of the aggregate call, so the pin cannot be satisfied by a mapping that forwards everything), and `ObjectMetricWidget.compareTo.test.tsx` gains the row that mounts `type: 'object-metric'` through SchemaRenderer, so the claim that the block's gate forwards the key untouched is asserted rather than assumed. `aggregate` and `filter` share one new file, written so the aggregate rows author no filter and stay independent of the filter spelling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
1 parent aeaa0f6 commit 04ee897

5 files changed

Lines changed: 474 additions & 6 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
---
3+
4+
Pin the four `object-metric` members that shape the aggregate query behind the
5+
number — `dataSource`, `aggregate`, `filter`, `compareTo` (objectui#8071, slice
6+
8). `MEMBER_PIN_EXEMPTIONS` drops 37 → 33 and `MEMBER_PIN_EXEMPTION_CEILING`
7+
follows in the same commit; `trend` and `drillDown`, which shape what is drawn
8+
around the number rather than the number, stay exempt.
9+
10+
Test only; no package is released by this change.

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,22 @@ const MEMBER_PINS: Record<string, MemberPin> = {
22652265
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',
22662266
pins: 'ONE nested position and no more: `schema.grouping?.fields?.[0]?.field` is the FALLBACK source of `swimlaneField`, and that is the entire member contract this board carries for the key. Three rows make it a reading rather than a claim — the swimlane layout appears keyed by `fields[0].field` where without the key there is none; an explicit `swimlaneField` WINS over it; and a second `fields` entry changes nothing, which is what pins the read at `[0]` rather than at "the fields list". The declared description says the rest is inert precisely so the declaration does not recommend a write the renderer cannot honour — this file is what keeps that sentence true. The spec row is `z.unknown()`, so the read site is the whole member contract (objectui#8313).',
22672267
},
2268+
'object-metric.aggregate': {
2269+
file: 'packages/plugin-dashboard/src/__tests__/objectMetricQueryMembers-8071.test.tsx',
2270+
pins: 'A three-member options bag whose members are read TWICE — once into the adapter call, once back out of the response. `field`/`function`/`groupBy` become `ds.aggregate(object, { field, function, groupBy, filter })` with `groupBy` OPTIONAL and defaulting to `\'_all\'` (one bucket) and an authored value outranking it; the bag is asserted AS A WHOLE so a member added to or dropped from the call is red in either direction. The readback is the half that has no second chance: `function: \'count\'` sums `<field>_count` across EVERY returned row while any other function reads the FIRST row\'s `<field>_<function>`, and the two arms run on the SAME two-row response so only a readback reading both members can answer 120 on one and 7 on the other — a `field`/`function` that reached the query but not the readback paints 0 over a response that carried the right number. `<field>` unsuffixed is pinned as the chain\'s second limb. The key\'s ABSENCE is a member semantic in its own right and is the file\'s non-vacuity floor: with no `aggregate` the query VERB changes to `find()` and the value becomes the row count. Driven through the registered block, not the bare widget. New file (objectui#8071 slice 8).',
2271+
},
2272+
'object-metric.compareTo': {
2273+
file: 'packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.compareTo.test.tsx',
2274+
pins: 'The `{ kind, dimension? }` member set under objectstack#5011\'s converged shape: `kind` alone chooses BOTH the comparison window and the trend label, pinned on a QUARTER-scoped filter so the two admitted values disagree about each (`previousYear` = the same quarter one year back and "vs last year"; `previousPeriod` = the quarter before and "vs last quarter") — a year-scoped fixture would have let a `previousYear` that fell through to the other branch pass. `dimension` is CARRIED but inert on this inline path (it addresses a dataset time dimension the executor resolves), asserted by the query\'s filter keys being unchanged by it. An absent `compareTo` runs a single pass and paints no trend, which is the control that keeps the other rows from reading as a coincidence. Pre-existing file (objectui#3337), promoted here after being read end to end; objectui#8071 slice 8 added the row that mounts `type: \'object-metric\'` through `SchemaRenderer`, so the claim that the block\'s gate forwards this key untouched is asserted rather than assumed.',
2275+
},
2276+
'object-metric.dataSource': {
2277+
file: 'packages/plugin-dashboard/src/ObjectMetric.elementDataSource.test.tsx',
2278+
pins: 'The per-element binding\'s members as a WHITELIST in both directions. Acting: `object` is what gets aggregated, and a named `view`\'s own `filter` becomes the metric\'s scope — with an unresolvable `view` REPORTING instead of aggregating the whole object, which for a metric is the quiet failure (one number, no rows, nothing to notice). Not acting: the same view fixture declares `columns`, `sort` and `pagination`, and the aggregate options bag is asserted whole to keep all three OUT — `OBJECT_METRIC_DATA_SOURCE` names only `filter`, because a metric is one aggregated number with no projection, ordering or page for the rest to act on. A metric with NO binding behaving exactly as before is the control. Pre-existing file (objectstack#6953), promoted here after being read end to end; objectui#8071 slice 8 added the whole-bag row, without which the pin would have been satisfied by a mapping that forwarded everything.',
2279+
},
2280+
'object-metric.filter': {
2281+
file: 'packages/plugin-dashboard/src/__tests__/objectMetricQueryMembers-8071.test.tsx',
2282+
pins: 'No named member set — the renderer never inspects the predicate — so the member shape is the SPELLING it arrives under, and there are two, chosen by an adapter capability the author cannot see: FLAT under its own name inside the aggregate options bag, and WRAPPED as `$filter` on the no-`aggregate()` `find()` fallback. Collapsing them into one drops the predicate on whichever path lost and the tile counts every row — the same defect `element:number.filter` was pinned for (slice 7) on a different renderer. Two further halves: placeholders are resolved BEFORE the query (an authored `{current_quarter_start}` reaches the adapter as a real date, and a macro surviving onto the wire is a literal nobody matches), and the key is read BY VALUE rather than by identity (`JSON.stringify` memo) — a deep-equal rebuild by a re-rendering parent must NOT re-probe while a changed comparand MUST and carries the new predicate, each arm the other\'s control against a dependency "simplified" to the raw object. New file (objectui#8071 slice 8).',
2283+
},
22682284
'page:accordion.items': {
22692285
file: 'packages/components/src/__tests__/pageAccordionItemMembers-8071.test.tsx',
22702286
pins: 'Which member of one panel definition becomes which part of the rendered accordion — the four `PageAccordionItem` members (`label`, `icon`, `collapsed`, `children`) asserted as a SET through the real renderer, which nothing did before: `label` becomes the trigger\'s accessible name and `children` the panel BODY, asserted against each other so a renderer painting the wrong one cannot pass. `collapsed` is the reading with real semantics to get wrong and it is strictly `=== false`: `collapsed: false` OPENS a panel while `collapsed: true` AND an omitted `collapsed` both leave it shut — the omitted-key arm is the control an "obvious" edit to `!it.collapsed` breaks, and a no-opener fixture keeps the two shut rows from passing on a renderer that opens nothing. The single/multiple split is pinned on the SAME items so only `allowMultiple` varies: single mode takes `defaultOpen[0]` and drops later openers, multiple mode opens them all without opening panels that never asked. `icon` is covered narrowly on purpose — `page-accordion-icon.test.tsx` (objectui#4721) is its pin and is not re-litigated. New file (objectui#8071 slice 7).',
@@ -2508,12 +2524,11 @@ const MEMBER_PIN_EXEMPTIONS: Record<string, string> = {
25082524
'object-master-detail-form.initialValues': AWAITING_A_PIN,
25092525
'object-master-detail-form.sections': AWAITING_A_PIN,
25102526

2511-
// object-metric
2512-
'object-metric.aggregate': AWAITING_A_PIN,
2513-
'object-metric.compareTo': AWAITING_A_PIN,
2514-
'object-metric.dataSource': AWAITING_A_PIN,
2527+
// object-metric — objectui#8071 slice 8 pinned the four members that shape
2528+
// the aggregate query behind the NUMBER (`dataSource`, `aggregate`, `filter`,
2529+
// `compareTo`). The two left shape what is drawn AROUND the number once it
2530+
// exists, and neither reaches `fetchMetric`.
25152531
'object-metric.drillDown': AWAITING_A_PIN,
2516-
'object-metric.filter': AWAITING_A_PIN,
25172532
'object-metric.trend': AWAITING_A_PIN,
25182533

25192534
// page:accordion — objectui#8071 slice 7 pinned `items`, the block's one
@@ -2811,11 +2826,86 @@ const NEWLY_JUDGED_UNPINNED_MEMBERS = [
28112826
* behaviour in the new file, which authors `bodyShape` and asserts it is
28122827
* dropped.
28132828
*
2829+
* ## 37 -> 33, the eighth slice, and the FIRST partial bite of a multi-key block
2830+
*
2831+
* objectui#8071's eighth slice is the first that could not close a block by
2832+
* taking its last key — slice 7 exhausted that shape — so it takes a PART of
2833+
* one, and the part is chosen by a rule rather than by convenience:
2834+
*
2835+
* **every `object-metric` member that shapes the aggregate query behind the
2836+
* NUMBER the tile paints.**
2837+
*
2838+
* Four qualify — `dataSource`, `aggregate`, `filter` and `compareTo` — so the
2839+
* ceiling follows to 33 in the same commit. The rule is mechanical, not
2840+
* editorial: those four are exactly the members that reach
2841+
* `ObjectMetricWidget`'s `fetchMetric` / `computeOne` (`dataSource` gates the
2842+
* effect; `aggregateKey`, `resolvedFilterKey` and `compareToKey` are its other
2843+
* dependencies), and the two left — `trend` and `drillDown` — reach neither.
2844+
* They shape what is drawn AROUND the number once it exists: a STATIC badge
2845+
* beside it, and a drawer opened by clicking it. Neither can change the number.
2846+
*
2847+
* ⇒ That is what the next slice inherits, and it is cheap to state: the two
2848+
* PRESENTATION members of `object-metric`, `trend` and `drillDown`. They were
2849+
* left together on purpose rather than one being swept in — `drillDown` does
2850+
* issue a query of its own (the drawer's record list, off the same resolved
2851+
* filter), so "reaches an adapter" would have been the wrong cut and is stated
2852+
* here so the next reader does not have to re-derive why it was not used.
2853+
*
2854+
* TWO of the four pins PROMOTE pre-existing files, each read end to end before
2855+
* being credited and each given the one row it was missing:
2856+
*
2857+
* - `ObjectMetric.elementDataSource.test.tsx` (objectstack#6953) already drove
2858+
* the binding's `object` and `view` members through the real renderer, with
2859+
* an unresolvable `view` reporting rather than aggregating the whole object
2860+
* and a binding-less metric unchanged. What it did not have was the
2861+
* NEGATIVE direction, so this slice adds it: the view fixture's `columns`,
2862+
* `sort` and `pagination` are asserted absent from the aggregate call.
2863+
* Without that the pin would have been satisfied by a mapping that forwarded
2864+
* everything, which here would mean a metric silently ordered and paged by a
2865+
* list view's presentation settings.
2866+
* - `ObjectMetricWidget.compareTo.test.tsx` (objectui#3337) already asserted
2867+
* the `{ kind, dimension? }` member set — `kind` choosing both window and
2868+
* label on a quarter-scoped fixture where the two values disagree about
2869+
* each, `dimension` carried but inert, and the no-`compareTo` single-pass
2870+
* control. It drove `ObjectMetricWidget` directly, so this slice adds the
2871+
* row that mounts `type: 'object-metric'` through `SchemaRenderer`: the
2872+
* block's `ElementDataSourceGate` shell re-binds only `objectName` and
2873+
* `filter` and forwards the rest untouched, and that is now asserted rather
2874+
* than assumed. ⚠️ It is also why the file names the block at all — the
2875+
* locator requires it, and a docblock mention alone would have been the
2876+
* locator satisfied by prose rather than by behaviour.
2877+
*
2878+
* `aggregate` and `filter` share ONE new file,
2879+
* `objectMetricQueryMembers-8071.test.tsx` — the shape slice 3 used for
2880+
* `record-picker-label-placeholder-i18n.test.tsx` and slice 6 for the
2881+
* chatter/discussion pair — because the two keys meet inside a single adapter
2882+
* call and the assertions have to be written against each other to stay
2883+
* independent: the `aggregate` rows author NO filter (so the bag comparison is
2884+
* blind to the filter spelling), and the `filter` rows are the ones that pin it.
2885+
*
2886+
* Every other candidate was read end to end and rejected with a reason, not
2887+
* dismissed on its greps: `public-block-binding-reach.test.tsx` states its own
2888+
* narrowness — one question per block, "did any call carry the object name" —
2889+
* and lists `aggregate` only as a plausible sample value;
2890+
* `widget-dom-leak-sweep.test.tsx` is the DOM-attribute canary, where
2891+
* `aggregate` is a stub returning `[]`; `ObjectMetricWidget.i18nLabel.test.tsx`
2892+
* authors `aggregate`, `trend` and `drillDown` purely to make the drill-down
2893+
* reachable, and its subject is `I18nLabel` resolution — crediting it for
2894+
* `trend` or `drillDown` would have been slice 7's
2895+
* `action-bodyShape-forward.test.tsx` mistake in a new place. The remaining
2896+
* seven files that name `object-metric` do so in prose, in a membership list,
2897+
* or in a designer-inspector fixture.
2898+
*
2899+
* ⚠️ Unchanged by this slice: `NEWLY_JUDGED_UNPINNED_MEMBERS` (no block it names
2900+
* was touched) and `record:related_list.actions`, whose `NO_READ_SITE_TO_PIN`
2901+
* reading was not re-measured here because this slice took a different block —
2902+
* slice 7's measurement stands as the last one taken.
2903+
*
28142904
* ⇒ The rule for every future slice of objectui#8071: delete the entry, register
28152905
* the pin, and set this constant to the new count. Not to the new count plus
28162906
* room.
28172907
*/
2818-
const MEMBER_PIN_EXEMPTION_CEILING = 37;
2908+
const MEMBER_PIN_EXEMPTION_CEILING = 33;
28192909

28202910
/**
28212911
* Every test file a member pin can live in, as LAZY `?raw` loaders.

‎packages/plugin-dashboard/src/ObjectMetric.elementDataSource.test.tsx‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717
* `object` and `filter` are the only mapped keys: a metric is one aggregated
1818
* number, so there is no projection, no ordering and no page for the binding's
1919
* remaining keys to act on.
20+
*
21+
* ## Promoted to the member pin for `object-metric.dataSource` (objectui#8071)
22+
*
23+
* objectui#8071's eighth slice registers this file as the pin for that key. The
24+
* member set it asserts is a WHITELIST in both directions: `object` and `view`
25+
* are the members that act — the bound object is what gets aggregated and the
26+
* named view's own `filter` becomes the metric's scope — while the view's
27+
* `columns`, `sort` and `pagination` are carried in the fixture and asserted
28+
* ABSENT from the call, because `OBJECT_METRIC_DATA_SOURCE` names only
29+
* `filter`. Without that second direction the pin would be satisfied by a
30+
* mapping that forwarded everything, which for this block would mean a metric
31+
* silently ordered and paged by a list view's presentation settings.
2032
*/
2133

2234
import { describe, it, expect, vi } from 'vitest';
@@ -99,6 +111,32 @@ describe('object-metric — dataSource: { object, view } (objectstack#6953)', ()
99111
expect(adapter.aggregate).not.toHaveBeenCalled();
100112
});
101113

114+
it('maps `object` and the view’s `filter` and NOTHING else the view carries', async () => {
115+
// The whitelist direction. `HOT_VIEW` declares `columns`, `sort` and
116+
// `pagination`; none of the three is a member this block reads, so none may
117+
// appear in the aggregate call. Asserted on the WHOLE options bag rather
118+
// than key by key, so a mapping widened to forward the rest is red here.
119+
const adapter = makeAdapter();
120+
renderBlock(
121+
{
122+
type: 'object-metric',
123+
label: 'Pipeline',
124+
aggregate: AGGREGATE,
125+
dataSource: { object: 'account', view: 'hot' },
126+
},
127+
adapter,
128+
);
129+
130+
await waitFor(() => expect(adapter.aggregate).toHaveBeenCalled());
131+
const [, params] = adapter.aggregate.mock.calls[0] as [string, any];
132+
expect(params).toEqual({
133+
field: 'amount',
134+
function: 'sum',
135+
groupBy: '_all',
136+
filter: [['rating', '=', 'hot']],
137+
});
138+
});
139+
102140
it('leaves a metric with NO dataSource exactly as it was', async () => {
103141
const adapter = makeAdapter();
104142
renderBlock(

‎packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.compareTo.test.tsx‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,30 @@
1313
* before) and the label (`vs last year` / `vs last quarter`), so each case can
1414
* only pass by reading `.kind`. A year-scoped filter would have let a
1515
* `previousYear` that fell through to the previousPeriod branch pass anyway.
16+
*
17+
* ## Promoted to the member pin for `object-metric.compareTo` (objectui#8071)
18+
*
19+
* objectui#8071's eighth slice registers this file as the pin for that key: the
20+
* cases below already assert the member SET the renderer reads — `kind` selects
21+
* both the comparison window and the label, `dimension` is carried but inert on
22+
* this inline path, and an absent `compareTo` leaves the tile at one pass with
23+
* no trend — which is what a member-shape claim has to say.
24+
*
25+
* The four cases above drive `ObjectMetricWidget` directly. The registered
26+
* block `object-metric` is the same reader: its `ElementDataSourceGate` shell
27+
* re-binds only `objectName` and `filter` and forwards every other authored key
28+
* untouched, so `compareTo` arrives at this component exactly as authored. That
29+
* is asserted rather than assumed by the last case in this file, which mounts
30+
* `type: 'object-metric'` through `SchemaRenderer`.
1631
*/
1732

1833
import { describe, it, expect, vi, afterEach } from 'vitest';
1934
import { render, screen, cleanup, waitFor } from '@testing-library/react';
35+
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
2036
import { ObjectMetricWidget } from '../ObjectMetricWidget';
37+
// Registers `object-metric` at MODULE scope, never inside a hook
38+
// (object-ui/no-dynamic-import-in-test-hook).
39+
import '../index';
2140

2241
afterEach(cleanup);
2342

@@ -101,4 +120,30 @@ describe('ObjectMetricWidget — compareTo under { kind }', () => {
101120
await waitFor(() => expect(src.aggregate).toHaveBeenCalledTimes(1));
102121
expect(screen.queryByText(/vs last/i)).not.toBeInTheDocument();
103122
});
123+
124+
it('arrives unchanged through the registered `object-metric` block', async () => {
125+
// What makes the four cases above a pin on the BLOCK's key rather than on
126+
// one component's prop: authored on the schema, `compareTo` passes the
127+
// `ElementDataSourceGate` shell untouched and shifts the window exactly as
128+
// it does when handed straight to the widget.
129+
const src = makeSource();
130+
render(
131+
<SchemaRendererProvider dataSource={src as never}>
132+
<SchemaRenderer
133+
schema={{
134+
type: 'object-metric',
135+
objectName: 'deal',
136+
label: 'Revenue',
137+
aggregate: { field: 'amount', function: 'sum' },
138+
filter: { close_date: { $gte: '{current_quarter_start}', $lte: '{current_quarter_end}' } },
139+
compareTo: { kind: 'previousYear' },
140+
} as never}
141+
/>
142+
</SchemaRendererProvider>,
143+
);
144+
145+
await waitFor(() => expect(src.aggregate).toHaveBeenCalledTimes(2));
146+
expect(comparisonFilterOf(src).$gte).toBe(PREVIOUS_YEAR_FROM);
147+
expect(await screen.findByText(/vs last year/i)).toBeInTheDocument();
148+
});
104149
});

0 commit comments

Comments
 (0)