Content
View differences
Updated by Alexander Coles 3 days ago
_N.B. This is "hypothetical bug" - an issue found with AI tooling. It is unclear whether this manifests itself as a user-facing bug currently._
## Context
Found while adding test coverage in #OP-19666 (custom-element morph policy consolidation) for a code path that had zero prior tests. Pre-existing, not introduced by that work -- confirmed the buggy `replaceWith` call is unchanged on `dev`.
## The bug
`frontend/src/stimulus/controllers/dynamic/work-packages/dialog/preview.controller.ts`, in `DialogPreviewController`'s `DialogPreviewController`'s `beforeNodeMorphed` callback (shared by `date-picker/preview.controller.ts` and `progress/preview.controller.ts`):
```js
if (oldNode.tagName?.startsWith('OPCE-')) {
if (schedulingChanged) {
oldNode.replaceWith(newNode);
}
return false;
}
```
`oldNode.replaceWith(newNode)` detaches `oldNode` from the DOM as a native side effect. idiomorph's idiomorph's `morphChildren` loop (`idiomorph.esm.js`, `morphChildren`) tracks its walk position via `insertionPoint = bestMatch.nextSibling`, evaluated _after_ *after* `morphNode` returns -- but `bestMatch` is the now-detached `oldNode`, so `nextSibling` is `null`. The loop then falls through to "insert/remove" "insert/remove" tail logic instead of continuing to morph siblings.
**Net effect:** when `schedulingChanged` is true and an `OPCE-*` node is replaced, any DOM sibling that follows it in the same parent is never morphed -- it's it's left stale.
Verified empirically with a standalone debug spec calling `Idiomorph.morph()` directly:
```text
```
BEFORE replaceWith, nextSibling: <div data-marker="old">old</div>
AFTER replaceWith, oldNode.nextSibling: null
AFTER replaceWith, oldNode.parentNode: null
FINAL HTML: <opce-test-marker data-marker="new"></opce-test-marker><div data-marker="old">old</div>
```
## Why it went unnoticed
No existing spec mounted a sibling alongside the OPCE node in the `schedulingChanged`\-replace `schedulingChanged`-replace case, so the gap was invisible until #OP-19666 added coverage for it.
## Possible directions (not decided)
* - Trigger a full container morph/re-render after the manual `replaceWith`, rather than relying on idiomorph's idiomorph's loop position afterward.
*
- Restructure so the replace happens without leaving idiomorph's idiomorph's bookkeeping in an inconsistent state (e.g. let idiomorph handle the replacement itself instead of doing it manually inside the callback).
## Notes
Relates to #OP-19666. The test added there (`date-picker/preview.controller.spec.ts`, "replaces "replaces an OPCE-\* OPCE-* node when scheduling has changed") changed") was narrowed to assert only the OPCE-\* node's OPCE-* node's replacement, with a comment pointing to this ticket for the sibling-morph gap.
## Context
Found while adding test coverage in #OP-19666 (custom-element morph policy consolidation) for a code path that had zero prior tests. Pre-existing, not introduced by that work -- confirmed the buggy `replaceWith` call is unchanged on `dev`.
## The bug
`frontend/src/stimulus/controllers/dynamic/work-packages/dialog/preview.controller.ts`, in `DialogPreviewController`'s
```js
if (oldNode.tagName?.startsWith('OPCE-')) {
if (schedulingChanged) {
oldNode.replaceWith(newNode);
}
return false;
}
```
`oldNode.replaceWith(newNode)` detaches `oldNode` from the DOM as a native side effect. idiomorph's
**Net effect:** when `schedulingChanged` is true and an `OPCE-*` node is replaced, any DOM sibling that follows it in the same parent is never morphed -- it's
Verified empirically with a standalone debug spec calling `Idiomorph.morph()` directly:
```text
AFTER replaceWith, oldNode.nextSibling: null
AFTER replaceWith, oldNode.parentNode: null
FINAL HTML: <opce-test-marker data-marker="new"></opce-test-marker><div data-marker="old">old</div>
```
## Why it went unnoticed
No existing spec mounted a sibling alongside the OPCE node in the `schedulingChanged`\-replace
## Possible directions (not decided)
*
*
-
Relates to #OP-19666. The test added there (`date-picker/preview.controller.spec.ts`, "replaces