wayBetter for X
This commit is contained in:
commit
2f1e35ba20
720 changed files with 147465 additions and 0 deletions
292
apps/desktop/tests/backlogFixes.test.ts
Normal file
292
apps/desktop/tests/backlogFixes.test.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// Fixes for docs/X-BUG-BACKLOG.md: "See new posts" jump, silent media upload failures, Following order.
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { chronologicalVariables, installFollowingChronological, rewriteFollowingBody, rewriteFollowingUrl } from '../src/fixes/followingOrder.ts';
|
||||
import {
|
||||
MAX_UPLOAD_RETRIES, classifyUpload, installUploadRetry, parseUpload, processingFailure, shouldRetryUpload, uploadFailureMessage, uploadRetryDelay,
|
||||
} from '../src/fixes/uploadRetry.ts';
|
||||
import { isNewPostsShortcut, restoreStep } from '../src/fixes/timeline.ts';
|
||||
|
||||
const BASE = 'https://x.com/home';
|
||||
const LATEST = '/i/api/graphql/OQPHTgwczzp9RMAPt6BH9A/HomeLatestTimeline';
|
||||
const UPLOAD = 'https://upload.x.com/i/media/upload.json';
|
||||
|
||||
// ---------- fake XHR (records what reaches the wire, replies on demand) ----------
|
||||
|
||||
type Reply = { status: number } | 'error' | 'timeout';
|
||||
function xhrEnv() {
|
||||
const wire: { method: string; url: string; body: unknown; headers: [string, string][]; xhr: FakeXHR }[] = [];
|
||||
class FakeXHR extends EventTarget {
|
||||
#s = { readyState: 0, status: 0, text: '', method: '', url: '', headers: [] as [string, string][] };
|
||||
responseType: XMLHttpRequestResponseType = '';
|
||||
timeout = 0;
|
||||
withCredentials = false;
|
||||
get readyState() { return this.#s.readyState; }
|
||||
get status() { return this.#s.status; }
|
||||
get responseText() { return this.#s.text; }
|
||||
open(method: string, url: string) {
|
||||
Object.assign(this.#s, { method, url, readyState: 1, status: 0, text: '', headers: [] });
|
||||
this.dispatchEvent(new Event('readystatechange'));
|
||||
}
|
||||
setRequestHeader(name: string, value: string) { this.#s.headers.push([name, value]); }
|
||||
send(body: unknown) { wire.push({ method: this.#s.method, url: this.#s.url, body, headers: [...this.#s.headers], xhr: this }); }
|
||||
reply(reply: Reply, text = '{}') {
|
||||
const failed = typeof reply === 'string';
|
||||
Object.assign(this.#s, { readyState: 4, status: failed ? 0 : reply.status, text: failed ? '' : text });
|
||||
for (const type of ['readystatechange', failed ? reply : 'load', 'loadend']) this.dispatchEvent(new Event(type));
|
||||
}
|
||||
abort() {
|
||||
if (this.#s.readyState === 4 || this.#s.readyState === 0) return;
|
||||
Object.assign(this.#s, { readyState: 4, status: 0 });
|
||||
for (const type of ['readystatechange', 'abort', 'loadend']) this.dispatchEvent(new Event(type));
|
||||
this.#s.readyState = 0;
|
||||
}
|
||||
}
|
||||
const timers: (() => void)[] = [];
|
||||
const delays: number[] = [];
|
||||
const win = {
|
||||
fetch: (async () => new Response('{}')) as typeof fetch,
|
||||
location: { href: BASE },
|
||||
XMLHttpRequest: FakeXHR as unknown as typeof XMLHttpRequest,
|
||||
setTimeout: (run: () => void, ms: number) => { delays.push(ms); timers.push(run); return timers.length; },
|
||||
clearTimeout: (id: number) => { timers[id - 1] = () => {}; },
|
||||
};
|
||||
const runTimers = () => { while (timers.some(Boolean)) { const index = timers.findIndex(Boolean); const run = timers[index]!; timers[index] = undefined as never; run(); } };
|
||||
return { win, wire, delays, runTimers, FakeXHR };
|
||||
}
|
||||
|
||||
/** X's uploader: open, on* handlers after open, headers, send; records what X's handlers saw. */
|
||||
function xUpload(win: { XMLHttpRequest: typeof XMLHttpRequest }, command: string, extra = '', body: unknown = 'chunk') {
|
||||
const xhr = new win.XMLHttpRequest();
|
||||
const seen: string[] = [];
|
||||
xhr.open(command === 'STATUS' ? 'GET' : 'POST', `${UPLOAD}?command=${command}&media_id=77${extra}`, true);
|
||||
for (const type of ['load', 'error', 'timeout', 'abort', 'loadend']) xhr.addEventListener(type, () => seen.push(type === 'load' ? `load ${xhr.status}` : type));
|
||||
xhr.addEventListener('readystatechange', () => { if (xhr.readyState === 4) seen.push('rs4'); });
|
||||
xhr.setRequestHeader('authorization', 'Bearer test');
|
||||
xhr.setRequestHeader('x-csrf-token', 'csrf');
|
||||
xhr.send(body as XMLHttpRequestBodyInit);
|
||||
return { xhr, seen };
|
||||
}
|
||||
|
||||
// ---------- Following in time order ----------
|
||||
|
||||
test('chronologicalVariables turns only enableRanking: true off', () => {
|
||||
assert.deepEqual(chronologicalVariables({ count: 40, enableRanking: true, seenTweetIds: ['1'] }), { count: 40, enableRanking: false, seenTweetIds: ['1'] });
|
||||
for (const value of [{ enableRanking: false }, { count: 20 }, { enableRanking: 'true' }, null, [], 'x']) {
|
||||
assert.equal(chronologicalVariables(value), null, JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
|
||||
test('Following rewrite: GET query and POST body of HomeLatestTimeline, nothing else', () => {
|
||||
const get = `${LATEST}?variables=${encodeURIComponent(JSON.stringify({ count: 20, enableRanking: true, requestContext: 'launch' }))}&features=%7B%7D`;
|
||||
const next = rewriteFollowingUrl(get, BASE)!;
|
||||
assert.ok(next.startsWith(LATEST), 'relative stays relative');
|
||||
const params = new URL(next, BASE).searchParams;
|
||||
assert.deepEqual(JSON.parse(params.get('variables')!), { count: 20, enableRanking: false, requestContext: 'launch' });
|
||||
assert.equal(params.get('features'), '{}', 'other parameters kept');
|
||||
assert.match(rewriteFollowingUrl(`https://x.com${get}`, BASE)!, /^https:\/\/x\.com\//);
|
||||
|
||||
const body = JSON.stringify({ variables: { count: 40, cursor: 'c', enableRanking: true, seenTweetIds: ['9'] }, features: { a: true }, queryId: 'q' });
|
||||
assert.deepEqual(JSON.parse(rewriteFollowingBody(`https://x.com${LATEST}`, BASE, body)!),
|
||||
{ variables: { count: 40, cursor: 'c', enableRanking: false, seenTweetIds: ['9'] }, features: { a: true }, queryId: 'q' });
|
||||
|
||||
const recent = JSON.stringify({ variables: { enableRanking: false } });
|
||||
assert.equal(rewriteFollowingBody(LATEST, BASE, recent), null, 'X\'s own "Recent" sort untouched');
|
||||
assert.equal(rewriteFollowingBody('/i/api/graphql/q/HomeTimeline', BASE, body), null, 'For you untouched');
|
||||
assert.equal(rewriteFollowingBody(`https://api.example${LATEST}`, BASE, body), null, 'other origins untouched');
|
||||
assert.equal(rewriteFollowingBody(LATEST, BASE, '{bad'), null);
|
||||
assert.equal(rewriteFollowingBody(LATEST, BASE, new Blob([body])), null);
|
||||
assert.equal(rewriteFollowingUrl(`${LATEST}?variables=%7Bbad`, BASE), null);
|
||||
assert.equal(rewriteFollowingUrl('/i/api/graphql/q/HomeTimeline?variables=%7B%22enableRanking%22%3Atrue%7D', BASE), null);
|
||||
});
|
||||
|
||||
test('Following rewrite on X\'s XHR: POST body and GET URL rewritten, others untouched, dispose restores', () => {
|
||||
const env = xhrEnv();
|
||||
const nativeOpen = env.FakeXHR.prototype.open;
|
||||
const nativeSend = env.FakeXHR.prototype.send;
|
||||
const handle = installFollowingChronological(env.win);
|
||||
|
||||
const post = new env.win.XMLHttpRequest();
|
||||
post.open('POST', `https://x.com${LATEST}`);
|
||||
post.send(JSON.stringify({ variables: { enableRanking: true, count: 40 }, queryId: 'q' }));
|
||||
assert.equal(JSON.parse(String(env.wire[0]!.body)).variables.enableRanking, false);
|
||||
assert.equal(env.wire[0]!.xhr, post, 'sent on X\'s own object');
|
||||
|
||||
const get = new env.win.XMLHttpRequest();
|
||||
get.open('GET', `https://x.com${LATEST}?variables=${encodeURIComponent('{"enableRanking":true}')}`);
|
||||
get.send(null);
|
||||
assert.deepEqual(JSON.parse(new URL(env.wire[1]!.url).searchParams.get('variables')!), { enableRanking: false });
|
||||
|
||||
const other = new env.win.XMLHttpRequest();
|
||||
const otherBody = JSON.stringify({ variables: { enableRanking: true } });
|
||||
other.open('POST', 'https://x.com/i/api/graphql/q/HomeTimeline');
|
||||
other.send(otherBody);
|
||||
assert.equal(env.wire[2]!.body, otherBody);
|
||||
|
||||
handle.dispose();
|
||||
assert.equal(env.FakeXHR.prototype.open, nativeOpen);
|
||||
assert.equal(env.FakeXHR.prototype.send, nativeSend);
|
||||
});
|
||||
|
||||
// ---------- media upload retry ----------
|
||||
|
||||
test('parseUpload: X\'s chunked upload commands on upload.x.com only', () => {
|
||||
assert.deepEqual(parseUpload(`${UPLOAD}?command=APPEND&media_id=77&segment_index=3`), { command: 'APPEND', mediaId: '77', segment: '3' });
|
||||
assert.deepEqual(parseUpload('https://upload.twitter.com/i/media/upload.json?command=STATUS&media_id=77'), { command: 'STATUS', mediaId: '77', segment: null });
|
||||
assert.equal(parseUpload(`${UPLOAD}?command=INIT&total_bytes=1`)?.command, 'INIT');
|
||||
for (const url of [
|
||||
`${UPLOAD}?command=DELETE`, UPLOAD, 'https://evil.example/i/media/upload.json?command=APPEND',
|
||||
'http://upload.x.com/i/media/upload.json?command=APPEND', 'https://x.com/i/api/graphql/q/CreateTweet', 'not a url',
|
||||
]) assert.equal(parseUpload(url), null, url);
|
||||
});
|
||||
|
||||
test('retry policy: only APPEND/STATUS, only failures X gives up on, at most 3 times with backoff', () => {
|
||||
const append = parseUpload(`${UPLOAD}?command=APPEND&media_id=1&segment_index=0`)!;
|
||||
const status = parseUpload(`${UPLOAD}?command=STATUS&media_id=1`)!;
|
||||
const init = parseUpload(`${UPLOAD}?command=INIT`)!;
|
||||
const finalize = parseUpload(`${UPLOAD}?command=FINALIZE&media_id=1`)!;
|
||||
for (const outcome of [{ kind: 'error' }, { kind: 'timeout' }, { kind: 'load', status: 503 }, { kind: 'load', status: 0 }] as const) {
|
||||
assert.equal(classifyUpload(outcome), 'fatal');
|
||||
assert.equal(shouldRetryUpload(append, outcome, 0), true);
|
||||
assert.equal(shouldRetryUpload(status, outcome, 2), true);
|
||||
assert.equal(shouldRetryUpload(append, outcome, MAX_UPLOAD_RETRIES), false, 'bounded');
|
||||
assert.equal(shouldRetryUpload(init, outcome, 0), false, 'INIT never repeated');
|
||||
assert.equal(shouldRetryUpload(finalize, outcome, 0), false, 'FINALIZE never repeated');
|
||||
}
|
||||
for (const outcome of [{ kind: 'load', status: 200 }, { kind: 'load', status: 204 }, { kind: 'load', status: 400 }, { kind: 'load', status: 500 }, { kind: 'abort' }] as const) {
|
||||
assert.equal(shouldRetryUpload(append, outcome, 0), false, JSON.stringify(outcome));
|
||||
}
|
||||
assert.equal(classifyUpload({ kind: 'load', status: 500 }), 'failed', 'X retries this one itself');
|
||||
assert.equal(classifyUpload({ kind: 'abort' }), 'aborted');
|
||||
assert.deepEqual([0, 1, 2].map(uploadRetryDelay), [1000, 2000, 4000]);
|
||||
assert.equal(MAX_UPLOAD_RETRIES, 3);
|
||||
});
|
||||
|
||||
test('processingFailure and failure messages', () => {
|
||||
assert.match(processingFailure('{"media_id":1,"processing_info":{"state":"failed","error":{"code":1,"name":"InvalidMedia","message":"Unsupported video format"}}}')!, /Unsupported video format/);
|
||||
assert.match(processingFailure('{"processing_info":{"state":"failed"}}')!, /could not process/);
|
||||
for (const text of ['{"processing_info":{"state":"succeeded"}}', '{"processing_info":{"state":"in_progress"}}', '{}', '{bad', 'null']) assert.equal(processingFailure(text), null, text);
|
||||
const append = parseUpload(`${UPLOAD}?command=APPEND&media_id=1&segment_index=0`)!;
|
||||
assert.match(uploadFailureMessage(append, { kind: 'load', status: 503 }, 3), /APPEND, HTTP 503 after 3 retries/);
|
||||
assert.match(uploadFailureMessage(append, { kind: 'timeout' }, 0), /APPEND, timed out\)/);
|
||||
assert.match(uploadFailureMessage(append, { kind: 'error' }, 1), /network error after 1 retry/);
|
||||
});
|
||||
|
||||
test('upload retry: a failed APPEND is re-sent on X\'s own XHR; X only sees the final success', () => {
|
||||
const env = xhrEnv();
|
||||
const notes: string[] = [];
|
||||
const handle = installUploadRetry(env.win, message => notes.push(message));
|
||||
const form = { chunk: 'bytes' };
|
||||
const { xhr, seen } = xUpload(env.win, 'APPEND', '&segment_index=2', form);
|
||||
assert.equal(env.wire.length, 1);
|
||||
(env.wire[0]!.xhr).reply('error');
|
||||
assert.deepEqual(seen, [], 'failed attempt hidden from X');
|
||||
env.runTimers();
|
||||
assert.equal(env.wire.length, 2, 'one retry');
|
||||
(env.wire[1]!.xhr).reply({ status: 503 });
|
||||
env.runTimers();
|
||||
(env.wire[2]!.xhr).reply('timeout');
|
||||
env.runTimers();
|
||||
(env.wire[3]!.xhr).reply({ status: 204 }, '');
|
||||
assert.deepEqual(env.delays, [1000, 2000, 4000]);
|
||||
for (const sent of env.wire) {
|
||||
assert.equal(sent.xhr, xhr, 'same XHR object');
|
||||
assert.equal(sent.body, form, 'same body');
|
||||
assert.equal(sent.url, env.wire[0]!.url, 'same request');
|
||||
assert.deepEqual(sent.headers, [['authorization', 'Bearer test'], ['x-csrf-token', 'csrf']], 'headers re-applied once');
|
||||
}
|
||||
assert.deepEqual(seen, ['rs4', 'load 204', 'loadend']);
|
||||
assert.deepEqual(notes, []);
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
test('upload retry: gives up after 3 retries, X sees the failure and a toast explains it', () => {
|
||||
const env = xhrEnv();
|
||||
const notes: string[] = [];
|
||||
installUploadRetry(env.win, message => notes.push(message));
|
||||
const { seen } = xUpload(env.win, 'STATUS');
|
||||
for (let attempt = 0; attempt <= MAX_UPLOAD_RETRIES; attempt++) {
|
||||
env.wire[attempt]!.xhr.reply({ status: 503 });
|
||||
env.runTimers();
|
||||
}
|
||||
assert.equal(env.wire.length, MAX_UPLOAD_RETRIES + 1);
|
||||
assert.deepEqual(seen, ['rs4', 'load 503', 'loadend']);
|
||||
assert.equal(notes.length, 1);
|
||||
assert.match(notes[0]!, /STATUS, HTTP 503 after 3 retries/);
|
||||
});
|
||||
|
||||
test('upload retry: INIT/FINALIZE and ordinary HTTP errors pass straight through', () => {
|
||||
const env = xhrEnv();
|
||||
const notes: string[] = [];
|
||||
installUploadRetry(env.win, message => notes.push(message));
|
||||
const init = xUpload(env.win, 'INIT');
|
||||
env.wire[0]!.xhr.reply('error');
|
||||
env.runTimers();
|
||||
assert.equal(env.wire.length, 1, 'INIT not repeated');
|
||||
assert.deepEqual(init.seen, ['rs4', 'error', 'loadend']);
|
||||
assert.match(notes[0]!, /INIT, network error\)/);
|
||||
|
||||
const bad = xUpload(env.win, 'APPEND', '&segment_index=0');
|
||||
env.wire[1]!.xhr.reply({ status: 400 }, '{"errors":[]}');
|
||||
env.runTimers();
|
||||
assert.equal(env.wire.length, 2, 'X\'s own retry policy handles this one');
|
||||
assert.deepEqual(bad.seen, ['rs4', 'load 400', 'loadend']);
|
||||
assert.equal(notes.length, 1);
|
||||
|
||||
const finalize = xUpload(env.win, 'FINALIZE');
|
||||
env.wire[2]!.xhr.reply({ status: 200 }, '{"processing_info":{"state":"failed","error":{"message":"Video too long"}}}');
|
||||
assert.deepEqual(finalize.seen, ['rs4', 'load 200', 'loadend']);
|
||||
assert.match(notes[1]!, /Video too long/);
|
||||
});
|
||||
|
||||
test('upload retry: X cancelling during the backoff gets abort/loadend and nothing is re-sent', () => {
|
||||
const env = xhrEnv();
|
||||
const notes: string[] = [];
|
||||
installUploadRetry(env.win, message => notes.push(message));
|
||||
const { xhr, seen } = xUpload(env.win, 'APPEND', '&segment_index=0');
|
||||
env.wire[0]!.xhr.reply('error');
|
||||
xhr.abort();
|
||||
env.runTimers();
|
||||
assert.equal(env.wire.length, 1);
|
||||
assert.deepEqual(seen, ['abort', 'loadend']);
|
||||
assert.deepEqual(notes, []);
|
||||
|
||||
const second = xUpload(env.win, 'APPEND', '&segment_index=1');
|
||||
second.xhr.abort();
|
||||
env.runTimers();
|
||||
assert.equal(env.wire.length, 2);
|
||||
assert.deepEqual(second.seen, ['rs4', 'abort', 'loadend'], 'in-flight abort is native');
|
||||
});
|
||||
|
||||
test('upload retry: non-upload XHRs are untouched and dispose unhooks', () => {
|
||||
const env = xhrEnv();
|
||||
const nativeSend = env.FakeXHR.prototype.send;
|
||||
const handle = installUploadRetry(env.win, () => {});
|
||||
const xhr = new env.win.XMLHttpRequest();
|
||||
xhr.open('POST', 'https://x.com/i/api/graphql/q/CreateTweet');
|
||||
xhr.send('{}');
|
||||
assert.equal(Object.getOwnPropertyNames(xhr).includes('abort'), false, 'no per-object overrides');
|
||||
handle.dispose();
|
||||
assert.equal(env.FakeXHR.prototype.send, nativeSend);
|
||||
});
|
||||
|
||||
// ---------- "See new posts" ----------
|
||||
|
||||
test('"." shortcut counts only without modifiers and outside text fields', () => {
|
||||
const none = { ctrl: false, alt: false, meta: false };
|
||||
assert.equal(isNewPostsShortcut('.', none, false), true);
|
||||
assert.equal(isNewPostsShortcut('.', none, true), false);
|
||||
assert.equal(isNewPostsShortcut('.', { ...none, ctrl: true }, false), false);
|
||||
assert.equal(isNewPostsShortcut(',', none, false), false);
|
||||
});
|
||||
|
||||
test('pill restore: X jumped to the top, the post is brought back, then held while posts insert above', () => {
|
||||
const state = { fallback: true, jumped: false, stable: 0 };
|
||||
// Right after X's scrollTo(0, 0): the post is still rendered, 3000px lower than before.
|
||||
assert.deepEqual(restoreStep(state, { top: 3120, targetTop: 120, scrollY: 0, targetScrollY: 3000, viewport: 800 }), { kind: 'scrollBy', dy: 3000 });
|
||||
// Virtualized away: jump near the old offset once, then look again.
|
||||
assert.deepEqual(restoreStep(state, { top: null, targetTop: 120, scrollY: 0, targetScrollY: 3000, viewport: 800 }), { kind: 'scrollTo', y: 3000 });
|
||||
// New posts inserted above push it down by their height: compensated.
|
||||
assert.deepEqual(restoreStep({ ...state, jumped: true }, { top: 1020, targetTop: 120, scrollY: 3000, targetScrollY: 3000, viewport: 800 }), { kind: 'scrollBy', dy: 900 });
|
||||
assert.deepEqual(restoreStep({ ...state, stable: 2 }, { top: 121, targetTop: 120, scrollY: 3900, targetScrollY: 3000, viewport: 800 }), { kind: 'done' });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue