Skip to content

[OP-19617] Add a test seam to the Turbo integration#23980

Open
myabc wants to merge 4 commits into
devfrom
code-maintenance/OP-19617-turbo-test-seam
Open

[OP-19617] Add a test seam to the Turbo integration#23980
myabc wants to merge 4 commits into
devfrom
code-maintenance/OP-19617-turbo-test-seam

Conversation

@myabc

@myabc myabc commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/OP-19617

What are you trying to accomplish?

The Turbo integration is the most upgrade-fragile frontend code we have — listeners bound to the global document, custom stream actions, and a monkeypatch reaching into Turbo's private FrameController behind @ts-nocheck — yet it had no unit coverage beyond a single Selenium navigation spec. A Turbo upgrade could rename an internal method or fix a bug underneath us and neither the compiler nor a test would notice.

This PR adds that missing coverage and pins the patch to the Turbo version it was written against.

What approach did you choose and why?

Three commits, smallest blast radius first:

  1. Add a test seam to the Turbo integration. The installers bound to the global document and returned void, so vitest had no way to drive synthetic turbo:* events or tear listeners down between cases. This threads an optional event target and AbortSignal through the installers; callers stay unchanged through the defaults. Adds specs for the seamed installers and all five custom stream actions.

  2. Guard the Turbo navigation monkeypatch. The frame-visit patch reached into Turbo internals behind @ts-nocheck and a blanket eslint-disable. This documents the single divergence from upstream (sourcing the history snapshot from the live document rather than the fetch response), pins it to Turbo 8.0.x, and adds a tripwire spec that fails on either a method rename or an upstream fix — so the patch is re-evaluated instead of silently breaking. The upstream fix is in flight as Fix back-button stale frame on advance navigation hotwired/turbo#1549; once it ships in a release we depend on, this patch can be dropped.

  3. Cover the turbo#1300 back-button fix. The vitest tripwire guards the patched method's shape but not its runtime effect. This adds the behavioural half on the departments page, whose content area is a data-turbo-action="advance" frame: advancing through the tree and pressing back must restore the parent frame. The bug only reproduces with Rails-rendered frames, which is why it cannot live in the vitest suite.

  4. Seam the Angular-Turbo re-bootstrap. turbo-angular-wrapper.ts — which destroys and re-bootstraps the whole Angular root on every Turbo navigation — was the one frontend/src/turbo/ installer left without coverage, and the most consequential: a getPluginContext timing change or a Turbo upgrade can leave every opce-* element inert after the first navigation with nothing to catch it. This threads the same (target, signal) seam and injects the plugin-context lookup and bootstrap call, so vitest can drive synthetic turbo:load events without window.OpenProject or a real Angular application. The RxJS skip(1) pipeline and the destroy-then-bootstrap contract are unchanged; callers stay on the defaults.

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves maintainability and testability of the Turbo integration by (1) introducing an explicit “test seam” for event listener registration via optional AbortSignal (to allow clean teardown in unit tests) and (2) adding Vitest coverage for several Turbo-related behaviors, including a “tripwire” spec guarding a monkeypatch for hotwired/turbo#1300.

Changes:

  • Add a documented monkeypatch for Turbo frame navigation/history snapshot behavior and a spec that fails loudly on upstream API/behavior changes.
  • Update Turbo listener registration helpers to accept an optional AbortSignal (and target Document) and add unit tests for key listener behaviors.
  • Add unit tests for custom Turbo Stream actions (flash, dispatch event, input caption, live region).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
