Fix: Block editor charCount/wordCount/readingTime missing from Page API response#34952
Fix: Block editor charCount/wordCount/readingTime missing from Page API response#34952
Conversation
…onse In onBlockEditorChange, eagerly patch the doc attrs from the current CharacterCountStorage before emitting the value to the form control. Previously, these attrs were only injected via updateCharCount() which runs 250ms after the keyup event, causing the saved JSON to miss them. Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
zJaaal
left a comment
There was a problem hiding this comment.
PR Review: #34952 - Fix: Block editor charCount/wordCount/readingTime missing from API response
Summary
This PR fixes a bug where charCount, wordCount, and readingTime were not included in the Block Editor API response. The fix eagerly merges these stats into the JSONContent value before the debounced emit fires. The production change is small (19 lines), clean, and well-typed. However, the accompanying test suite has structural issues that undermine its reliability.
Files Changed: 2 frontend files
Risk Level: Low
Risk Assessment
| Area | Level | Notes |
|---|---|---|
| Security | None | No security surface changes |
| Breaking Changes | None | Additive — new fields on existing object |
| Performance Impact | None | Synchronous property reads before existing emit |
| Test Coverage | Partial | Tests exist but have false positive and bypass issues |
Frontend Findings
TypeScript Type Safety
Quality Issues 🔵 (75-84)
Missing explicit void return type on onBlockEditorChange (Confidence: 75)
// Current
onBlockEditorChange(value: JSONContent) {
// Fix
onBlockEditorChange(value: JSONContent): void {Minor style alignment with the project's standards for explicit return types at public boundaries.
Angular Patterns
No issues found. The changed lines are purely logic within an existing method — no new Angular patterns, subscriptions, lifecycle hooks, or structural directives introduced.
Test Quality
Critical Issues 🔴 (95-100)
1. Direct property mutation bypasses Angular binding / ControlValueAccessor path (Confidence: 97)
Multiple tests directly set blockEditorComponent.value, blockEditorComponent.disabled, blockEditorComponent.editor, and blockEditorComponent.charLimit bypassing Angular's input binding pipeline and the ControlValueAccessor API.
// WRONG — bypasses CVA
blockEditorComponent.value = BLOCK_EDITOR_FIELD;
// CORRECT — exercises the CVA path
blockEditorComponent.writeValue(BLOCK_EDITOR_FIELD);2. Line 66-73 test is a false positive (Confidence: 85)
The test "should set form value when binary file changes" passes trivially because the FormControl was already set to JSON.stringify(BLOCK_EDITOR_FIELD) during MockFormComponent construction. The blockEditorComponent.value = ... assignment does not invoke onChange and has no effect on the form. It never actually tests what the name claims.
// Fix: exercise the actual change path
blockEditorComponent.onBlockEditorChange(BLOCK_EDITOR_FIELD);
expect(spectator.component.form.get('block').value)
.toEqual(JSON.stringify(BLOCK_EDITOR_FIELD));Important Issues 🟡 (85-94)
3. Missing detectChanges after state mutations (Confidence: 96)
Tests mutate disabled and then assert without calling spectator.detectChanges(), meaning template-level effects are invisible.
4. No detectChanges: false in factory (Confidence: 88)
ngOnInit fires immediately on createComponent() with async service calls. Without controlled initialization, component may silently partially initialize if mocked providers don't cover all consumed services.
5. charLimit tested via direct mutation, not via fieldVariables lifecycle (Confidence: 86)
blockEditorComponent.charLimit = 100 bypasses the setFieldVariable() path called in ngOnInit. Tests can pass even if that lifecycle code is broken.
Quality Issues 🔵 (75-84)
6. Zero DOM/template assertions (Confidence: 78) — all tests assert on component properties only, not user-visible outcomes.
7. Typo in describe block name (Confidence: 76) — "ControlValueAccesor" should be "ControlValueAccessor".
Approval Recommendation
The production fix is correct, small, and well-typed — the bug is genuinely addressed. The test suite, however, contains a confirmed false positive (the "should set form value" test always passes regardless of the production code) and multiple tests that bypass the lifecycle paths they claim to exercise.
Statistics:
- Critical Issues: 2 (tests only)
- Important Issues: 3 (tests only)
- Quality Issues: 3 total (2 tests, 1 TypeScript style)
Next Steps:
- Fix the false positive test at line 66-73 to call
onBlockEditorChangeinstead of directly settingvalue - Replace direct
@Inputproperty assignments withwriteValue/setDisabledStateCVA calls where testing CVA behavior - Add
spectator.detectChanges()after state mutations - Consider
detectChanges: falsein the factory to control initialization
The production code change itself is safe to merge. The test issues are pre-existing patterns in this file that the PR perpetuates — flagged here so they don't compound further.
🤖 Generated with Claude Code
- Add void return type to onBlockEditorChange - Fix false positive test: reset form value then call onBlockEditorChange - Replace direct disabled property mutations with setDisabledState() CVA method - Add detectChanges() after state mutations in CSS-class test - Fix typo ControlValueAccesor -> ControlValueAccessor - Add comment explaining why characterCount uses optional chaining Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
charCount,wordCount, andreadingTimeare visible in the Block Editor UI footer but were absent from the serialized JSON sent to the API. Root cause:onBlockEditorChangeemitted the value immediately viaonChange, whileupdateCharCount()(which dispatches theSetDocAttrStepattrs into the doc) runs 250 ms later behind a debounced(keyup). Any save within that window — or any programmatic write viawriteValue— produced JSON without these attrs.Changes
dot-block-editor.component.tsonBlockEditorChange, eagerly read live counts from TipTap'sCharacterCountStorageand patch them into the doc attrs before callingonChange. No new transactions dispatched; safe against re-entrancy.voidreturn type.dot-block-editor.component.spec.tsblockEditorComponent.value = ...had no effect on the form; replaced withcontrol.setValue(null)+onBlockEditorChange(...)to actually exercise the CVAonChangepath.blockEditorComponent.disabled = true/falseassignments withsetDisabledState()(the CVA contract).spectator.detectChanges()aftersetDisabledState()calls in the CSS-class template test.ControlValueAccesor→ControlValueAccessor.Checklist
Additional Info
The debounced
updateCharCount()path (keeping the internal ProseMirror doc attrs in sync for undo history) is untouched. The patch inonBlockEditorChangeis purely for the outbound serialized value.Screenshots
charCount/wordCount/readingTimeabsent from API JSONOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.