@specsage/cli 0.1.7 → 0.1.8
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/lib/browser.js +42 -5
- package/lib/cli.rb +19 -2
- package/lib/results_uploader.rb +5 -2
- package/lib/runner.rb +5 -2
- package/package.json +2 -2
package/lib/browser.js
CHANGED
|
@@ -248,6 +248,7 @@ async function enumerateElements() {
|
|
|
248
248
|
w: Math.round(box.width),
|
|
249
249
|
h: Math.round(box.height)
|
|
250
250
|
},
|
|
251
|
+
locator: el,
|
|
251
252
|
mechanism: 'native',
|
|
252
253
|
source: 'native'
|
|
253
254
|
});
|
|
@@ -336,6 +337,7 @@ async function enumerateElements() {
|
|
|
336
337
|
w: Math.round(box.width),
|
|
337
338
|
h: Math.round(box.height)
|
|
338
339
|
},
|
|
340
|
+
locator: el,
|
|
339
341
|
mechanism: 'scripted',
|
|
340
342
|
source: source
|
|
341
343
|
});
|
|
@@ -427,7 +429,8 @@ async function enumerateElements() {
|
|
|
427
429
|
}
|
|
428
430
|
|
|
429
431
|
lastElements = uniqueElements;
|
|
430
|
-
|
|
432
|
+
// Return elements without the locator property (not JSON-serializable)
|
|
433
|
+
return lastElements.map(({ locator, ...rest }) => rest);
|
|
431
434
|
}
|
|
432
435
|
|
|
433
436
|
async function debugOverlay(x, y) {
|
|
@@ -499,9 +502,26 @@ async function handleCommand(msg) {
|
|
|
499
502
|
if (!element) throw new Error(`Element not found: ${element_id}`);
|
|
500
503
|
lastClickedElement = element; // Store for keypress context
|
|
501
504
|
|
|
502
|
-
|
|
505
|
+
let { x, y, w, h } = element.bounding_box;
|
|
506
|
+
|
|
507
|
+
// Auto-scroll element into view if its center is outside the viewport
|
|
508
|
+
const viewportSize = page.viewportSize();
|
|
509
|
+
let centerY = y + h / 2;
|
|
510
|
+
if (element.locator && (centerY < 0 || centerY >= viewportSize.height)) {
|
|
511
|
+
await element.locator.scrollIntoViewIfNeeded({ timeout: 3000 });
|
|
512
|
+
await new Promise(r => setTimeout(r, 200));
|
|
513
|
+
const newBox = await element.locator.boundingBox();
|
|
514
|
+
if (newBox) {
|
|
515
|
+
x = Math.round(newBox.x);
|
|
516
|
+
y = Math.round(newBox.y);
|
|
517
|
+
w = Math.round(newBox.width);
|
|
518
|
+
h = Math.round(newBox.height);
|
|
519
|
+
element.bounding_box = { x, y, w, h };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
503
523
|
const centerX = x + w / 2;
|
|
504
|
-
|
|
524
|
+
centerY = y + h / 2;
|
|
505
525
|
|
|
506
526
|
await debugOverlay(centerX, centerY);
|
|
507
527
|
|
|
@@ -559,9 +579,26 @@ async function handleCommand(msg) {
|
|
|
559
579
|
if (!element) throw new Error(`Element not found: ${element_id}`);
|
|
560
580
|
|
|
561
581
|
// Find the select element by its bounding box center
|
|
562
|
-
|
|
582
|
+
let { x, y, w, h } = element.bounding_box;
|
|
583
|
+
|
|
584
|
+
// Auto-scroll element into view if its center is outside the viewport
|
|
585
|
+
const viewportSize = page.viewportSize();
|
|
586
|
+
let centerY = y + h / 2;
|
|
587
|
+
if (element.locator && (centerY < 0 || centerY >= viewportSize.height)) {
|
|
588
|
+
await element.locator.scrollIntoViewIfNeeded({ timeout: 3000 });
|
|
589
|
+
await new Promise(r => setTimeout(r, 200));
|
|
590
|
+
const newBox = await element.locator.boundingBox();
|
|
591
|
+
if (newBox) {
|
|
592
|
+
x = Math.round(newBox.x);
|
|
593
|
+
y = Math.round(newBox.y);
|
|
594
|
+
w = Math.round(newBox.width);
|
|
595
|
+
h = Math.round(newBox.height);
|
|
596
|
+
element.bounding_box = { x, y, w, h };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
563
600
|
const centerX = x + w / 2;
|
|
564
|
-
|
|
601
|
+
centerY = y + h / 2;
|
|
565
602
|
|
|
566
603
|
// Use Playwright's selectOption by locating the element at the position
|
|
567
604
|
const selectEl = page.locator('select').filter({
|
package/lib/cli.rb
CHANGED
|
@@ -71,6 +71,19 @@ class SpecSageCLI
|
|
|
71
71
|
parser.parse!(@args)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
def build_ci_metadata
|
|
75
|
+
metadata = {}
|
|
76
|
+
metadata[:commit_sha] = ENV['GITHUB_SHA'] if ENV['GITHUB_SHA']
|
|
77
|
+
metadata[:branch] = ENV['GITHUB_REF_NAME'] if ENV['GITHUB_REF_NAME']
|
|
78
|
+
metadata[:pr_number] = ENV['GITHUB_PR_NUMBER'].to_i if ENV['GITHUB_PR_NUMBER']
|
|
79
|
+
metadata[:commit_message] = ENV['GITHUB_COMMIT_MESSAGE'] if ENV['GITHUB_COMMIT_MESSAGE']
|
|
80
|
+
metadata
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self_test_mode?
|
|
84
|
+
ENV['SPECSAGE_APP_TEST'] == 'true'
|
|
85
|
+
end
|
|
86
|
+
|
|
74
87
|
def run_ci_mode
|
|
75
88
|
website = ENV['TARGET_WEBSITE_SLUG']
|
|
76
89
|
api_key = ENV['SPEC_SAGE_API_KEY']
|
|
@@ -91,6 +104,9 @@ class SpecSageCLI
|
|
|
91
104
|
puts "Version: #{VERSION}"
|
|
92
105
|
puts "Website: #{website}"
|
|
93
106
|
puts "Base URL: #{base_url}"
|
|
107
|
+
if self_test_mode?
|
|
108
|
+
puts "Self-test mode: orchestration via #{base_url}"
|
|
109
|
+
end
|
|
94
110
|
puts ""
|
|
95
111
|
|
|
96
112
|
publisher = ResultsUploader.new(api_key: api_key)
|
|
@@ -108,7 +124,7 @@ class SpecSageCLI
|
|
|
108
124
|
puts ""
|
|
109
125
|
|
|
110
126
|
begin
|
|
111
|
-
run_response = publisher.create_ci_run(website)
|
|
127
|
+
run_response = publisher.create_ci_run(website, ci_metadata: build_ci_metadata)
|
|
112
128
|
server_run_id = run_response['server_run_id']
|
|
113
129
|
rescue ResultsUploader::UploadError => e
|
|
114
130
|
puts "Error creating run: #{e.message}"
|
|
@@ -129,7 +145,8 @@ class SpecSageCLI
|
|
|
129
145
|
visible: @options[:visible],
|
|
130
146
|
record: @options[:record],
|
|
131
147
|
publisher: publisher,
|
|
132
|
-
server_run_id: server_run_id
|
|
148
|
+
server_run_id: server_run_id,
|
|
149
|
+
orchestration_base_url: self_test_mode? ? base_url : nil
|
|
133
150
|
)
|
|
134
151
|
|
|
135
152
|
verdict = runner.run
|
package/lib/results_uploader.rb
CHANGED
|
@@ -58,8 +58,11 @@ class ResultsUploader
|
|
|
58
58
|
|
|
59
59
|
# Create a CI run on the server
|
|
60
60
|
# Returns hash with server_run_id and base_url
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
# ci_metadata can include: commit_sha, branch, pr_number, commit_message
|
|
62
|
+
def create_ci_run(website_identifier, ci_metadata: {})
|
|
63
|
+
body = { website: website_identifier }
|
|
64
|
+
body.merge!(ci_metadata) if ci_metadata.any?
|
|
65
|
+
post("/api/v1/ci/runs", body)
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
private
|
package/lib/runner.rb
CHANGED
|
@@ -21,7 +21,9 @@ class Runner
|
|
|
21
21
|
SAFE_PATH_SEGMENT = /\A[a-zA-Z0-9_-]+\z/
|
|
22
22
|
|
|
23
23
|
# Initialize runner with scenario data from server
|
|
24
|
-
|
|
24
|
+
# orchestration_base_url: URL for step submissions (LLM orchestration). Defaults to publisher.base_url.
|
|
25
|
+
# Set to CI_APP_URL when SPECSAGE_APP_TEST=true to test server code locally.
|
|
26
|
+
def initialize(scenario_data, visible: false, record: false, publisher: nil, server_run_id: nil, orchestration_base_url: nil)
|
|
25
27
|
@scenario = normalize_scenario_data(scenario_data)
|
|
26
28
|
@scenario_id = @scenario['id']
|
|
27
29
|
@scenario_name = @scenario['name'] || @scenario['id'] || 'unnamed'
|
|
@@ -34,6 +36,7 @@ class Runner
|
|
|
34
36
|
@next_request_id = 1
|
|
35
37
|
@node_channel_poisoned = false
|
|
36
38
|
@publisher = publisher
|
|
39
|
+
@orchestration_base_url = orchestration_base_url || publisher&.base_url
|
|
37
40
|
@step_client = nil
|
|
38
41
|
@server_run_id = server_run_id
|
|
39
42
|
@credentials = {} # Credentials received from server { "NAME" => "value" }
|
|
@@ -47,7 +50,7 @@ class Runner
|
|
|
47
50
|
raise ArgumentError, 'server_run_id is required' unless @server_run_id
|
|
48
51
|
|
|
49
52
|
@step_client = StepClient.new(
|
|
50
|
-
base_url: @
|
|
53
|
+
base_url: @orchestration_base_url,
|
|
51
54
|
server_run_id: @server_run_id,
|
|
52
55
|
api_key: @publisher.api_key
|
|
53
56
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specsage/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "SpecSage CLI - AI-powered end-to-end testing automation (Node wrapper for Ruby CLI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"node": ">=18.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"playwright": "
|
|
34
|
+
"playwright": "1.57.0"
|
|
35
35
|
}
|
|
36
36
|
}
|