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
+
+
+
+
+
+
+
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}")