Issue 159 Defaked XHR Sync Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Make defaked FakeXMLHttpRequest instances propagate post-open() request-side property changes to the underlying native XHR, fixing issue #159 without relying on Proxy.
Architecture: Keep the current defake model, where open() swaps selected methods over to a real XHR, but replace the one-off responseType sync in send() with a small shared request-state sync helper inside FakeXMLHttpRequest.defake. Start with an audit of mutable request-side properties and defaked methods, then use that audit to define a narrow sync list for the helper and apply it before send() and any other defaked methods that need current fake-side state.
Tech Stack: Node.js, Mocha, Referee, Sinon, nise fake XHR implementation
Task 1: Audit defaked request-side state and method usage
Files:
- Modify: docs/plans/2026-03-03-issue-159-defaked-xhr-sync.md
- Review: lib/fake-xhr/index.js:323-380
- Review: lib/fake-xhr/index.js:589-610
- Review: lib/fake-xhr/index.js:760-804
- Review: lib/fake-xhr/index.test.js:2334-2450
Step 1: Inspect defake control flow
Review how open() enters FakeXMLHttpRequest.defake() when filters match, and list which methods are redirected to the working XHR.
Step 2: Enumerate mutable fake-side request properties
Inspect fake XHR initialization and request flow to identify request-side properties that can change after open() and could affect the native XHR. At minimum, audit:
[
"responseType",
"withCredentials",
"timeout",
]
Also explicitly note why properties such as upload, event handlers, method, url, username, password, requestHeaders, and response-side fields should remain outside this sync helper unless the audit proves otherwise.
Step 3: Decide which defaked methods need pre-call sync
For each redirected method below, record whether it should sync request-side state before delegation:
[
"open",
"setRequestHeader",
"abort",
"getResponseHeader",
"getAllResponseHeaders",
"addEventListener",
"overrideMimeType",
"removeEventListener",
"send",
]
Expected outcome: the audit should justify option 2, with a shared helper used at least by send(), and document whether any additional method needs the same sync now instead of leaving that as guesswork.
Task 2: Lock in the bug with focused defake tests
Files:
- Modify: lib/fake-xhr/index.test.js:2404-2450
- Test: lib/fake-xhr/index.test.js
Step 1: Write the failing test for withCredentials
Add a defake-focused test next to the existing responseType coverage that:
it("passes withCredentials to working XHR object on send", function () {
var workingXHRInstance;
var workingXHROverride = function () {
workingXHRInstance = this;
this.withCredentials = false;
this.open = function () {};
this.send = function () {};
};
runWithWorkingXHROveride(workingXHROverride, function () {
FakeXMLHttpRequest.defake(fakeXhr, []);
fakeXhr.withCredentials = true;
fakeXhr.send();
assert.equals(workingXHRInstance.withCredentials, true);
});
});
Step 2: Write the failing test for timeout if the audit keeps it in scope
If Task 1 concludes that timeout should sync, add a matching test asserting a post-open() timeout mutation reaches the working XHR before send().
Step 3: Run targeted tests to verify they fail
Run: npm test -- --grep "passes .* to working XHR object"
Expected: FAIL for the new withCredentials case on the current implementation, and FAIL for timeout only if you added that test.
Task 3: Implement the shared request-state sync helper
Files:
- Modify: lib/fake-xhr/index.js:323-347
- Test: lib/fake-xhr/index.test.js
Step 1: Replace the special-case responseType sync with a helper
Inside FakeXMLHttpRequest.defake(), introduce a helper that copies a narrow audited list of request-side properties from fakeXhr to xhr only when the values differ.
var syncRequestState = function (attrs) {
attrs.forEach(function (attr) {
if (xhr[attr] !== fakeXhr[attr]) {
xhr[attr] = fakeXhr[attr];
}
});
};
Use the audit result to drive the exact attribute list. Start from:
["responseType", "withCredentials", "timeout"]
and remove any property the audit shows is unsafe or ineffective to sync.
Step 2: Call the helper before delegating send()
Update the defaked send() implementation to call the helper before apply(xhr, "send", arguments).
Step 3: Apply sync to any additional defaked method justified by the audit
If Task 1 identifies another method that depends on current fake-side request state, call the same helper there. Otherwise, keep the change limited to send() and record that this is an intentional narrow fix.
Step 4: Preserve the existing synchronous XHR guard
Ensure the current issue #61 behavior remains intact: when synchronous requests expose a guarded responseType setter, the helper must not introduce extra setter calls beyond the existing contract.
Task 4: Verify regressions around defaked and filtered XHR behavior
Files:
- Test: lib/fake-xhr/index.test.js
Step 1: Run the targeted defake block
Run: npm test -- --grep "defake"
Expected: PASS
Step 2: Run filtered defake coverage
Run: npm test -- --grep "defaked XHR filters"
Expected: PASS
Step 3: Run the full fake XHR suite
Run: npm test -- lib/fake-xhr/index.test.js
Expected: PASS
Task 5: Final verification and branch hygiene
Files:
- Modify: lib/fake-xhr/index.js
- Modify: lib/fake-xhr/index.test.js
Step 1: Run lint if implementation changed shape materially
Run: npm run lint
Expected: PASS
Step 2: Summarize the audit in the final PR description or commit message
Document:
- why Proxy was rejected
- which request-side properties were audited
- which ones were synced
- which defaked methods were evaluated
- why the final sync hook is limited to the chosen methods
Step 3: Commit
git add lib/fake-xhr/index.js lib/fake-xhr/index.test.js docs/plans/2026-03-03-issue-159-defaked-xhr-sync.md
git commit -m "plan: add issue 159 defaked xhr sync plan"