From b35b5a2c85f613d3fc91d422f05f68b3bb4d4c69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:08:42 +0000 Subject: [PATCH 1/4] Initial plan From 05e8be7a4b71d366880394ffb034e747438423cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:13:35 +0000 Subject: [PATCH 2/4] Fix click functions: add clickMatches implementation, parameter escaping, count support Co-authored-by: galaxyeye <1701451+galaxyeye@users.noreply.github.com> --- .../crawl/fetch/driver/AbstractWebDriver.kt | 7 +- .../src/main/resources/js/__pulsar_utils__.js | 80 ++++++++++++++++--- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/crawl/fetch/driver/AbstractWebDriver.kt b/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/crawl/fetch/driver/AbstractWebDriver.kt index aad75166c0..b363431c67 100644 --- a/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/crawl/fetch/driver/AbstractWebDriver.kt +++ b/pulsar-core/pulsar-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/crawl/fetch/driver/AbstractWebDriver.kt @@ -612,13 +612,16 @@ abstract class AbstractWebDriver( @Throws(WebDriverException::class) override suspend fun clickTextMatches(selector: String, pattern: String, count: Int) { val safeSelector = Strings.escapeJsString(selector) - evaluate("__pulsar_utils__.clickTextMatches('$safeSelector', '$pattern')") + val safePattern = Strings.escapeJsString(pattern) + evaluate("__pulsar_utils__.clickTextMatches('$safeSelector', '$safePattern', $count)") } @Throws(WebDriverException::class) override suspend fun clickMatches(selector: String, attrName: String, pattern: String, count: Int) { val safeSelector = Strings.escapeJsString(selector) - evaluate("__pulsar_utils__.clickMatches('$safeSelector', '$attrName', '$pattern')") + val safeAttrName = Strings.escapeJsString(attrName) + val safePattern = Strings.escapeJsString(pattern) + evaluate("__pulsar_utils__.clickMatches('$safeSelector', '$safeAttrName', '$safePattern', $count)") } @Throws(WebDriverException::class) diff --git a/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js b/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js index d1678cae09..f2f0f697b6 100644 --- a/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js +++ b/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js @@ -615,27 +615,83 @@ __pulsar_utils__.click = function(selector) { * @param {String} pattern * @return */ -__pulsar_utils__.clickTextMatches = function(selector, pattern) { +/** + * Click elements matching selector whose text content matches pattern. + * + * @param {string} selector CSS selector + * @param {string} pattern Pattern to match text content (regex) + * @param {number} count Maximum number of elements to click (default: 1) + * @return {number} Number of elements clicked + */ +__pulsar_utils__.clickTextMatches = function(selector, pattern, count = 1) { // TODO: handle selector `*` - let elements = document.querySelectorAll(selector) - for (let ele of elements) { - if (ele instanceof HTMLElement) { - let text = ele.textContent - if (text.match(pattern)) { - ele.scrollIntoView() - ele.click() + try { + let elements = document.querySelectorAll(selector) + let clicked = 0 + let regex = new RegExp(pattern) + + for (let ele of elements) { + if (clicked >= count) { + break + } + if (ele instanceof HTMLElement) { + let text = ele.textContent + if (regex.test(text)) { + ele.scrollIntoView() + ele.click() + clicked++ + } } } + return clicked + } catch (e) { + console.error('Error in clickTextMatches:', e) + return 0 } } /** - * Select the first element and click it. + * Click elements matching selector whose attribute matches pattern. * - * @param {number} n The n-th anchor. - * @param {string|null} rootSelector The n-th anchor. - * @return {string|null} + * @param {string} selector CSS selector + * @param {string} attrName Attribute name to match + * @param {string} pattern Pattern to match attribute value (regex) + * @param {number} count Maximum number of elements to click (default: 1) + * @return {number} Number of elements clicked + */ +__pulsar_utils__.clickMatches = function(selector, attrName, pattern, count = 1) { + try { + let elements = document.querySelectorAll(selector) + let clicked = 0 + let regex = new RegExp(pattern) + + for (let ele of elements) { + if (clicked >= count) { + break + } + if (ele instanceof HTMLElement) { + let attrValue = ele.getAttribute(attrName) + if (attrValue && regex.test(attrValue)) { + ele.scrollIntoView() + ele.click() + clicked++ + } + } + } + return clicked + } catch (e) { + console.error('Error in clickMatches:', e) + return 0 + } +} + +/** + * Click the n-th anchor element within the specified root. + * + * @param {number} n The index of anchor to click (1-based). + * @param {string|null} rootSelector CSS selector of the root element. + * @return {string|null} The href of the clicked anchor, or null if not found. */ __pulsar_utils__.clickNthAnchor = function(n, rootSelector) { let rootNode From 0cf6424c3651a1068d206fc7aef35a5050b68138 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:23:12 +0000 Subject: [PATCH 3/4] Add comprehensive tests for click functions fixes Co-authored-by: galaxyeye <1701451+galaxyeye@users.noreply.github.com> --- .../resources/static/assets/click-test.html | 118 +++++++++++ .../browser/PulsarWebDriverClickTests.kt | 195 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 pulsar-tests-common/src/main/resources/static/assets/click-test.html create mode 100644 pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt diff --git a/pulsar-tests-common/src/main/resources/static/assets/click-test.html b/pulsar-tests-common/src/main/resources/static/assets/click-test.html new file mode 100644 index 0000000000..9fceaa550c --- /dev/null +++ b/pulsar-tests-common/src/main/resources/static/assets/click-test.html @@ -0,0 +1,118 @@ + + + + + + Click Functions Test Page + + + +

Click Functions Test Page

+ +
+

Test clickTextMatches

+ + + + +
No clicks yet
+
+ +
+

Test clickMatches (attribute matching)

+ + + + +
No clicks yet
+
+ +
+

Test clickNthAnchor

+ First Link
+ Second Link
+ Third Link
+ Fourth Link
+ Fifth Link
+
No clicks yet
+
+ + + + diff --git a/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt b/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt new file mode 100644 index 0000000000..66a22dfd8d --- /dev/null +++ b/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt @@ -0,0 +1,195 @@ +package ai.platon.pulsar.browser + +import ai.platon.pulsar.WebDriverTestBase +import ai.platon.pulsar.common.printlnPro +import kotlinx.coroutines.delay +import org.junit.jupiter.api.Tag +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Test suite for PulsarWebDriver click series functions. + * Tests the fixes for: + * - clickTextMatches with count parameter and pattern escaping + * - clickMatches with count parameter and pattern/attrName escaping + * - clickNthAnchor functionality + */ +@Tag("ClickFunctionTest") +class PulsarWebDriverClickTests : WebDriverTestBase() { + + private val testPageUrl get() = "$assetsBaseURL/click-test.html" + + @Test + fun testClickTextMatchesBasic() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) // Let page stabilize + + // Clear any previous clicks + driver.evaluate("window.clearClickLog()") + + // Test clicking button with text matching "Submit" + driver.clickTextMatches("button.test-button", "Submit") + delay(300) + + // Verify that a click was logged + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog, "Click log should not be null") + assertTrue(clickLog.isNotEmpty(), "At least one button should have been clicked") + + printlnPro("Click log size: ${clickLog.size}") + } + + @Test + fun testClickTextMatchesWithCount() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test clicking with count = 2, should click max 2 buttons matching "Submit" + driver.clickTextMatches("button.test-button", "Submit", 2) + delay(300) + + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + // Should have clicked exactly 2 buttons (there are "Submit Form" and "Submit Data") + assertEquals(2, clickLog.size, "Should have clicked exactly 2 buttons matching 'Submit'") + + printlnPro("Clicked ${clickLog.size} buttons with count=2") + } + + @Test + fun testClickTextMatchesWithSpecialCharacters() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test pattern with special regex characters that need escaping + // Looking for exact text "Submit Form" using escaped dots + driver.clickTextMatches("button.test-button", "Submit.*Form") + delay(300) + + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + assertTrue(clickLog.size >= 1, "Should have clicked at least one button") + + printlnPro("Clicked button with special character pattern") + } + + @Test + fun testClickMatchesBasic() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test clicking button with data-action attribute matching "save" + driver.clickMatches("button.test-button", "data-action", "save") + delay(300) + + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + assertTrue(clickLog.isNotEmpty(), "Should have clicked at least one button with data-action='save'") + + printlnPro("Click matches test passed") + } + + @Test + fun testClickMatchesWithCount() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test clicking with count = 1, should click only first button matching pattern + driver.clickMatches("button.test-button", "data-action", "save.*", 1) + delay(300) + + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + // Should have clicked exactly 1 button (either "save" or "save-draft") + assertEquals(1, clickLog.size, "Should have clicked exactly 1 button with count=1") + + printlnPro("Click matches with count=1 test passed") + } + + @Test + fun testClickMatchesWithMultipleMatches() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test clicking multiple buttons matching pattern "save.*" (matches "save" and "save-draft") + driver.clickMatches("button.test-button", "data-action", "save.*", 2) + delay(300) + + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + assertEquals(2, clickLog.size, "Should have clicked exactly 2 buttons matching 'save.*'") + + printlnPro("Click matches with multiple matches test passed") + } + + @Test + fun testClickNthAnchor() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test clicking the 3rd anchor (1-based index) + val href = driver.clickNthAnchor(3, "body") + delay(300) + + assertNotNull(href, "Should have returned href of clicked anchor") + assertTrue(href.contains("link3"), "Should have clicked the 3rd link (link3)") + + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + assertTrue(clickLog.isNotEmpty(), "Should have logged the anchor click") + + printlnPro("Clicked nth anchor, href: $href") + } + + @Test + fun testClickNthAnchorFirstLink() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test clicking the 1st anchor + val href = driver.clickNthAnchor(1, "body") + delay(300) + + assertNotNull(href) + assertTrue(href.contains("link1"), "Should have clicked the 1st link") + + printlnPro("Clicked first anchor, href: $href") + } + + @Test + fun testEscapingPreventsInjection() = runEnhancedWebDriverTest(testPageUrl, browser) { driver -> + driver.waitForSelector("body") + delay(500) + + driver.evaluate("window.clearClickLog()") + + // Test that malicious patterns are properly escaped + // This should not throw an error and should not execute arbitrary JS + try { + driver.clickTextMatches("button.test-button", "'; alert('xss'); '") + delay(300) + + // If we get here without exception, the escaping worked + printlnPro("Pattern escaping prevented potential injection") + } catch (e: Exception) { + // Some regex errors are acceptable (invalid regex pattern) + printlnPro("Pattern caused error (expected for invalid regex): ${e.message}") + } + } +} From 9a80cbdbbda933fb51d9da3400cc871ad989ffb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:24:39 +0000 Subject: [PATCH 4/4] Address code review feedback: improve error messages and test assertions Co-authored-by: galaxyeye <1701451+galaxyeye@users.noreply.github.com> --- .../src/main/resources/js/__pulsar_utils__.js | 4 ++-- .../ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js b/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js index f2f0f697b6..06cbeabdb6 100644 --- a/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js +++ b/pulsar-core/pulsar-tools/pulsar-browser/src/main/resources/js/__pulsar_utils__.js @@ -646,7 +646,7 @@ __pulsar_utils__.clickTextMatches = function(selector, pattern, count = 1) { } return clicked } catch (e) { - console.error('Error in clickTextMatches:', e) + console.error('Error in clickTextMatches with selector=' + selector + ', pattern=' + pattern + ':', e) return 0 } } @@ -681,7 +681,7 @@ __pulsar_utils__.clickMatches = function(selector, attrName, pattern, count = 1) } return clicked } catch (e) { - console.error('Error in clickMatches:', e) + console.error('Error in clickMatches with selector=' + selector + ', attrName=' + attrName + ', pattern=' + pattern + ':', e) return 0 } } diff --git a/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt b/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt index 66a22dfd8d..17ee673fbf 100644 --- a/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt +++ b/pulsar-tests/src/test/kotlin/ai/platon/pulsar/browser/PulsarWebDriverClickTests.kt @@ -185,8 +185,12 @@ class PulsarWebDriverClickTests : WebDriverTestBase() { driver.clickTextMatches("button.test-button", "'; alert('xss'); '") delay(300) - // If we get here without exception, the escaping worked - printlnPro("Pattern escaping prevented potential injection") + // Verify no elements were clicked since the pattern won't match any text + val clickLog = driver.evaluate("window.getClickLog()") as? List<*> + assertNotNull(clickLog) + assertEquals(0, clickLog.size, "No elements should be clicked with escaped injection pattern") + + printlnPro("Pattern escaping prevented potential injection - no elements clicked") } catch (e: Exception) { // Some regex errors are acceptable (invalid regex pattern) printlnPro("Pattern caused error (expected for invalid regex): ${e.message}")