donobu 2.31.0 → 2.32.0
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.
- package/dist/apis/GptConfigsApi.d.ts.map +1 -1
- package/dist/apis/GptConfigsApi.js +2 -0
- package/dist/apis/GptConfigsApi.js.map +1 -1
- package/dist/apis/SpecialFlowsApi.d.ts.map +1 -1
- package/dist/apis/SpecialFlowsApi.js +1 -6
- package/dist/apis/SpecialFlowsApi.js.map +1 -1
- package/dist/assets/generated/version +1 -1
- package/dist/assets/interactive-elements-tracker.js +81 -60
- package/dist/assets/page-interactions-tracker.js +9 -69
- package/dist/assets/smart-selector-generator.js +839 -259
- package/dist/bindings/PageInteractionTracker.d.ts +27 -1
- package/dist/bindings/PageInteractionTracker.d.ts.map +1 -1
- package/dist/bindings/PageInteractionTracker.js +172 -61
- package/dist/bindings/PageInteractionTracker.js.map +1 -1
- package/dist/clients/AnthropicAwsBedrockGptClient.d.ts +12 -4
- package/dist/clients/AnthropicAwsBedrockGptClient.d.ts.map +1 -1
- package/dist/clients/AnthropicAwsBedrockGptClient.js +8 -8
- package/dist/clients/AnthropicAwsBedrockGptClient.js.map +1 -1
- package/dist/clients/AnthropicGptClient.d.ts +12 -4
- package/dist/clients/AnthropicGptClient.d.ts.map +1 -1
- package/dist/clients/AnthropicGptClient.js +12 -10
- package/dist/clients/AnthropicGptClient.js.map +1 -1
- package/dist/clients/GoogleGenerativeAiGptClient.d.ts +12 -4
- package/dist/clients/GoogleGenerativeAiGptClient.d.ts.map +1 -1
- package/dist/clients/GoogleGenerativeAiGptClient.js +8 -8
- package/dist/clients/GoogleGenerativeAiGptClient.js.map +1 -1
- package/dist/clients/GoogleVertexGptClient.d.ts +12 -4
- package/dist/clients/GoogleVertexGptClient.d.ts.map +1 -1
- package/dist/clients/GoogleVertexGptClient.js +8 -8
- package/dist/clients/GoogleVertexGptClient.js.map +1 -1
- package/dist/clients/GptClient.d.ts +12 -4
- package/dist/clients/GptClient.d.ts.map +1 -1
- package/dist/clients/GptClient.js.map +1 -1
- package/dist/clients/OpenAiGptClient.d.ts +12 -4
- package/dist/clients/OpenAiGptClient.d.ts.map +1 -1
- package/dist/clients/OpenAiGptClient.js +12 -10
- package/dist/clients/OpenAiGptClient.js.map +1 -1
- package/dist/clients/VercelAiGptClient.d.ts +14 -5
- package/dist/clients/VercelAiGptClient.d.ts.map +1 -1
- package/dist/clients/VercelAiGptClient.js +54 -37
- package/dist/clients/VercelAiGptClient.js.map +1 -1
- package/dist/esm/apis/GptConfigsApi.d.ts.map +1 -1
- package/dist/esm/apis/GptConfigsApi.js +2 -0
- package/dist/esm/apis/GptConfigsApi.js.map +1 -1
- package/dist/esm/apis/SpecialFlowsApi.d.ts.map +1 -1
- package/dist/esm/apis/SpecialFlowsApi.js +1 -6
- package/dist/esm/apis/SpecialFlowsApi.js.map +1 -1
- package/dist/esm/assets/generated/version +1 -1
- package/dist/esm/assets/interactive-elements-tracker.js +81 -60
- package/dist/esm/assets/page-interactions-tracker.js +9 -69
- package/dist/esm/assets/smart-selector-generator.js +839 -259
- package/dist/esm/bindings/PageInteractionTracker.d.ts +27 -1
- package/dist/esm/bindings/PageInteractionTracker.d.ts.map +1 -1
- package/dist/esm/bindings/PageInteractionTracker.js +172 -61
- package/dist/esm/bindings/PageInteractionTracker.js.map +1 -1
- package/dist/esm/clients/AnthropicAwsBedrockGptClient.d.ts +12 -4
- package/dist/esm/clients/AnthropicAwsBedrockGptClient.d.ts.map +1 -1
- package/dist/esm/clients/AnthropicAwsBedrockGptClient.js +8 -8
- package/dist/esm/clients/AnthropicAwsBedrockGptClient.js.map +1 -1
- package/dist/esm/clients/AnthropicGptClient.d.ts +12 -4
- package/dist/esm/clients/AnthropicGptClient.d.ts.map +1 -1
- package/dist/esm/clients/AnthropicGptClient.js +12 -10
- package/dist/esm/clients/AnthropicGptClient.js.map +1 -1
- package/dist/esm/clients/GoogleGenerativeAiGptClient.d.ts +12 -4
- package/dist/esm/clients/GoogleGenerativeAiGptClient.d.ts.map +1 -1
- package/dist/esm/clients/GoogleGenerativeAiGptClient.js +8 -8
- package/dist/esm/clients/GoogleGenerativeAiGptClient.js.map +1 -1
- package/dist/esm/clients/GoogleVertexGptClient.d.ts +12 -4
- package/dist/esm/clients/GoogleVertexGptClient.d.ts.map +1 -1
- package/dist/esm/clients/GoogleVertexGptClient.js +8 -8
- package/dist/esm/clients/GoogleVertexGptClient.js.map +1 -1
- package/dist/esm/clients/GptClient.d.ts +12 -4
- package/dist/esm/clients/GptClient.d.ts.map +1 -1
- package/dist/esm/clients/GptClient.js.map +1 -1
- package/dist/esm/clients/OpenAiGptClient.d.ts +12 -4
- package/dist/esm/clients/OpenAiGptClient.d.ts.map +1 -1
- package/dist/esm/clients/OpenAiGptClient.js +12 -10
- package/dist/esm/clients/OpenAiGptClient.js.map +1 -1
- package/dist/esm/clients/VercelAiGptClient.d.ts +14 -5
- package/dist/esm/clients/VercelAiGptClient.d.ts.map +1 -1
- package/dist/esm/clients/VercelAiGptClient.js +54 -37
- package/dist/esm/clients/VercelAiGptClient.js.map +1 -1
- package/dist/esm/exceptions/UserInterruptException.d.ts +7 -0
- package/dist/esm/exceptions/UserInterruptException.d.ts.map +1 -0
- package/dist/esm/exceptions/UserInterruptException.js +12 -0
- package/dist/esm/exceptions/UserInterruptException.js.map +1 -0
- package/dist/esm/lib/fixtures/gptClients.d.ts.map +1 -1
- package/dist/esm/lib/fixtures/gptClients.js +3 -4
- package/dist/esm/lib/fixtures/gptClients.js.map +1 -1
- package/dist/esm/lib/testExtension.d.ts.map +1 -1
- package/dist/esm/lib/testExtension.js +9 -4
- package/dist/esm/lib/testExtension.js.map +1 -1
- package/dist/esm/lib/utils/donobuTestStack.d.ts.map +1 -1
- package/dist/esm/lib/utils/donobuTestStack.js +2 -1
- package/dist/esm/lib/utils/donobuTestStack.js.map +1 -1
- package/dist/esm/main.d.ts +3 -1
- package/dist/esm/main.d.ts.map +1 -1
- package/dist/esm/main.js +4 -2
- package/dist/esm/main.js.map +1 -1
- package/dist/esm/managers/AdminApiController.d.ts +8 -1
- package/dist/esm/managers/AdminApiController.d.ts.map +1 -1
- package/dist/esm/managers/AdminApiController.js +10 -4
- package/dist/esm/managers/AdminApiController.js.map +1 -1
- package/dist/esm/managers/AgentsManager.d.ts +9 -0
- package/dist/esm/managers/AgentsManager.d.ts.map +1 -1
- package/dist/esm/managers/AgentsManager.js +52 -0
- package/dist/esm/managers/AgentsManager.js.map +1 -1
- package/dist/esm/managers/CodeGenerator.d.ts.map +1 -1
- package/dist/esm/managers/CodeGenerator.js +23 -11
- package/dist/esm/managers/CodeGenerator.js.map +1 -1
- package/dist/esm/managers/DonobuFlow.d.ts +12 -7
- package/dist/esm/managers/DonobuFlow.d.ts.map +1 -1
- package/dist/esm/managers/DonobuFlow.js +146 -155
- package/dist/esm/managers/DonobuFlow.js.map +1 -1
- package/dist/esm/managers/DonobuFlowsManager.d.ts +3 -1
- package/dist/esm/managers/DonobuFlowsManager.d.ts.map +1 -1
- package/dist/esm/managers/DonobuFlowsManager.js +7 -2
- package/dist/esm/managers/DonobuFlowsManager.js.map +1 -1
- package/dist/esm/managers/DonobuStack.d.ts +2 -1
- package/dist/esm/managers/DonobuStack.d.ts.map +1 -1
- package/dist/esm/managers/DonobuStack.js +2 -2
- package/dist/esm/managers/DonobuStack.js.map +1 -1
- package/dist/esm/managers/PageInspector.d.ts.map +1 -1
- package/dist/esm/managers/PageInspector.js +80 -14
- package/dist/esm/managers/PageInspector.js.map +1 -1
- package/dist/esm/managers/ToolManager.d.ts.map +1 -1
- package/dist/esm/managers/ToolManager.js +4 -3
- package/dist/esm/managers/ToolManager.js.map +1 -1
- package/dist/esm/models/BrowserConfig.d.ts +64 -64
- package/dist/esm/models/CodeGenerationOptions.d.ts +6 -0
- package/dist/esm/models/CodeGenerationOptions.d.ts.map +1 -1
- package/dist/esm/models/CodeGenerationOptions.js +9 -0
- package/dist/esm/models/CodeGenerationOptions.js.map +1 -1
- package/dist/esm/models/ControlPanel.d.ts +29 -0
- package/dist/esm/models/ControlPanel.d.ts.map +1 -0
- package/dist/esm/models/ControlPanel.js +16 -0
- package/dist/esm/models/ControlPanel.js.map +1 -0
- package/dist/esm/models/CreateDonobuFlow.d.ts +56 -56
- package/dist/esm/models/FlowMetadata.d.ts +62 -62
- package/dist/esm/models/InteractableElement.d.ts +4 -3
- package/dist/esm/models/InteractableElement.d.ts.map +1 -1
- package/dist/esm/models/InteractableElement.js +9 -4
- package/dist/esm/models/InteractableElement.js.map +1 -1
- package/dist/esm/models/ToolCallContext.d.ts +1 -1
- package/dist/esm/models/ToolCallContext.d.ts.map +1 -1
- package/dist/esm/persistence/DonobuSqliteDb.d.ts.map +1 -1
- package/dist/esm/persistence/DonobuSqliteDb.js +44 -0
- package/dist/esm/persistence/DonobuSqliteDb.js.map +1 -1
- package/dist/esm/tools/AcknowledgeUserInstruction.d.ts +1 -1
- package/dist/esm/tools/AcknowledgeUserInstruction.d.ts.map +1 -1
- package/dist/esm/tools/AcknowledgeUserInstruction.js +2 -6
- package/dist/esm/tools/AcknowledgeUserInstruction.js.map +1 -1
- package/dist/esm/tools/AssertPageTool.d.ts +4 -4
- package/dist/esm/tools/ClickTool.js +1 -1
- package/dist/esm/tools/ClickTool.js.map +1 -1
- package/dist/esm/tools/InputRandomizedEmailAddressTool.d.ts +6 -6
- package/dist/esm/tools/InputTextTool.d.ts +9 -0
- package/dist/esm/tools/InputTextTool.d.ts.map +1 -1
- package/dist/esm/tools/InputTextTool.js +8 -2
- package/dist/esm/tools/InputTextTool.js.map +1 -1
- package/dist/esm/tools/PressKeyTool.d.ts.map +1 -1
- package/dist/esm/tools/PressKeyTool.js +8 -3
- package/dist/esm/tools/PressKeyTool.js.map +1 -1
- package/dist/esm/tools/ReplayableInteraction.d.ts.map +1 -1
- package/dist/esm/tools/ReplayableInteraction.js +15 -15
- package/dist/esm/tools/ReplayableInteraction.js.map +1 -1
- package/dist/esm/tools/RunAccessibilityTestTool.d.ts +0 -8
- package/dist/esm/tools/RunAccessibilityTestTool.d.ts.map +1 -1
- package/dist/esm/tools/RunAccessibilityTestTool.js +20 -38
- package/dist/esm/tools/RunAccessibilityTestTool.js.map +1 -1
- package/dist/esm/tools/ScrollPageTool.d.ts +52 -11
- package/dist/esm/tools/ScrollPageTool.d.ts.map +1 -1
- package/dist/esm/tools/ScrollPageTool.js +63 -57
- package/dist/esm/tools/ScrollPageTool.js.map +1 -1
- package/dist/esm/tools/TriggerDonobuFlowTool.d.ts +136 -136
- package/dist/esm/utils/BrowserUtils.js +1 -1
- package/dist/esm/utils/BrowserUtils.js.map +1 -1
- package/dist/esm/utils/PlaywrightUtils.d.ts.map +1 -1
- package/dist/esm/utils/PlaywrightUtils.js +0 -4
- package/dist/esm/utils/PlaywrightUtils.js.map +1 -1
- package/dist/exceptions/UserInterruptException.d.ts +7 -0
- package/dist/exceptions/UserInterruptException.d.ts.map +1 -0
- package/dist/exceptions/UserInterruptException.js +12 -0
- package/dist/exceptions/UserInterruptException.js.map +1 -0
- package/dist/lib/fixtures/gptClients.d.ts.map +1 -1
- package/dist/lib/fixtures/gptClients.js +3 -4
- package/dist/lib/fixtures/gptClients.js.map +1 -1
- package/dist/lib/testExtension.d.ts.map +1 -1
- package/dist/lib/testExtension.js +9 -4
- package/dist/lib/testExtension.js.map +1 -1
- package/dist/lib/utils/donobuTestStack.d.ts.map +1 -1
- package/dist/lib/utils/donobuTestStack.js +2 -1
- package/dist/lib/utils/donobuTestStack.js.map +1 -1
- package/dist/main.d.ts +3 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -2
- package/dist/main.js.map +1 -1
- package/dist/managers/AdminApiController.d.ts +8 -1
- package/dist/managers/AdminApiController.d.ts.map +1 -1
- package/dist/managers/AdminApiController.js +10 -4
- package/dist/managers/AdminApiController.js.map +1 -1
- package/dist/managers/AgentsManager.d.ts +9 -0
- package/dist/managers/AgentsManager.d.ts.map +1 -1
- package/dist/managers/AgentsManager.js +52 -0
- package/dist/managers/AgentsManager.js.map +1 -1
- package/dist/managers/CodeGenerator.d.ts.map +1 -1
- package/dist/managers/CodeGenerator.js +23 -11
- package/dist/managers/CodeGenerator.js.map +1 -1
- package/dist/managers/DonobuFlow.d.ts +12 -7
- package/dist/managers/DonobuFlow.d.ts.map +1 -1
- package/dist/managers/DonobuFlow.js +146 -155
- package/dist/managers/DonobuFlow.js.map +1 -1
- package/dist/managers/DonobuFlowsManager.d.ts +3 -1
- package/dist/managers/DonobuFlowsManager.d.ts.map +1 -1
- package/dist/managers/DonobuFlowsManager.js +7 -2
- package/dist/managers/DonobuFlowsManager.js.map +1 -1
- package/dist/managers/DonobuStack.d.ts +2 -1
- package/dist/managers/DonobuStack.d.ts.map +1 -1
- package/dist/managers/DonobuStack.js +2 -2
- package/dist/managers/DonobuStack.js.map +1 -1
- package/dist/managers/PageInspector.d.ts.map +1 -1
- package/dist/managers/PageInspector.js +80 -14
- package/dist/managers/PageInspector.js.map +1 -1
- package/dist/managers/ToolManager.d.ts.map +1 -1
- package/dist/managers/ToolManager.js +4 -3
- package/dist/managers/ToolManager.js.map +1 -1
- package/dist/models/BrowserConfig.d.ts +64 -64
- package/dist/models/CodeGenerationOptions.d.ts +6 -0
- package/dist/models/CodeGenerationOptions.d.ts.map +1 -1
- package/dist/models/CodeGenerationOptions.js +9 -0
- package/dist/models/CodeGenerationOptions.js.map +1 -1
- package/dist/models/ControlPanel.d.ts +29 -0
- package/dist/models/ControlPanel.d.ts.map +1 -0
- package/dist/models/ControlPanel.js +16 -0
- package/dist/models/ControlPanel.js.map +1 -0
- package/dist/models/CreateDonobuFlow.d.ts +56 -56
- package/dist/models/FlowMetadata.d.ts +62 -62
- package/dist/models/InteractableElement.d.ts +4 -3
- package/dist/models/InteractableElement.d.ts.map +1 -1
- package/dist/models/InteractableElement.js +9 -4
- package/dist/models/InteractableElement.js.map +1 -1
- package/dist/models/ToolCallContext.d.ts +1 -1
- package/dist/models/ToolCallContext.d.ts.map +1 -1
- package/dist/persistence/DonobuSqliteDb.d.ts.map +1 -1
- package/dist/persistence/DonobuSqliteDb.js +44 -0
- package/dist/persistence/DonobuSqliteDb.js.map +1 -1
- package/dist/tools/AcknowledgeUserInstruction.d.ts +1 -1
- package/dist/tools/AcknowledgeUserInstruction.d.ts.map +1 -1
- package/dist/tools/AcknowledgeUserInstruction.js +2 -6
- package/dist/tools/AcknowledgeUserInstruction.js.map +1 -1
- package/dist/tools/AssertPageTool.d.ts +4 -4
- package/dist/tools/ClickTool.js +1 -1
- package/dist/tools/ClickTool.js.map +1 -1
- package/dist/tools/InputRandomizedEmailAddressTool.d.ts +6 -6
- package/dist/tools/InputTextTool.d.ts +9 -0
- package/dist/tools/InputTextTool.d.ts.map +1 -1
- package/dist/tools/InputTextTool.js +8 -2
- package/dist/tools/InputTextTool.js.map +1 -1
- package/dist/tools/PressKeyTool.d.ts.map +1 -1
- package/dist/tools/PressKeyTool.js +8 -3
- package/dist/tools/PressKeyTool.js.map +1 -1
- package/dist/tools/ReplayableInteraction.d.ts.map +1 -1
- package/dist/tools/ReplayableInteraction.js +15 -15
- package/dist/tools/ReplayableInteraction.js.map +1 -1
- package/dist/tools/RunAccessibilityTestTool.d.ts +0 -8
- package/dist/tools/RunAccessibilityTestTool.d.ts.map +1 -1
- package/dist/tools/RunAccessibilityTestTool.js +20 -38
- package/dist/tools/RunAccessibilityTestTool.js.map +1 -1
- package/dist/tools/ScrollPageTool.d.ts +52 -11
- package/dist/tools/ScrollPageTool.d.ts.map +1 -1
- package/dist/tools/ScrollPageTool.js +63 -57
- package/dist/tools/ScrollPageTool.js.map +1 -1
- package/dist/tools/TriggerDonobuFlowTool.d.ts +136 -136
- package/dist/utils/BrowserUtils.js +1 -1
- package/dist/utils/BrowserUtils.js.map +1 -1
- package/dist/utils/PlaywrightUtils.d.ts.map +1 -1
- package/dist/utils/PlaywrightUtils.js +0 -4
- package/dist/utils/PlaywrightUtils.js.map +1 -1
- package/package.json +2 -1
- package/dist/assets/axe.js +0 -47397
- package/dist/assets/control-panel.js +0 -646
- package/dist/esm/assets/axe.js +0 -47397
- package/dist/esm/assets/control-panel.js +0 -646
- package/dist/esm/managers/ControlPanel.d.ts +0 -127
- package/dist/esm/managers/ControlPanel.d.ts.map +0 -1
- package/dist/esm/managers/ControlPanel.js +0 -291
- package/dist/esm/managers/ControlPanel.js.map +0 -1
- package/dist/esm/tools/GetEntityDataFromGoogleMapResult.d.ts +0 -31
- package/dist/esm/tools/GetEntityDataFromGoogleMapResult.d.ts.map +0 -1
- package/dist/esm/tools/GetEntityDataFromGoogleMapResult.js +0 -45
- package/dist/esm/tools/GetEntityDataFromGoogleMapResult.js.map +0 -1
- package/dist/managers/ControlPanel.d.ts +0 -127
- package/dist/managers/ControlPanel.d.ts.map +0 -1
- package/dist/managers/ControlPanel.js +0 -291
- package/dist/managers/ControlPanel.js.map +0 -1
- package/dist/tools/GetEntityDataFromGoogleMapResult.d.ts +0 -31
- package/dist/tools/GetEntityDataFromGoogleMapResult.d.ts.map +0 -1
- package/dist/tools/GetEntityDataFromGoogleMapResult.js +0 -45
- package/dist/tools/GetEntityDataFromGoogleMapResult.js.map +0 -1
|
@@ -1,348 +1,928 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Smart-selector generator: (c) 2025 Donobu
|
|
3
|
+
*
|
|
4
|
+
* This library generates CSS selectors and XPath expressions that can reliably
|
|
5
|
+
* locate DOM elements across page loads and updates. It's designed to run as
|
|
6
|
+
* a Playwright init script, creating selectors that are robust against:
|
|
7
|
+
* - Dynamic class names and IDs (hash-like values)
|
|
8
|
+
* - DOM structure changes
|
|
9
|
+
* - Shadow DOM boundaries
|
|
10
|
+
*
|
|
11
|
+
* The library prioritizes semantic selectors (aria-label, data-testid, text content)
|
|
12
|
+
* over brittle positional selectors, making test automation more maintainable.
|
|
13
|
+
*/
|
|
1
14
|
(() => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Setup
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const DONOBU_KEY = '__donobu';
|
|
19
|
+
|
|
20
|
+
if (!window[DONOBU_KEY]) {
|
|
21
|
+
Object.defineProperty(window, DONOBU_KEY, {
|
|
22
|
+
value: Object.create(null),
|
|
23
|
+
writable: false,
|
|
5
24
|
enumerable: false,
|
|
6
|
-
writable: true,
|
|
7
25
|
configurable: false,
|
|
8
26
|
});
|
|
9
27
|
}
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
const escapeCss =
|
|
30
|
+
typeof CSS !== 'undefined' && CSS.escape
|
|
31
|
+
? CSS.escape
|
|
32
|
+
: (s) => s.replace(/[^\w-]/g, (c) => '\\' + c);
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tiny utils
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Properly quotes an attribute value for use in CSS selectors.
|
|
40
|
+
* Handles escaping of backslashes and single quotes.
|
|
41
|
+
* Example: quoteCssAttr("user's name") → "'user\\'s name'"
|
|
42
|
+
*/
|
|
43
|
+
const quoteCssAttr = (v) =>
|
|
44
|
+
`'${
|
|
45
|
+
String(v)
|
|
46
|
+
.replace(/\\/g, '\\\\') // escape backslashes
|
|
47
|
+
.replace(/'/g, "\\'") // escape single quotes
|
|
48
|
+
.replace(/\r?\n/g, '\\A ') // escape newlines as CSS \A (newline)
|
|
49
|
+
}'`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Counts how many elements match a CSS selector within a given scope.
|
|
53
|
+
* Handles invalid selectors gracefully by returning 0 and logging warnings.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} sel - CSS selector to test
|
|
56
|
+
* @param {Document|ShadowRoot|Element} scope - Root element to search within
|
|
57
|
+
* @returns {number} Number of matching elements
|
|
58
|
+
*/
|
|
59
|
+
const countMatchesCSS = (sel, scope) => {
|
|
60
|
+
if (!sel || !scope || typeof scope.querySelectorAll !== 'function') {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
try {
|
|
65
|
+
return scope.querySelectorAll(sel).length;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.warn('[Donobu] Invalid CSS selector:', sel, e.message);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Counts how many elements match an XPath expression within a given scope.
|
|
74
|
+
* XPath is more powerful than CSS for text-based matching.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} xp - XPath expression to evaluate
|
|
77
|
+
* @param {Document|ShadowRoot|Element} scope - Root element to search within
|
|
78
|
+
* @returns {number} Number of matching elements
|
|
79
|
+
*/
|
|
80
|
+
const countMatchesXPath = (xp, scope) => {
|
|
81
|
+
if (!xp || !scope) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const doc = getDocumentForScope(scope);
|
|
87
|
+
|
|
88
|
+
if (!doc) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return doc.evaluate(
|
|
93
|
+
xp,
|
|
94
|
+
scope,
|
|
95
|
+
null,
|
|
96
|
+
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
97
|
+
null,
|
|
98
|
+
).snapshotLength;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.warn('[Donobu] Invalid XPath expression:', xp, e.message);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Gets the appropriate Document object for XPath evaluation.
|
|
107
|
+
* Shadow roots need their host's document, regular elements use their ownerDocument.
|
|
108
|
+
*/
|
|
109
|
+
const getDocumentForScope = (scope) => {
|
|
110
|
+
if (scope instanceof Document) {
|
|
111
|
+
return scope;
|
|
112
|
+
} else if (scope instanceof ShadowRoot) {
|
|
113
|
+
return scope.host?.ownerDocument || document;
|
|
114
|
+
} else {
|
|
115
|
+
return scope?.ownerDocument || document;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/* --------------------------------------------------------------- */
|
|
120
|
+
/* Collect all (open) shadow hosts between element and document */
|
|
121
|
+
/* --------------------------------------------------------------- */
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collects all shadow hosts in the path from an element to the document root.
|
|
125
|
+
* This is essential for generating selectors that work across shadow boundaries.
|
|
126
|
+
*
|
|
127
|
+
* Modern web apps heavily use Shadow DOM (web components, React portals, etc.)
|
|
128
|
+
* and selectors must account for these boundaries.
|
|
129
|
+
*
|
|
130
|
+
* @param {Element} el - Target element
|
|
131
|
+
* @returns {Array<{host: Element, open: boolean}>} Chain of shadow hosts, nearest-to-document first
|
|
132
|
+
*/
|
|
133
|
+
function gatherShadowChain(el) {
|
|
134
|
+
const chain = []; // [{ host, open }, …] nearest-to-document first
|
|
135
|
+
let node = el;
|
|
136
|
+
const visited = new WeakSet(); // Prevent infinite loops
|
|
137
|
+
|
|
138
|
+
while (node && node !== document && !visited.has(node)) {
|
|
139
|
+
visited.add(node);
|
|
140
|
+
const root = node.getRootNode?.();
|
|
141
|
+
|
|
142
|
+
if (root instanceof ShadowRoot) {
|
|
143
|
+
chain.unshift({ host: root.host, open: root.mode === 'open' });
|
|
144
|
+
node = root.host;
|
|
36
145
|
} else {
|
|
37
|
-
|
|
38
|
-
this.count = document.querySelectorAll(selector).length;
|
|
146
|
+
node = node.parentNode;
|
|
39
147
|
}
|
|
40
148
|
}
|
|
149
|
+
return chain;
|
|
41
150
|
}
|
|
42
151
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Detects machine-generated identifiers that are likely to change.
|
|
154
|
+
* These heuristics help avoid creating brittle selectors based on:
|
|
155
|
+
* - Webpack hash IDs
|
|
156
|
+
* - UUID-style identifiers
|
|
157
|
+
* - Long hexadecimal strings
|
|
158
|
+
*
|
|
159
|
+
* @param {string|SVGAnimatedString} raw - ID or class name to test
|
|
160
|
+
* @returns {boolean} True if the value looks machine-generated
|
|
161
|
+
*/
|
|
162
|
+
const isHashLike = (raw) => {
|
|
163
|
+
if (raw == null) return false;
|
|
164
|
+
const str =
|
|
165
|
+
typeof raw === 'string'
|
|
166
|
+
? raw
|
|
167
|
+
: typeof raw.baseVal === 'string' // SVGAnimatedString
|
|
168
|
+
? raw.baseVal
|
|
169
|
+
: String(raw);
|
|
170
|
+
|
|
171
|
+
/* Heuristics */
|
|
172
|
+
return (
|
|
173
|
+
// 1. Pure 6+ hex digits
|
|
174
|
+
/^[a-f0-9]{6,}$/i.test(str) ||
|
|
175
|
+
// 2. UUID v4 style
|
|
176
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
177
|
+
str,
|
|
178
|
+
) ||
|
|
179
|
+
// 3. ≥2 long hex-ish segments joined with - or _
|
|
180
|
+
str.split(/[-_]/).filter((s) => /^[a-f0-9]{6,}$/i.test(s)).length >= 2
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Semantic HTML5 elements that are likely to be unique and stable.
|
|
185
|
+
const LANDMARK_TAGS = ['header', 'nav', 'main', 'footer', 'form'];
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Safely escapes text for use in XPath expressions.
|
|
189
|
+
* XPath has complex quoting rules, especially when text contains both single and double quotes.
|
|
190
|
+
* This function handles all edge cases including control characters and Unicode.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} txt - Text to escape for XPath
|
|
193
|
+
* @returns {string} Properly escaped XPath string literal
|
|
194
|
+
*/
|
|
195
|
+
const safeXpath = (txt) => {
|
|
196
|
+
// Remove control characters but preserve valid Unicode including surrogate pairs
|
|
197
|
+
const cleaned = String(txt)
|
|
198
|
+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
|
|
199
|
+
.replace(/\\/g, '\\\\');
|
|
200
|
+
// If no quotes at all, use single quotes (simplest case)
|
|
201
|
+
if (!cleaned.includes("'") && !cleaned.includes('"')) {
|
|
202
|
+
return `'${cleaned}'`;
|
|
48
203
|
}
|
|
49
204
|
|
|
50
|
-
// If
|
|
51
|
-
if (!
|
|
52
|
-
return `
|
|
205
|
+
// If only double quotes, use single quotes
|
|
206
|
+
if (!cleaned.includes("'")) {
|
|
207
|
+
return `'${cleaned}'`;
|
|
53
208
|
}
|
|
54
209
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return `concat('${parts.join("', \"'\", '")}')`;
|
|
60
|
-
}
|
|
210
|
+
// If only single quotes, use double quotes
|
|
211
|
+
if (!cleaned.includes('"')) {
|
|
212
|
+
return `"${cleaned}"`;
|
|
213
|
+
}
|
|
61
214
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
215
|
+
// Both types of quotes present - need to use concat()
|
|
216
|
+
const parts = cleaned.split("'");
|
|
217
|
+
const concatParts = [];
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < parts.length; i++) {
|
|
220
|
+
if (i > 0) {
|
|
221
|
+
// Add the single quote that was removed by split
|
|
222
|
+
concatParts.push('"\'"');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (parts[i]) {
|
|
226
|
+
// If this part contains double quotes, escape them
|
|
227
|
+
const part = parts[i].includes('"')
|
|
228
|
+
? `'${parts[i]}'` // Safe to use single quotes
|
|
229
|
+
: `"${parts[i]}"`; // Use double quotes for variety/readability
|
|
230
|
+
concatParts.push(part);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return `concat(${concatParts.join(', ')})`;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Safely extracts className from both regular DOM and SVG elements.
|
|
239
|
+
* SVG elements have className as an SVGAnimatedString object, not a string.
|
|
240
|
+
*
|
|
241
|
+
* @param {Element} el - Element to get class value from
|
|
242
|
+
* @returns {string} Class value as a string
|
|
243
|
+
*/
|
|
244
|
+
const getClassValue = (el) => {
|
|
245
|
+
if (!el.className) {
|
|
246
|
+
return '';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (typeof el.className === 'string') {
|
|
250
|
+
// Regular DOM element
|
|
251
|
+
return el.className;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// SVG element with SVGAnimatedString
|
|
255
|
+
if (
|
|
256
|
+
typeof el.className === 'object' &&
|
|
257
|
+
el.className?.baseVal !== undefined
|
|
258
|
+
) {
|
|
259
|
+
return el.className.baseVal;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback for unknown className types
|
|
263
|
+
return String(el.className);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Core
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The main class that generates smart selectors for a given element.
|
|
272
|
+
*
|
|
273
|
+
* Strategy overview:
|
|
274
|
+
* 1. Try semantic anchors first (ID, ARIA, data attributes, text content).
|
|
275
|
+
* 2. Fall back to positional selectors when semantic ones aren't unique.
|
|
276
|
+
* 3. Rank results by uniqueness, then by semantic value, then by length.
|
|
277
|
+
*
|
|
278
|
+
* Weight system (higher = higher priority):
|
|
279
|
+
* - 100: Unique, human-readable ID
|
|
280
|
+
* - 95: data-testid, data-test attributes
|
|
281
|
+
* - 90: aria-label
|
|
282
|
+
* - 88: Label associations
|
|
283
|
+
* - 85: Unique text content
|
|
284
|
+
* - 80: name attribute
|
|
285
|
+
* - 70: title attribute
|
|
286
|
+
* - 60: role attribute
|
|
287
|
+
* - 50: href attribute
|
|
288
|
+
* - 40: other data-* attributes
|
|
289
|
+
* - 35: stable class names
|
|
290
|
+
* - 20: positional with stable ancestors
|
|
291
|
+
* - 1: full DOM path (last resort)
|
|
292
|
+
*/
|
|
293
|
+
class SelectorBuilder {
|
|
294
|
+
constructor(el) {
|
|
295
|
+
this.el = el;
|
|
296
|
+
this.tag = el.tagName.toLowerCase();
|
|
297
|
+
this.root = el.getRootNode?.() || document;
|
|
298
|
+
this.list = new Map(); // selector → {cnt, weight}
|
|
65
299
|
}
|
|
66
300
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
function selectorTiebreakerPriority(sel) {
|
|
118
|
-
// highest-to-lowest: aria-label, placeholder, text-based XPath, DOM position, id, other
|
|
119
|
-
if (/\[aria-label\s*=/.test(sel) || /@aria-label/.test(sel)) return 0;
|
|
120
|
-
if (/\[placeholder\s*=/.test(sel) || /@placeholder/.test(sel)) return 1;
|
|
121
|
-
if (sel.startsWith('//')) return 2;
|
|
122
|
-
if (sel.includes(' > ')) return 3; // DOM position selector
|
|
123
|
-
if (/^#/.test(sel)) return 4;
|
|
124
|
-
return 5;
|
|
301
|
+
/**
|
|
302
|
+
* Main entry point - generates and ranks all possible selectors.
|
|
303
|
+
* @returns {Array<string>} Ordered array of selectors, best first.
|
|
304
|
+
*/
|
|
305
|
+
build() {
|
|
306
|
+
/* 1. Semantic anchors */
|
|
307
|
+
this.idAnchor(); // #my-button
|
|
308
|
+
this.ariaAnchor(); // [aria-label="Submit form"]
|
|
309
|
+
this.attrAnchors(); // [data-testid="login-btn"]
|
|
310
|
+
this.placeholderAnchor(); // [placeholder="Enter email"]
|
|
311
|
+
this.textAnchor(); // .//button[text()="Click me"]
|
|
312
|
+
this.labelAnchor(); // .//label[text()="Email"]/input
|
|
313
|
+
this.classAnchor(); // button.primary-btn
|
|
314
|
+
|
|
315
|
+
/* 2. Positional fall-backs - used when semantic anchors aren't unique */
|
|
316
|
+
this.stableAncestorAnchors(); // #header > nav > button:nth-of-type(2)
|
|
317
|
+
this.fullDomPath(); // html > body > div:nth-of-type(3) > button
|
|
318
|
+
|
|
319
|
+
/* 3. Rank by uniqueness (lower count = better), then weight, then length */
|
|
320
|
+
const results = [...this.list.entries()]
|
|
321
|
+
.filter(([, m]) => m.cnt > 0) // valid only
|
|
322
|
+
.sort((a, b) => {
|
|
323
|
+
const da = a[1],
|
|
324
|
+
db = b[1];
|
|
325
|
+
if (da.cnt !== db.cnt) return da.cnt - db.cnt;
|
|
326
|
+
if (da.weight !== db.weight) return db.weight - da.weight; // Higher weight = higher priority
|
|
327
|
+
return a[0].length - b[0].length;
|
|
328
|
+
})
|
|
329
|
+
.map(([sel]) => sel);
|
|
330
|
+
|
|
331
|
+
// Clean up to prevent memory leaks
|
|
332
|
+
this.list.clear();
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* ---------------------------------------------------------------------- */
|
|
337
|
+
/* Utils */
|
|
338
|
+
/* ---------------------------------------------------------------------- */
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Adds a selector to the candidate list if it actually matches the target element.
|
|
342
|
+
* Supports both CSS selectors and XPath expressions.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} sel - Selector to test
|
|
345
|
+
* @param {number} weight - Priority weight (higher = more preferred)
|
|
346
|
+
*/
|
|
347
|
+
push(sel, weight) {
|
|
348
|
+
if (!sel || typeof sel !== 'string') {
|
|
349
|
+
return;
|
|
125
350
|
}
|
|
126
351
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
352
|
+
let cnt = 0;
|
|
353
|
+
let matchesOurElement = false;
|
|
354
|
+
const isXPath =
|
|
355
|
+
sel.startsWith('/') || sel.startsWith('.//') || sel.startsWith('(');
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
if (isXPath) {
|
|
359
|
+
const doc = getDocumentForScope(this.root);
|
|
360
|
+
|
|
361
|
+
if (!doc) {
|
|
362
|
+
return;
|
|
134
363
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
selectorTiebreakerPriority(a.selector) -
|
|
144
|
-
selectorTiebreakerPriority(b.selector)
|
|
364
|
+
|
|
365
|
+
const snap = doc.evaluate(
|
|
366
|
+
sel,
|
|
367
|
+
this.root,
|
|
368
|
+
null,
|
|
369
|
+
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
370
|
+
null,
|
|
145
371
|
);
|
|
146
|
-
|
|
147
|
-
|
|
372
|
+
cnt = snap.snapshotLength;
|
|
373
|
+
|
|
374
|
+
if (cnt > 0) {
|
|
375
|
+
for (let i = 0; i < cnt; i++) {
|
|
376
|
+
if (snap.snapshotItem(i) === this.el) {
|
|
377
|
+
matchesOurElement = true;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// CSS Selector
|
|
384
|
+
const results = this.root.querySelectorAll(sel);
|
|
385
|
+
cnt = results.length;
|
|
386
|
+
|
|
387
|
+
if (cnt > 0) {
|
|
388
|
+
matchesOurElement = [...results].includes(this.el);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch (e) {
|
|
392
|
+
// Log for debugging but don't throw
|
|
393
|
+
console.warn('[Donobu] Invalid selector:', sel, e.message);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
148
396
|
|
|
149
|
-
|
|
397
|
+
if (matchesOurElement) {
|
|
398
|
+
this.list.set(sel, { cnt, weight });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Adds a base selector and tries to make it unique by adding parent context.
|
|
404
|
+
* This is key to the "smart" behavior - we start with semantic selectors
|
|
405
|
+
* and add just enough context to make them unique.
|
|
406
|
+
*
|
|
407
|
+
* Example: button.submit → #form > button.submit → #header > #form > button.submit
|
|
408
|
+
*
|
|
409
|
+
* @param {string} baseSel - Base CSS selector
|
|
410
|
+
* @param {number} weight - Priority weight
|
|
411
|
+
* @param {number} maxDepth - Maximum ancestor levels to try
|
|
412
|
+
*/
|
|
413
|
+
scopedUntilUnique(baseSel, weight, maxDepth = 5) {
|
|
414
|
+
this.push(baseSel, weight - 10); // keep raw anchor (lower priority)
|
|
415
|
+
|
|
416
|
+
if (countMatchesCSS(baseSel, this.root) === 1) {
|
|
417
|
+
this.push(baseSel, weight); // High priority if unique
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let cur = this.el;
|
|
422
|
+
let depth = 0;
|
|
423
|
+
let sel = baseSel;
|
|
424
|
+
const visited = new WeakSet(); // Prevent infinite loops
|
|
425
|
+
|
|
426
|
+
while (cur.parentElement && depth < maxDepth && !visited.has(cur)) {
|
|
427
|
+
visited.add(cur);
|
|
428
|
+
|
|
429
|
+
const nextElem = cur.parentElement;
|
|
430
|
+
const tag = nextElem.tagName.toLowerCase();
|
|
431
|
+
const siblings = nextElem.parentElement
|
|
432
|
+
? [...nextElem.parentElement.children]
|
|
433
|
+
: [];
|
|
434
|
+
const siblingsSame = siblings.filter(
|
|
435
|
+
(c) => c.tagName.toLowerCase() === tag,
|
|
436
|
+
);
|
|
437
|
+
const parentSeg =
|
|
438
|
+
siblingsSame.length > 1
|
|
439
|
+
? `${tag}:nth-of-type(${siblingsSame.indexOf(nextElem) + 1})`
|
|
440
|
+
: tag;
|
|
441
|
+
|
|
442
|
+
sel = `${parentSeg} > ${sel}`;
|
|
443
|
+
|
|
444
|
+
if (countMatchesCSS(sel, this.root) === 1) {
|
|
445
|
+
this.push(sel, weight); // unique variant
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
cur = nextElem;
|
|
450
|
+
depth += 1;
|
|
451
|
+
}
|
|
150
452
|
}
|
|
151
453
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
454
|
+
/* ---------------------------------------------------------------------- */
|
|
455
|
+
/* Anchors – High weight = high priority */
|
|
456
|
+
/* ---------------------------------------------------------------------- */
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Generates ID-based selectors, but penalizes machine-generated IDs.
|
|
460
|
+
* Machine-generated IDs (hashes, UUIDs) are likely to change between builds.
|
|
461
|
+
*/
|
|
462
|
+
idAnchor() {
|
|
463
|
+
const id = this.el.id;
|
|
464
|
+
|
|
465
|
+
if (!id) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const dynamic = isHashLike(id) || id.length > 24;
|
|
470
|
+
this.push(`#${escapeCss(id)}`, dynamic ? 20 : 100);
|
|
155
471
|
}
|
|
156
472
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
473
|
+
/**
|
|
474
|
+
* Generates ARIA label selectors - these are excellent for accessibility
|
|
475
|
+
* and tend to be stable since they're user-facing.
|
|
476
|
+
*/
|
|
477
|
+
ariaAnchor() {
|
|
478
|
+
const aria = this.el.getAttribute('aria-label');
|
|
479
|
+
|
|
480
|
+
if (aria) {
|
|
481
|
+
this.scopedUntilUnique(`[aria-label=${quoteCssAttr(aria)}]`, 90);
|
|
482
|
+
}
|
|
165
483
|
}
|
|
166
484
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
485
|
+
/**
|
|
486
|
+
* Generates selectors for various attributes, prioritizing test-specific ones.
|
|
487
|
+
* data-testid and data-test are specifically added for testing and are very stable.
|
|
488
|
+
*/
|
|
489
|
+
attrAnchors() {
|
|
490
|
+
const ATTRS = {
|
|
491
|
+
'data-testid': 95,
|
|
492
|
+
'data-test': 95,
|
|
493
|
+
name: 80,
|
|
494
|
+
title: 70,
|
|
495
|
+
role: 60,
|
|
496
|
+
href: 50,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
for (const { name, value } of [...this.el.attributes]) {
|
|
500
|
+
if (!Object.keys(ATTRS).includes(name) && !name.startsWith('data-')) {
|
|
501
|
+
continue;
|
|
502
|
+
} else if (name.startsWith('data-donobu-')) {
|
|
503
|
+
continue;
|
|
184
504
|
}
|
|
185
|
-
}
|
|
186
505
|
|
|
187
|
-
|
|
506
|
+
const sel = `[${name}=${quoteCssAttr(value)}]`;
|
|
507
|
+
this.scopedUntilUnique(sel, ATTRS[name] ?? 40);
|
|
508
|
+
}
|
|
188
509
|
}
|
|
189
510
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Generates selectors based on placeholder text.
|
|
513
|
+
* Placeholders are user-visible and typically stable.
|
|
514
|
+
*/
|
|
515
|
+
placeholderAnchor() {
|
|
516
|
+
const ph = this.el.getAttribute('placeholder');
|
|
193
517
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return selectors;
|
|
518
|
+
if (ph) {
|
|
519
|
+
this.scopedUntilUnique(`[placeholder=${quoteCssAttr(ph)}]`, 75);
|
|
197
520
|
}
|
|
521
|
+
}
|
|
198
522
|
|
|
199
|
-
|
|
523
|
+
/**
|
|
524
|
+
* Generates XPath selectors based on text content.
|
|
525
|
+
* Text-based selectors are very semantic but can be brittle if text changes.
|
|
526
|
+
* Uses XPath because CSS can't match on text content directly.
|
|
527
|
+
*/
|
|
528
|
+
textAnchor() {
|
|
529
|
+
const raw = this.el.textContent ?? '';
|
|
530
|
+
const text = raw.trim();
|
|
531
|
+
|
|
532
|
+
if (!text || ['body', 'html'].includes(this.tag) || text.length > 100) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
200
535
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
536
|
+
const normalizedText = text.replace(/\s+/g, ' ').trim();
|
|
537
|
+
const baseXP = `.//${this.tag}[normalize-space(.)=${safeXpath(normalizedText)}]`;
|
|
538
|
+
const cnt = countMatchesXPath(baseXP, this.root);
|
|
539
|
+
|
|
540
|
+
if (cnt === 0) {
|
|
541
|
+
return;
|
|
205
542
|
}
|
|
206
543
|
|
|
207
|
-
|
|
544
|
+
this.push(baseXP, cnt === 1 ? 85 : 30); // Semantic anchor is valuable even if not unique
|
|
545
|
+
|
|
546
|
+
if (cnt > 1) {
|
|
547
|
+
const doc = getDocumentForScope(this.root);
|
|
548
|
+
|
|
549
|
+
if (!doc) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
const snap = doc.evaluate(
|
|
555
|
+
baseXP,
|
|
556
|
+
this.root,
|
|
557
|
+
null,
|
|
558
|
+
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
559
|
+
null,
|
|
560
|
+
);
|
|
561
|
+
let idx = -1;
|
|
562
|
+
|
|
563
|
+
for (let i = 0; i < cnt; i += 1) {
|
|
564
|
+
if (snap.snapshotItem(i) === this.el) {
|
|
565
|
+
idx = i + 1;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (idx > 0) {
|
|
571
|
+
const uniqueXP = `(${baseXP})[${idx}]`;
|
|
572
|
+
this.push(uniqueXP, 85); // same weight as a unique semantic anchor
|
|
573
|
+
}
|
|
574
|
+
} catch (e) {
|
|
575
|
+
console.warn('[Donobu] XPath evaluation failed:', baseXP, e.message);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
208
578
|
}
|
|
209
579
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Generates selectors for form inputs based on their associated labels.
|
|
582
|
+
* This creates very semantic selectors that mirror how users think about forms.
|
|
583
|
+
* Handles both for/id associations and wrapping label elements.
|
|
584
|
+
*/
|
|
585
|
+
labelAnchor() {
|
|
586
|
+
if (!['input', 'textarea', 'select'].includes(this.tag)) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// --- Helper for <label for> and wrapping <label> ---
|
|
591
|
+
// This helper assumes a close structural relationship (sibling/descendant)
|
|
592
|
+
const addForLabel = (lab) => {
|
|
593
|
+
const txt = (lab.textContent || '').trim();
|
|
594
|
+
|
|
595
|
+
if (!txt) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
214
598
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
599
|
+
const xp = safeXpath(txt);
|
|
600
|
+
// Assumes input is a descendant
|
|
601
|
+
this.push(`.//label[normalize-space()=${xp}]//${this.tag}`, 88);
|
|
602
|
+
// Assumes input is a following sibling
|
|
603
|
+
this.push(
|
|
604
|
+
`.//label[normalize-space()=${xp}]/following-sibling::${this.tag}`,
|
|
605
|
+
88,
|
|
219
606
|
);
|
|
607
|
+
};
|
|
220
608
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
selectors.push(
|
|
226
|
-
`${parent.tagName.toLowerCase()} > ${tagName}:nth-of-type(${typeIndex})`,
|
|
609
|
+
// --- Pattern 1: <label for="..."> ---
|
|
610
|
+
if (this.el.id) {
|
|
611
|
+
const lab = this.root.querySelector(
|
|
612
|
+
`label[for=${quoteCssAttr(this.el.id)}]`,
|
|
227
613
|
);
|
|
614
|
+
|
|
615
|
+
if (lab) {
|
|
616
|
+
addForLabel(lab);
|
|
617
|
+
}
|
|
228
618
|
}
|
|
229
619
|
|
|
230
|
-
|
|
231
|
-
|
|
620
|
+
// --- Pattern 2: Wrapping <label> ---
|
|
621
|
+
const wrapLab = this.el.closest('label');
|
|
232
622
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
? [`[placeholder='${this.escapeCss(placeholder)}']`]
|
|
237
|
-
: [];
|
|
238
|
-
}
|
|
623
|
+
if (wrapLab) {
|
|
624
|
+
addForLabel(wrapLab);
|
|
625
|
+
}
|
|
239
626
|
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
return ariaLabel ? [`[aria-label='${this.escapeCss(ariaLabel)}']`] : [];
|
|
243
|
-
}
|
|
627
|
+
// --- Pattern 3: aria-labelledby ---
|
|
628
|
+
const labelledby = this.el.getAttribute('aria-labelledby');
|
|
244
629
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (id) {
|
|
252
|
-
const label = document.querySelector(`label[for='${id}']`);
|
|
253
|
-
if (label) {
|
|
254
|
-
const labelText = label.textContent?.trim();
|
|
255
|
-
if (labelText) {
|
|
256
|
-
const safeLabelText = xpathLiteral(labelText);
|
|
257
|
-
return [
|
|
258
|
-
`//label[text()=${safeLabelText}]/following-sibling::${tagName}`,
|
|
259
|
-
`//label[text()=${safeLabelText}]/${tagName}`,
|
|
260
|
-
];
|
|
261
|
-
}
|
|
630
|
+
if (labelledby) {
|
|
631
|
+
const labelIds = labelledby.split(/\s+/);
|
|
632
|
+
|
|
633
|
+
for (const labelId of labelIds) {
|
|
634
|
+
if (!labelId) {
|
|
635
|
+
continue;
|
|
262
636
|
}
|
|
263
|
-
}
|
|
264
637
|
|
|
265
|
-
|
|
266
|
-
const wrappingLabel = this.element.closest('label');
|
|
638
|
+
const lab = this.root.querySelector(`#${escapeCss(labelId)}`);
|
|
267
639
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// then trim again
|
|
271
|
-
const labelText = wrappingLabel.textContent
|
|
272
|
-
.trim()
|
|
273
|
-
.replace(this.element.value || '', '')
|
|
274
|
-
.trim();
|
|
640
|
+
if (lab) {
|
|
641
|
+
const txt = (lab.textContent || '').trim();
|
|
275
642
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
643
|
+
if (txt) {
|
|
644
|
+
const textXp = safeXpath(txt);
|
|
645
|
+
const labelTag = lab.tagName.toLowerCase();
|
|
646
|
+
// This is the key: a robust XPath that finds the input by matching
|
|
647
|
+
// its aria-labelledby attribute to the ID of a label found by its text.
|
|
648
|
+
// It works regardless of where the label and input are in the DOM.
|
|
649
|
+
// The `contains(concat(' ',...` part safely checks for a word in a space-separated list.
|
|
650
|
+
const robustXp = `//${this.tag}[contains(concat(' ', normalize-space(@aria-labelledby), ' '), concat(' ', //${labelTag}[normalize-space()=${textXp}]/@id, ' '))]`;
|
|
651
|
+
this.push(robustXp, 90); // Give it a high weight, similar to aria-label
|
|
652
|
+
}
|
|
279
653
|
}
|
|
280
654
|
}
|
|
281
655
|
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Generates class-based selectors, but only for human-readable class names.
|
|
660
|
+
* Avoids CSS-in-JS generated classes and other machine-generated names.
|
|
661
|
+
*/
|
|
662
|
+
classAnchor() {
|
|
663
|
+
const classValue = getClassValue(this.el);
|
|
664
|
+
|
|
665
|
+
if (!classValue) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const clsToken = classValue.split(/\s+/).find((c) => c && !isHashLike(c));
|
|
282
670
|
|
|
283
|
-
|
|
671
|
+
if (!clsToken) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
this.scopedUntilUnique(`${this.tag}.${escapeCss(clsToken)}`, 35);
|
|
284
676
|
}
|
|
285
677
|
|
|
286
|
-
|
|
287
|
-
const selectors = [];
|
|
678
|
+
/* -------------------------- Positional --------------------------------- */
|
|
288
679
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
680
|
+
/**
|
|
681
|
+
* Generates selectors by finding stable ancestors and creating relative paths.
|
|
682
|
+
* This creates selectors like: #header > nav > button:nth-of-type(2)
|
|
683
|
+
*
|
|
684
|
+
* The key insight is that while the target element might not have good identifiers,
|
|
685
|
+
* its ancestors might, and we can create a stable path from those ancestors.
|
|
686
|
+
*/
|
|
687
|
+
stableAncestorAnchors() {
|
|
688
|
+
let anc = this.el.parentElement;
|
|
689
|
+
let depth = 0;
|
|
690
|
+
const visited = new WeakSet(); // Prevent infinite loops
|
|
292
691
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let tagName = currentElement.tagName.toLowerCase();
|
|
296
|
-
const parent = currentElement.parentElement;
|
|
692
|
+
while (anc && depth < 7 && !visited.has(anc)) {
|
|
693
|
+
visited.add(anc);
|
|
297
694
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
695
|
+
const sel = this.bestStableSelector(anc);
|
|
696
|
+
|
|
697
|
+
if (sel) {
|
|
698
|
+
const rel = this.relPath(anc, this.el);
|
|
699
|
+
|
|
700
|
+
if (rel) {
|
|
701
|
+
this.push(`${sel} > ${rel}`, 20);
|
|
305
702
|
}
|
|
306
703
|
}
|
|
307
704
|
|
|
308
|
-
|
|
309
|
-
|
|
705
|
+
anc = anc.parentElement;
|
|
706
|
+
depth += 1;
|
|
310
707
|
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Generates a full DOM path selector as absolute last resort.
|
|
712
|
+
* These are very brittle but will always work for the current page state.
|
|
713
|
+
* Example: html > body > div:nth-of-type(3) > section > button:nth-of-type(1)
|
|
714
|
+
*/
|
|
715
|
+
fullDomPath() {
|
|
716
|
+
let cur = this.el;
|
|
717
|
+
const segs = [];
|
|
718
|
+
const visited = new WeakSet(); // Prevent infinite loops
|
|
719
|
+
|
|
720
|
+
while (
|
|
721
|
+
cur &&
|
|
722
|
+
cur.parentElement &&
|
|
723
|
+
cur !== this.root &&
|
|
724
|
+
!visited.has(cur)
|
|
725
|
+
) {
|
|
726
|
+
visited.add(cur);
|
|
727
|
+
|
|
728
|
+
let part = cur.tagName.toLowerCase();
|
|
729
|
+
const p = cur.parentElement;
|
|
730
|
+
const like = [...p.children].filter(
|
|
731
|
+
(c) => c.tagName.toLowerCase() === part,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
if (like.length > 1) {
|
|
735
|
+
let i = like.indexOf(cur) + 1;
|
|
736
|
+
part += `:nth-of-type(${i})`;
|
|
737
|
+
}
|
|
311
738
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
selectors.push(path.join(' > '));
|
|
739
|
+
segs.unshift(part);
|
|
740
|
+
cur = p;
|
|
315
741
|
}
|
|
316
742
|
|
|
317
|
-
|
|
743
|
+
if (segs.length) {
|
|
744
|
+
// Lowest priority
|
|
745
|
+
this.push(segs.join(' > '), 1);
|
|
746
|
+
}
|
|
318
747
|
}
|
|
319
748
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
749
|
+
/* ---------------------------------------------------------------------- */
|
|
750
|
+
/* Helpers */
|
|
751
|
+
/* ---------------------------------------------------------------------- */
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Finds the best stable selector for a given element.
|
|
755
|
+
* "Stable" means likely to survive page updates and not be machine-generated.
|
|
756
|
+
*
|
|
757
|
+
* Priority order:
|
|
758
|
+
* 1. Human-readable unique ID.
|
|
759
|
+
* 2. Unique data attributes (especially test-related).
|
|
760
|
+
* 3. Unique tag + stable class combination.
|
|
761
|
+
* 4. Unique landmark tags.
|
|
762
|
+
*
|
|
763
|
+
* @param {Element} el - Element to find selector for
|
|
764
|
+
* @returns {string|null} Best stable selector, or null if none found
|
|
765
|
+
*/
|
|
766
|
+
bestStableSelector(el) {
|
|
767
|
+
const scope = el.getRootNode();
|
|
768
|
+
|
|
769
|
+
if (el.id && !isHashLike(el.id) && el.id.length <= 24) {
|
|
770
|
+
if (countMatchesCSS(`#${escapeCss(el.id)}`, scope) === 1) {
|
|
771
|
+
return `#${escapeCss(el.id)}`;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const ATTRS = [
|
|
776
|
+
'data-testid',
|
|
777
|
+
'data-test',
|
|
778
|
+
'name',
|
|
779
|
+
'title',
|
|
780
|
+
'role',
|
|
781
|
+
'aria-label',
|
|
782
|
+
];
|
|
783
|
+
|
|
784
|
+
for (const a of ATTRS) {
|
|
785
|
+
const v = el.getAttribute(a);
|
|
786
|
+
|
|
787
|
+
if (v && countMatchesCSS(`[${a}=${quoteCssAttr(v)}]`, scope) === 1) {
|
|
788
|
+
return `[${a}=${quoteCssAttr(v)}]`;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const tag = el.tagName.toLowerCase();
|
|
793
|
+
const classValue = getClassValue(el);
|
|
794
|
+
|
|
795
|
+
if (classValue) {
|
|
796
|
+
const cls = classValue.split(/\s+/).find((c) => c && !isHashLike(c));
|
|
797
|
+
|
|
798
|
+
if (cls && countMatchesCSS(`${tag}.${escapeCss(cls)}`, scope) === 1) {
|
|
799
|
+
return `${tag}.${escapeCss(cls)}`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (LANDMARK_TAGS.includes(tag) && countMatchesCSS(tag, scope) === 1) {
|
|
804
|
+
return tag;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return null;
|
|
323
808
|
}
|
|
324
809
|
|
|
325
|
-
|
|
326
|
-
|
|
810
|
+
/**
|
|
811
|
+
* Creates a relative path between an ancestor and target element.
|
|
812
|
+
* Used to build selectors like: ancestor > child:nth-of-type(2) > target
|
|
813
|
+
*
|
|
814
|
+
* @param {Element} anc - Ancestor element
|
|
815
|
+
* @param {Element} tgt - Target element
|
|
816
|
+
* @returns {string} Relative path selector
|
|
817
|
+
*/
|
|
818
|
+
relPath(anc, tgt) {
|
|
819
|
+
const bits = [];
|
|
820
|
+
let cur = tgt;
|
|
821
|
+
const visited = new WeakSet(); // Prevent infinite loops
|
|
822
|
+
|
|
823
|
+
while (cur && cur !== anc && !visited.has(cur)) {
|
|
824
|
+
visited.add(cur);
|
|
825
|
+
let seg = cur.tagName.toLowerCase();
|
|
826
|
+
const p = cur.parentElement;
|
|
827
|
+
|
|
828
|
+
if (p) {
|
|
829
|
+
const like = [...p.children].filter(
|
|
830
|
+
(c) => c.tagName.toLowerCase() === seg,
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
if (like.length > 1) {
|
|
834
|
+
seg += `:nth-of-type(${like.indexOf(cur) + 1})`;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
bits.unshift(seg);
|
|
839
|
+
cur = p;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return bits.join(' > ');
|
|
327
843
|
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* --------------------------------------------------------------- */
|
|
847
|
+
/* Public: returns Array<Array<string>> */
|
|
848
|
+
/* --------------------------------------------------------------- */
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Generates selectors that work across shadow DOM boundaries.
|
|
852
|
+
*
|
|
853
|
+
* Modern web applications often use shadow DOM for encapsulation (web components,
|
|
854
|
+
* React portals, design systems). A single element might be nested within multiple
|
|
855
|
+
* shadow roots, each requiring separate selectors.
|
|
856
|
+
*
|
|
857
|
+
* This function returns an array of selector arrays - one for each shadow boundary
|
|
858
|
+
* that needs to be crossed to reach the target element.
|
|
859
|
+
*
|
|
860
|
+
* Example output for an element deep in shadow DOM:
|
|
861
|
+
* [
|
|
862
|
+
* ['#app'], // Selector for outermost shadow host
|
|
863
|
+
* ['my-component'], // Selector for middle shadow host
|
|
864
|
+
* ['button.primary', '#btn1'] // Selectors for target element
|
|
865
|
+
* ]
|
|
866
|
+
*
|
|
867
|
+
* @param {Element} el - Target element (possibly inside shadow DOM)
|
|
868
|
+
* @returns {Array<Array<string>>} Array of selector layers
|
|
869
|
+
*/
|
|
870
|
+
function generateSmartSelectorLayers(el) {
|
|
871
|
+
const chain = gatherShadowChain(el);
|
|
872
|
+
const layers = [];
|
|
873
|
+
|
|
874
|
+
for (const { host, open } of chain) {
|
|
875
|
+
if (!open) {
|
|
876
|
+
layers.push(['✖︎ closed shadow-host']);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
328
879
|
|
|
329
|
-
|
|
330
|
-
return window.__donobu.cssEscape(str);
|
|
880
|
+
layers.push(window[DONOBU_KEY].generateSmartSelectors(host));
|
|
331
881
|
}
|
|
882
|
+
|
|
883
|
+
layers.push(window[DONOBU_KEY].generateSmartSelectors(el));
|
|
884
|
+
return layers;
|
|
332
885
|
}
|
|
333
886
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
887
|
+
// ---------------------------------------------------------------------------
|
|
888
|
+
// Public API
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
if (!window[DONOBU_KEY].generateSmartSelectors) {
|
|
891
|
+
Object.defineProperty(window[DONOBU_KEY], 'generateSmartSelectors', {
|
|
892
|
+
value: (el) => {
|
|
337
893
|
try {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
894
|
+
if (!el || typeof el.tagName !== 'string') {
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return new SelectorBuilder(el).build();
|
|
899
|
+
} catch (err) {
|
|
900
|
+
console.warn('[Donobu] selector generation failed:', err);
|
|
341
901
|
return [];
|
|
342
902
|
}
|
|
343
903
|
},
|
|
904
|
+
writable: false,
|
|
344
905
|
enumerable: false,
|
|
906
|
+
configurable: false,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!window[DONOBU_KEY].generateSmartSelectorLayers) {
|
|
911
|
+
Object.defineProperty(window[DONOBU_KEY], 'generateSmartSelectorLayers', {
|
|
912
|
+
value: (el) => {
|
|
913
|
+
try {
|
|
914
|
+
if (!el || typeof el.tagName !== 'string') {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return generateSmartSelectorLayers(el);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
console.warn('[Donobu] selector layer generation failed:', err);
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
},
|
|
345
924
|
writable: false,
|
|
925
|
+
enumerable: false,
|
|
346
926
|
configurable: false,
|
|
347
927
|
});
|
|
348
928
|
}
|