Skip to content

Commit 7ee5c56

Browse files
committed
test(lint): pin the inline grid column read per carrier, with the child-object control
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 405e80f commit 7ee5c56

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

‎packages/lint/src/validate-field-consumers.test.ts‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,83 @@ describe('[#19289] validateFieldConsumers — a `user` field displays a field on
589589
expect(run).toThrow(/`reference` is an object/);
590590
});
591591
});
592+
593+
/**
594+
* [#20929] An inline grid column's `name` names a field of the CHILD object.
595+
*
596+
* `name` is a `LITERAL_KEYS` literal, so the general walk never read a column's
597+
* `name`, and the recommended identity-only column (`{ name: 'qty' }`) says
598+
* nothing else. `os validate` then warned that a field the grid draws was
599+
* inert. The fix reads `name` at that one position, against the child object
600+
* each carrier resolves. It does not drop `name` from the literals.
601+
*
602+
* One fixture carries every assertion. `inv` is the parent and `line` the
603+
* child, related by `line.invoice`. On `line`, `qty` is named only by the grid
604+
* column under test, and `memo` is named nowhere: `memo` is the control, still
605+
* reported. The parent declares a `qty` of its own that nothing reads, so it is
606+
* reported too. A column credited to the wrong object shows up as `inv.qty`
607+
* going quiet while `line.qty` stays reported.
608+
*/
609+
describe('[#20929] validateFieldConsumers — an inline grid column names a field of the CHILD object', () => {
610+
const data = { provider: 'object', object: 'inv' };
611+
const columns = [{ name: 'qty' }];
612+
613+
const stack = (relationship: AnyRec, view: AnyRec = {}, extra: AnyRec = {}): AnyRec => ({
614+
objects: [
615+
{ name: 'inv', fields: { name: { type: 'text' }, qty: { type: 'number' } } },
616+
{
617+
name: 'line',
618+
fields: {
619+
name: { type: 'text' },
620+
invoice: { type: 'master_detail', reference: 'inv', ...relationship },
621+
qty: { type: 'number' },
622+
memo: { type: 'text' },
623+
},
624+
},
625+
],
626+
views: [{ list: { type: 'grid', data, columns: [{ field: 'name' }] }, ...view }],
627+
...extra,
628+
});
629+
630+
/** `object.field` → verdict, for every field the rule reports. */
631+
const verdicts = (s: AnyRec): Record<string, string> =>
632+
Object.fromEntries(validateFieldConsumers(s).map((f) => [`${f.object}.${f.field}`, f.verdict]));
633+
634+
/** The child's `qty` credited; the parent's `qty` and the control still reported. */
635+
const CREDITED = { 'inv.qty': 'inert', 'line.memo': 'inert' };
636+
637+
it('baseline: with no grid anywhere, all three fields are reported inert', () => {
638+
expect(verdicts(stack({}))).toEqual({ 'inv.qty': 'inert', 'line.qty': 'inert', 'line.memo': 'inert' });
639+
});
640+
641+
it("a relationship field's `inlineColumns`: the child is the object that DECLARES the field, not the related one", () => {
642+
expect(verdicts(stack({ inlineEdit: 'grid', inlineColumns: columns }))).toEqual(CREDITED);
643+
});
644+
645+
it("a form view's `subforms[].columns`: the child is the entry's `childObject`, not the view's object", () => {
646+
const form = { type: 'simple', data, subforms: [{ childObject: 'line', columns }] };
647+
expect(verdicts(stack({}, { form }))).toEqual(CREDITED);
648+
});
649+
650+
it("each `formViews` entry's `subforms[].columns`, the same way", () => {
651+
const edit = { type: 'simple', data, subforms: [{ childObject: 'line', columns }] };
652+
expect(verdicts(stack({}, { formViews: { edit } }))).toEqual(CREDITED);
653+
});
654+
655+
it('`inlineColumns` on a field that does not set `inlineEdit` draws no grid: a carrier, listed for removal', () => {
656+
const findings = validateFieldConsumers(stack({ inlineColumns: columns }));
657+
expect(Object.fromEntries(findings.map((f) => [`${f.object}.${f.field}`, f.verdict]))).toEqual({
658+
'inv.qty': 'inert',
659+
'line.qty': 'carrier-only',
660+
'line.memo': 'inert',
661+
});
662+
expect(findings.find((f) => f.object === 'line' && f.field === 'qty')?.carriers).toEqual([
663+
'objects[1].fields.invoice.inlineColumns[0].name',
664+
]);
665+
});
666+
667+
it('`name` anywhere else stays a literal: a dataset measure named like the field credits nothing', () => {
668+
const datasets = [{ name: 'line_stats', object: 'line', measures: [{ name: 'qty', aggregate: 'count' }] }];
669+
expect(verdicts(stack({}, {}, { datasets }))).toEqual({ 'inv.qty': 'inert', 'line.qty': 'inert', 'line.memo': 'inert' });
670+
});
671+
});

0 commit comments

Comments
 (0)