frontend/src/turbo/turbo-navigation-patch.ts Adds detailed rationale/docs for the Turbo #1300 workaround and implements live-document snapshot sourcing.
frontend/src/turbo/turbo-navigation-patch.spec.ts Adds a “tripwire” spec to detect upstream changes that should trigger patch removal/review.
frontend/src/turbo/turbo-global-listeners.ts Adds target/signal parameters and uses signal for listener teardown support.
frontend/src/turbo/turbo-global-listeners.spec.ts Adds unit test for the OPCE morph guard behavior.
frontend/src/turbo/turbo-event-listeners.ts Adds target/signal parameters and uses signal for listener teardown support.
frontend/src/turbo/turbo-event-listeners.spec.ts Adds unit tests for dialog-close behavior and nonce header injection.
frontend/src/turbo/live-region-stream-action.spec.ts Adds unit tests for the live-region stream action behavior.
frontend/src/turbo/input-caption-stream-action.spec.ts Adds unit tests for input caption stream action behavior.
frontend/src/turbo/flash-stream-action.spec.ts Adds unit tests for flash stream action append/replace behavior.
frontend/src/turbo/dispatch-event-stream-action.spec.ts Adds unit tests for dispatch-event stream action behavior.
frontend/src/turbo/action-menu-morph-remount.ts Adds target/signal parameters and uses signal for listener teardown support.
frontend/src/turbo/action-menu-morph-remount.spec.ts Adds unit tests ensuring action-menu hosts are remounted only when needed.
Comments suppressed due to low confidence (1)

frontend/src/turbo/turbo-event-listeners.ts:17

  • In the turbo:submit-end handler, destructuring target from the event shadows the function parameter target (the document you attach listeners to), which is confusing now that the outer function takes a target: Document. Also, closest('dialog')! defeats TypeScript’s nullability checks while the code still relies on a runtime null check.

