@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 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
- return lastElements;
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
- const { x, y, w, h } = element.bounding_box;
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
- const centerY = y + h / 2;
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
- const { x, y, w, h } = element.bounding_box;
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
- const centerY = y + h / 2;
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
@@ -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
- def create_ci_run(website_identifier)
62
- post("/api/v1/ci/runs", { website: website_identifier })
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
- def initialize(scenario_data, visible: false, record: false, publisher: nil, server_run_id: nil)
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: @publisher.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.7",
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": "^1.57.0"
34
+ "playwright": "1.57.0"
35
35
  }
36
36
  }