{"record":{"id":"e1cd806fb656ca19","repo":"jackwener/OpenCLI","slug":"button-not-found","errorCode":"BUTTON_NOT_FOUND","errorMessage":"BUTTON_NOT_FOUND: neither Follow nor Following button found (page not rendered, blocked, or selectors changed)","messagePattern":"BUTTON_NOT_FOUND: neither Follow nor Following button found \\(page not rendered, blocked, or selectors changed\\)","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"clis/tiktok/unfollow.js","lineNumber":55,"sourceCode":"  ensureLoggedInOrThrow();\n  ensureNoRateLimitOrThrow();\n\n  const FOLLOW_LABELS = ['Follow', '关注', 'フォロー'];\n  const FOLLOWING_LABELS = ['Following', '已关注', 'フォロー中'];\n  const FRIENDS_LABELS = ['Friends', '互关', 'フレンド'];\n  const RELATION_LABELS = FOLLOWING_LABELS.concat(FRIENDS_LABELS);\n  const CONFIRM_LABELS = ['Unfollow', '取消关注'];\n\n  // Idempotent fast path: not currently following.\n  if (!buttonExists(RELATION_LABELS) && buttonExists(FOLLOW_LABELS)) {\n    return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-not-following' }];\n  }\n\n  const relationBtn = findButtonByText(RELATION_LABELS);\n  if (!relationBtn) {\n    // Neither Follow nor Following — page may not have rendered, or\n    // private account / blocked: refuse to silently succeed.\n    throw new Error('BUTTON_NOT_FOUND: neither Follow nor Following button found (page not rendered, blocked, or selectors changed)');\n  }\n\n  const target = relationBtn.closest('button') || relationBtn.closest('[role=\"button\"]') || relationBtn;\n  target.click();\n\n  // TikTok shows a confirm-unfollow dialog. Wait for it to render.\n  const dialogShown = await waitFor(() => buttonExists(CONFIRM_LABELS), { timeoutMs: 3000 });\n  if (dialogShown) {\n    const confirmBtn = findButtonByText(CONFIRM_LABELS);\n    if (!confirmBtn) {\n      throw new Error('BUTTON_NOT_FOUND: confirm-unfollow dialog detected but confirm button could not be located');\n    }\n    confirmBtn.click();\n  }\n\n  const flipped = await waitFor(\n    () => buttonExists(FOLLOW_LABELS) && !buttonExists(RELATION_LABELS),\n    { timeoutMs: 5000 },","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/tiktok/unfollow.js#L37-L73","documentation":"The unfollow script builds a browser script that locates the Follow/Following relation button by text (RELATION_LABELS). If no such element exists it throws Error with code BUTTON_NOT_FOUND, refusing to silently succeed on a page that never rendered the profile action row. This guards against unfollowing based on a broken or blocked page.","triggerScenarios":"buildUnfollowScript's findButtonByText(RELATION_LABELS) returns null: profile page didn't render (bot wall/captcha), navigation timed out, account is private or blocked you, or TikTok changed the button text/attributes so labels no longer match.","commonSituations":"Not logged in so profile shows a different layout; headless browser detected and served a challenge; profile of a suspended user; RELATION_LABELS text (e.g. localized 'Follow') no longer matching after a TikTok UI or locale change.","solutions":["Ensure a logged-in session and that the profile page loads fully (check screenshot/HTML) before unfollowing","Verify the target username's profile renders a Follow/Following button in a normal browser","Update RELATION_LABELS / findButtonByText to match current button text and locale if TikTok changed the UI","Retry with a real-browser fingerprint (less headless detection) if a challenge page is being served"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"await page.goto(`https://www.tiktok.com/@${username}`, { waitUntil: 'networkidle' });\nconst hasRelationBtn = await page.evaluate(() => document.body.innerText.includes('Follow') || document.body.innerText.includes('Following'));\nif (!hasRelationBtn) throw new Error(`Profile page for ${username} did not render a relation button`);","typeGuard":"function relationButtonExists(doc, labels) {\n  return labels.some(l => Array.from(doc.querySelectorAll('button, [role=\"button\"]')).some(el => el.textContent.trim().toLowerCase().includes(l.toLowerCase())));\n}","tryCatchPattern":"try {\n  await unfollowUser(username);\n} catch (e) {\n  if (e.message.includes('BUTTON_NOT_FOUND')) {\n    console.error(`Cannot unfollow ${username}: page not rendered, blocked, or selectors changed`);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Ensure a logged-in session; logged-out profiles render different layouts","Screenshot the page on failure to distinguish bot walls from selector drift","Keep RELATION_LABELS in sync with the current TikTok UI and locales","Do not attempt unfollows on private/blocked accounts — pre-check reachability"],"tags":["tiktok","automation","selector-not-found","dom","auth"],"backgroundTag":"dom-element-not-found","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}