Rename the destructured variable and remove the non-null assertion to keep intent clear and types accurate.

  target.addEventListener('turbo:submit-end', (event) => {
    const { detail: { success }, target } = event;

    if (success && target instanceof HTMLFormElement) {
      const dialog = target.closest('dialog')!;

@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./modules/overviews/spec/features/project_description_widget_spec.rb[1:1:1:1:1]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #23980, linked for reference only):

- `rspec ./modules/overviews/spec/features/project_description_widget_spec.rb[1:1:1:1:1]`

Treat this as a standalone task, unrelated to PR #23980. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #23980 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@myabc myabc force-pushed the code-maintenance/OP-19617-turbo-test-seam branch from ebfd557 to 75ff281 Compare June 27, 2026 20:30
@myabc myabc changed the title Code maintenance/op 19617 turbo test seam [OP-19617] Add a test seam to the Turbo integration Jun 27, 2026
@myabc myabc requested a review from Copilot June 27, 2026 20:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

frontend/src/turbo/turbo-event-listeners.ts:17

  • In this handler, destructuring target from the event now shadows the function parameter target (the Document). This makes the code harder to read and also prevents using the passed-in Document for the dialog:close dispatch (useful for the new test seam). Consider renaming the event target and dispatching on the passed-in target Document.
  target.addEventListener('turbo:submit-end', (event) => {
    const { detail: { success }, target } = event;

    if (success && target instanceof HTMLFormElement) {
      const dialog = target.closest('dialog')!;

Comment thread frontend/src/turbo/turbo-event-listeners.ts Outdated
Comment thread frontend/src/turbo/turbo-event-listeners.ts Outdated
Comment thread frontend/src/turbo/turbo-global-listeners.ts Outdated
Comment thread frontend/src/turbo/turbo-navigation-patch.ts Outdated
Comment thread frontend/src/turbo/dispatch-event-stream-action.spec.ts Outdated
@myabc myabc force-pushed the code-maintenance/OP-19617-turbo-test-seam branch from 75ff281 to 20c9900 Compare June 30, 2026 19:08
@myabc myabc requested a review from Copilot June 30, 2026 19:11
@myabc myabc marked this pull request as ready for review June 30, 2026 19:11
@github-actions

Copy link
Copy Markdown

Caution

The provided work package version does not match the core version

Details:

Please make sure that:

  • The work package version OR your pull request target branch is correct

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

frontend/src/turbo/turbo-event-listeners.ts:17

  • addTurboEventListeners now accepts a target Document for testability, but the turbo:submit-end handler still dispatches dialog:close on the global document. Because the handler also destructures target from the event (shadowing the outer target), this makes the new seam inconsistent and would dispatch to the wrong document if a non-default target is ever provided (e.g., an iframe document in tests).
  target.addEventListener('turbo:submit-end', (event) => {
    const { detail: { success }, target } = event;

    if (success && target instanceof HTMLFormElement) {
      const dialog = target.closest('dialog')!;

@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/work_packages/details/inplace_editor/subject_editor_spec.rb[1:2:1:1:2]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #23980, linked for reference only):

- `rspec ./spec/features/work_packages/details/inplace_editor/subject_editor_spec.rb[1:2:1:1:2]`

Treat this as a standalone task, unrelated to PR #23980. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #23980 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@myabc myabc force-pushed the code-maintenance/OP-19617-turbo-test-seam branch from aa671d1 to 479b08d Compare June 30, 2026 19:37
@myabc myabc requested a review from Copilot June 30, 2026 19:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

frontend/src/turbo/turbo-event-listeners.ts:17

  • addTurboEventListeners now accepts a target document, but the turbo:submit-end handler destructures target from the event, which shadows the outer target parameter. This makes the code harder to follow and also prevents dispatching dialog:close on the injected target (it currently always dispatches on the global document). Renaming the event target (e.g., eventTarget) and dispatching via the outer target keeps the test seam consistent.
  target.addEventListener('turbo:submit-end', (event) => {
    const { detail: { success }, target } = event;

    if (success && target instanceof HTMLFormElement) {
      const dialog = target.closest('dialog')!;

Comment thread frontend/src/turbo/turbo-angular-wrapper.ts
myabc added 4 commits June 30, 2026 21:40
The Turbo integration listeners bound to the global `document` and
returned `void`, so the most Turbo-upgrade-fragile frontend code had no
unit coverage beyond a single Selenium navigation spec.

Threads an optional event target and `AbortSignal` through the
installers so vitest can drive synthetic `turbo:*` events and tear the
listeners down between cases. Adds specs for the seamed installers and
all five custom stream actions; callers stay unchanged through the
defaults.
The frame-visit monkeypatch reached into Turbo internals behind
`@ts-nocheck` and a blanket eslint-disable, so neither the compiler nor
a test would notice a Turbo upgrade renaming the method or fixing
hotwired/turbo#1300 underneath it.

Documents the single divergence from upstream -- sourcing the history
snapshot from the live document rather than the fetch response -- pins
it to Turbo 8.0.x, and adds a tripwire spec that fails on either upgrade
so the patch is re-evaluated instead of silently breaking.
The vitest tripwire guards the patched method's upstream shape but not
its runtime effect. This adds the behavioural half on the departments
page, whose content area is a `data-turbo-action="advance"` frame:
advancing through the tree and pressing back must restore the parent
frame.

Without `turbo-navigation-patch.ts` the URL reverts while the frame
keeps the child content, so the exact breadcrumb match fails. The bug
only reproduces with Rails-rendered frames, which is why it cannot live
in the vitest suite.
The wrapper destroys and re-bootstraps the whole Angular root on every
Turbo navigation -- the most consequential frontend/src/turbo/ installer
yet the only one left without coverage.

Threads the same (target, signal) seam and injects the plugin-context
lookup and bootstrap call so vitest can drive synthetic turbo:load
events. The RxJS skip(1) pipeline and destroy-then-bootstrap contract
are unchanged; callers stay on the defaults.
@myabc myabc force-pushed the code-maintenance/OP-19617-turbo-test-seam branch from 479b08d to b660548 Compare June 30, 2026 20:41
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/activities/work_package/activities_spec.rb[1:5:2:2]
  • rspec ./spec/features/roles/report_spec.rb[1:2]
  • rspec ./spec/features/roles/report_spec.rb[1:3]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #23980, linked for reference only):

- `rspec ./spec/features/activities/work_package/activities_spec.rb[1:5:2:2]`
- `rspec ./spec/features/roles/report_spec.rb[1:2]`
- `rspec ./spec/features/roles/report_spec.rb[1:3]`

Treat this as a standalone task, unrelated to PR #23980. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #23980 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants