Skip to content

fix(twitter): use search input for SPA navigation instead of pushState#695

Merged
jackwener merged 3 commits intojackwener:mainfrom
0xsline:fix/twitter-search-spa-navigation
Apr 2, 2026
Merged

fix(twitter): use search input for SPA navigation instead of pushState#695
jackwener merged 3 commits intojackwener:mainfrom
0xsline:fix/twitter-search-spa-navigation

Conversation

@0xsline
Copy link
Copy Markdown
Contributor

@0xsline 0xsline commented Apr 2, 2026

Summary

Root Cause

The failure is intermittent, not permanent. Likely causes include Twitter A/B tests, timing race conditions (pathname not updated when checked), or browser/session state differences. In my testing, pushState failed consistently at first, then succeeded consistently later — consistent with server-side variation.

Changes

  • Wrap the primaryColumn selector wait in a try/catch so a timeout doesn't throw immediately — it falls through to the path check
  • After 2 failed pushState attempts, fall back to the search input approach (nativeSetter + Enter keydown)
  • The fallback also uses selector-based waiting, not fixed delays
  • For live filter with the fallback path, click the "Latest" tab after navigation

Testing

  • Original pushState: failed 2/2, then later passed 3/3 (confirms intermittent nature)
  • Search input fallback: passed 2/2 when pushState was failing
  • Combined (this PR): covers both scenarios

Fixes #690

Copy link
Copy Markdown
Contributor

@Astro-Han Astro-Han left a comment

Choose a reason for hiding this comment

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

Thanks for the PR and for reproducing the issue. The dual-strategy approach is a good idea, but I have a few concerns after investigating this:

1. Root cause analysis may not be accurate

The PR states that pushState + popstate no longer works after Twitter's React Router update. However, I ran twitter search locally 5 times in a row and all succeeded. If the router change completely broke this approach, it should fail consistently. The issue reporter's Final path: (empty) suggests a timing race condition (pathname hasn't updated yet when checked) rather than complete failure. It's more likely that Twitter is running an A/B test, or behavior varies by browser version / login state.

2. The legacy fallback has a regression

The PR replaces wait({ selector: '[data-testid="primaryColumn"]' }) with wait(3) (fixed 3-second wait). This is a step back — it wastes time on fast networks and may still be insufficient on slow ones. The selector-based wait is the right approach; if anything, we should keep it or increase the timeout rather than switch to a fixed delay.

3. The search input approach was already tried and abandoned

Commit d1986f0 (#105) switched away from the search input + Enter keydown approach because "the synthetic KeyboardEvent is ignored by React, leaving the page on /explore with zero API calls captured." The code in this PR is nearly identical to the old approach. It would be helpful to explain what changed on Twitter's side that would make this work now when it didn't before.

Suggested direction:

  • Keep pushState + popstate as the primary strategy (it works in most environments)
  • Add more retries or a longer backoff for slow networks
  • If keeping the search input as fallback, add a note about why it works now despite #105
  • Fix the fallback to use selector-based waiting instead of fixed delay

…on failures

The pushState + popstate approach works in most environments but fails
intermittently for some users (see jackwener#690), likely due to Twitter A/B
tests or timing race conditions where the pathname hasn't updated when
checked.

This commit adds a fallback strategy: when pushState fails after 2
retries, we type the query into the search input on /explore and press
Enter. This triggers Twitter's own form handler, performing SPA
navigation without a full page reload (keeping the fetch interceptor
alive).

Both strategies use selector-based waiting ([data-testid="primaryColumn"])
rather than fixed delays, with graceful fallthrough on timeout.

Fixes jackwener#690
@0xsline 0xsline force-pushed the fix/twitter-search-spa-navigation branch from 0505cee to 597eaf7 Compare April 2, 2026 11:41
@0xsline
Copy link
Copy Markdown
Contributor Author

0xsline commented Apr 2, 2026

Thanks @Astro-Han for the thorough review, and @jackwener for testing. You were both right — I've updated the PR based on your feedback.

What changed in v2:

  1. Root cause corrected: The failure is intermittent, not permanent. In my testing, pushState failed consistently at first (2/2), then succeeded consistently later (3/3) — consistent with Twitter A/B testing or timing races, not a complete router change.

  2. pushState remains primary: The pushState + popstate approach is kept as the primary strategy with 2 retries, exactly as before.

  3. Selector-based wait restored: Both the primary and fallback paths now use wait({ selector: '[data-testid="primaryColumn"]' }) instead of fixed delays. The selector wait is wrapped in try/catch so a timeout falls through gracefully instead of throwing.

  4. Search input as fallback only: The search input approach is only attempted after pushState fails twice. Regarding commit fix(twitter): use pushState for search SPA navigation #105 — the difference is that it was used as the primary approach before, whereas here it's a last-resort fallback. If pushState works (which it does most of the time), the search input path is never reached.

The net effect: existing behavior is preserved for the majority of users, and the fallback covers the intermittent failures reported in #690.

The search input fallback adds one extra evaluate() call when pushState
fails. Update the mock chain and assertion count accordingly.
@Astro-Han
Copy link
Copy Markdown
Contributor

Thanks for the update — v2 is much cleaner. Root cause correction, selector-based wait restored, pushState kept as primary — all good.

One thing I'm still curious about: commit d1986f0 (#105) moved away from the search input + Enter approach because the synthetic KeyboardEvent was ignored by React. The code here looks similar. Do you know what changed on Twitter's side that makes this work now? Even a brief note in the comment would help future maintainers understand why the fallback is expected to be reliable.

Not a blocker since the fallback can't break existing behavior, but worth documenting.

- Add optional chaining on getOwnPropertyDescriptor().set to handle
  edge cases where Twitter's sandbox overrides the HTMLInputElement
  prototype.
- Add test case covering the full fallback path: pushState fails twice,
  search input fallback succeeds, results are returned correctly.
@jackwener jackwener merged commit b8f1abc into jackwener:main Apr 2, 2026
9 checks passed
@jackwener
Copy link
Copy Markdown
Owner

@0xsline can you contact me with Wechat?

My email is jakevingoo@gmail.com.

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

Labels

None yet

Projects

None yet

3 participants