synapse-hub 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.agents/agents/orchestrator.md +3 -0
  2. package/.agents/agents/pom-crafter.md +3 -0
  3. package/.agents/agents/quality-gatekeeper.md +1 -0
  4. package/.agents/agents/test-runner.md +1 -0
  5. package/.agents/rules/graphify.md +14 -14
  6. package/.agents/rules/mandatory-playwright-live.md +29 -29
  7. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  8. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  9. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  10. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  11. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  12. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  13. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  14. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  15. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  16. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  17. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  18. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  19. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  20. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  21. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  22. package/.agents/templates/completion_report.md +57 -57
  23. package/.agents/workflows/graphify.md +10 -10
  24. package/.cursor/rules/language.mdc +7 -7
  25. package/README.en.md +9 -3
  26. package/README.md +9 -3
  27. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +21 -21
  28. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +21 -21
  29. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +21 -21
  30. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +21 -21
  31. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +21 -21
  32. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +20 -20
  33. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +20 -20
  34. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +20 -20
  35. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +20 -20
  36. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +20 -20
  37. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +22 -22
  38. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +22 -22
  39. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +22 -22
  40. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +22 -22
  41. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +22 -22
  42. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +21 -21
  43. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +21 -21
  44. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +21 -21
  45. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +21 -21
  46. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +22 -22
  47. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +21 -21
  48. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +21 -21
  49. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +21 -21
  50. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +21 -21
  51. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +21 -21
  52. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +94 -69
  53. package/benchmarks/qa_eval/reports/eval_results.json +460 -104
  54. package/bin/agent-qa.js +6 -1
  55. package/docs/ARCHITECTURE.md +13 -2
  56. package/package.json +6 -6
  57. package/scripts/install.ps1 +112 -112
  58. package/scripts/install_global_plugin.ps1 +1 -1
  59. package/scripts/install_global_plugin.sh +1 -1
  60. package/scripts/lint_skills.ps1 +57 -57
  61. package/skills_manifest.json +1 -1
  62. package/synapse.config.json +1 -1
  63. package/tools/TOOLS_RUNBOOK.md +6 -1
  64. package/tools/__init__.py +1 -1
  65. package/tools/agent_canary.py +501 -501
  66. package/tools/api_fuzzer.py +11 -2
  67. package/tools/browser_helpers.js +2 -2
  68. package/tools/bump_version.py +8 -0
  69. package/tools/check_version_sync.py +18 -0
  70. package/tools/cli.py +210 -48
  71. package/tools/count_tokens.py +4 -4
  72. package/tools/dashboard_generator.py +10 -10
  73. package/tools/e2e_flow_prober.sh +0 -0
  74. package/tools/generate_allure_report.sh +0 -0
  75. package/tools/generate_test_reports.sh +0 -0
  76. package/tools/mcp_excel_server.py +3 -1
  77. package/tools/mcp_excel_server.sh +0 -0
  78. package/tools/mcp_server.py +1 -1
  79. package/tools/micro_mutator.py +4 -2
  80. package/tools/pinpoint_failure.py +88 -88
  81. package/tools/setup_project.py +1 -1
  82. package/tools/skillspector.py +31 -0
  83. package/tools/update_sentinel.py +3 -3
  84. package/tools/verify_all.py +16 -6
  85. package/tools/verify_all.sh +0 -0
  86. package/tools/visual_som_helper.py +14 -3
  87. package/tools/welcome_page.py +1 -1
  88. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +0 -83
@@ -1,28 +1,28 @@
1
- import com.enterprise.pages.BasePage
2
- import com.enterprise.pages.LoginPage
3
- import com.enterprise.pages.DataMiningPage
4
- import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
5
-
6
- // Bước 1: Đảm bảo phiên đăng nhập hợp lệ
7
- LoginPage.ensureLoggedIn()
8
-
9
- // Bước 2: Điều hướng đến phân hệ mục tiêu
10
- DataMiningPage.navigateToCumCongNghiep()
11
-
12
- // Bước 3: Chuẩn bị dữ liệu kiểm thử duy nhất
13
- String testCode = BasePage.uniqueCode("CCN_")
14
- String testName = "CCN Thử Nghiệm " + testCode
15
-
16
- // Bước 4: Thực thi tạo mới
17
- DataMiningPage.createCluster(testCode, testName, "Hà Nội", "Đang hoạt động")
18
-
19
- // Bước 5: Tìm kiếm 2 pha và xác thực theo Mã
20
- DataMiningPage.searchRecord(testCode)
21
- DataMiningPage.verifyRecordExists(testCode)
22
-
23
- // Bước 6: Chỉnh sửa và xác thực cập nhật
24
- String updatedName = testName + " [ĐÃ CẬP NHẬT]"
25
- DataMiningPage.editRecord(testCode, updatedName)
26
-
27
- // Bước 7: Dọn dẹp dữ liệu (Xóa & Verify Không còn tồn tại)
28
- DataMiningPage.deleteRecord(testCode)
1
+ import com.enterprise.pages.BasePage
2
+ import com.enterprise.pages.LoginPage
3
+ import com.enterprise.pages.DataMiningPage
4
+ import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
5
+
6
+ // Bước 1: Đảm bảo phiên đăng nhập hợp lệ
7
+ LoginPage.ensureLoggedIn()
8
+
9
+ // Bước 2: Điều hướng đến phân hệ mục tiêu
10
+ DataMiningPage.navigateToCumCongNghiep()
11
+
12
+ // Bước 3: Chuẩn bị dữ liệu kiểm thử duy nhất
13
+ String testCode = BasePage.uniqueCode("CCN_")
14
+ String testName = "CCN Thử Nghiệm " + testCode
15
+
16
+ // Bước 4: Thực thi tạo mới
17
+ DataMiningPage.createCluster(testCode, testName, "Hà Nội", "Đang hoạt động")
18
+
19
+ // Bước 5: Tìm kiếm 2 pha và xác thực theo Mã
20
+ DataMiningPage.searchRecord(testCode)
21
+ DataMiningPage.verifyRecordExists(testCode)
22
+
23
+ // Bước 6: Chỉnh sửa và xác thực cập nhật
24
+ String updatedName = testName + " [ĐÃ CẬP NHẬT]"
25
+ DataMiningPage.editRecord(testCode, updatedName)
26
+
27
+ // Bước 7: Dọn dẹp dữ liệu (Xóa & Verify Không còn tồn tại)
28
+ DataMiningPage.deleteRecord(testCode)
@@ -1,138 +1,138 @@
1
- package com.enterprise.pages
2
-
3
- import org.openqa.selenium.By
4
- import org.openqa.selenium.JavascriptExecutor
5
- import org.openqa.selenium.Keys
6
- import org.openqa.selenium.WebDriver
7
- import org.openqa.selenium.WebElement
8
- import com.kms.katalon.core.webui.driver.DriverFactory
9
-
10
- /**
11
- * Enterprise Clean POM common methods reusable across Page Classes.
12
- * Kế thừa trực tiếp từ BasePage.
13
- */
14
- class PomPatterns extends BasePage {
15
-
16
- /**
17
- * Hành vi tìm kiếm 2 pha: Bấm Search -> Điền từ khóa -> Enter -> Chờ bảng cập nhật
18
- */
19
- public static void searchRecord(String keyword) {
20
- WebDriver driver = DriverFactory.getWebDriver()
21
- List<WebElement> inputs = driver.findElements(By.cssSelector("input.p-inputtext[placeholder*='Tìm kiếm'], input[placeholder*='tên'], input[placeholder*='mã']"))
22
- if (inputs.isEmpty() || !inputs.get(0).isDisplayed()) {
23
- WebElement searchToggle = findElement(By.cssSelector("button.p-button-icon-only[icon*='search'], button[title*='Tìm kiếm']"))
24
- searchToggle.click()
25
- waitForElementVisible(By.cssSelector("input.p-inputtext[placeholder*='Tìm kiếm'], input[placeholder*='tên'], input[placeholder*='mã']"), 5)
26
- }
27
- WebElement input = findElement(By.cssSelector("input.p-inputtext[placeholder*='Tìm kiếm'], input[placeholder*='tên'], input[placeholder*='mã']"))
28
- jsSetInput(input, keyword)
29
- input.sendKeys(Keys.ENTER)
30
- waitForAngular(5)
31
- waitForElementVisible(By.cssSelector("p-table table tbody tr"), 5)
32
- }
33
-
34
- /**
35
- * Chỉnh sửa bản ghi tại dòng đầu tiên và xác thực theo tên mới
36
- */
37
- public static void editRecord(String oldKeyword, String newName) {
38
- searchRecord(oldKeyword)
39
- WebElement editBtn = findElement(By.cssSelector("p-table tbody tr:first-child button[icon*='pencil'], p-table tbody tr:first-child button[icon*='edit']"))
40
- clickElement(editBtn)
41
- waitForElementVisible(By.cssSelector("input[formcontrolname='name'], input[name='name']"), 5)
42
-
43
- WebElement nameInput = findElement(By.cssSelector("input[formcontrolname='name'], input[name='name']"))
44
- jsSetInput(nameInput, newName)
45
-
46
- WebElement saveBtn = findElement(By.xpath("//button[@title='Xác nhận' or contains(.,'Lưu') or contains(.,'Xác nhận')]"))
47
- clickElement(saveBtn)
48
- waitForAngular(5)
49
-
50
- searchRecord(newName)
51
- WebElement firstRow = findElement(By.cssSelector("p-table tbody tr:first-child"))
52
- assert firstRow.getText().contains(newName) : "Lỗi: Bản ghi chưa cập nhật tên mới!"
53
- }
54
-
55
- /**
56
- * Xóa bản ghi tại dòng đầu tiên và xác nhận p-confirmdialog
57
- */
58
- public static void deleteRecord(String keyword) {
59
- WebDriver driver = DriverFactory.getWebDriver()
60
- searchRecord(keyword)
61
- WebElement deleteBtn = findElement(By.cssSelector("p-table tbody tr:first-child button.p-button-danger, p-table tbody tr:first-child button[icon*='trash']"))
62
- clickElement(deleteBtn)
63
- waitForElementVisible(By.xpath("//div[contains(@class,'p-dialog-footer') or contains(@class,'p-confirm-dialog')]//button[contains(@class,'p-confirm-dialog-accept') or contains(.,'Xóa') or contains(.,'Đồng ý') or contains(.,'Xác nhận')]"), 5)
64
-
65
- WebElement confirmBtn = findElement(By.xpath("//div[contains(@class,'p-dialog-footer') or contains(@class,'p-confirm-dialog')]//button[contains(@class,'p-confirm-dialog-accept') or contains(.,'Xóa') or contains(.,'Đồng ý') or contains(.,'Xác nhận')]"))
66
- clickElement(confirmBtn)
67
- waitForAngular(5)
68
-
69
- verifyRecordNotExists(keyword)
70
- }
71
-
72
- /**
73
- * Điền ngày vào p-calendar an toàn với Angular
74
- */
75
- public static void setCalendarDate(WebElement element, String dateStr) {
76
- WebDriver driver = DriverFactory.getWebDriver()
77
- JavascriptExecutor js = (JavascriptExecutor) driver
78
- js.executeScript("""
79
- arguments[0].value = arguments[1];
80
- arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
81
- arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
82
- arguments[0].dispatchEvent(new Event('blur', { bubbles: true }));
83
- """, element, dateStr)
84
- }
85
-
86
- /**
87
- * Chọn nhiều lựa chọn trong p-multiselect
88
- */
89
- public static void selectMultiOptions(WebElement multiSelectEl, List<String> optionTexts) {
90
- clickElement(multiSelectEl)
91
- waitForElementVisible(By.cssSelector(".p-multiselect-panel"), 5)
92
- WebDriver driver = DriverFactory.getWebDriver()
93
- for (String opt : optionTexts) {
94
- WebElement item = driver.findElement(By.xpath("//li[contains(@class, 'p-multiselect-item') and contains(., '" + opt + "')]"))
95
- clickElement(item)
96
- }
97
- WebElement closeBtn = driver.findElement(By.cssSelector(".p-multiselect-close"))
98
- if (closeBtn != null && closeBtn.isDisplayed()) {
99
- closeBtn.click()
100
- }
101
- }
102
-
103
- /**
104
- * Kiểm tra bản ghi tồn tại trong bảng p-table
105
- */
106
- public static void verifyRecordExists(String keyword) {
107
- WebDriver driver = DriverFactory.getWebDriver()
108
- List<WebElement> rows = driver.findElements(By.cssSelector("p-table table tbody tr"))
109
- assert !rows.isEmpty() : "Lỗi: Bảng không có dữ liệu nào!"
110
- String firstRowText = rows.get(0).getText()
111
- assert firstRowText.contains(keyword) : "Lỗi: Không tìm thấy từ khóa '${keyword}' trong hàng đầu tiên!"
112
- }
113
-
114
- /**
115
- * Kiểm tra bản ghi đã bị xóa hoàn toàn khỏi bảng
116
- */
117
- public static void verifyRecordNotExists(String keyword) {
118
- WebDriver driver = DriverFactory.getWebDriver()
119
- searchRecord(keyword)
120
- List<WebElement> rows = driver.findElements(By.cssSelector("p-table table tbody tr"))
121
- if (rows.isEmpty()) return
122
- String firstRowText = rows.get(0).getText()
123
- assert !firstRowText.contains(keyword) || firstRowText.contains("Không có dữ liệu") : "Lỗi: Bản ghi '${keyword}' vẫn còn tồn tại trong bảng!"
124
- }
125
-
126
- /**
127
- * Gán giá trị an toàn kích hoạt Angular Change Detection
128
- */
129
- public static void jsSetInput(WebElement element, String value) {
130
- WebDriver driver = DriverFactory.getWebDriver()
131
- JavascriptExecutor js = (JavascriptExecutor) driver
132
- js.executeScript("""
133
- arguments[0].value = arguments[1];
134
- arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
135
- arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
136
- """, element, value)
137
- }
138
- }
1
+ package com.enterprise.pages
2
+
3
+ import org.openqa.selenium.By
4
+ import org.openqa.selenium.JavascriptExecutor
5
+ import org.openqa.selenium.Keys
6
+ import org.openqa.selenium.WebDriver
7
+ import org.openqa.selenium.WebElement
8
+ import com.kms.katalon.core.webui.driver.DriverFactory
9
+
10
+ /**
11
+ * Enterprise Clean POM common methods reusable across Page Classes.
12
+ * Kế thừa trực tiếp từ BasePage.
13
+ */
14
+ class PomPatterns extends BasePage {
15
+
16
+ /**
17
+ * Hành vi tìm kiếm 2 pha: Bấm Search -> Điền từ khóa -> Enter -> Chờ bảng cập nhật
18
+ */
19
+ public static void searchRecord(String keyword) {
20
+ WebDriver driver = DriverFactory.getWebDriver()
21
+ List<WebElement> inputs = driver.findElements(By.cssSelector("input.p-inputtext[placeholder*='Tìm kiếm'], input[placeholder*='tên'], input[placeholder*='mã']"))
22
+ if (inputs.isEmpty() || !inputs.get(0).isDisplayed()) {
23
+ WebElement searchToggle = findElement(By.cssSelector("button.p-button-icon-only[icon*='search'], button[title*='Tìm kiếm']"))
24
+ searchToggle.click()
25
+ waitForElementVisible(By.cssSelector("input.p-inputtext[placeholder*='Tìm kiếm'], input[placeholder*='tên'], input[placeholder*='mã']"), 5)
26
+ }
27
+ WebElement input = findElement(By.cssSelector("input.p-inputtext[placeholder*='Tìm kiếm'], input[placeholder*='tên'], input[placeholder*='mã']"))
28
+ jsSetInput(input, keyword)
29
+ input.sendKeys(Keys.ENTER)
30
+ waitForAngular(5)
31
+ waitForElementVisible(By.cssSelector("p-table table tbody tr"), 5)
32
+ }
33
+
34
+ /**
35
+ * Chỉnh sửa bản ghi tại dòng đầu tiên và xác thực theo tên mới
36
+ */
37
+ public static void editRecord(String oldKeyword, String newName) {
38
+ searchRecord(oldKeyword)
39
+ WebElement editBtn = findElement(By.cssSelector("p-table tbody tr:first-child button[icon*='pencil'], p-table tbody tr:first-child button[icon*='edit']"))
40
+ clickElement(editBtn)
41
+ waitForElementVisible(By.cssSelector("input[formcontrolname='name'], input[name='name']"), 5)
42
+
43
+ WebElement nameInput = findElement(By.cssSelector("input[formcontrolname='name'], input[name='name']"))
44
+ jsSetInput(nameInput, newName)
45
+
46
+ WebElement saveBtn = findElement(By.xpath("//button[@title='Xác nhận' or contains(.,'Lưu') or contains(.,'Xác nhận')]"))
47
+ clickElement(saveBtn)
48
+ waitForAngular(5)
49
+
50
+ searchRecord(newName)
51
+ WebElement firstRow = findElement(By.cssSelector("p-table tbody tr:first-child"))
52
+ assert firstRow.getText().contains(newName) : "Lỗi: Bản ghi chưa cập nhật tên mới!"
53
+ }
54
+
55
+ /**
56
+ * Xóa bản ghi tại dòng đầu tiên và xác nhận p-confirmdialog
57
+ */
58
+ public static void deleteRecord(String keyword) {
59
+ WebDriver driver = DriverFactory.getWebDriver()
60
+ searchRecord(keyword)
61
+ WebElement deleteBtn = findElement(By.cssSelector("p-table tbody tr:first-child button.p-button-danger, p-table tbody tr:first-child button[icon*='trash']"))
62
+ clickElement(deleteBtn)
63
+ waitForElementVisible(By.xpath("//div[contains(@class,'p-dialog-footer') or contains(@class,'p-confirm-dialog')]//button[contains(@class,'p-confirm-dialog-accept') or contains(.,'Xóa') or contains(.,'Đồng ý') or contains(.,'Xác nhận')]"), 5)
64
+
65
+ WebElement confirmBtn = findElement(By.xpath("//div[contains(@class,'p-dialog-footer') or contains(@class,'p-confirm-dialog')]//button[contains(@class,'p-confirm-dialog-accept') or contains(.,'Xóa') or contains(.,'Đồng ý') or contains(.,'Xác nhận')]"))
66
+ clickElement(confirmBtn)
67
+ waitForAngular(5)
68
+
69
+ verifyRecordNotExists(keyword)
70
+ }
71
+
72
+ /**
73
+ * Điền ngày vào p-calendar an toàn với Angular
74
+ */
75
+ public static void setCalendarDate(WebElement element, String dateStr) {
76
+ WebDriver driver = DriverFactory.getWebDriver()
77
+ JavascriptExecutor js = (JavascriptExecutor) driver
78
+ js.executeScript("""
79
+ arguments[0].value = arguments[1];
80
+ arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
81
+ arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
82
+ arguments[0].dispatchEvent(new Event('blur', { bubbles: true }));
83
+ """, element, dateStr)
84
+ }
85
+
86
+ /**
87
+ * Chọn nhiều lựa chọn trong p-multiselect
88
+ */
89
+ public static void selectMultiOptions(WebElement multiSelectEl, List<String> optionTexts) {
90
+ clickElement(multiSelectEl)
91
+ waitForElementVisible(By.cssSelector(".p-multiselect-panel"), 5)
92
+ WebDriver driver = DriverFactory.getWebDriver()
93
+ for (String opt : optionTexts) {
94
+ WebElement item = driver.findElement(By.xpath("//li[contains(@class, 'p-multiselect-item') and contains(., '" + opt + "')]"))
95
+ clickElement(item)
96
+ }
97
+ WebElement closeBtn = driver.findElement(By.cssSelector(".p-multiselect-close"))
98
+ if (closeBtn != null && closeBtn.isDisplayed()) {
99
+ closeBtn.click()
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Kiểm tra bản ghi tồn tại trong bảng p-table
105
+ */
106
+ public static void verifyRecordExists(String keyword) {
107
+ WebDriver driver = DriverFactory.getWebDriver()
108
+ List<WebElement> rows = driver.findElements(By.cssSelector("p-table table tbody tr"))
109
+ assert !rows.isEmpty() : "Lỗi: Bảng không có dữ liệu nào!"
110
+ String firstRowText = rows.get(0).getText()
111
+ assert firstRowText.contains(keyword) : "Lỗi: Không tìm thấy từ khóa '${keyword}' trong hàng đầu tiên!"
112
+ }
113
+
114
+ /**
115
+ * Kiểm tra bản ghi đã bị xóa hoàn toàn khỏi bảng
116
+ */
117
+ public static void verifyRecordNotExists(String keyword) {
118
+ WebDriver driver = DriverFactory.getWebDriver()
119
+ searchRecord(keyword)
120
+ List<WebElement> rows = driver.findElements(By.cssSelector("p-table table tbody tr"))
121
+ if (rows.isEmpty()) return
122
+ String firstRowText = rows.get(0).getText()
123
+ assert !firstRowText.contains(keyword) || firstRowText.contains("Không có dữ liệu") : "Lỗi: Bản ghi '${keyword}' vẫn còn tồn tại trong bảng!"
124
+ }
125
+
126
+ /**
127
+ * Gán giá trị an toàn kích hoạt Angular Change Detection
128
+ */
129
+ public static void jsSetInput(WebElement element, String value) {
130
+ WebDriver driver = DriverFactory.getWebDriver()
131
+ JavascriptExecutor js = (JavascriptExecutor) driver
132
+ js.executeScript("""
133
+ arguments[0].value = arguments[1];
134
+ arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
135
+ arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
136
+ """, element, value)
137
+ }
138
+ }