Skip to content

test(core): previewAdapter contract failing tests (T10 spec for R7)#1286

Merged
vanceingalls merged 11 commits into
mainfrom
06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_
Jun 9, 2026
Merged

test(core): previewAdapter contract failing tests (T10 spec for R7)#1286
vanceingalls merged 11 commits into
mainfrom
06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the T10 PreviewAdapter contract test suite as failing specs (TDD red phase for Task 3)
  • Tests cover elementAtPoint ancestor walk + stage-root exclusion, applyDraft/revertDraft CSS custom property drafts, commitPreview patch extraction, and getElementTimings
  • All 20 tests fail until Task 3 implements createPreviewAdapter — confirms tests are real (not tautological)

Test plan

@vanceingalls vanceingalls force-pushed the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch from f2deb71 to f81528b Compare June 9, 2026 00:28
@vanceingalls vanceingalls force-pushed the 06-07-feat_core_sourcemutation_data-hf-id_targeting_r1_t7_ branch 2 times, most recently from 2bd6c0a to df9947d Compare June 9, 2026 01:52
@vanceingalls vanceingalls force-pushed the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch from f81528b to 2cb29b1 Compare June 9, 2026 01:52
@vanceingalls vanceingalls changed the base branch from 06-07-feat_core_sourcemutation_data-hf-id_targeting_r1_t7_ to graphite-base/1286 June 9, 2026 03:33
@vanceingalls vanceingalls force-pushed the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch from 2cb29b1 to fa6996c Compare June 9, 2026 03:45
@vanceingalls vanceingalls changed the base branch from graphite-base/1286 to 06-07-feat_core_sourcemutation_data-hf-id_targeting_r1_t7_ June 9, 2026 03:46
@vanceingalls vanceingalls force-pushed the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch from fa6996c to c125ecc Compare June 9, 2026 04:01
@vanceingalls vanceingalls force-pushed the 06-07-feat_core_sourcemutation_data-hf-id_targeting_r1_t7_ branch from 4261480 to 9ba8ee2 Compare June 9, 2026 04:03
@vanceingalls vanceingalls force-pushed the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch from c125ecc to 7d1bf83 Compare June 9, 2026 04:03
miguel-heygen
miguel-heygen previously approved these changes Jun 9, 2026

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean TDD red-phase setup. 20 real assertions that fail on the throw-stub and will pass once Task 3 lands — exactly the right shape for a spec PR.

One design note worth tracking into Task 3: elementAtPoint accepts opts?: { atTime?: number } but the contract tests don't verify that atTime triggers any time-seek on the GSAP player — they just verify live-opacity filtering (by setting el.style.opacity between calls). That's the right thing to test in isolation, but it means atTime semantics are unspecified in the contract. If the intent is to seek-then-read, that behavior should be captured in a getElementTimings or adapter-integration test before R7 ships.

Otherwise ✅ on the spec.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T10 spec is well-shaped — clean TDD red-phase. The conversion from it.todo stubs to 20 real assertions tightens the contract, and the test bodies are minimal-fixture-per-case (the make() helper is the right level of indirection) so the reviewer can read each test independently.

Walked the 20 tests against the contract surface declared in the stub (previewAdapter.ts:1-28):

  • elementAtPoint — 4 tests cover root exclusion, ancestor walk, no-id-orphan, opacity skip.
  • applyDraft / revertDraft — 5 tests cover move payload, resize payload, gesture marker, prop clear, original-translate restore.
  • Edge cases — 4 tests pin double-apply overwrite, idempotent revert, mid-drag opacity re-eval, outer-vs-nested stage-root.
  • commitPreview — 4 tests pin null-on-no-gesture, move patch, resize patch, marker clear.
  • getElementTimings — 3 tests pin authored timing read, no-id ignore, defined-but-empty when id-present-but-no-timing.

That's 20 = 4+5+4+4+3, matching the body claim. The deletions (-52 on the test file) are pure it.todo placeholders going to real it() bodies — no coverage regression, just shape conversion.

Concerns

[atTime semantic is under-specified in the spec] Two of the tests pass { atTime: 1.0 } to elementAtPoint:

  • L83-89: "skips elements whose computed opacity is 0 at the given playhead time" — sets opacity: "0" inline, then expects null at atTime: 1.0.
  • L182-188: "elementAtPoint filtering is stable when playhead changes mid-drag — opacity re-evaluated per call" — mutates elem.style.opacity = "0" between calls and re-checks.

Both tests assert behavior that can be satisfied by reading current getComputedStyle and ignoring atTime entirely — which is exactly what #1291's impl does. The spec doesn't actually pin "the value at atTime is what's used"; it pins "the current computed style is what's read, regardless of atTime." That's a perfectly defensible MVP contract (caller seeks the timeline to atTime before invoking), but the test names imply the adapter uses atTime for evaluation.

Two options to tighten the spec:

  1. Rename to make the current-read semantics explicit: "skips elements whose currently-computed opacity is 0" and drop the atTime arg from these two tests (since it's unused by the contract being pinned). The interface keeps atTime?: number as an unused-for-now hint.
  2. Add an it.todo (or it.skip) for the true atTime semantic — "uses GSAP timeline state at atTime to evaluate opacity, not the current playhead" — so the contract carries the gap forward.

The second is better long-term; the first is fastest. Worth picking one before the spec is the contract that lives in main.

[Nested-root test is one assertion away from pinning a fragile heuristic] L189-201 verifies that an inner data-hf-root with data-hf-id IS a target. The contract this pins is "data-hf-id beats data-hf-root." But what about an inner data-hf-root without data-hf-id (a nested sub-comp that the author forgot to tag, or one that's tagged at a deeper level)? The current spec would return null for that case — same as the outer stage root — which is probably wrong for the use case (a nested sub-comp without an explicit id is still a draggable surface).

Either:

  • Add a fourth assertion to the nested-roots test: a nested data-hf-root with NO data-hf-id returns null — pins "any data-hf-root without data-hf-id is treated as stage-root, including nested." This documents the limitation as intentional.
  • Or: change the heuristic so only the OUTERMOST data-hf-root is excluded (e.g. by checking ancestor chain for another data-hf-root parent), and update both the impl and the test.

Picking one closes the latent ambiguity. The first is cheaper.

[createPreviewAdapter stub throws on call rather than at import] The stub at previewAdapter.ts:23-28:

export function createPreviewAdapter(
  _document: Document,
  _opts?: { resolvePoint?: (x: number, y: number) => Element | null },
): PreviewAdapter {
  throw new Error("not implemented — Task 3");
}

This is the correct red-phase shape (each test fails in its own assertion with a clear error rather than the whole file failing to load), but it means CI on #1286 alone reports 20 failing tests. If the merge model is "stack lands atomically through Graphite MQ," that's fine. If individual stack members can land independently — e.g. someone needs to revert #1289-#1292 but keep #1286 — main breaks for everyone pulling.

Worth confirming the merge-order guarantee, or noting in the PR body that this PR must NOT land alone (already says "20 fail on this branch, 20 pass after Task 3 (#1291) merges" in the test plan, but a banner-style "DO NOT LAND ALONE" in the description would help the MQ pickup decision).

Nits

[// fallow-ignore-file code-duplication] L1 lint suppression — assume this is a project-local linter directive (didn't see it elsewhere). If fallow is the typo'd intent of "follow" and this is a long-lived directive, it should be in a comment that lints don't strip. If it's a typo of an existing tool, worth normalizing.

[adapterWith helper hides the test's adapter creation] adapterWith at L46-48 wraps createPreviewAdapter(document, { resolvePoint }) — fine for hit-test paths, but tests that DON'T use hit-testing (applyDraft, revertDraft, commitPreview, getElementTimings) still call adapterWith(() => null). That's correct but reads slightly noisy. A second helper adapter() returning createPreviewAdapter(document) (no opts) for non-hit-test cases would tighten ~12 call sites. Cosmetic.

Questions

[Where does R7 plan getElementTimings get used?] The signature returns timings keyed by hfId, where the values can have start/end undefined — but the test L266-272 just verifies the entry "is defined." If the caller is going to treat missing-timing entries differently from absent-entry-entirely, the contract should be tighter ("returns the keys for all data-hf-id elements, with start/end undefined when not authored" vs "may or may not return such entries"). The current test allows both.

What I didn't verify

  • That all 20 tests will pass on #1291's impl. Walked the impl logic against each test, looks aligned, but I didn't run the test suite locally.
  • That getComputedStyle(el).opacity returns "1" in jsdom for elements without explicit opacity. The test "returns the nearest ancestor with data-hf-id" relies on this — if jsdom returns "" instead, parseFloat("") → NaN, and the #1291 impl's parseFloat(...) || 0 would treat it as opacity=0 → null. I trust jsdom returns "1" (standard behavior), but linkedom is referenced in the file header — if the test runner is linkedom-backed instead of jsdom, this changes.
  • Whether the regression-shards in CI run unit tests too, or just the regression-style render fixtures. If unit tests live in a separate workflow that I'm not seeing in the rollup, the 20-fail state may not actually break CI.

Verdict

Solid test contract. The two contract-tightening items above are worth landing in this PR — they're the difference between "tests pin behavior" and "tests describe behavior accurately." Once those are sharpened, the spec is locked and #1291's impl just has to satisfy it. Ready from where I sit; leaving as a comment.

Review by Rames D Jusso

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building on Rames's review.

Rames flagged three things I'd amplify:

Escalating: "DO NOT LAND ALONE" is not explicit enough in the PR body. The body says "20 tests fail on this branch, 20 pass after Task 3 merges" — that's correct but passive. If Graphite MQ ever picks up this PR in isolation (e.g. a rebase conflict drops one of the downstackers), CI on main fails for every downstream consumer. Add a bold ⚠️ Must merge atomically with #1289–#1292 via Graphite to the body description — MQ picks up from the body description metadata.

Confirming: nested inner data-hf-root without data-hf-id returns null — needs a pinned test. The current spec doesn't say whether a nested sub-composition data-hf-root with no data-hf-id is treated the same as the outermost stage root (correct) or as a valid drag target (wrong). Either add the fourth assertion to the nested-roots test now, or the ambiguity will produce a bug report later. Rames laid out both options — option 1 (pin the current null behavior) is the 3-line path.

New: getElementTimings "is defined" assertion is too loose. The test at L266-272 asserts timings["hf-notimed"] is defined but doesn't assert its shape ({start: undefined, end: undefined}). #1291's impl and #1292's NaN fix both happen to produce the same shape, but a future impl that returns timings["hf-notimed"] = { start: 0, end: 0 } (zeroed, not undefined) would also pass the test. One more assertion (expect(timings["hf-notimed"].start).toBeUndefined()) tightens the contract.

Everything else from Rames's pass is accurate. The spec is otherwise solid.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2 — HEAD is unchanged at 7d1bf83 since my round-1 review. The round-2 work for the spec items I flagged (atTime semantic disambiguation in test names, nested data-hf-root without data-hf-id) landed in #1292's top commit d916901 rather than here:

  • previewAdapter.test.ts:67-72 — test rename to "...currently-computed opacity is 0 (atTime is a caller-seek hint, not evaluated by the adapter)".
  • previewAdapter.test.ts:74-82 — new test "returns null for nested data-hf-root without data-hf-id (treated same as outer stage root)".
  • previewAdapter.test.ts:143-152 — new test "resize → move switch clears width/height props" (pins the cross-type prop leak I flagged on #1291).
  • previewAdapter.test.ts:178-180 — test rename to "...inline opacity changes mid-drag — computed style re-evaluated per call" + drops the unused atTime args.

So the spec-tightening is now in the stack — just at the top rather than here. My round-1 review on the test file (the contract-shape walkthrough + the red-phase / stub structure) still stands as-is for the code at this SHA.

Ready from where I sit; leaving as a comment.

Review by Rames D Jusso

jrusso1020
jrusso1020 previously approved these changes Jun 9, 2026

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved on behalf of James.

vanceingalls and others added 6 commits June 9, 2026 06:28
…review)

Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with
id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable,
exact-match) by design — targeting uses exact [data-hf-id="…"] match and does
not require the hf- shape; legacy values re-mint only at the R7 write-back. Not
a bug. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The original comment said legacy data-hf-id values "are re-minted only
once the R7 write-back persists freshly-minted ids to source" — which is
incorrect. ensureHfIds skips elements that already carry data-hf-id, so
legacy values (e.g. data-hf-id="my-title") persist indefinitely and are
NOT automatically re-minted. Exact-match targeting still works correctly.
Update comment to reflect actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eview)

Addresses Rames' review on #1271: execDataAttrPattern returned the first regex
match without checking for a second. A duplicate id/data-hf-id in source (id
drift) would silently patch one element and leave the other stale. Now warns
when more than one element matches. By the mint contract it should never fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3 review)

Adds test: "hfId match is authoritative — selector is not used as a
narrowing filter". When hfId matches element A and selector points at
element B, findTagByTarget returns A without consulting selector as a
narrowing filter. Pins the intended behaviour so a future refactor
cannot silently start narrowing by selector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vanceingalls and others added 4 commits June 9, 2026 06:29
Elements now get data-hf-id minted by ensureHfIds; parser reads
data-hf-id as model id, so HTML id attrs are no longer the model id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Locks the preservation guarantee the write-back design depends on: a
Studio edit targeting by id or selector (it never sends hfId) must not strip
an existing data-hf-id, or the stable handle is destroyed by the next edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…review)

Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated
target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value
injection guard) and warn when a hfId matches more than one element instead of
silently patching an arbitrary one. Adds an injection-guard test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vanceingalls vanceingalls force-pushed the 06-07-feat_core_sourcemutation_data-hf-id_targeting_r1_t7_ branch from 9ba8ee2 to 2e8a61d Compare June 9, 2026 06:29
@vanceingalls vanceingalls force-pushed the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch from 7d1bf83 to 4fceb69 Compare June 9, 2026 06:30
Base automatically changed from 06-07-feat_core_sourcemutation_data-hf-id_targeting_r1_t7_ to main June 9, 2026 07:27
@vanceingalls vanceingalls dismissed stale reviews from jrusso1020 and miguel-heygen June 9, 2026 07:27

The base branch was changed.

@vanceingalls vanceingalls merged commit 72e4f1a into main Jun 9, 2026
39 of 40 checks passed
@vanceingalls vanceingalls deleted the 06-08-test_core_previewadapter_contract_failing_tests_t10_spec_for_r7_ branch June 9, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants