From f12faf79883e62ee6f1a935f93a66eafdba4f0b2 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Fri, 31 Jul 2026 17:51:39 -0400 Subject: [PATCH] facebook: log any blocking UIs we hit --- .../FacebookViewModel/jobs_delete.test.ts | 75 +++++++++++++++++++ .../FacebookViewModel/jobs_delete.ts | 66 +++++++++++++++- 2 files changed, 137 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts index bfd8241f..e59097d4 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts @@ -121,6 +121,24 @@ function mockSafeExecuteJavaScript( return { success: true, value: false }; case "countSelectableItems": return { success: true, value: itemsPerBatch }; + case "describeBlockingUI": + // A password confirmation modal over the activity log + return { + success: true, + value: { + url: "https://www.facebook.com/123/allactivity", + passwordInputCount: 1, + passwordInputs: [{ name: "pass", ariaLabel: "Password" }], + dialogs: [ + { + ariaLabel: "Confirm password", + hasPasswordInput: true, + buttons: ["Continue"], + text: "Please re-enter your password", + }, + ], + }, + }; case "clickDeletePostsOption": return { success: true, value: clickTrashSuccess }; case "confirmDeletion": @@ -280,5 +298,62 @@ describe("FacebookViewModel Delete Jobs", () => { // The job errored before completing, so it is never marked finished expect(vm.progress.isDeleteActivityFinished).toBe(false); }); + + it("never captures a password", () => { + const PASSWORD = "correct-horse-battery-staple"; + + document.title = "Facebook"; + document.body.innerHTML = ` +
+

Please re-enter your password

+
+ +
Continue
+
+
+ `; + + const input = document.querySelector( + 'input[type="password"]', + ) as HTMLInputElement; + // As a user types it, and as a page could reflect it into the markup + input.value = PASSWORD; + input.setAttribute("value", PASSWORD); + + const snapshot = eval(DeleteJobs.BLOCKING_UI_JS); + + expect(JSON.stringify(snapshot)).not.toContain(PASSWORD); + // Still captures what we need to write a selector + expect(snapshot.passwordInputs[0]).toMatchObject({ + name: "pass", + ariaLabel: "Password", + }); + expect(snapshot.dialogs[0].buttons).toContain("Continue"); + + document.body.innerHTML = ""; + }); + + it("attaches a description of whatever blocked us to the error report", async () => { + const vm = createMockFacebookViewModel({ + facebookAccount: createMockFacebookAccount({ deleteWallPosts: true }), + }); + mockSafeExecuteJavaScript(vm, { + batchesPerCategory: 1, + clickTrashSuccess: false, + }); + + await DeleteJobs.runJobDeleteActivity(vm, 3); + + const sensitiveContext = vi.mocked(vm.error).mock.calls[0][2]; + expect(sensitiveContext.blockingUI).toMatchObject({ + passwordInputCount: 1, + dialogs: [ + expect.objectContaining({ + ariaLabel: "Confirm password", + hasPasswordInput: true, + }), + ], + }); + }); }); }); diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index f6a5c477..f745104c 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -16,14 +16,71 @@ const BATCH_COOLDOWN_MS = 5000; const PROCESSING_BACKOFF_MS = 60000; const MAX_PROCESSING_RETRIES = 15; +/** + * Attributes and rendered text only: never `.value` (so we don't capture a password) + * Exported so the tests can run the shipping code against this + */ +export const BLOCKING_UI_JS = `(() => { + const text = (el) => ((el.innerText || el.textContent) || '').replace(/\\s+/g, ' ').trim(); + const attr = (el, name) => (el.getAttribute ? el.getAttribute(name) : null); + + return { + url: document.location ? document.location.href.split('?')[0] : '', + title: (document.title || '').slice(0, 120), + bodyText: text(document.body).slice(0, 500), + passwordInputCount: document.querySelectorAll('input[type="password"]').length, + passwordInputs: Array.from(document.querySelectorAll('input[type="password"]')) + .slice(0, 3) + .map((input) => ({ + name: attr(input, 'name'), + id: attr(input, 'id'), + ariaLabel: attr(input, 'aria-label'), + placeholder: attr(input, 'placeholder'), + autocomplete: attr(input, 'autocomplete'), + })), + dialogs: Array.from(document.querySelectorAll('div[role="dialog"]')) + .slice(0, 5) + .map((dialog) => ({ + ariaLabel: attr(dialog, 'aria-label'), + ariaModal: attr(dialog, 'aria-modal'), + hasPasswordInput: dialog.querySelector('input[type="password"]') !== null, + buttons: Array.from(dialog.querySelectorAll('div[role="button"], button')) + .slice(0, 8) + .map((button) => (attr(button, 'aria-label') || text(button)).slice(0, 40)) + .filter((label) => label.length > 0), + text: text(dialog).slice(0, 300), + })), + }; +})()`; + +/** + * Describe whatever Facebook has put on screen, so an error report identifies the modal + * that blocked us such as a password confirmation + */ +async function describeBlockingUI( + vm: FacebookViewModel, +): Promise> { + const result = await vm.safeExecuteJavaScript>( + BLOCKING_UI_JS, + "describeBlockingUI", + ); + return result.success + ? result.value + : { error: `Could not inspect the page: ${result.error}` }; +} + async function reportDeleteWallPostsError( vm: FacebookViewModel, jobIndex: number, errorType: AutomationErrorType, errorReportData: Record, ) { + const blockingUI = await describeBlockingUI(vm); + vm.log("reportDeleteWallPostsError", { errorType, blockingUI }); + await vm.error(errorType, errorReportData, { currentURL: vm.webview?.getURL(), + blockingUI, }); await Helpers.errorJob(vm, jobIndex); } @@ -305,10 +362,11 @@ async function deleteCategory( // Select all currently loaded items const toggled = await toggleSelectAllCheckbox(vm, true); if (!toggled) { - vm.log( - "deleteCategory", - `Could not select items for category ${category.setting}`, - ); + // Exits without an error report, so log the page + vm.log("deleteCategory", { + message: `Could not select items for category ${category.setting}`, + blockingUI: await describeBlockingUI(vm), + }); break; }