Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ describe("helpers_unfollow.ts", () => {
it("should successfully unfollow an account", async () => {
const result = await DeleteHelpers.unfollowEveryoneUnfollowAccount(vm, 0);

expect(result).toEqual({ success: true, shouldReload: false });
expect(result).toEqual({
success: true,
shouldRetry: false,
shouldReload: false,
});
expect(vm.scriptMouseoverElementNth).toHaveBeenCalledWith(
'div[data-testid="cellInnerDiv"] button button',
0,
Expand All @@ -72,22 +76,32 @@ describe("helpers_unfollow.ts", () => {
expect(vm.scriptClickElement).toHaveBeenCalledWith(
'button[data-testid="confirmationSheetConfirm"]',
);
expect(mockElectron.X.isRateLimited).toHaveBeenCalled();
expect(vm.waitForRateLimit).not.toHaveBeenCalled();
});

it("should fail if mouseover fails", async () => {
vi.spyOn(vm, "scriptMouseoverElementNth").mockResolvedValue(false);

const result = await DeleteHelpers.unfollowEveryoneUnfollowAccount(vm, 0);

expect(result).toEqual({ success: false, shouldReload: true });
expect(result).toEqual({
success: false,
shouldRetry: false,
shouldReload: true,
});
});

it("should fail if click following button fails", async () => {
vi.spyOn(vm, "scriptClickElementNth").mockResolvedValue(false);

const result = await DeleteHelpers.unfollowEveryoneUnfollowAccount(vm, 0);

expect(result).toEqual({ success: false, shouldReload: true });
expect(result).toEqual({
success: false,
shouldRetry: false,
shouldReload: true,
});
});

it("should handle errors during confirmation", async () => {
Expand All @@ -99,7 +113,48 @@ describe("helpers_unfollow.ts", () => {

const result = await DeleteHelpers.unfollowEveryoneUnfollowAccount(vm, 0);

expect(result).toEqual({ success: false, shouldReload: true });
expect(result).toEqual({
success: false,
shouldRetry: false,
shouldReload: true,
});
expect(vm.waitForRateLimit).not.toHaveBeenCalled();
});

it("should wait and retry when the confirm button never appears due to a rate limit", async () => {
vi.spyOn(vm, "waitForSelector").mockRejectedValue(new Error("Error"));
mockElectron.X.isRateLimited.mockResolvedValue({
isRateLimited: true,
rateLimitReset: 0,
});

const result = await DeleteHelpers.unfollowEveryoneUnfollowAccount(vm, 0);

expect(result).toEqual({
success: false,
shouldRetry: true,
shouldReload: true,
});
expect(vm.waitForRateLimit).toHaveBeenCalled();
});

it("should wait and retry when rate limited after clicking confirm", async () => {
mockElectron.X.isRateLimited.mockResolvedValue({
isRateLimited: true,
rateLimitReset: 0,
});

const result = await DeleteHelpers.unfollowEveryoneUnfollowAccount(vm, 0);

expect(result).toEqual({
success: false,
shouldRetry: true,
shouldReload: true,
});
expect(vm.scriptClickElement).toHaveBeenCalledWith(
'button[data-testid="confirmationSheetConfirm"]',
);
expect(vm.waitForRateLimit).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -175,6 +230,31 @@ describe("helpers_unfollow.ts", () => {
);
});

it("should not trigger an error and should keep the same index when rate limited", async () => {
// Rate limited after clicking confirm: the account wasn't actually unfollowed,
// so the job must retry it rather than report an error or move on.
vm.progress.isUnfollowEveryoneFinished = false;
mockElectron.X.isRateLimited.mockResolvedValue({
isRateLimited: true,
rateLimitReset: 0,
});

const result = await DeleteHelpers.unfollowEveryoneProcessIteration(
vm,
7,
100,
);

expect(result.success).toBe(false);
expect(result.errorTriggered).toBe(false);
expect(result.errorType).toBe(null);
expect(result.shouldReload).toBe(true);
expect(result.newAccountIndex).toBe(7); // Same account gets retried
expect(vm.waitForRateLimit).toHaveBeenCalled();
// The account should not be counted as unfollowed
expect(vm.progress.accountsUnfollowed).toBe(0);
});

it("should keep same index when reload needed on error", async () => {
vm.progress.isUnfollowEveryoneFinished = false;
vi.spyOn(vm, "scriptMouseoverElementNth").mockResolvedValue(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@ export async function unfollowEveryoneCheckIfFinished(

/**
* Unfollow a single account
* @returns Object with success flag and whether to reload page
* @returns Object with success flag, whether the account was rate limited and should be retried
* and whether to reload the page
*/
export async function unfollowEveryoneUnfollowAccount(
vm: XViewModel,
accountIndex: number,
): Promise<{ success: boolean; shouldReload: boolean }> {
): Promise<{ success: boolean; shouldRetry: boolean; shouldReload: boolean }> {
// Mouseover the "Following" button on the next user
if (
!(await vm.scriptMouseoverElementNth(
'div[data-testid="cellInnerDiv"] button button',
accountIndex,
))
) {
return { success: false, shouldReload: true };
return { success: false, shouldRetry: false, shouldReload: true };
}

// Click the unfollow button
Expand All @@ -42,7 +43,7 @@ export async function unfollowEveryoneUnfollowAccount(
accountIndex,
))
) {
return { success: false, shouldReload: true };
return { success: false, shouldRetry: false, shouldReload: true };
}

// Wait for confirm button
Expand All @@ -52,11 +53,12 @@ export async function unfollowEveryoneUnfollowAccount(
vm.rateLimitInfo = await window.electron.X.isRateLimited(vm.account.id);
if (vm.rateLimitInfo.isRateLimited) {
await vm.waitForRateLimit();
return { success: false, shouldRetry: true, shouldReload: true };
}
vm.log("unfollowEveryoneUnfollowAccount", [
"wait for confirm button failed",
]);
return { success: false, shouldReload: true };
return { success: false, shouldRetry: false, shouldReload: true };
}

// Click the confirm button
Expand All @@ -65,10 +67,19 @@ export async function unfollowEveryoneUnfollowAccount(
'button[data-testid="confirmationSheetConfirm"]',
))
) {
return { success: false, shouldReload: true };
return { success: false, shouldRetry: false, shouldReload: true };
}

return { success: true, shouldReload: false };
// if we were rate limited the account wasn't actually unfollowed, so
// wait it out and retry instead of moving on.
await vm.sleep(500);
vm.rateLimitInfo = await window.electron.X.isRateLimited(vm.account.id);
if (vm.rateLimitInfo.isRateLimited) {
await vm.waitForRateLimit();
return { success: false, shouldRetry: true, shouldReload: true };
}

return { success: true, shouldRetry: false, shouldReload: false };
}

/**
Expand Down Expand Up @@ -100,6 +111,17 @@ export async function unfollowEveryoneProcessIteration(
// Unfollow the account
const result = await unfollowEveryoneUnfollowAccount(vm, accountIndex);
if (!result.success) {
// A rate limit isn't an error: we already waited it out, so just reload and retry
// this same account instead of ending the job.
if (result.shouldRetry) {
return {
success: false,
errorTriggered: false,
errorType: null,
shouldReload: result.shouldReload,
newAccountIndex: accountIndex,
};
}
return {
success: false,
errorTriggered: true,
Expand Down
Loading