From 48a9e1d80e344badb4c874fdb0baaf63170f43d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Lizaga?= Date: Fri, 6 Feb 2026 18:06:55 +0100 Subject: [PATCH 01/11] fix: disconnected event is not being fired (#5807) --- src/Client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Client.js b/src/Client.js index 3ab22186d0..e9f8b98981 100644 --- a/src/Client.js +++ b/src/Client.js @@ -293,6 +293,9 @@ class Client extends EventEmitter { window.AuthStore.Cmd.on('logout', async () => { await window.onLogoutEvent(); }); + window.AuthStore.Cmd.on('logout_from_bridge', async () => { + await window.onLogoutEvent(); + }); }); } From 9b53bebfd2bfd77b4964a0e23d98619411738f77 Mon Sep 17 00:00:00 2001 From: BenyFilho <168232825+BenyFilho@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:09:11 -0300 Subject: [PATCH 02/11] Fix PDF Caption (#5794) --- src/Client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Client.js b/src/Client.js index e9f8b98981..3cf24fe15e 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1026,6 +1026,7 @@ class Client extends EventEmitter { sendMediaAsDocument: options.sendMediaAsDocument, sendMediaAsHd: options.sendMediaAsHd, caption: options.caption, + isCaptionByUser: options.caption ? true : false, quotedMessageId: options.quotedMessageId, parseVCards: options.parseVCards !== false, mentionedJidList: options.mentions || [], From 9e0fe29218cdf84da84f904ad2d2a100c4ce3ff0 Mon Sep 17 00:00:00 2001 From: BenyFilho <168232825+BenyFilho@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:42:51 -0300 Subject: [PATCH 03/11] New Chat property isLocked (#5798) * New property isLocked * New property isLocked --- index.d.ts | 2 ++ src/structures/Chat.js | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/index.d.ts b/index.d.ts index ae31dc4b07..de9b781bf4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1717,6 +1717,8 @@ declare namespace WAWebJS { lastMessage: Message, /** Indicates if the Chat is pinned */ pinned: boolean, + /** Indicates if the Chat is locked */ + isLocked: boolean, /** Archives this chat */ archive: () => Promise, diff --git a/src/structures/Chat.js b/src/structures/Chat.js index 93d9a0c8dc..9cd76790c2 100644 --- a/src/structures/Chat.js +++ b/src/structures/Chat.js @@ -63,6 +63,12 @@ class Chat extends Base { */ this.pinned = !!data.pin; + /** + * Indicates if the Chat is locked + * @type {boolean} + */ + this.isLocked = data.isLocked; + /** * Indicates if the chat is muted or not * @type {boolean} From 2056ae83e1b30fff1de65decf8a50c27d460dd96 Mon Sep 17 00:00:00 2001 From: BenyFilho <168232825+BenyFilho@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:44:15 -0300 Subject: [PATCH 04/11] Fix getFormattedNumber (#5806) --- src/util/Injected/Store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/Injected/Store.js b/src/util/Injected/Store.js index 8f56a56ba6..1b9a8cd876 100644 --- a/src/util/Injected/Store.js +++ b/src/util/Injected/Store.js @@ -121,7 +121,8 @@ exports.ExposeStore = () => { }; window.Store.NumberInfo = { ...window.require('WAPhoneUtils'), - ...window.require('WAPhoneFindCC') + ...window.require('WAPhoneFindCC'), + ...window.require('WAWebPhoneUtils') }; window.Store.ForwardUtils = { ...window.require('WAWebChatForwardMessage') From 9ba93d60ec07ede8ff89493d4b2fa0247a3f1291 Mon Sep 17 00:00:00 2001 From: BenyFilho <168232825+BenyFilho@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:45:26 -0300 Subject: [PATCH 05/11] Fix loading_screen event (#5808) --- src/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.js b/src/Client.js index 3cf24fe15e..0dcac904e3 100644 --- a/src/Client.js +++ b/src/Client.js @@ -287,7 +287,7 @@ class Client extends EventEmitter { await this.pupPage.evaluate(() => { window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); }); window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); }); - window.AuthStore.Cmd.on('offline_progress_update', () => { + window.AuthStore.Cmd.on('offline_progress_update_from_bridge', () => { window.onOfflineProgressUpdateEvent(window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress()); }); window.AuthStore.Cmd.on('logout', async () => { From 4166311dafcfd354189be1d62e3a6ef4a07b4b49 Mon Sep 17 00:00:00 2001 From: 2^1 <217781969+2hoch1@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:48:40 +0100 Subject: [PATCH 06/11] ci: add rate limits for issues and prs (#66265) --- .github/workflows/issue-rate-limit.yml | 91 ++++++++++++++++++++++++++ .github/workflows/pr-rate-limit.yml | 91 ++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 .github/workflows/issue-rate-limit.yml create mode 100644 .github/workflows/pr-rate-limit.yml diff --git a/.github/workflows/issue-rate-limit.yml b/.github/workflows/issue-rate-limit.yml new file mode 100644 index 0000000000..f871fa99bd --- /dev/null +++ b/.github/workflows/issue-rate-limit.yml @@ -0,0 +1,91 @@ +name: Issue rate limit + +on: + issues: + types: [opened, reopened] + +permissions: + issues: write + contents: read + +jobs: + limit: + runs-on: ubuntu-latest + env: + BLOCK_USER_ENV: ${{ secrets.BLOCK_USER_ENV }} + steps: + - uses: actions/github-script@v7 + with: + script: | + const timeLimitMinutes = 30; + const blockThreshold = 10; + const skipMaintainers = true; + const skipBots = true; + + const author = context.payload.issue.user.login; + const authorType = context.payload.issue.user.type; + const currentIssue = context.payload.issue.number; + const since = new Date(Date.now() - timeLimitMinutes * 60 * 1000).toISOString(); + const blockToken = process.env.BLOCK_USER_ENV || ""; + + if (skipBots && (authorType === "Bot" || author === "github-bot")) { + core.info(`Skipping rate limit for bot account: ${author}`); + return; + } + + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: author + }); + + if (skipMaintainers && ["admin", "maintain", "write"].includes(permission.data.permission)) { + core.info(`Skipping rate limit for maintainer: ${author}`); + return; + } + + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue author:${author} created:>=${since}`; + const { search } = await github.graphql( + `query($searchQuery: String!) { + search(type: ISSUE, query: $searchQuery, first: 1) { + issueCount + } + }`, + { searchQuery } + ); + + const count = search.issueCount; + + core.info(`Issues in last ${timeLimitMinutes} min by ${author}: ${count}`); + + if (count > 5) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: currentIssue, + body: `🚫 Rate limit reached: Please wait a while, before creating more issues.` + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: currentIssue, + state: "closed" + }); + } + + if (count > blockThreshold) { + if (!blockToken) { + core.warning(`Missing BLOCK_USER_ENV token. Skipping block for ${author}.`); + return; + } + + const { getOctokit } = require("@actions/github"); + const blockClient = getOctokit(blockToken); + + await blockClient.rest.users.block({ + username: author + }); + + core.info(`Blocked user ${author} after ${count} issues in ${timeLimitMinutes} min.`); + } diff --git a/.github/workflows/pr-rate-limit.yml b/.github/workflows/pr-rate-limit.yml new file mode 100644 index 0000000000..eaea63d403 --- /dev/null +++ b/.github/workflows/pr-rate-limit.yml @@ -0,0 +1,91 @@ +name: PR rate limit + +on: + pull_request: + types: [opened, reopened] + +permissions: + pull-requests: write + contents: read + +jobs: + limit: + runs-on: ubuntu-latest + env: + BLOCK_USER_ENV: ${{ secrets.BLOCK_USER_ENV }} + steps: + - uses: actions/github-script@v7 + with: + script: | + const timeLimitMinutes = 30; + const blockThreshold = 10; + const skipMaintainers = true; + const skipBots = true; + + const author = context.payload.pull_request.user.login; + const authorType = context.payload.pull_request.user.type; + const currentPr = context.payload.pull_request.number; + const since = new Date(Date.now() - timeLimitMinutes * 60 * 1000).toISOString(); + const blockToken = process.env.BLOCK_USER_ENV || ""; + + if (skipBots && (authorType === "Bot" || author === "github-bot")) { + core.info(`Skipping rate limit for bot account: ${author}`); + return; + } + + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: author + }); + + if (skipMaintainers && ["admin", "maintain", "write"].includes(permission.data.permission)) { + core.info(`Skipping rate limit for maintainer: ${author}`); + return; + } + + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr author:${author} created:>=${since}`; + const { search } = await github.graphql( + `query($searchQuery: String!) { + search(type: ISSUE, query: $searchQuery, first: 1) { + issueCount + } + }`, + { searchQuery } + ); + + const count = search.issueCount; + + core.info(`PRs in last ${timeLimitMinutes} min by ${author}: ${count}`); + + if (count > 5) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: currentPr, + body: `🚫 Rate limit reached: Please wait a while, before creating more pull requests.` + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: currentPr, + state: "closed" + }); + } + + if (count > blockThreshold) { + if (!blockToken) { + core.warning(`Missing BLOCK_USER_ENV token. Skipping block for ${author}.`); + return; + } + + const { getOctokit } = require("@actions/github"); + const blockClient = getOctokit(blockToken); + + await blockClient.rest.users.block({ + username: author + }); + + core.info(`Blocked user ${author} after ${count} PRs in ${timeLimitMinutes} min.`); + } From 073cf3185ddf3af2f72cb97bc149b4125f207577 Mon Sep 17 00:00:00 2001 From: 2^1 <217781969+2hoch1@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:55:12 +0100 Subject: [PATCH 07/11] Revert "ci: add rate limits for issues and prs (#66265)" (#104563) This reverts commit 4166311dafcfd354189be1d62e3a6ef4a07b4b49. --- .github/workflows/issue-rate-limit.yml | 91 -------------------------- .github/workflows/pr-rate-limit.yml | 91 -------------------------- 2 files changed, 182 deletions(-) delete mode 100644 .github/workflows/issue-rate-limit.yml delete mode 100644 .github/workflows/pr-rate-limit.yml diff --git a/.github/workflows/issue-rate-limit.yml b/.github/workflows/issue-rate-limit.yml deleted file mode 100644 index f871fa99bd..0000000000 --- a/.github/workflows/issue-rate-limit.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Issue rate limit - -on: - issues: - types: [opened, reopened] - -permissions: - issues: write - contents: read - -jobs: - limit: - runs-on: ubuntu-latest - env: - BLOCK_USER_ENV: ${{ secrets.BLOCK_USER_ENV }} - steps: - - uses: actions/github-script@v7 - with: - script: | - const timeLimitMinutes = 30; - const blockThreshold = 10; - const skipMaintainers = true; - const skipBots = true; - - const author = context.payload.issue.user.login; - const authorType = context.payload.issue.user.type; - const currentIssue = context.payload.issue.number; - const since = new Date(Date.now() - timeLimitMinutes * 60 * 1000).toISOString(); - const blockToken = process.env.BLOCK_USER_ENV || ""; - - if (skipBots && (authorType === "Bot" || author === "github-bot")) { - core.info(`Skipping rate limit for bot account: ${author}`); - return; - } - - const permission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: author - }); - - if (skipMaintainers && ["admin", "maintain", "write"].includes(permission.data.permission)) { - core.info(`Skipping rate limit for maintainer: ${author}`); - return; - } - - const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue author:${author} created:>=${since}`; - const { search } = await github.graphql( - `query($searchQuery: String!) { - search(type: ISSUE, query: $searchQuery, first: 1) { - issueCount - } - }`, - { searchQuery } - ); - - const count = search.issueCount; - - core.info(`Issues in last ${timeLimitMinutes} min by ${author}: ${count}`); - - if (count > 5) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: currentIssue, - body: `🚫 Rate limit reached: Please wait a while, before creating more issues.` - }); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: currentIssue, - state: "closed" - }); - } - - if (count > blockThreshold) { - if (!blockToken) { - core.warning(`Missing BLOCK_USER_ENV token. Skipping block for ${author}.`); - return; - } - - const { getOctokit } = require("@actions/github"); - const blockClient = getOctokit(blockToken); - - await blockClient.rest.users.block({ - username: author - }); - - core.info(`Blocked user ${author} after ${count} issues in ${timeLimitMinutes} min.`); - } diff --git a/.github/workflows/pr-rate-limit.yml b/.github/workflows/pr-rate-limit.yml deleted file mode 100644 index eaea63d403..0000000000 --- a/.github/workflows/pr-rate-limit.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: PR rate limit - -on: - pull_request: - types: [opened, reopened] - -permissions: - pull-requests: write - contents: read - -jobs: - limit: - runs-on: ubuntu-latest - env: - BLOCK_USER_ENV: ${{ secrets.BLOCK_USER_ENV }} - steps: - - uses: actions/github-script@v7 - with: - script: | - const timeLimitMinutes = 30; - const blockThreshold = 10; - const skipMaintainers = true; - const skipBots = true; - - const author = context.payload.pull_request.user.login; - const authorType = context.payload.pull_request.user.type; - const currentPr = context.payload.pull_request.number; - const since = new Date(Date.now() - timeLimitMinutes * 60 * 1000).toISOString(); - const blockToken = process.env.BLOCK_USER_ENV || ""; - - if (skipBots && (authorType === "Bot" || author === "github-bot")) { - core.info(`Skipping rate limit for bot account: ${author}`); - return; - } - - const permission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: author - }); - - if (skipMaintainers && ["admin", "maintain", "write"].includes(permission.data.permission)) { - core.info(`Skipping rate limit for maintainer: ${author}`); - return; - } - - const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr author:${author} created:>=${since}`; - const { search } = await github.graphql( - `query($searchQuery: String!) { - search(type: ISSUE, query: $searchQuery, first: 1) { - issueCount - } - }`, - { searchQuery } - ); - - const count = search.issueCount; - - core.info(`PRs in last ${timeLimitMinutes} min by ${author}: ${count}`); - - if (count > 5) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: currentPr, - body: `🚫 Rate limit reached: Please wait a while, before creating more pull requests.` - }); - - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: currentPr, - state: "closed" - }); - } - - if (count > blockThreshold) { - if (!blockToken) { - core.warning(`Missing BLOCK_USER_ENV token. Skipping block for ${author}.`); - return; - } - - const { getOctokit } = require("@actions/github"); - const blockClient = getOctokit(blockToken); - - await blockClient.rest.users.block({ - username: author - }); - - core.info(`Blocked user ${author} after ${count} PRs in ${timeLimitMinutes} min.`); - } From 9b1eb76b2ba0fd26e1d8f46e0bf8ca52bea2506c Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 11 Feb 2026 01:24:54 +0200 Subject: [PATCH 08/11] general: tree-shaking pre-MD and old comet versions (#5675) Co-authored-by: BenyFilho <168232825+BenyFilho@users.noreply.github.com> Co-authored-by: tuyuribr <45042245+tuyuribr@users.noreply.github.com> --- index.d.ts | 27 +--- package.json | 1 - src/Client.js | 151 +++++++----------- src/structures/GroupChat.js | 5 +- src/structures/Message.js | 8 +- src/util/Constants.js | 2 +- .../Injected/AuthStore/LegacyAuthStore.js | 22 --- src/util/Injected/LegacyStore.js | 146 ----------------- tests/client.js | 132 +-------------- tests/helper.js | 45 +----- 10 files changed, 70 insertions(+), 469 deletions(-) delete mode 100644 src/util/Injected/AuthStore/LegacyAuthStore.js delete mode 100644 src/util/Injected/LegacyStore.js diff --git a/index.d.ts b/index.d.ts index de9b781bf4..f7473cdaa9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -323,10 +323,6 @@ declare namespace WAWebJS { /** Emitted when authentication is successful */ on(event: 'authenticated', listener: ( - /** - * Object containing session information, when using LegacySessionAuth. Can be used to restore the session - */ - session?: ClientSession ) => void): this /** @@ -567,7 +563,7 @@ declare namespace WAWebJS { evalOnNewDoc?: Function, /** Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/ */ puppeteer?: puppeteer.PuppeteerNodeLaunchOptions & puppeteer.ConnectOptions - /** Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used. */ + /** Determines how to save and restore sessions. Otherwise, NoAuth will be used. */ authStrategy?: AuthStrategy, /** The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved. */ webVersion?: string, @@ -575,15 +571,7 @@ declare namespace WAWebJS { webVersionCache?: WebCacheOptions, /** How many times should the qrcode be refreshed before giving up * @default 0 (disabled) */ - qrMaxRetries?: number, - /** - * @deprecated This option should be set directly on the LegacySessionAuth - */ - restartOnAuthFail?: boolean - /** - * @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly. - */ - session?: ClientSession + qrMaxRetries?: number /** If another whatsapp web session is detected (another browser), take over the session in the current browser * @default false */ takeoverOnConflict?: boolean, @@ -700,17 +688,6 @@ declare namespace WAWebJS { extract: (options: { session: string, path: string }) => Promise | any, } - /** - * Legacy session auth strategy - * Not compatible with multi-device accounts. - */ - export class LegacySessionAuth extends AuthStrategy { - constructor(options?: { - session?: ClientSession, - restartOnAuthFail?: boolean, - }) - } - /** * Represents a WhatsApp client session */ diff --git a/package.json b/package.json index 9b1e05da48..87716808e2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ }, "homepage": "https://wwebjs.dev/", "dependencies": { - "@pedroslopez/moduleraid": "^5.0.2", "fluent-ffmpeg": "2.1.3", "mime": "^3.0.0", "node-fetch": "^2.6.9", diff --git a/src/Client.js b/src/Client.js index 0dcac904e3..9ee80469fd 100644 --- a/src/Client.js +++ b/src/Client.js @@ -2,15 +2,12 @@ const EventEmitter = require('events'); const puppeteer = require('puppeteer'); -const moduleRaid = require('@pedroslopez/moduleraid/moduleraid'); const Util = require('./util/Util'); const InterfaceController = require('./util/InterfaceController'); const { WhatsWebURL, DefaultOptions, Events, WAState, MessageTypes } = require('./util/Constants'); const { ExposeAuthStore } = require('./util/Injected/AuthStore/AuthStore'); const { ExposeStore } = require('./util/Injected/Store'); -const { ExposeLegacyAuthStore } = require('./util/Injected/AuthStore/LegacyAuthStore'); -const { ExposeLegacyStore } = require('./util/Injected/LegacyStore'); const { LoadUtils } = require('./util/Injected/Utils'); const ChatFactory = require('./factories/ChatFactory'); const ContactFactory = require('./factories/ContactFactory'); @@ -23,15 +20,13 @@ const {exposeFunctionIfAbsent} = require('./util/Puppeteer'); * Starting point for interacting with the WhatsApp Web API * @extends {EventEmitter} * @param {object} options - Client options - * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used. + * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Otherwise, NoAuth will be used. * @param {string} options.webVersion - The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved. * @param {object} options.webVersionCache - Determines how to retrieve the WhatsApp Web version. Defaults to a local cache (LocalWebCache) that falls back to latest if the requested version is not found. * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer * @param {function} options.evalOnNewDoc - function to eval on new doc * @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/ * @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up - * @param {string} options.restartOnAuthFail - @deprecated This option should be set directly on the LegacySessionAuth. - * @param {object} options.session - @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly. * @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser * @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session * @param {string} options.userAgent - User agent to use in puppeteer @@ -113,13 +108,8 @@ class Client extends EventEmitter { await this.setDeviceName(this.options.deviceName, this.options.browserName); const pairWithPhoneNumber = this.options.pairWithPhoneNumber; const version = await this.getWWebVersion(); - const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000; - if (isCometOrAbove) { - await this.pupPage.evaluate(ExposeAuthStore); - } else { - await this.pupPage.evaluate(ExposeLegacyAuthStore, moduleRaid.toString()); - } + await this.pupPage.evaluate(ExposeAuthStore); const needAuthentication = await this.pupPage.evaluate(async () => { let state = window.AuthStore.AppState.state; @@ -231,14 +221,8 @@ class Client extends EventEmitter { await webCache.persist(this.currentIndexHtml, version); } - if (isCometOrAbove) { - await this.pupPage.evaluate(ExposeStore); - } else { - // make sure all modules are ready before injection - // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed - await new Promise(r => setTimeout(r, 2000)); - await this.pupPage.evaluate(ExposeLegacyStore); - } + await this.pupPage.evaluate(ExposeStore); + let start = Date.now(); let res = false; while(start > (Date.now() - 30000)){ @@ -354,17 +338,14 @@ class Client extends EventEmitter { } // ocVersion (isOfficialClient patch) - // remove after 2.3000.x hard release await page.evaluateOnNewDocument(() => { - const originalError = Error; - window.originalError = originalError; + window.originalError = Error; //eslint-disable-next-line no-global-assign - Error = function (message) { - const error = new originalError(message); - const originalStack = error.stack; - if (error.stack.includes('moduleRaid')) error.stack = originalStack + '\n at https://web.whatsapp.com/vendors~lazy_loaded_low_priority_components.05e98054dbd60f980427.js:2:44'; + Error = ((message) => { + const error = new window.originalError(message); + error.stack = error.stack + '\n at https://web.whatsapp.com/vendors~lazy_loaded_low_priority_components.05e98054dbd60f980427.js:2:44'; return error; - }; + }).bind(Error); }); await page.goto(WhatsWebURL, { @@ -778,70 +759,54 @@ class Client extends EventEmitter { }); window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);}); - if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620')) { - const module = window.Store.AddonReactionTable; - const ogMethod = module.bulkUpsert; - module.bulkUpsert = ((...args) => { - window.onReaction(args[0].map(reaction => { - const msgKey = reaction.id; - const parentMsgKey = reaction.reactionParentKey; - const timestamp = reaction.reactionTimestamp / 1000; - const sender = reaction.author ?? reaction.from; - const senderUserJid = sender._serialized; - - return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp }; - })); - - return ogMethod(...args); - }).bind(module); - - const pollVoteModule = window.Store.AddonPollVoteTable; - const ogPollVoteMethod = pollVoteModule.bulkUpsert; - - pollVoteModule.bulkUpsert = (async (...args) => { - const votes = await Promise.all(args[0].map(async vote => { - const msgKey = vote.id; - const parentMsgKey = vote.pollUpdateParentKey; - const timestamp = vote.t / 1000; - const sender = vote.author ?? vote.from; - const senderUserJid = sender._serialized; - - let parentMessage = window.Store.Msg.get(parentMsgKey._serialized); - if (!parentMessage) { - const fetched = await window.Store.Msg.getMessagesById([parentMsgKey._serialized]); - parentMessage = fetched?.messages?.[0] || null; - } + const module = window.Store.AddonReactionTable; + const ogMethod = module.bulkUpsert; + module.bulkUpsert = ((...args) => { + window.onReaction(args[0].map(reaction => { + const msgKey = reaction.id; + const parentMsgKey = reaction.reactionParentKey; + const timestamp = reaction.reactionTimestamp / 1000; + const sender = reaction.author ?? reaction.from; + const senderUserJid = sender._serialized; + + return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp }; + })); - return { - ...vote, - msgKey, - sender, - parentMsgKey, - senderUserJid, - timestamp, - parentMessage - }; - })); - - window.onPollVoteEvent(votes); - - return ogPollVoteMethod.apply(pollVoteModule, args); - }).bind(pollVoteModule); - } else { - const module = window.Store.createOrUpdateReactionsModule; - const ogMethod = module.createOrUpdateReactions; - module.createOrUpdateReactions = ((...args) => { - window.onReaction(args[0].map(reaction => { - const msgKey = window.Store.MsgKey.fromString(reaction.msgKey); - const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey); - const timestamp = reaction.timestamp / 1000; - - return {...reaction, msgKey, parentMsgKey, timestamp }; - })); - - return ogMethod(...args); - }).bind(module); - } + return ogMethod(...args); + }).bind(module); + + const pollVoteModule = window.Store.AddonPollVoteTable; + const ogPollVoteMethod = pollVoteModule.bulkUpsert; + + pollVoteModule.bulkUpsert = (async (...args) => { + const votes = await Promise.all(args[0].map(async vote => { + const msgKey = vote.id; + const parentMsgKey = vote.pollUpdateParentKey; + const timestamp = vote.t / 1000; + const sender = vote.author ?? vote.from; + const senderUserJid = sender._serialized; + + let parentMessage = window.Store.Msg.get(parentMsgKey._serialized); + if (!parentMessage) { + const fetched = await window.Store.Msg.getMessagesById([parentMsgKey._serialized]); + parentMessage = fetched?.messages?.[0] || null; + } + + return { + ...vote, + msgKey, + sender, + parentMsgKey, + senderUserJid, + timestamp, + parentMessage + }; + })); + + window.onPollVoteEvent(votes); + + return ogPollVoteMethod.apply(pollVoteModule, args); + }).bind(pollVoteModule); }); } @@ -1564,9 +1529,7 @@ class Client extends EventEmitter { const profilePic = await this.pupPage.evaluate(async contactId => { try { const chatWid = window.Store.WidFactory.createWid(contactId); - return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0') - ? await window.Store.ProfilePic.profilePicFind(chatWid) - : await window.Store.ProfilePic.requestProfilePicFromServer(chatWid); + return await window.Store.ProfilePic.requestProfilePicFromServer(chatWid); } catch (err) { if(err.name === 'ServerStatusCodeError') return undefined; throw err; diff --git a/src/structures/GroupChat.js b/src/structures/GroupChat.js index ae4c95de5d..63f8832c9a 100644 --- a/src/structures/GroupChat.js +++ b/src/structures/GroupChat.js @@ -389,11 +389,8 @@ class GroupChat extends Chat { */ async getInviteCode() { const codeRes = await this.client.pupPage.evaluate(async chatId => { - const chatWid = window.Store.WidFactory.createWid(chatId); try { - return window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1020730154') - ? await window.Store.GroupInvite.fetchMexGroupInviteCode(chatId) - : await window.Store.GroupInvite.queryGroupInviteCode(chatWid, true); + return await window.Store.GroupInvite.fetchMexGroupInviteCode(chatId); } catch (err) { if(err.name === 'ServerStatusCodeError') return undefined; diff --git a/src/structures/Message.js b/src/structures/Message.js index f563c1aeb0..62bb7dceef 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -515,14 +515,10 @@ class Message extends Base { window.Store.MsgActionChecks.canSenderRevokeMsg(msg) || window.Store.MsgActionChecks.canAdminRevokeMsg(msg); if (everyone && canRevoke) { - return window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.0') - ? window.Store.Cmd.sendRevokeMsgs(chat, { list: [msg], type: 'message' }, { clearMedia: clearMedia }) - : window.Store.Cmd.sendRevokeMsgs(chat, [msg], { clearMedia: true, type: msg.id.fromMe ? 'Sender' : 'Admin' }); + return window.Store.Cmd.sendRevokeMsgs(chat, { list: [msg], type: 'message' }, { clearMedia: clearMedia }); } - return window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.0') - ? window.Store.Cmd.sendDeleteMsgs(chat, { list: [msg], type: 'message' }, clearMedia) - : window.Store.Cmd.sendDeleteMsgs(chat, [msg], clearMedia); + return window.Store.Cmd.sendDeleteMsgs(chat, { list: [msg], type: 'message' }, clearMedia); }, this.id._serialized, everyone, clearMedia); } diff --git a/src/util/Constants.js b/src/util/Constants.js index 053acd34e6..12624245ad 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -7,7 +7,7 @@ exports.DefaultOptions = { headless: true, defaultViewport: null }, - webVersion: '2.3000.1017054665', + webVersion: '2.3000.1030947950', webVersionCache: { type: 'local', }, diff --git a/src/util/Injected/AuthStore/LegacyAuthStore.js b/src/util/Injected/AuthStore/LegacyAuthStore.js deleted file mode 100644 index c3016f7d17..0000000000 --- a/src/util/Injected/AuthStore/LegacyAuthStore.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -//TODO: To be removed by version 2.3000.x hard release - -exports.ExposeLegacyAuthStore = (moduleRaidStr) => { - eval('var moduleRaid = ' + moduleRaidStr); - // eslint-disable-next-line no-undef - window.mR = moduleRaid(); - window.AuthStore = {}; - window.AuthStore.AppState = window.mR.findModule('Socket')[0].Socket; - window.AuthStore.Cmd = window.mR.findModule('Cmd')[0].Cmd; - window.AuthStore.Conn = window.mR.findModule('Conn')[0].Conn; - window.AuthStore.OfflineMessageHandler = window.mR.findModule('OfflineMessageHandler')[0].OfflineMessageHandler; - window.AuthStore.PairingCodeLinkUtils = window.mR.findModule('initializeAltDeviceLinking')[0]; - window.AuthStore.Base64Tools = window.mR.findModule('encodeB64')[0]; - window.AuthStore.RegistrationUtils = { - ...window.mR.findModule('getCompanionWebClientFromBrowser')[0], - ...window.mR.findModule('verifyKeyIndexListAccountSignature')[0], - ...window.mR.findModule('waNoiseInfo')[0], - ...window.mR.findModule('waSignalStore')[0], - }; -}; \ No newline at end of file diff --git a/src/util/Injected/LegacyStore.js b/src/util/Injected/LegacyStore.js deleted file mode 100644 index e9584d9b7a..0000000000 --- a/src/util/Injected/LegacyStore.js +++ /dev/null @@ -1,146 +0,0 @@ -'use strict'; - -//TODO: To be removed by version 2.3000.x hard release - -// Exposes the internal Store to the WhatsApp Web client -exports.ExposeLegacyStore = () => { - window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default); - window.Store.AppState = window.mR.findModule('Socket')[0].Socket; - window.Store.Conn = window.mR.findModule('Conn')[0].Conn; - window.Store.BlockContact = window.mR.findModule('blockContact')[0]; - window.Store.Call = window.mR.findModule((module) => module.default && module.default.Call)[0].default.Call; - window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd; - window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0]; - window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager; - window.Store.GroupMetadata = window.mR.findModule('GroupMetadata')[0].default.GroupMetadata; - window.Store.GroupQueryAndUpdate = window.mR.findModule('queryAndUpdateGroupMetadataById')[0].queryAndUpdateGroupMetadataById; - window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection; - window.Store.MediaPrep = window.mR.findModule('prepRawMedia')[0]; - window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0]; - window.Store.NumberInfo = window.mR.findModule('formattedPhoneNumber')[0]; - window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0]; - window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0]; - window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default; - window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default; - window.Store.QueryProduct = window.mR.findModule('queryProduct')[0]; - window.Store.QueryOrder = window.mR.findModule('queryOrder')[0]; - window.Store.SendClear = window.mR.findModule('sendClear')[0]; - window.Store.SendDelete = window.mR.findModule('sendDelete')[0]; - window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0]; - window.Store.EditMessage = window.mR.findModule('addAndSendMessageEdit')[0]; - window.Store.SendSeen = window.mR.findModule('sendSeen')[0]; - window.Store.User = window.mR.findModule('getMaybeMeUser')[0]; - window.Store.ContactMethods = window.mR.findModule('getUserid')[0]; - window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default; - window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default; - window.Store.Validators = window.mR.findModule('findLinks')[0]; - window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0]; - window.Store.WidFactory = window.mR.findModule('createWid')[0]; - window.Store.ProfilePic = window.mR.findModule('profilePicResync')[0]; - window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0]; - window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0]; - window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups; - window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0]; - window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0]; - window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg; - window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0]; - window.Store.EphemeralFields = window.mR.findModule('getEphemeralFields')[0]; - window.Store.MsgActionChecks = window.mR.findModule('canSenderRevokeMsg')[0]; - window.Store.QuotedMsg = window.mR.findModule('getQuotedMsgObj')[0]; - window.Store.LinkPreview = window.mR.findModule('getLinkPreview')[0]; - window.Store.Socket = window.mR.findModule('deprecatedSendIq')[0]; - window.Store.SocketWap = window.mR.findModule('wap')[0]; - window.Store.SearchContext = window.mR.findModule('getSearchContext')[0].getSearchContext; - window.Store.DrawerManager = window.mR.findModule('DrawerManager')[0].DrawerManager; - window.Store.LidUtils = window.mR.findModule('getCurrentLid')[0]; - window.Store.WidToJid = window.mR.findModule('widToUserJid')[0]; - window.Store.JidToWid = window.mR.findModule('userJidToUserWid')[0]; - window.Store.getMsgInfo = (window.mR.findModule('sendQueryMsgInfo')[0] || {}).sendQueryMsgInfo || window.mR.findModule('queryMsgInfo')[0].queryMsgInfo; - window.Store.pinUnpinMsg = window.mR.findModule('sendPinInChatMsg')[0].sendPinInChatMsg; - - /* eslint-disable no-undef, no-cond-assign */ - window.Store.QueryExist = ((m = window.mR.findModule('queryExists')[0]) ? m.queryExists : window.mR.findModule('queryExist')[0].queryWidExists); - window.Store.ReplyUtils = (m = window.mR.findModule('canReplyMsg')).length > 0 && m[0]; - /* eslint-enable no-undef, no-cond-assign */ - - window.Store.Settings = { - ...window.mR.findModule('ChatlistPanelState')[0], - setPushname: window.mR.findModule((m) => m.setPushname && !m.ChatlistPanelState)[0].setPushname - }; - window.Store.StickerTools = { - ...window.mR.findModule('toWebpSticker')[0], - ...window.mR.findModule('addWebpMetadata')[0] - }; - window.Store.GroupUtils = { - ...window.mR.findModule('createGroup')[0], - ...window.mR.findModule('setGroupDescription')[0], - ...window.mR.findModule('sendExitGroup')[0], - ...window.mR.findModule('sendSetPicture')[0] - }; - window.Store.GroupParticipants = { - ...window.mR.findModule('promoteParticipants')[0], - ...window.mR.findModule('sendAddParticipantsRPC')[0] - }; - window.Store.GroupInvite = { - ...window.mR.findModule('resetGroupInviteCode')[0], - ...window.mR.findModule('queryGroupInvite')[0] - }; - window.Store.GroupInviteV4 = { - ...window.mR.findModule('queryGroupInviteV4')[0], - ...window.mR.findModule('sendGroupInviteMessage')[0] - }; - window.Store.MembershipRequestUtils = { - ...window.mR.findModule('getMembershipApprovalRequests')[0], - ...window.mR.findModule('sendMembershipRequestsActionRPC')[0] - }; - - if (!window.Store.Chat._find) { - window.Store.Chat._find = e => { - const target = window.Store.Chat.get(e); - return target ? Promise.resolve(target) : Promise.resolve({ - id: e - }); - }; - } - - // eslint-disable-next-line no-undef - if ((m = window.mR.findModule('ChatCollection')[0]) && m.ChatCollection && typeof m.ChatCollection.findImpl === 'undefined' && typeof m.ChatCollection._find !== 'undefined') m.ChatCollection.findImpl = m.ChatCollection._find; - - const _isMDBackend = window.mR.findModule('isMDBackend'); - if(_isMDBackend && _isMDBackend[0] && _isMDBackend[0].isMDBackend) { - window.Store.MDBackend = _isMDBackend[0].isMDBackend(); - } else { - window.Store.MDBackend = true; - } - - const _features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0]; - if(_features) { - window.Store.Features = _features.LegacyPhoneFeatures; - } - - /** - * Target options object description - * @typedef {Object} TargetOptions - * @property {string|number} module The name or a key of the target module to search - * @property {number} index The index value of the target module - * @property {string} function The function name to get from a module - */ - - /** - * Function to modify functions - * @param {TargetOptions} target Options specifying the target function to search for modifying - * @param {Function} callback Modified function - */ - window.injectToFunction = (target, callback) => { - const module = typeof target.module === 'string' - ? window.mR.findModule(target.module) - : window.mR.modules[target.module]; - const originalFunction = module[target.index][target.function]; - const modifiedFunction = (...args) => callback(originalFunction, ...args); - module[target.index][target.function] = modifiedFunction; - }; - - window.injectToFunction({ module: 'mediaTypeFromProtobuf', index: 0, function: 'mediaTypeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage ? null : func(...args); }); - - window.injectToFunction({ module: 'typeAttributeFromProtobuf', index: 0, function: 'typeAttributeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage || proto.groupInviteMessage ? 'text' : func(...args); }); -}; \ No newline at end of file diff --git a/tests/client.js b/tests/client.js index 0118c2010e..ac2883d9cc 100644 --- a/tests/client.js +++ b/tests/client.js @@ -8,14 +8,12 @@ const Contact = require('../src/structures/Contact'); const Message = require('../src/structures/Message'); const MessageMedia = require('../src/structures/MessageMedia'); const Location = require('../src/structures/Location'); -const LegacySessionAuth = require('../src/authStrategies/LegacySessionAuth'); -const { MessageTypes, WAState, DefaultOptions } = require('../src/util/Constants'); +const { MessageTypes, DefaultOptions } = require('../src/util/Constants'); const expect = chai.expect; chai.use(chaiAsPromised); const remoteId = helper.remoteId; -const isMD = helper.isMD(); describe('Client', function() { describe('User Agent', function () { @@ -139,139 +137,11 @@ describe('Client', function() { await client.initialize(); expect(authenticatedCallback.called).to.equal(true); - - if(helper.isUsingLegacySession()) { - const newSession = authenticatedCallback.args[0][0]; - expect(newSession).to.have.key([ - 'WABrowserId', - 'WASecretBundle', - 'WAToken1', - 'WAToken2' - ]); - } - expect(readyCallback.called).to.equal(true); expect(qrCallback.called).to.equal(false); await client.destroy(); }); - - describe('LegacySessionAuth', function () { - it('should fail auth if session is invalid', async function() { - this.timeout(40000); - - const authFailCallback = sinon.spy(); - const qrCallback = sinon.spy(); - const readyCallback = sinon.spy(); - - const client = helper.createClient({ - options: { - authStrategy: new LegacySessionAuth({ - session: { - WABrowserId: 'invalid', - WASecretBundle: 'invalid', - WAToken1: 'invalid', - WAToken2: 'invalid' - }, - restartOnAuthFail: false, - }), - } - }); - - client.on('qr', qrCallback); - client.on('auth_failure', authFailCallback); - client.on('ready', readyCallback); - - client.initialize(); - - await helper.sleep(25000); - - expect(authFailCallback.called).to.equal(true); - expect(authFailCallback.args[0][0]).to.equal('Unable to log in. Are the session details valid?'); - - expect(readyCallback.called).to.equal(false); - expect(qrCallback.called).to.equal(false); - - await client.destroy(); - }); - - it('can restart without a session if session was invalid and restartOnAuthFail=true', async function() { - this.timeout(40000); - - const authFailCallback = sinon.spy(); - const qrCallback = sinon.spy(); - - const client = helper.createClient({ - options: { - authStrategy: new LegacySessionAuth({ - session: { - WABrowserId: 'invalid', - WASecretBundle: 'invalid', - WAToken1: 'invalid', - WAToken2: 'invalid' - }, - restartOnAuthFail: true, - }), - } - }); - - client.on('auth_failure', authFailCallback); - client.on('qr', qrCallback); - - client.initialize(); - - await helper.sleep(35000); - - expect(authFailCallback.called).to.equal(true); - expect(qrCallback.called).to.equal(true); - expect(qrCallback.args[0][0]).to.have.length.greaterThanOrEqual(152); - - await client.destroy(); - }); - }); - - describe('Non-MD only', function () { - if(!isMD) { - it('can take over if client was logged in somewhere else with takeoverOnConflict=true', async function() { - this.timeout(40000); - - const readyCallback1 = sinon.spy(); - const readyCallback2 = sinon.spy(); - const disconnectedCallback1 = sinon.spy(); - const disconnectedCallback2 = sinon.spy(); - - const client1 = helper.createClient({ - authenticated: true, - options: { takeoverOnConflict: true, takeoverTimeoutMs: 5000 } - }); - const client2 = helper.createClient({authenticated: true}); - - client1.on('ready', readyCallback1); - client2.on('ready', readyCallback2); - client1.on('disconnected', disconnectedCallback1); - client2.on('disconnected', disconnectedCallback2); - - await client1.initialize(); - expect(readyCallback1.called).to.equal(true); - expect(readyCallback2.called).to.equal(false); - expect(disconnectedCallback1.called).to.equal(false); - expect(disconnectedCallback2.called).to.equal(false); - - await client2.initialize(); - expect(readyCallback2.called).to.equal(true); - expect(disconnectedCallback1.called).to.equal(false); - expect(disconnectedCallback2.called).to.equal(false); - - // wait for takeoverTimeoutMs to kick in - await helper.sleep(5200); - expect(disconnectedCallback1.called).to.equal(false); - expect(disconnectedCallback2.called).to.equal(true); - expect(disconnectedCallback2.calledWith(WAState.CONFLICT)).to.equal(true); - - await client1.destroy(); - }); - } - }); }); describe('Authenticated', function() { diff --git a/tests/helper.js b/tests/helper.js index fc9ddae556..8a60d97dd7 100644 --- a/tests/helper.js +++ b/tests/helper.js @@ -1,50 +1,19 @@ -const path = require('path'); -const { Client, LegacySessionAuth, LocalAuth } = require('..'); +const { Client, LocalAuth } = require('..'); require('dotenv').config(); const remoteId = process.env.WWEBJS_TEST_REMOTE_ID; if(!remoteId) throw new Error('The WWEBJS_TEST_REMOTE_ID environment variable has not been set.'); -function isUsingLegacySession() { - return Boolean(process.env.WWEBJS_TEST_SESSION || process.env.WWEBJS_TEST_SESSION_PATH); -} - -function isMD() { - return Boolean(process.env.WWEBJS_TEST_MD); -} - -if(isUsingLegacySession() && isMD()) throw 'Cannot use legacy sessions with WWEBJS_TEST_MD=true'; - -function getSessionFromEnv() { - if (!isUsingLegacySession()) return null; - - const envSession = process.env.WWEBJS_TEST_SESSION; - if(envSession) return JSON.parse(envSession); - - const envSessionPath = process.env.WWEBJS_TEST_SESSION_PATH; - if(envSessionPath) { - const absPath = path.resolve(process.cwd(), envSessionPath); - return require(absPath); - } -} - function createClient({authenticated, options: additionalOpts}={}) { const options = {}; if(authenticated) { - const legacySession = getSessionFromEnv(); - if(legacySession) { - options.authStrategy = new LegacySessionAuth({ - session: legacySession - }); - } else { - const clientId = process.env.WWEBJS_TEST_CLIENT_ID; - if(!clientId) throw new Error('No session found in environment.'); - options.authStrategy = new LocalAuth({ - clientId - }); - } + const clientId = process.env.WWEBJS_TEST_CLIENT_ID; + if(!clientId) throw new Error('No session found in environment.'); + options.authStrategy = new LocalAuth({ + clientId + }); } const allOpts = {...options, ...(additionalOpts || {})}; @@ -58,7 +27,5 @@ function sleep(ms) { module.exports = { sleep, createClient, - isUsingLegacySession, - isMD, remoteId, }; \ No newline at end of file From 9e42f1e0c393b7e2652f3698e0cac9eab237392e Mon Sep 17 00:00:00 2001 From: BenyFilho <168232825+BenyFilho@users.noreply.github.com> Date: Wed, 18 Feb 2026 04:03:01 -0300 Subject: [PATCH 09/11] Fix: Frozen WhatsApp Start or Auth Timeout (#127048) --- src/Client.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Client.js b/src/Client.js index 9ee80469fd..def3d2e29c 100644 --- a/src/Client.js +++ b/src/Client.js @@ -337,17 +337,6 @@ class Client extends EventEmitter { await page.evaluateOnNewDocument(this.options.evalOnNewDoc); } - // ocVersion (isOfficialClient patch) - await page.evaluateOnNewDocument(() => { - window.originalError = Error; - //eslint-disable-next-line no-global-assign - Error = ((message) => { - const error = new window.originalError(message); - error.stack = error.stack + '\n at https://web.whatsapp.com/vendors~lazy_loaded_low_priority_components.05e98054dbd60f980427.js:2:44'; - return error; - }).bind(Error); - }); - await page.goto(WhatsWebURL, { waitUntil: 'load', timeout: 0, From f0bcaeda85b1ac10765cb02ab92a8f6d2e0cebf0 Mon Sep 17 00:00:00 2001 From: Adi Aharoni Date: Sat, 28 Feb 2026 12:45:36 +0200 Subject: [PATCH 10/11] fix: make inject() resilient to page navigation during initialization Replace manual evaluate-based polling loops with waitForFunction, which natively survives execution context destruction caused by page navigation (e.g. Chrome's internal IndexedDB recovery after system sleep/resume). Also move the framenavigated listener registration before the initial inject() call, so navigation events during inject are handled by the existing listener. Co-Authored-By: Claude Opus 4.6 --- src/Client.js | 39 ++---- tests/ab-comparison.js | 203 +++++++++++++++++++++++++++++++ tests/inject-navigation.js | 243 +++++++++++++++++++++++++++++++++++++ 3 files changed, 458 insertions(+), 27 deletions(-) create mode 100644 tests/ab-comparison.js create mode 100644 tests/inject-navigation.js diff --git a/src/Client.js b/src/Client.js index def3d2e29c..dacc3d9275 100644 --- a/src/Client.js +++ b/src/Client.js @@ -91,20 +91,11 @@ class Client extends EventEmitter { * Private function */ async inject() { - if(this.options.authTimeoutMs === undefined || this.options.authTimeoutMs==0){ - this.options.authTimeoutMs = 30000; - } - let start = Date.now(); - let timeout = this.options.authTimeoutMs; - let res = false; - while(start > (Date.now() - timeout)){ - res = await this.pupPage.evaluate('window.Debug?.VERSION != undefined'); - if(res){break;} - await new Promise(r => setTimeout(r, 200)); - } - if(!res){ - throw 'auth timeout'; - } + const authTimeout = this.options.authTimeoutMs || 30000; + await this.pupPage.waitForFunction( + 'window.Debug?.VERSION != undefined', + { timeout: authTimeout } + ).catch(() => { throw 'auth timeout'; }); await this.setDeviceName(this.options.deviceName, this.options.browserName); const pairWithPhoneNumber = this.options.pairWithPhoneNumber; const version = await this.getWWebVersion(); @@ -223,17 +214,11 @@ class Client extends EventEmitter { await this.pupPage.evaluate(ExposeStore); - let start = Date.now(); - let res = false; - while(start > (Date.now() - 30000)){ - // Check window.Store Injection - res = await this.pupPage.evaluate('window.Store != undefined'); - if(res){break;} - await new Promise(r => setTimeout(r, 200)); - } - if(!res){ - throw 'ready timeout'; - } + // Check window.Store Injection + await this.pupPage.waitForFunction( + 'window.Store != undefined', + { timeout: 30000 } + ).catch(() => { throw 'ready timeout'; }); /** * Current connection information @@ -343,8 +328,6 @@ class Client extends EventEmitter { referer: 'https://whatsapp.com/' }); - await this.inject(); - this.pupPage.on('framenavigated', async (frame) => { if(frame.url().includes('post_logout=1') || this.lastLoggedOut) { this.emit(Events.DISCONNECTED, 'LOGOUT'); @@ -355,6 +338,8 @@ class Client extends EventEmitter { } await this.inject(); }); + + await this.inject(); } /** diff --git a/tests/ab-comparison.js b/tests/ab-comparison.js new file mode 100644 index 0000000000..e682a79c1f --- /dev/null +++ b/tests/ab-comparison.js @@ -0,0 +1,203 @@ +/** + * A/B Comparison: Old inject vs New inject during navigation + * + * Reproduces the exact error: "Execution context was destroyed" + * + * Uses a local HTTP server to serve real pages with working scripts. + * Navigation is triggered from Node.js (same effect as Chrome's internal navigation). + */ + +const http = require('http'); +const puppeteer = require('puppeteer'); +const chai = require('chai'); +const expect = chai.expect; + +// Page that sets Debug.VERSION after a delay +function makePage(delayMs) { + return `
WhatsApp Web
+ +`; +} + +// Old inject: manual polling with page.evaluate (commit 6f909bc, lines 105-112) +async function oldInjectPolling(page, timeout = 10000) { + const start = Date.now(); + let res = false; + while (start > (Date.now() - timeout)) { + res = await page.evaluate('window.Debug?.VERSION != undefined'); + if (res) break; + await new Promise(r => setTimeout(r, 200)); + } + if (!res) throw new Error('auth timeout'); + return true; +} + +// New inject: waitForFunction (current fork main, line 98) +async function newInjectPolling(page, timeout = 10000) { + await page.waitForFunction('window.Debug?.VERSION != undefined', { timeout }); + return true; +} + +describe('A/B: Old vs New inject during navigation', function () { + this.timeout(30000); + let browser; + let server; + let serverUrl; + + before(async function () { + // Start local HTTP server + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + // Page sets Debug.VERSION after 800ms + res.end(makePage(800)); + }); + await new Promise(resolve => { + server.listen(0, '127.0.0.1', () => { + serverUrl = `http://127.0.0.1:${server.address().port}`; + resolve(); + }); + }); + + browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); + }); + + after(async function () { + if (browser) await browser.close(); + if (server) await new Promise(resolve => server.close(resolve)); + }); + + // ────────────────────────────────────────────────────────── + // A: page.evaluate FAILS when context is destroyed (deterministic proof) + // ────────────────────────────────────────────────────────── + it('A (PROOF): page.evaluate throws "context destroyed" during navigation', async function () { + const page = await browser.newPage(); + try { + await page.goto(serverUrl, { waitUntil: 'load' }); + + // Start a long-running evaluate (simulates an evaluate in-flight during navigation) + const evalPromise = page.evaluate(async () => { + await new Promise(r => setTimeout(r, 5000)); + return window.Debug?.VERSION; + }); + + // Navigate while evaluate is running (like IndexedDB recovery) + await new Promise(r => setTimeout(r, 300)); + await page.goto(serverUrl, { waitUntil: 'load' }); + + let error = null; + try { + await evalPromise; + } catch (err) { + error = err; + } + + // evaluate should have thrown with context-destroyed + expect(error).to.not.be.null; + expect(error.message.toLowerCase()).to.satisfy(msg => + msg.includes('context') || + msg.includes('navigat') || + msg.includes('detach') || + msg.includes('target') + ); + console.log(' [A] page.evaluate threw:', error.message); + } finally { + await page.close(); + } + }); + + // ────────────────────────────────────────────────────────── + // B: NEW CODE - waitForFunction SURVIVES navigation + // ────────────────────────────────────────────────────────── + it('B (NEW CODE): waitForFunction SURVIVES navigation', async function () { + const page = await browser.newPage(); + try { + await page.goto(serverUrl, { waitUntil: 'load' }); + + // Start new-style polling + const pollPromise = newInjectPolling(page, 15000); + + // Same navigation trigger + await new Promise(r => setTimeout(r, 300)); + page.evaluate(() => { + window.location.reload(); + }).catch(() => {}); + + // Should survive and resolve + const result = await pollPromise; + expect(result).to.equal(true); + + const version = await page.evaluate('window.Debug.VERSION'); + expect(version).to.equal('2.3000.0'); + console.log(' [B] Survived navigation! Got version:', version); + } finally { + await page.close(); + } + }); + + // ────────────────────────────────────────────────────────── + // C: FULL FIX - both mechanisms together + // ────────────────────────────────────────────────────────── + it('C (FULL FIX): framenavigated + waitForFunction', async function () { + const page = await browser.newPage(); + try { + let framenavigatedCount = 0; + let injectViaListenerOk = false; + + // Register BEFORE inject (our fix) + page.on('framenavigated', async () => { + framenavigatedCount++; + try { + await page.waitForFunction('window.Debug?.VERSION != undefined', { timeout: 10000 }); + await page.evaluate('window.Debug.VERSION'); + injectViaListenerOk = true; + } catch (e) { /* ignore */ } + }); + + await page.goto(serverUrl, { waitUntil: 'load' }); + + const pollPromise = newInjectPolling(page, 15000); + + // Navigation mid-inject + await new Promise(r => setTimeout(r, 300)); + page.evaluate(() => { + window.location.reload(); + }).catch(() => {}); + + await pollPromise; + await new Promise(r => setTimeout(r, 2000)); + + expect(framenavigatedCount).to.be.greaterThan(0); + expect(injectViaListenerOk).to.equal(true); + console.log(' [C] framenavigated:', framenavigatedCount, '| inject via listener:', injectViaListenerOk); + } finally { + await page.close(); + } + }); + + // ────────────────────────────────────────────────────────── + // D: SANITY - both work WITHOUT navigation + // ────────────────────────────────────────────────────────── + it('D (SANITY): both work when there is no navigation', async function () { + const page1 = await browser.newPage(); + try { + await page1.goto(serverUrl, { waitUntil: 'load' }); + await oldInjectPolling(page1, 10000); + console.log(' [D] Old code works without navigation'); + } finally { + await page1.close(); + } + + const page2 = await browser.newPage(); + try { + await page2.goto(serverUrl, { waitUntil: 'load' }); + await newInjectPolling(page2, 10000); + console.log(' [D] New code works without navigation'); + } finally { + await page2.close(); + } + }); +}); diff --git a/tests/inject-navigation.js b/tests/inject-navigation.js new file mode 100644 index 0000000000..fc91ed60f0 --- /dev/null +++ b/tests/inject-navigation.js @@ -0,0 +1,243 @@ +/** + * E2E tests: waitForFunction survives page navigation (context destruction) + * + * Simulates the real-world scenario: + * After sleep/resume, Chrome's IndexedDB recovery causes an internal + * page navigation that destroys the execution context. + * waitForFunction should continue polling in the new context. + */ + +const puppeteer = require('puppeteer'); +const chai = require('chai'); +const expect = chai.expect; + +describe('inject() navigation resilience', function () { + this.timeout(30000); + + let browser; + let page; + + beforeEach(async function () { + browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); + page = await browser.newPage(); + }); + + afterEach(async function () { + if (browser) await browser.close(); + }); + + // ────────────────────────────────────────────────────────── + // Test 1: waitForFunction survives navigation + // ────────────────────────────────────────────────────────── + describe('waitForFunction survives navigation', function () { + it('should resolve after navigation destroys and recreates the context', async function () { + // Page 1: variable is NOT set + await page.setContent('
Page 1
'); + + // Start waiting for a variable that doesn't exist yet + const waitPromise = page.waitForFunction( + 'window.testReady === true', + { timeout: 15000 } + ); + + // Simulate navigation mid-wait (like IndexedDB recovery) + await new Promise(r => setTimeout(r, 500)); + await page.goto('data:text/html,
Page 2
'); + + // waitForFunction should resolve in the new context + await waitPromise; + + const result = await page.evaluate('window.testReady'); + expect(result).to.equal(true); + }); + + it('should resolve even with multiple navigations', async function () { + await page.setContent('Page 1'); + + const waitPromise = page.waitForFunction( + 'window.finalReady === true', + { timeout: 15000 } + ); + + // Navigation 1 + await new Promise(r => setTimeout(r, 300)); + await page.goto('data:text/html,Page 2'); + + // Navigation 2 + await new Promise(r => setTimeout(r, 300)); + await page.goto('data:text/html,Page 3'); + + // Navigation 3 - finally sets the variable + await new Promise(r => setTimeout(r, 300)); + await page.goto('data:text/html,Page 4'); + + await waitPromise; + + const result = await page.evaluate('window.finalReady'); + expect(result).to.equal(true); + }); + }); + + // ────────────────────────────────────────────────────────── + // Test 2: Simulates the exact WhatsApp inject scenario + // ────────────────────────────────────────────────────────── + describe('WhatsApp inject scenario simulation', function () { + it('should survive IndexedDB-recovery-like navigation during Debug.VERSION wait', async function () { + // Initial page load - WhatsApp Web loading + await page.setContent(`
Loading WhatsApp...
+ + `); + + // Start the same waitForFunction that inject() uses + const waitPromise = page.waitForFunction( + 'window.Debug?.VERSION != undefined', + { timeout: 15000 } + ); + + // Simulate IndexedDB recovery navigation after 500ms + await new Promise(r => setTimeout(r, 500)); + await page.goto(`data:text/html,
WhatsApp Recovered
+ + `); + + // waitForFunction should survive the navigation and resolve in new context + await waitPromise; + + const version = await page.evaluate('window.Debug.VERSION'); + expect(version).to.equal('2.3000.0'); + }); + }); + + // ────────────────────────────────────────────────────────── + // Test 3: framenavigated listener fires after navigation + // ────────────────────────────────────────────────────────── + describe('framenavigated listener ordering', function () { + it('should fire framenavigated when registered before navigation-triggering code', async function () { + await page.setContent('
Initial
'); + + let framenavigatedFired = false; + let navigatedUrl = ''; + + // Register listener BEFORE any inject-like code (our fix) + page.on('framenavigated', (frame) => { + framenavigatedFired = true; + navigatedUrl = frame.url(); + }); + + // Simulate navigation (like IndexedDB recovery) + await page.goto('data:text/html,
After Navigation
'); + + expect(framenavigatedFired).to.equal(true); + expect(navigatedUrl).to.include('data:text/html'); + }); + + it('should call inject-like function via framenavigated after context destruction', async function () { + await page.setContent('
Initial
'); + + let injectCallCount = 0; + const injectFn = async () => { + await page.evaluate('1 + 1'); // simple evaluate to verify context works + injectCallCount++; + }; + + // Register framenavigated BEFORE (our fix) + page.on('framenavigated', async () => { + await injectFn(); + }); + + // Navigate - this destroys old context, creates new one + await page.goto('data:text/html,
Recovered
'); + + // Give the async listener time to run + await new Promise(r => setTimeout(r, 500)); + + expect(injectCallCount).to.be.greaterThan(0); + }); + }); + + // ────────────────────────────────────────────────────────── + // Test 4: page.evaluate FAILS during navigation (contrast) + // ────────────────────────────────────────────────────────── + describe('page.evaluate does NOT survive navigation (contrast test)', function () { + it('should throw when evaluate runs during navigation', async function () { + await page.setContent('Initial'); + + let evaluateError = null; + + // Start a long-running evaluate, then navigate mid-way + const evalPromise = page.evaluate(async () => { + // Wait inside the browser context + await new Promise(r => setTimeout(r, 5000)); + return 'done'; + }).catch(err => { + evaluateError = err; + }); + + // Navigate while evaluate is running + await new Promise(r => setTimeout(r, 200)); + await page.goto('data:text/html,New Page'); + + await evalPromise; + + // evaluate should have thrown (context destroyed) + expect(evaluateError).to.not.be.null; + expect(evaluateError.message.toLowerCase()).to.satisfy( + msg => msg.includes('context') || msg.includes('navigat') || msg.includes('detach') + ); + }); + }); + + // ────────────────────────────────────────────────────────── + // Test 5: Full flow - framenavigated + waitForFunction together + // ────────────────────────────────────────────────────────── + describe('Full flow: framenavigated + waitForFunction', function () { + it('should recover fully when combining both mechanisms', async function () { + await page.setContent(`
Loading
+ + `); + + let framenavigatedInjectCalled = false; + + // Step 1: Register framenavigated BEFORE inject (our fix) + page.on('framenavigated', async () => { + try { + // Simulate inject: wait for Debug.VERSION then do work + await page.waitForFunction('window.Debug?.VERSION != undefined', { timeout: 10000 }); + await page.evaluate('window.Debug.VERSION'); // simulate store injection + framenavigatedInjectCalled = true; + } catch (e) { + // Ignore - test will fail on assertion if this doesn't work + } + }); + + // Step 2: Start inject (waitForFunction) + const injectPromise = page.waitForFunction( + 'window.Debug?.VERSION != undefined', + { timeout: 15000 } + ); + + // Step 3: Navigation destroys context mid-wait + await new Promise(r => setTimeout(r, 500)); + await page.goto(`data:text/html,
Recovered
+ + `); + + // Step 4: Both should succeed + await injectPromise; // waitForFunction survives navigation + + // Give framenavigated handler time to complete + await new Promise(r => setTimeout(r, 2000)); + expect(framenavigatedInjectCalled).to.equal(true); + }); + }); +}); From 6cc0468580077674477ff638557906e90913621f Mon Sep 17 00:00:00 2001 From: Adi Aharoni Date: Sat, 28 Feb 2026 23:12:03 +0200 Subject: [PATCH 11/11] chore: remove test files from upstream PR Co-Authored-By: Claude Opus 4.6 --- tests/ab-comparison.js | 203 ------------------------------- tests/inject-navigation.js | 243 ------------------------------------- 2 files changed, 446 deletions(-) delete mode 100644 tests/ab-comparison.js delete mode 100644 tests/inject-navigation.js diff --git a/tests/ab-comparison.js b/tests/ab-comparison.js deleted file mode 100644 index e682a79c1f..0000000000 --- a/tests/ab-comparison.js +++ /dev/null @@ -1,203 +0,0 @@ -/** - * A/B Comparison: Old inject vs New inject during navigation - * - * Reproduces the exact error: "Execution context was destroyed" - * - * Uses a local HTTP server to serve real pages with working scripts. - * Navigation is triggered from Node.js (same effect as Chrome's internal navigation). - */ - -const http = require('http'); -const puppeteer = require('puppeteer'); -const chai = require('chai'); -const expect = chai.expect; - -// Page that sets Debug.VERSION after a delay -function makePage(delayMs) { - return `
WhatsApp Web
- -`; -} - -// Old inject: manual polling with page.evaluate (commit 6f909bc, lines 105-112) -async function oldInjectPolling(page, timeout = 10000) { - const start = Date.now(); - let res = false; - while (start > (Date.now() - timeout)) { - res = await page.evaluate('window.Debug?.VERSION != undefined'); - if (res) break; - await new Promise(r => setTimeout(r, 200)); - } - if (!res) throw new Error('auth timeout'); - return true; -} - -// New inject: waitForFunction (current fork main, line 98) -async function newInjectPolling(page, timeout = 10000) { - await page.waitForFunction('window.Debug?.VERSION != undefined', { timeout }); - return true; -} - -describe('A/B: Old vs New inject during navigation', function () { - this.timeout(30000); - let browser; - let server; - let serverUrl; - - before(async function () { - // Start local HTTP server - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }); - // Page sets Debug.VERSION after 800ms - res.end(makePage(800)); - }); - await new Promise(resolve => { - server.listen(0, '127.0.0.1', () => { - serverUrl = `http://127.0.0.1:${server.address().port}`; - resolve(); - }); - }); - - browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); - }); - - after(async function () { - if (browser) await browser.close(); - if (server) await new Promise(resolve => server.close(resolve)); - }); - - // ────────────────────────────────────────────────────────── - // A: page.evaluate FAILS when context is destroyed (deterministic proof) - // ────────────────────────────────────────────────────────── - it('A (PROOF): page.evaluate throws "context destroyed" during navigation', async function () { - const page = await browser.newPage(); - try { - await page.goto(serverUrl, { waitUntil: 'load' }); - - // Start a long-running evaluate (simulates an evaluate in-flight during navigation) - const evalPromise = page.evaluate(async () => { - await new Promise(r => setTimeout(r, 5000)); - return window.Debug?.VERSION; - }); - - // Navigate while evaluate is running (like IndexedDB recovery) - await new Promise(r => setTimeout(r, 300)); - await page.goto(serverUrl, { waitUntil: 'load' }); - - let error = null; - try { - await evalPromise; - } catch (err) { - error = err; - } - - // evaluate should have thrown with context-destroyed - expect(error).to.not.be.null; - expect(error.message.toLowerCase()).to.satisfy(msg => - msg.includes('context') || - msg.includes('navigat') || - msg.includes('detach') || - msg.includes('target') - ); - console.log(' [A] page.evaluate threw:', error.message); - } finally { - await page.close(); - } - }); - - // ────────────────────────────────────────────────────────── - // B: NEW CODE - waitForFunction SURVIVES navigation - // ────────────────────────────────────────────────────────── - it('B (NEW CODE): waitForFunction SURVIVES navigation', async function () { - const page = await browser.newPage(); - try { - await page.goto(serverUrl, { waitUntil: 'load' }); - - // Start new-style polling - const pollPromise = newInjectPolling(page, 15000); - - // Same navigation trigger - await new Promise(r => setTimeout(r, 300)); - page.evaluate(() => { - window.location.reload(); - }).catch(() => {}); - - // Should survive and resolve - const result = await pollPromise; - expect(result).to.equal(true); - - const version = await page.evaluate('window.Debug.VERSION'); - expect(version).to.equal('2.3000.0'); - console.log(' [B] Survived navigation! Got version:', version); - } finally { - await page.close(); - } - }); - - // ────────────────────────────────────────────────────────── - // C: FULL FIX - both mechanisms together - // ────────────────────────────────────────────────────────── - it('C (FULL FIX): framenavigated + waitForFunction', async function () { - const page = await browser.newPage(); - try { - let framenavigatedCount = 0; - let injectViaListenerOk = false; - - // Register BEFORE inject (our fix) - page.on('framenavigated', async () => { - framenavigatedCount++; - try { - await page.waitForFunction('window.Debug?.VERSION != undefined', { timeout: 10000 }); - await page.evaluate('window.Debug.VERSION'); - injectViaListenerOk = true; - } catch (e) { /* ignore */ } - }); - - await page.goto(serverUrl, { waitUntil: 'load' }); - - const pollPromise = newInjectPolling(page, 15000); - - // Navigation mid-inject - await new Promise(r => setTimeout(r, 300)); - page.evaluate(() => { - window.location.reload(); - }).catch(() => {}); - - await pollPromise; - await new Promise(r => setTimeout(r, 2000)); - - expect(framenavigatedCount).to.be.greaterThan(0); - expect(injectViaListenerOk).to.equal(true); - console.log(' [C] framenavigated:', framenavigatedCount, '| inject via listener:', injectViaListenerOk); - } finally { - await page.close(); - } - }); - - // ────────────────────────────────────────────────────────── - // D: SANITY - both work WITHOUT navigation - // ────────────────────────────────────────────────────────── - it('D (SANITY): both work when there is no navigation', async function () { - const page1 = await browser.newPage(); - try { - await page1.goto(serverUrl, { waitUntil: 'load' }); - await oldInjectPolling(page1, 10000); - console.log(' [D] Old code works without navigation'); - } finally { - await page1.close(); - } - - const page2 = await browser.newPage(); - try { - await page2.goto(serverUrl, { waitUntil: 'load' }); - await newInjectPolling(page2, 10000); - console.log(' [D] New code works without navigation'); - } finally { - await page2.close(); - } - }); -}); diff --git a/tests/inject-navigation.js b/tests/inject-navigation.js deleted file mode 100644 index fc91ed60f0..0000000000 --- a/tests/inject-navigation.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * E2E tests: waitForFunction survives page navigation (context destruction) - * - * Simulates the real-world scenario: - * After sleep/resume, Chrome's IndexedDB recovery causes an internal - * page navigation that destroys the execution context. - * waitForFunction should continue polling in the new context. - */ - -const puppeteer = require('puppeteer'); -const chai = require('chai'); -const expect = chai.expect; - -describe('inject() navigation resilience', function () { - this.timeout(30000); - - let browser; - let page; - - beforeEach(async function () { - browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); - page = await browser.newPage(); - }); - - afterEach(async function () { - if (browser) await browser.close(); - }); - - // ────────────────────────────────────────────────────────── - // Test 1: waitForFunction survives navigation - // ────────────────────────────────────────────────────────── - describe('waitForFunction survives navigation', function () { - it('should resolve after navigation destroys and recreates the context', async function () { - // Page 1: variable is NOT set - await page.setContent('
Page 1
'); - - // Start waiting for a variable that doesn't exist yet - const waitPromise = page.waitForFunction( - 'window.testReady === true', - { timeout: 15000 } - ); - - // Simulate navigation mid-wait (like IndexedDB recovery) - await new Promise(r => setTimeout(r, 500)); - await page.goto('data:text/html,
Page 2
'); - - // waitForFunction should resolve in the new context - await waitPromise; - - const result = await page.evaluate('window.testReady'); - expect(result).to.equal(true); - }); - - it('should resolve even with multiple navigations', async function () { - await page.setContent('Page 1'); - - const waitPromise = page.waitForFunction( - 'window.finalReady === true', - { timeout: 15000 } - ); - - // Navigation 1 - await new Promise(r => setTimeout(r, 300)); - await page.goto('data:text/html,Page 2'); - - // Navigation 2 - await new Promise(r => setTimeout(r, 300)); - await page.goto('data:text/html,Page 3'); - - // Navigation 3 - finally sets the variable - await new Promise(r => setTimeout(r, 300)); - await page.goto('data:text/html,Page 4'); - - await waitPromise; - - const result = await page.evaluate('window.finalReady'); - expect(result).to.equal(true); - }); - }); - - // ────────────────────────────────────────────────────────── - // Test 2: Simulates the exact WhatsApp inject scenario - // ────────────────────────────────────────────────────────── - describe('WhatsApp inject scenario simulation', function () { - it('should survive IndexedDB-recovery-like navigation during Debug.VERSION wait', async function () { - // Initial page load - WhatsApp Web loading - await page.setContent(`
Loading WhatsApp...
- - `); - - // Start the same waitForFunction that inject() uses - const waitPromise = page.waitForFunction( - 'window.Debug?.VERSION != undefined', - { timeout: 15000 } - ); - - // Simulate IndexedDB recovery navigation after 500ms - await new Promise(r => setTimeout(r, 500)); - await page.goto(`data:text/html,
WhatsApp Recovered
- - `); - - // waitForFunction should survive the navigation and resolve in new context - await waitPromise; - - const version = await page.evaluate('window.Debug.VERSION'); - expect(version).to.equal('2.3000.0'); - }); - }); - - // ────────────────────────────────────────────────────────── - // Test 3: framenavigated listener fires after navigation - // ────────────────────────────────────────────────────────── - describe('framenavigated listener ordering', function () { - it('should fire framenavigated when registered before navigation-triggering code', async function () { - await page.setContent('
Initial
'); - - let framenavigatedFired = false; - let navigatedUrl = ''; - - // Register listener BEFORE any inject-like code (our fix) - page.on('framenavigated', (frame) => { - framenavigatedFired = true; - navigatedUrl = frame.url(); - }); - - // Simulate navigation (like IndexedDB recovery) - await page.goto('data:text/html,
After Navigation
'); - - expect(framenavigatedFired).to.equal(true); - expect(navigatedUrl).to.include('data:text/html'); - }); - - it('should call inject-like function via framenavigated after context destruction', async function () { - await page.setContent('
Initial
'); - - let injectCallCount = 0; - const injectFn = async () => { - await page.evaluate('1 + 1'); // simple evaluate to verify context works - injectCallCount++; - }; - - // Register framenavigated BEFORE (our fix) - page.on('framenavigated', async () => { - await injectFn(); - }); - - // Navigate - this destroys old context, creates new one - await page.goto('data:text/html,
Recovered
'); - - // Give the async listener time to run - await new Promise(r => setTimeout(r, 500)); - - expect(injectCallCount).to.be.greaterThan(0); - }); - }); - - // ────────────────────────────────────────────────────────── - // Test 4: page.evaluate FAILS during navigation (contrast) - // ────────────────────────────────────────────────────────── - describe('page.evaluate does NOT survive navigation (contrast test)', function () { - it('should throw when evaluate runs during navigation', async function () { - await page.setContent('Initial'); - - let evaluateError = null; - - // Start a long-running evaluate, then navigate mid-way - const evalPromise = page.evaluate(async () => { - // Wait inside the browser context - await new Promise(r => setTimeout(r, 5000)); - return 'done'; - }).catch(err => { - evaluateError = err; - }); - - // Navigate while evaluate is running - await new Promise(r => setTimeout(r, 200)); - await page.goto('data:text/html,New Page'); - - await evalPromise; - - // evaluate should have thrown (context destroyed) - expect(evaluateError).to.not.be.null; - expect(evaluateError.message.toLowerCase()).to.satisfy( - msg => msg.includes('context') || msg.includes('navigat') || msg.includes('detach') - ); - }); - }); - - // ────────────────────────────────────────────────────────── - // Test 5: Full flow - framenavigated + waitForFunction together - // ────────────────────────────────────────────────────────── - describe('Full flow: framenavigated + waitForFunction', function () { - it('should recover fully when combining both mechanisms', async function () { - await page.setContent(`
Loading
- - `); - - let framenavigatedInjectCalled = false; - - // Step 1: Register framenavigated BEFORE inject (our fix) - page.on('framenavigated', async () => { - try { - // Simulate inject: wait for Debug.VERSION then do work - await page.waitForFunction('window.Debug?.VERSION != undefined', { timeout: 10000 }); - await page.evaluate('window.Debug.VERSION'); // simulate store injection - framenavigatedInjectCalled = true; - } catch (e) { - // Ignore - test will fail on assertion if this doesn't work - } - }); - - // Step 2: Start inject (waitForFunction) - const injectPromise = page.waitForFunction( - 'window.Debug?.VERSION != undefined', - { timeout: 15000 } - ); - - // Step 3: Navigation destroys context mid-wait - await new Promise(r => setTimeout(r, 500)); - await page.goto(`data:text/html,
Recovered
- - `); - - // Step 4: Both should succeed - await injectPromise; // waitForFunction survives navigation - - // Give framenavigated handler time to complete - await new Promise(r => setTimeout(r, 2000)); - expect(framenavigatedInjectCalled).to.equal(true); - }); - }); -});