From b3543a88208c20f20f80aa5aa1dab4b9ca637064 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 26 Aug 2026 14:24:24 -0400 Subject: [PATCH 1/2] Stamp data-card-field on rendered field boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selector-based screenshot capture and region discovery need to address fields in templates that never opted into any capture markup. Every rendered field boundary now carries data-card-field=: - the card-as-field container and the compound-field wrapper in field-component.gts - the plural-field wrappers in contains-many-component.gts and links-to-many-component.gts (each plural item boundary repeats the plural field's name, since every item is a boundary of that field) The attribute is stamped unconditionally and is inert for CSS. A card rendered at the root has no field context, so it carries no data-card-field — which also keeps the exact-HTML snapshot assertions in realm-indexing-test byte-identical (they cover root renders only). data-card-format on the card root was considered and skipped: the root already carries data-boxel-card-format into rendered and indexed HTML, so a second format attribute would duplicate it without a consumer. Primitive leaf fields render bare (no wrapper element), so they are deliberately unstamped — adding a wrapper would not be CSS-inert. New integration test covers all four boundary kinds plus the root-carries-nothing case. Verified against the dev stack: card-basics 437/437, realm indexing 145/145. Includes prettier reflow of one pre-existing over-width line in each plural component. Co-Authored-By: Claude Fable 5 --- packages/base/contains-many-component.gts | 8 +- packages/base/field-component.gts | 11 +++ packages/base/links-to-many-component.gts | 8 +- .../components/card-basics-test.gts | 88 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/packages/base/contains-many-component.gts b/packages/base/contains-many-component.gts index 3e045e9ef1e..e8a660c62ea 100644 --- a/packages/base/contains-many-component.gts +++ b/packages/base/contains-many-component.gts @@ -144,7 +144,12 @@ class ContainsManyEditor extends GlimmerComponent { {{on 'click' this.add}} data-test-add-new > - + Add {{getPlural @field.card.displayName}} @@ -362,6 +367,7 @@ export function getContainsManyComponent({ class='plural-field containsMany-field {{effectiveFormat}}-format {{unless arrayField.children.length "empty"}}' + data-card-field={{field.name}} data-test-plural-view={{field.fieldType}} data-test-plural-view-field={{field.name}} data-test-plural-view-format={{effectiveFormat}} diff --git a/packages/base/field-component.gts b/packages/base/field-component.gts index 5533c7261b7..1c95bf6e0dc 100644 --- a/packages/base/field-component.gts +++ b/packages/base/field-component.gts @@ -358,6 +358,15 @@ export function getBoxComponent( }} {{#if (isCard model.value)}} {{#let model.value as |card|}} + {{! Every rendered field boundary carries + data-card-field= — here, on the + compound-field wrapper below, and on the plural-field + wrappers in contains-many-component and + links-to-many-component — so selector-based screenshot + capture and region discovery can address fields in + templates that never opted in. Stamped + unconditionally; inert for CSS. Omitted when no field + context exists (a card rendered as the root). }} @@ -380,6 +389,7 @@ export function getBoxComponent( }} data-boxel-card-id={{card.id}} data-boxel-card-format={{effectiveFormats.cardDef}} + data-card-field={{field.name}} data-test-card={{card.id}} data-test-card-format={{effectiveFormats.cardDef}} data-test-field-component-card @@ -420,6 +430,7 @@ export function getBoxComponent(
- + Add {{getPlural @field.card.displayName}} @@ -703,6 +708,7 @@ export function getLinksToManyComponent({ {{effectiveFormat}}-effectiveFormat {{unless arrayField.children.length "empty"}} display-container-{{displayContainer}}' + data-card-field={{field.name}} data-test-plural-view-field={{field.name}} data-test-plural-view={{field.fieldType}} data-test-plural-view-format={{effectiveFormat}} diff --git a/packages/host/tests/integration/components/card-basics-test.gts b/packages/host/tests/integration/components/card-basics-test.gts index ccf201b8bae..2c01842f233 100644 --- a/packages/host/tests/integration/components/card-basics-test.gts +++ b/packages/host/tests/integration/components/card-basics-test.gts @@ -982,6 +982,94 @@ module('Integration | card-basics', function (hooks) { assert.dom('[data-test="number"]').containsText('10'); }); + // The capture/discovery contract: every rendered field boundary — a card + // rendered as a field, a compound field's wrapper, and the plural-field + // wrappers — carries data-card-field=, so selector-based + // screenshot capture and region discovery can address fields in templates + // that never opted in. The card root itself carries no field context, so + // it must not carry the attribute. + test('rendered field boundaries carry data-card-field', async function (assert) { + class Guest extends FieldDef { + @field name = contains(StringField); + static embedded = class Embedded extends Component { + + }; + } + + class Pet extends CardDef { + @field name = contains(StringField); + static embedded = class Embedded extends Component { + + }; + } + + class Person extends CardDef { + @field guest = contains(Guest); + @field nicknames = containsMany(StringField); + @field pet = linksTo(Pet); + @field pets = linksToMany(Pet); + static isolated = class Isolated extends Component { + + }; + } + + loader.shimModule(`${testRealmURL}test-cards`, { Person, Pet }); + + let mango = new Pet({ name: 'Mango' }); + let vanGogh = new Pet({ name: 'Van Gogh' }); + let person = new Person({ + guest: new Guest({ name: 'Hassan' }), + nicknames: ['Art', 'Arty'], + pet: mango, + pets: [mango, vanGogh], + }); + await saveCard(mango, `${testRealmURL}Pet/mango`, loader); + await saveCard(vanGogh, `${testRealmURL}Pet/van-gogh`, loader); + await saveCard(person, `${testRealmURL}Person/arthur`, loader); + + await renderCard(loader, person, 'isolated'); + + assert + .dom('[data-test-compound-field-component][data-card-field="guest"]') + .exists('a compound field wrapper carries its field name'); + assert + .dom( + '[data-test-plural-view-field="nicknames"][data-card-field="nicknames"]', + ) + .exists('a containsMany plural wrapper carries its field name'); + assert + .dom( + `[data-card-field="pet"][data-test-card="${testRealmURL}Pet/mango"]`, + ) + .exists('a linksTo card boundary carries its field name'); + assert + .dom('[data-test-plural-view-field="pets"][data-card-field="pets"]') + .exists('a linksToMany plural wrapper carries its field name'); + assert + .dom( + '[data-card-field="pets"] [data-card-field="pets"][data-test-field-component-card]', + ) + .exists( + { count: 2 }, + 'each linksToMany item boundary carries the plural field name', + ); + assert + .dom(`[data-test-card="${testRealmURL}Person/arthur"]`) + .doesNotHaveAttribute( + 'data-card-field', + 'the card root has no field context, so no data-card-field', + ); + }); + test('render a field in atom format', async function (assert) { class EmphasizedString extends FieldDef { static [primitive]: string; From 12b9754db777491a352c163e1ccc58a7760c8e2a Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 26 Aug 2026 16:38:40 -0400 Subject: [PATCH 2/2] Stamp data-card-field on the edit-format plural wrappers too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The view-format plural wrappers carried data-card-field but their edit-format counterparts did not, so region discovery over [data-card-field] could not address a containsMany/linksToMany field shown in edit format — and a containsMany of primitives had no boundary naming the field anywhere in edit, since its items render bare. Stamp contains-many-editor and links-to-many-editor to match, giving the contract one shape across both formats. The field-component.gts comment is corrected to match: it previously read "Every rendered field boundary carries data-card-field", which overclaimed against this gap and against primitive leaves that render with no wrapper to carry it. The boundary test now renders the same card in edit format and asserts both editor wrappers carry the field name. card-basics 439/439 against the dev stack. Co-Authored-By: Claude Fable 5 --- packages/base/contains-many-component.gts | 6 ++++- packages/base/field-component.gts | 22 ++++++++++------- packages/base/links-to-many-component.gts | 6 ++++- .../components/card-basics-test.gts | 24 ++++++++++++++----- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/base/contains-many-component.gts b/packages/base/contains-many-component.gts index e8a660c62ea..7426533321a 100644 --- a/packages/base/contains-many-component.gts +++ b/packages/base/contains-many-component.gts @@ -80,7 +80,11 @@ class ContainsManyEditor extends GlimmerComponent {