From 954a0660ff5d0fc60968000834827398eb9dee52 Mon Sep 17 00:00:00 2001 From: Marcos Sepulveda Date: Fri, 10 Apr 2026 12:26:16 -0400 Subject: [PATCH] fix: update selectors for X.com 2026 UI - Updated composer open flow: click button + keyboard n shortcut - Updated textbox selectors for X 2026 (contenteditable, role=textbox) - Updated post button selectors - Simplified writeTweet using fill() - Updated dismissOverlays to handle X 2026 mask/popups - Updated Playwright to ^1.50.0 --- package-lock.json | 2 +- package.json | 2 +- tweet.js | 86 ++++++++++++++++++++++++++++++----------------- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index bdaaa0b..4dd0b75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "dotenv": "^16.4.5", "otplib": "^12.0.1", - "playwright": "^1.44.1" + "playwright": "^1.50.0" } }, "node_modules/@otplib/core": { diff --git a/package.json b/package.json index df88737..2ba1270 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,6 @@ "dependencies": { "dotenv": "^16.4.5", "otplib": "^12.0.1", - "playwright": "^1.44.1" + "playwright": "^1.50.0" } } diff --git a/tweet.js b/tweet.js index bef7b6a..7a29c16 100644 --- a/tweet.js +++ b/tweet.js @@ -175,13 +175,26 @@ async function saveState(context) { } async function openComposer(page) { + // First: click sidebar button (works in X 2026) + const newTweetBtn = await page.$('[data-testid="SideNav_NewTweet_Button"]'); + if (newTweetBtn) { + await newTweetBtn.click(); + await page.waitForTimeout(1000); + } + + // Second: keyboard shortcut as backup + await page.keyboard.press('n'); + await page.waitForTimeout(500); + + // Check if opened + const textbox = await page.$('[role="textbox"]'); + if (textbox) return; + + // If not, try sidebar button const composeButtons = [ '[data-testid="SideNav_NewTweet_Button"]', '[data-testid="DashButton_Profile_SidebarCompose"]', - '[data-testid="app-bar-new-tweet-button"]', - '[data-testid="AppTabBar_ComposeButton"]', - '[data-testid="toolBarComposeButton"]', - '[data-testid="compositionButton"]' + '[data-testid="app-bar-new-tweet-button"]' ]; for (const selector of composeButtons) { @@ -191,9 +204,6 @@ async function openComposer(page) { return; } } - - // Keyboard shortcut on desktop. - await page.keyboard.press('n').catch(() => {}); } async function dismissOverlays(page) { @@ -206,7 +216,8 @@ async function dismissOverlays(page) { '[data-testid="confirmationSheetCancel"]', '[data-testid="dialog"] button', '[data-testid="sheetDialog"] button', - '[data-testid="twc-cc-mask"] + div [data-testid]' + 'button[aria-label="Cerrar"]', + 'button[aria-label="Close"]' ]; for (const sel of closeSelectors) { @@ -217,26 +228,37 @@ async function dismissOverlays(page) { } } - // Remove intercepting mask if still present. + // Remove intercepting mask if still present (X 2026 usa mask diferente) await page.evaluate(() => { - document.querySelectorAll('[data-testid="twc-cc-mask"]').forEach((el) => el.remove()); - document.querySelectorAll('#layers > div[role="presentation"], #layers [style*="pointer-events"]').forEach((el) => { - el.remove(); + // Todos los tipos de masks conocidos + const masks = [ + '[data-testid="twc-cc-mask"]', + '[data-testid="mask"]', + '[data-testid="modal-mask"]' + ]; + masks.forEach(sel => { + document.querySelectorAll(sel).forEach(el => el.remove()); }); - const layer = document.getElementById('layers'); - if (layer) { - layer.style.pointerEvents = 'none'; + + // Tambien limpiar layers + const layers = document.getElementById('layers'); + if (layers) { + layers.style.pointerEvents = 'auto'; + // Remover overlays que interceptan + const children = layers.querySelectorAll(':scope > div'); + children.forEach(child => { + if (child.getAttribute('role') === 'presentation' || + child.getAttribute('data-testid')?.includes('mask')) { + child.remove(); + } + }); } }).catch(() => {}); } async function writeTweet(page, composer, tweetText) { - await composer.focus().catch(() => {}); - // Select all and insert text to avoid partial input. - const selectAllKey = process.platform === 'darwin' ? 'Meta+A' : 'Control+A'; - await page.keyboard.press(selectAllKey).catch(() => {}); - await page.keyboard.press('Backspace').catch(() => {}); - await page.keyboard.insertText(tweetText); + // Use fill() which is more robust in X 2026 + await composer.fill(tweetText); await page.waitForTimeout(300); } @@ -329,7 +351,13 @@ async function discardDraftIfPrompted(page) { } async function postTweet(page, tweetText) { - await openComposer(page); + // Primero limpiar overlays + await dismissOverlays(page); + + // Abrir compositor con "n" (funciona en X 2026) + await page.keyboard.press('n'); + await page.waitForTimeout(1000); + await dismissOverlays(page); const composer = await getComposerTextbox(page); await composer.waitFor({ state: 'visible' }); @@ -355,14 +383,12 @@ async function postTweet(page, tweetText) { } async function getComposerTextbox(page) { + // Selectors actualizados para X 2026 - preferimos contenteditable const selectors = [ - 'div[data-testid="tweetTextarea_0"] div[role="textbox"]', - 'div[role="textbox"][data-testid="tweetTextarea_0"]', - 'div[data-testid="tweetTextarea_0"]', - 'div[data-testid="tweetTextarea_1"]', - 'div[role="textbox"][data-testid^="tweetTextarea_"]', - 'div[role="textbox"][aria-label*="What"]', - 'div[role="textbox"]' + '[contenteditable="true"][role="textbox"]', + '[contenteditable="true"]', + '[role="textbox"]', + '[data-testid="tweetTextarea_0"]' ]; for (const selector of selectors) { @@ -372,7 +398,7 @@ async function getComposerTextbox(page) { } } - throw new Error('Could not find tweet composer textbox.'); +throw new Error('Could not find tweet composer textbox.'); } async function ensureLogin(page, context, username, password) {