browserctl 0.13.0 → 0.13.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/browserctl/orphan_sweeper.rb +57 -0
- data/lib/browserctl/server.rb +2 -0
- data/lib/browserctl/version.rb +1 -1
- metadata +2 -15
- data/examples/test_automation_practices/advanced/ab_testing.rb +0 -38
- data/examples/test_automation_practices/advanced/broken_images.rb +0 -25
- data/examples/test_automation_practices/advanced/file_download.rb +0 -40
- data/examples/test_automation_practices/advanced/iframes.rb +0 -37
- data/examples/test_automation_practices/advanced/shadow_dom.rb +0 -35
- data/examples/test_automation_practices/dialogs/alerts.rb +0 -45
- data/examples/test_automation_practices/dynamic/tables.rb +0 -47
- data/examples/test_automation_practices/forms/file_upload.rb +0 -30
- data/examples/test_automation_practices/forms/forms.rb +0 -47
- data/examples/test_automation_practices/forms/slider.rb +0 -51
- data/examples/test_automation_practices/interactions/context_menu.rb +0 -54
- data/examples/test_automation_practices/interactions/drag_drop.rb +0 -41
- data/examples/test_automation_practices/interactions/exit_intent.rb +0 -47
- data/examples/test_automation_practices/interactions/hover.rb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a09c4e2ffdede17be7f5d14108d72ef30ea75c82bbae924b753474e77c5778c2
|
|
4
|
+
data.tar.gz: 27dbcdbf87b1e2460ebfc7913dbeecf9780e218f7824e5e9292668db5a0fb59e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bb8d31d716febfc0a3ffa593d7d1141dfdf1ad6d548d5d2b8e4ed53a8d04adb9f5861728e3864f65b7a296cc58c93c2e3d66b4f632988508fc699cb841837d0
|
|
7
|
+
data.tar.gz: 8791f86942d5447fec3dc1aec3391c106a8553f7ef9e99ab372750a7ac18a278e824940a09cca5492bdc8a87e90dcd09ad7e1d04a47c9258e4783c2573bc36e6
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,14 @@ All notable changes to this project will be documented in this file.
|
|
|
10
10
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
11
11
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
12
12
|
|
|
13
|
+
## [0.13.1](https://github.com/patrick204nqh/browserctl/compare/v0.13.0...v0.13.1) (2026-05-11)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **daemon:** sweep orphaned Ferrum chromes on browserd startup ([#177](https://github.com/patrick204nqh/browserctl/issues/177)) ([0f6f9ab](https://github.com/patrick204nqh/browserctl/commit/0f6f9aba9165fc1c03c3ccb0e0e32e32b6a19efb)), closes [#176](https://github.com/patrick204nqh/browserctl/issues/176)
|
|
19
|
+
* **test:** raise daemon start timeout to 30s for snap Chromium ([#172](https://github.com/patrick204nqh/browserctl/issues/172)) ([107fcdb](https://github.com/patrick204nqh/browserctl/commit/107fcdb67f4e34b18e228fb2d5ef7837ec509526))
|
|
20
|
+
|
|
13
21
|
## [0.13.0](https://github.com/patrick204nqh/browserctl/compare/v0.12.0...v0.13.0) (2026-05-10)
|
|
14
22
|
|
|
15
23
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browserctl
|
|
4
|
+
# Terminates orphaned Ferrum-spawned browser processes left behind by previous
|
|
5
|
+
# daemon or rspec runs that did not shut down cleanly (SIGKILL, OOM, crash).
|
|
6
|
+
#
|
|
7
|
+
# Only targets processes whose argv contains the Ferrum temp user-data-dir marker
|
|
8
|
+
# AND whose PPID is 1 — so children of any live daemon are never touched.
|
|
9
|
+
module OrphanSweeper
|
|
10
|
+
USER_DATA_DIR_MARKER = "ferrum_user_data_dir_"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def sweep(logger: Browserctl.logger, lister: method(:list_processes), killer: method(:kill_process))
|
|
15
|
+
return unless supported_platform?
|
|
16
|
+
|
|
17
|
+
orphans = find_orphans(lister.call)
|
|
18
|
+
return if orphans.empty?
|
|
19
|
+
|
|
20
|
+
logger&.info("orphan sweep: found #{orphans.size} stale browser process(es) from previous run")
|
|
21
|
+
orphans.each { |pid| killer.call(pid, logger) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_orphans(processes)
|
|
25
|
+
processes.filter_map do |p|
|
|
26
|
+
next unless p[:ppid] == 1 && p[:command].to_s.include?(USER_DATA_DIR_MARKER)
|
|
27
|
+
|
|
28
|
+
p[:pid]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def list_processes
|
|
33
|
+
output = `ps -eo pid=,ppid=,command= 2>/dev/null`
|
|
34
|
+
return [] if output.empty?
|
|
35
|
+
|
|
36
|
+
output.lines.filter_map do |line|
|
|
37
|
+
pid, ppid, command = line.strip.split(" ", 3)
|
|
38
|
+
next unless pid && ppid && command
|
|
39
|
+
|
|
40
|
+
{ pid: pid.to_i, ppid: ppid.to_i, command: command }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def kill_process(pid, logger)
|
|
45
|
+
Process.kill("TERM", pid)
|
|
46
|
+
logger&.info("orphan sweep: terminated PID #{pid}")
|
|
47
|
+
rescue Errno::ESRCH
|
|
48
|
+
# already gone
|
|
49
|
+
rescue Errno::EPERM
|
|
50
|
+
logger&.debug("orphan sweep: no permission to kill PID #{pid}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def supported_platform?
|
|
54
|
+
!RUBY_PLATFORM.match?(/mingw|mswin|windows/)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/browserctl/server.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "fileutils"
|
|
|
6
6
|
require "timeout"
|
|
7
7
|
require_relative "constants"
|
|
8
8
|
require_relative "logger"
|
|
9
|
+
require_relative "orphan_sweeper"
|
|
9
10
|
require_relative "driver/cdp_page"
|
|
10
11
|
require_relative "driver/cdp"
|
|
11
12
|
require_relative "server/command_dispatcher"
|
|
@@ -23,6 +24,7 @@ module Browserctl
|
|
|
23
24
|
|
|
24
25
|
def run
|
|
25
26
|
guard_already_running!
|
|
27
|
+
OrphanSweeper.sweep
|
|
26
28
|
write_pid
|
|
27
29
|
server, idle = setup_server
|
|
28
30
|
serve(server)
|
data/lib/browserctl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browserctl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.13.
|
|
4
|
+
version: 0.13.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -138,25 +138,11 @@ files:
|
|
|
138
138
|
- bin/browserd
|
|
139
139
|
- bin/setup
|
|
140
140
|
- examples/cloudflare_hitl.rb
|
|
141
|
-
- examples/test_automation_practices/advanced/ab_testing.rb
|
|
142
|
-
- examples/test_automation_practices/advanced/broken_images.rb
|
|
143
|
-
- examples/test_automation_practices/advanced/file_download.rb
|
|
144
|
-
- examples/test_automation_practices/advanced/iframes.rb
|
|
145
|
-
- examples/test_automation_practices/advanced/shadow_dom.rb
|
|
146
141
|
- examples/test_automation_practices/auth/login.rb
|
|
147
142
|
- examples/test_automation_practices/auth/login_negative.rb
|
|
148
|
-
- examples/test_automation_practices/dialogs/alerts.rb
|
|
149
143
|
- examples/test_automation_practices/dialogs/notifications.rb
|
|
150
144
|
- examples/test_automation_practices/dynamic/dynamic_elements.rb
|
|
151
|
-
- examples/test_automation_practices/dynamic/tables.rb
|
|
152
145
|
- examples/test_automation_practices/forms/checkboxes.rb
|
|
153
|
-
- examples/test_automation_practices/forms/file_upload.rb
|
|
154
|
-
- examples/test_automation_practices/forms/forms.rb
|
|
155
|
-
- examples/test_automation_practices/forms/slider.rb
|
|
156
|
-
- examples/test_automation_practices/interactions/context_menu.rb
|
|
157
|
-
- examples/test_automation_practices/interactions/drag_drop.rb
|
|
158
|
-
- examples/test_automation_practices/interactions/exit_intent.rb
|
|
159
|
-
- examples/test_automation_practices/interactions/hover.rb
|
|
160
146
|
- examples/test_automation_practices/interactions/key_press.rb
|
|
161
147
|
- examples/the_internet/add_remove_elements.rb
|
|
162
148
|
- examples/the_internet/checkboxes.rb
|
|
@@ -209,6 +195,7 @@ files:
|
|
|
209
195
|
- lib/browserctl/format_version.rb
|
|
210
196
|
- lib/browserctl/logger.rb
|
|
211
197
|
- lib/browserctl/migrations.rb
|
|
198
|
+
- lib/browserctl/orphan_sweeper.rb
|
|
212
199
|
- lib/browserctl/policy.rb
|
|
213
200
|
- lib/browserctl/recording.rb
|
|
214
201
|
- lib/browserctl/recording/log_writer.rb
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/advanced/ab_testing" do
|
|
4
|
-
desc "A/B testing page: verify one variant renders, click counter increments"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_advanced_ab_testing.png")
|
|
8
|
-
|
|
9
|
-
step "open A/B testing page and detect variant" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/ab-testing")
|
|
11
|
-
page(:main).wait("[data-test='ab-testing-page']", timeout: 10)
|
|
12
|
-
title = page(:main).evaluate(
|
|
13
|
-
"document.querySelector('[data-test=\"variant-title\"]')?.textContent?.trim()"
|
|
14
|
-
)
|
|
15
|
-
assert title&.length&.positive?, "expected variant title to be present, got: #{title.inspect}"
|
|
16
|
-
store(:variant_title, title)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
step "variant title contains A or B" do
|
|
20
|
-
title = fetch(:variant_title)
|
|
21
|
-
assert title.include?("A") || title.include?("B"),
|
|
22
|
-
"expected variant A or B in title, got: #{title.inspect}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
step "click button — counter increments" do
|
|
26
|
-
initial_text = page(:main).evaluate(
|
|
27
|
-
"document.querySelector('[data-test=\"variant-button\"]')?.textContent?.trim()"
|
|
28
|
-
)
|
|
29
|
-
page(:main).click("[data-test='variant-button']")
|
|
30
|
-
sleep 0.5
|
|
31
|
-
updated_text = page(:main).evaluate(
|
|
32
|
-
"document.querySelector('[data-test=\"variant-button\"]')?.textContent?.trim()"
|
|
33
|
-
)
|
|
34
|
-
assert initial_text != updated_text,
|
|
35
|
-
"expected button text to change after click (counter), still: #{updated_text.inspect}"
|
|
36
|
-
page(:main).screenshot(path: screenshot_path)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/advanced/broken_images" do
|
|
4
|
-
desc "Broken images page: verify one image loads successfully and two are broken (naturalWidth == 0)"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_advanced_broken_images.png")
|
|
8
|
-
|
|
9
|
-
step "open broken images page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/broken-images")
|
|
11
|
-
page(:main).wait("[data-test='image-container-0']", timeout: 10)
|
|
12
|
-
sleep 1
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
step "check which images loaded and which are broken" do
|
|
16
|
-
widths = page(:main).evaluate(
|
|
17
|
-
"[0,1,2].map(i => document.querySelector(`[data-test=\"image-${i}\"]`)?.naturalWidth ?? -1)"
|
|
18
|
-
)
|
|
19
|
-
loaded = widths.count(&:positive?)
|
|
20
|
-
broken = widths.count(&:zero?)
|
|
21
|
-
assert loaded == 1, "expected exactly 1 valid image, got #{loaded} (widths: #{widths.inspect})"
|
|
22
|
-
assert broken == 2, "expected exactly 2 broken images, got #{broken} (widths: #{widths.inspect})"
|
|
23
|
-
page(:main).screenshot(path: screenshot_path)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/advanced/file_download" do
|
|
4
|
-
desc "File download page: trigger each download, verify button enters disabled state, verify no error shown"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_advanced_file_download.png")
|
|
8
|
-
|
|
9
|
-
step "open file download page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/file-download")
|
|
11
|
-
page(:main).wait("[data-test='download-card-0']", timeout: 10)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
step "click TXT download — button becomes disabled during download" do
|
|
15
|
-
page(:main).click("[data-test='download-button-0']")
|
|
16
|
-
page(:main).wait("[data-test='download-button-0'][disabled]", timeout: 5)
|
|
17
|
-
disabled = page(:main).evaluate(
|
|
18
|
-
"document.querySelector('[data-test=\"download-button-0\"]')?.disabled"
|
|
19
|
-
)
|
|
20
|
-
assert disabled, "expected download-button-0 to be disabled while downloading"
|
|
21
|
-
page(:main).wait("[data-test='download-button-0']:not([disabled])", timeout: 10)
|
|
22
|
-
no_error = page(:main).evaluate("!document.querySelector('[data-test=\"error-message\"]')")
|
|
23
|
-
assert no_error, "expected no error-message after TXT download"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
step "click CSV download — completes without error" do
|
|
27
|
-
page(:main).click("[data-test='download-button-1']")
|
|
28
|
-
page(:main).wait("[data-test='download-button-1']:not([disabled])", timeout: 10)
|
|
29
|
-
no_error = page(:main).evaluate("!document.querySelector('[data-test=\"error-message\"]')")
|
|
30
|
-
assert no_error, "expected no error-message after CSV download"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
step "click PDF download — completes without error" do
|
|
34
|
-
page(:main).click("[data-test='download-button-2']")
|
|
35
|
-
page(:main).wait("[data-test='download-button-2']:not([disabled])", timeout: 10)
|
|
36
|
-
no_error = page(:main).evaluate("!document.querySelector('[data-test=\"error-message\"]')")
|
|
37
|
-
assert no_error, "expected no error-message after PDF download"
|
|
38
|
-
page(:main).screenshot(path: screenshot_path)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/advanced/iframes" do
|
|
4
|
-
desc "Iframes page: click inside sandboxed iframes, verify postMessage received in parent"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_advanced_iframes.png")
|
|
8
|
-
|
|
9
|
-
step "open iframes page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/iframes")
|
|
11
|
-
page(:main).wait("[data-test='iframe-container']", timeout: 10)
|
|
12
|
-
sleep 0.5
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
step "click button inside iframe1 — message appears in parent" do
|
|
16
|
-
page(:main).evaluate(
|
|
17
|
-
"document.querySelector('[data-test=\"iframe-iframe1\"]').contentDocument.querySelector('button').click()"
|
|
18
|
-
)
|
|
19
|
-
page(:main).wait("[data-test='iframe-message-0']", timeout: 5)
|
|
20
|
-
msg = page(:main).evaluate(
|
|
21
|
-
"document.querySelector('[data-test=\"iframe-message-0\"]')?.textContent?.trim()"
|
|
22
|
-
)
|
|
23
|
-
assert msg&.length&.positive?, "expected a message after clicking iframe1 button, got: #{msg.inspect}"
|
|
24
|
-
page(:main).screenshot(path: screenshot_path)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
step "click button inside iframe2 — second message appears" do
|
|
28
|
-
page(:main).evaluate(
|
|
29
|
-
"document.querySelector('[data-test=\"iframe-iframe2\"]').contentDocument.querySelector('button').click()"
|
|
30
|
-
)
|
|
31
|
-
page(:main).wait("[data-test='iframe-message-1']", timeout: 5)
|
|
32
|
-
count = page(:main).evaluate(
|
|
33
|
-
"document.querySelectorAll('[data-test^=\"iframe-message-\"]').length"
|
|
34
|
-
)
|
|
35
|
-
assert count >= 2, "expected at least 2 messages after both iframe clicks, got: #{count}"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/advanced/shadow_dom" do
|
|
4
|
-
desc "Shadow DOM page: query inside shadow root, click shadow-button, accept its alert"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_advanced_shadow_dom.png")
|
|
8
|
-
|
|
9
|
-
step "open shadow DOM page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/shadow-dom")
|
|
11
|
-
page(:main).wait("[data-test='shadow-host']", timeout: 10)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
step "verify shadow content is accessible via shadowRoot" do
|
|
15
|
-
text = page(:main).evaluate(
|
|
16
|
-
"document.querySelector('[data-test=\"shadow-host\"]').shadowRoot" \
|
|
17
|
-
"?.querySelector('[data-test=\"shadow-content\"]')?.textContent?.trim()"
|
|
18
|
-
)
|
|
19
|
-
assert text&.length&.positive?, "expected shadow-content text via shadowRoot, got: #{text.inspect}"
|
|
20
|
-
page(:main).screenshot(path: screenshot_path)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
step "click shadow-button — accept the alert — page stays responsive" do
|
|
24
|
-
page(:main).dialog_accept
|
|
25
|
-
page(:main).evaluate(
|
|
26
|
-
"document.querySelector('[data-test=\"shadow-host\"]').shadowRoot" \
|
|
27
|
-
".querySelector('[data-test=\"shadow-button\"]').click()"
|
|
28
|
-
)
|
|
29
|
-
sleep 0.3
|
|
30
|
-
still_present = page(:main).evaluate(
|
|
31
|
-
"!!document.querySelector('[data-test=\"shadow-host\"]')"
|
|
32
|
-
)
|
|
33
|
-
assert still_present, "expected shadow-host to remain in DOM after alert was accepted"
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/dialogs/alerts" do
|
|
4
|
-
desc "Alerts page: pre-register accept/dismiss handlers before triggering each JS dialog type"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_dialogs_alerts.png")
|
|
8
|
-
|
|
9
|
-
step "open alerts page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/alerts")
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
step "accept an alert — result-container shows confirmation" do
|
|
14
|
-
page(:main).dialog_accept
|
|
15
|
-
page(:main).click("[data-test='alert-button']")
|
|
16
|
-
page(:main).wait("[data-test='result-container']", timeout: 5)
|
|
17
|
-
result = page(:main).evaluate("document.querySelector('[data-test=\"result-container\"]')?.textContent?.trim()")
|
|
18
|
-
assert result == "Last action: Alert shown", "expected alert result, got: #{result.inspect}"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
step "accept a confirm — result-container shows true" do
|
|
22
|
-
page(:main).dialog_accept
|
|
23
|
-
page(:main).click("[data-test='confirm-button']")
|
|
24
|
-
page(:main).wait("[data-test='result-container']", timeout: 5)
|
|
25
|
-
result = page(:main).evaluate("document.querySelector('[data-test=\"result-container\"]')?.textContent?.trim()")
|
|
26
|
-
assert result == "Last action: Confirm dialog: true", "expected confirm true result, got: #{result.inspect}"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
step "dismiss a confirm — result-container shows false" do
|
|
30
|
-
page(:main).dialog_dismiss
|
|
31
|
-
page(:main).click("[data-test='confirm-button']")
|
|
32
|
-
page(:main).wait("[data-test='result-container']", timeout: 5)
|
|
33
|
-
result = page(:main).evaluate("document.querySelector('[data-test=\"result-container\"]')?.textContent?.trim()")
|
|
34
|
-
assert result == "Last action: Confirm dialog: false", "expected confirm false result, got: #{result.inspect}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
step "accept a prompt with text — result-container shows entered text" do
|
|
38
|
-
page(:main).dialog_accept(text: "browserctl")
|
|
39
|
-
page(:main).click("[data-test='prompt-button']")
|
|
40
|
-
page(:main).wait("[data-test='result-container']", timeout: 5)
|
|
41
|
-
result = page(:main).evaluate("document.querySelector('[data-test=\"result-container\"]')?.textContent?.trim()")
|
|
42
|
-
assert result&.include?("browserctl"), "expected prompt text in result, got: #{result.inspect}"
|
|
43
|
-
page(:main).screenshot(path: screenshot_path)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/dynamic/tables" do
|
|
4
|
-
desc "Tables page: verify initial data, sort by column ascending and descending"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_dynamic_tables.png")
|
|
8
|
-
|
|
9
|
-
step "open tables page and verify initial rows" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/tables")
|
|
11
|
-
page(:main).wait("[data-test='dynamic-table']", timeout: 10)
|
|
12
|
-
rows = page(:main).evaluate(
|
|
13
|
-
"Array.from(document.querySelectorAll('[data-test^=\"table-row-\"]')).map(r => r.dataset.test)"
|
|
14
|
-
)
|
|
15
|
-
assert rows.length == 3, "expected 3 rows, got: #{rows.length}"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
step "sort by name ascending — alphabetical order" do
|
|
19
|
-
page(:main).click("[data-test='table-header-name']")
|
|
20
|
-
sleep 0.2
|
|
21
|
-
names = page(:main).evaluate(
|
|
22
|
-
"Array.from(document.querySelectorAll('[data-test^=\"table-cell-name-\"]')).map(el => el.textContent?.trim())"
|
|
23
|
-
)
|
|
24
|
-
sorted = names.sort
|
|
25
|
-
assert names == sorted, "expected names sorted ascending, got: #{names.inspect}"
|
|
26
|
-
page(:main).screenshot(path: screenshot_path)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
step "sort by name descending — click header again" do
|
|
30
|
-
page(:main).click("[data-test='table-header-name']")
|
|
31
|
-
sleep 0.2
|
|
32
|
-
names = page(:main).evaluate(
|
|
33
|
-
"Array.from(document.querySelectorAll('[data-test^=\"table-cell-name-\"]')).map(el => el.textContent?.trim())"
|
|
34
|
-
)
|
|
35
|
-
sorted_desc = names.sort.reverse
|
|
36
|
-
assert names == sorted_desc, "expected names sorted descending, got: #{names.inspect}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
step "sort by status — Active/Inactive grouping" do
|
|
40
|
-
page(:main).click("[data-test='table-header-status']")
|
|
41
|
-
sleep 0.2
|
|
42
|
-
statuses = page(:main).evaluate(
|
|
43
|
-
"Array.from(document.querySelectorAll('[data-test^=\"table-cell-status-\"]')).map(el => el.textContent?.trim())"
|
|
44
|
-
)
|
|
45
|
-
assert statuses == statuses.sort, "expected statuses sorted ascending, got: #{statuses.inspect}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/forms/file_upload" do
|
|
4
|
-
desc "File upload page: attach a local file via the hidden file input, verify the file is selected"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path,
|
|
8
|
-
default: File.expand_path(".browserctl/screenshots/tap_forms_file_upload.png")
|
|
9
|
-
|
|
10
|
-
step "open file upload page" do
|
|
11
|
-
open_page(:main, url: "#{base_url}/#/file-upload")
|
|
12
|
-
page(:main).wait("[data-test='file-uploader']", timeout: 10)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
step "upload a file and verify it is selected" do
|
|
16
|
-
Dir.mktmpdir do |dir|
|
|
17
|
-
path = File.join(dir, "browserctl_test.txt")
|
|
18
|
-
File.write(path, "browserctl v0.7 upload test")
|
|
19
|
-
|
|
20
|
-
page(:main).upload("[data-test='file-input']", path)
|
|
21
|
-
page(:main).wait("[data-test='uploaded-file-0']", timeout: 10)
|
|
22
|
-
|
|
23
|
-
filename = page(:main).evaluate(
|
|
24
|
-
"document.querySelector('[data-test=\"uploaded-file-0\"]')?.querySelector('span.truncate')?.textContent?.trim()"
|
|
25
|
-
)
|
|
26
|
-
assert filename == "browserctl_test.txt", "expected filename in list, got: #{filename.inspect}"
|
|
27
|
-
end
|
|
28
|
-
page(:main).screenshot(path: screenshot_path)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/forms/forms" do
|
|
4
|
-
desc "Forms page: trigger inline validation errors, then submit a valid form"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_forms_forms.png")
|
|
8
|
-
|
|
9
|
-
step "open forms page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/forms")
|
|
11
|
-
page(:main).wait("[data-test='login-form']", timeout: 10)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
step "submit empty form — all three fields show errors" do
|
|
15
|
-
page(:main).click("[data-test='submit-button']")
|
|
16
|
-
page(:main).wait("[data-test='username-error']", timeout: 5)
|
|
17
|
-
username_err = page(:main).evaluate("document.querySelector('[data-test=\"username-error\"]')?.textContent?.trim()")
|
|
18
|
-
email_err = page(:main).evaluate("document.querySelector('[data-test=\"email-error\"]')?.textContent?.trim()")
|
|
19
|
-
password_err = page(:main).evaluate("document.querySelector('[data-test=\"password-error\"]')?.textContent?.trim()")
|
|
20
|
-
assert username_err&.length&.positive?, "expected username error, got: #{username_err.inspect}"
|
|
21
|
-
assert email_err&.length&.positive?, "expected email error, got: #{email_err.inspect}"
|
|
22
|
-
assert password_err&.length&.positive?, "expected password error, got: #{password_err.inspect}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
step "fill invalid email — email error shown" do
|
|
26
|
-
page(:main).fill("[data-test='email-input']", "not-an-email")
|
|
27
|
-
page(:main).click("[data-test='submit-button']")
|
|
28
|
-
page(:main).wait("[data-test='email-error']", timeout: 5)
|
|
29
|
-
email_err = page(:main).evaluate("document.querySelector('[data-test=\"email-error\"]')?.textContent?.trim()")
|
|
30
|
-
assert email_err&.length&.positive?, "expected email format error, got: #{email_err.inspect}"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
step "fill valid credentials and submit — no errors shown" do
|
|
34
|
-
page(:main).fill("[data-test='username-input']", "testuser")
|
|
35
|
-
page(:main).fill("[data-test='email-input']", "test@example.com")
|
|
36
|
-
page(:main).fill("[data-test='password-input']", "password123")
|
|
37
|
-
page(:main).click("[data-test='submit-button']")
|
|
38
|
-
sleep 0.5
|
|
39
|
-
username_err = page(:main).evaluate("!!document.querySelector('[data-test=\"username-error\"]')")
|
|
40
|
-
email_err = page(:main).evaluate("!!document.querySelector('[data-test=\"email-error\"]')")
|
|
41
|
-
password_err = page(:main).evaluate("!!document.querySelector('[data-test=\"password-error\"]')")
|
|
42
|
-
assert !username_err, "expected no username error on valid submit"
|
|
43
|
-
assert !email_err, "expected no email error on valid submit"
|
|
44
|
-
assert !password_err, "expected no password error on valid submit"
|
|
45
|
-
page(:main).screenshot(path: screenshot_path)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
SET_SLIDER_JS = <<~JS
|
|
4
|
-
(function(val) {
|
|
5
|
-
const el = document.querySelector('[data-test="slider"]');
|
|
6
|
-
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
|
|
7
|
-
.set.call(el, val);
|
|
8
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
9
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
10
|
-
})(%<value>d)
|
|
11
|
-
JS
|
|
12
|
-
|
|
13
|
-
Browserctl.workflow "test_automation_practices/forms/slider" do
|
|
14
|
-
desc "Slider page: set slider to specific values via evaluate, verify displayed value updates"
|
|
15
|
-
|
|
16
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
17
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_forms_slider.png")
|
|
18
|
-
|
|
19
|
-
step "open slider page" do
|
|
20
|
-
open_page(:main, url: "#{base_url}/#/slider")
|
|
21
|
-
page(:main).wait("[data-test='slider-container']", timeout: 10)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
step "set slider to 75 and verify display" do
|
|
25
|
-
page(:main).evaluate(format(SET_SLIDER_JS, value: 75))
|
|
26
|
-
sleep 0.1
|
|
27
|
-
displayed = page(:main).evaluate(
|
|
28
|
-
"document.querySelector('[data-test=\"slider-value\"]')?.textContent?.trim()"
|
|
29
|
-
)
|
|
30
|
-
assert displayed&.include?("75"), "expected slider-value to show 75, got: #{displayed.inspect}"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
step "set slider to minimum (0) and verify" do
|
|
34
|
-
page(:main).evaluate(format(SET_SLIDER_JS, value: 0))
|
|
35
|
-
sleep 0.1
|
|
36
|
-
displayed = page(:main).evaluate(
|
|
37
|
-
"document.querySelector('[data-test=\"slider-value\"]')?.textContent?.trim()"
|
|
38
|
-
)
|
|
39
|
-
assert displayed&.include?("0"), "expected slider-value to show 0, got: #{displayed.inspect}"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
step "set slider to maximum (100) and screenshot" do
|
|
43
|
-
page(:main).evaluate(format(SET_SLIDER_JS, value: 100))
|
|
44
|
-
sleep 0.1
|
|
45
|
-
displayed = page(:main).evaluate(
|
|
46
|
-
"document.querySelector('[data-test=\"slider-value\"]')?.textContent?.trim()"
|
|
47
|
-
)
|
|
48
|
-
assert displayed&.include?("100"), "expected slider-value to show 100, got: #{displayed.inspect}"
|
|
49
|
-
page(:main).screenshot(path: screenshot_path)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/interactions/context_menu" do
|
|
4
|
-
desc "Context menu page: trigger via dispatched contextmenu event, click each action, dismiss"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_interactions_context_menu.png")
|
|
8
|
-
|
|
9
|
-
step "open context menu page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/context-menu")
|
|
11
|
-
page(:main).wait("[data-test='context-menu-trigger']", timeout: 10)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
step "trigger context menu via dispatched event — menu appears" do
|
|
15
|
-
page(:main).evaluate(<<~JS)
|
|
16
|
-
document.querySelector('[data-test="context-menu-trigger"]').dispatchEvent(
|
|
17
|
-
new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 200, clientY: 300 })
|
|
18
|
-
)
|
|
19
|
-
JS
|
|
20
|
-
page(:main).wait("[data-test='context-menu']", timeout: 5)
|
|
21
|
-
visible = page(:main).evaluate("!!document.querySelector('[data-test=\"context-menu\"]')")
|
|
22
|
-
assert visible, "expected context-menu to appear after right-click event"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
step "all three menu items are present" do
|
|
26
|
-
has_edit = page(:main).evaluate("!!document.querySelector('[data-test=\"context-menu-edit\"]')")
|
|
27
|
-
has_delete = page(:main).evaluate("!!document.querySelector('[data-test=\"context-menu-delete\"]')")
|
|
28
|
-
has_properties = page(:main).evaluate("!!document.querySelector('[data-test=\"context-menu-properties\"]')")
|
|
29
|
-
assert has_edit, "expected context-menu-edit to be present"
|
|
30
|
-
assert has_delete, "expected context-menu-delete to be present"
|
|
31
|
-
assert has_properties, "expected context-menu-properties to be present"
|
|
32
|
-
page(:main).screenshot(path: screenshot_path)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
step "click Edit — menu closes" do
|
|
36
|
-
page(:main).click("[data-test='context-menu-edit']")
|
|
37
|
-
sleep 0.2
|
|
38
|
-
gone = page(:main).evaluate("!document.querySelector('[data-test=\"context-menu\"]')")
|
|
39
|
-
assert gone, "expected context-menu to close after clicking Edit"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
step "re-open and dismiss by clicking outside" do
|
|
43
|
-
page(:main).evaluate(<<~JS)
|
|
44
|
-
document.querySelector('[data-test="context-menu-trigger"]').dispatchEvent(
|
|
45
|
-
new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 200, clientY: 300 })
|
|
46
|
-
)
|
|
47
|
-
JS
|
|
48
|
-
page(:main).wait("[data-test='context-menu']", timeout: 5)
|
|
49
|
-
page(:main).evaluate("document.querySelector('[data-test=\"context-menu-area\"]').click()")
|
|
50
|
-
sleep 0.2
|
|
51
|
-
gone = page(:main).evaluate("!document.querySelector('[data-test=\"context-menu\"]')")
|
|
52
|
-
assert gone, "expected context-menu to close after clicking outside"
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/interactions/drag_drop" do
|
|
4
|
-
desc "Drag-drop page: reorder items via keyboard (Space to pick up, Arrow to move, Space to drop)"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_interactions_drag_drop.png")
|
|
8
|
-
|
|
9
|
-
step "open drag-drop page and read initial order" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/drag-drop")
|
|
11
|
-
page(:main).wait("[data-test='drag-drop-list']", timeout: 10)
|
|
12
|
-
items = page(:main).evaluate(
|
|
13
|
-
"Array.from(document.querySelectorAll('[data-test=\"drag-drop-list\"] li')).map(el => el.textContent?.trim())"
|
|
14
|
-
)
|
|
15
|
-
store(:initial_order, items)
|
|
16
|
-
assert items.length == 5, "expected 5 items in drag-drop list, got: #{items.length}"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
step "focus first item and move it down one position via keyboard" do
|
|
20
|
-
page(:main).evaluate(
|
|
21
|
-
"document.querySelectorAll('[data-test=\"drag-drop-list\"] li')[0].focus()"
|
|
22
|
-
)
|
|
23
|
-
sleep 0.1
|
|
24
|
-
page(:main).press("Space")
|
|
25
|
-
sleep 0.1
|
|
26
|
-
page(:main).press("ArrowDown")
|
|
27
|
-
sleep 0.1
|
|
28
|
-
page(:main).press("Space")
|
|
29
|
-
sleep 0.2
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
step "verify first item moved to second position" do
|
|
33
|
-
initial = fetch(:initial_order)
|
|
34
|
-
current = page(:main).evaluate(
|
|
35
|
-
"Array.from(document.querySelectorAll('[data-test=\"drag-drop-list\"] li')).map(el => el.textContent?.trim())"
|
|
36
|
-
)
|
|
37
|
-
assert current[1] == initial[0],
|
|
38
|
-
"expected '#{initial[0]}' at index 1 after move down, got order: #{current.inspect}"
|
|
39
|
-
page(:main).screenshot(path: screenshot_path)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/interactions/exit_intent" do
|
|
4
|
-
desc "Exit intent page: trigger modal by dispatching mouseleave at viewport top, test all dismiss paths"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_interactions_exit_intent.png")
|
|
8
|
-
|
|
9
|
-
step "open exit intent page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/exit-intent")
|
|
11
|
-
sleep 0.5
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
step "trigger exit intent — modal appears" do
|
|
15
|
-
page(:main).evaluate(<<~JS)
|
|
16
|
-
document.dispatchEvent(
|
|
17
|
-
new MouseEvent('mouseleave', { bubbles: true, cancelable: true, clientY: -1 })
|
|
18
|
-
)
|
|
19
|
-
JS
|
|
20
|
-
page(:main).wait("[data-test='exit-modal']", timeout: 5)
|
|
21
|
-
visible = page(:main).evaluate("!!document.querySelector('[data-test=\"exit-modal\"]')")
|
|
22
|
-
assert visible, "expected exit-modal to appear after mouseleave event"
|
|
23
|
-
page(:main).screenshot(path: screenshot_path)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
step "dismiss via X close button — modal disappears" do
|
|
27
|
-
page(:main).click("[data-test='close-modal']")
|
|
28
|
-
sleep 0.2
|
|
29
|
-
gone = page(:main).evaluate("!document.querySelector('[data-test=\"exit-modal\"]')")
|
|
30
|
-
assert gone, "expected exit-modal to close after clicking X"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
step "re-trigger and dismiss via No thanks" do
|
|
34
|
-
page(:main).navigate("#{base_url}/#/exit-intent")
|
|
35
|
-
sleep 0.5
|
|
36
|
-
page(:main).evaluate(<<~JS)
|
|
37
|
-
document.dispatchEvent(
|
|
38
|
-
new MouseEvent('mouseleave', { bubbles: true, cancelable: true, clientY: -1 })
|
|
39
|
-
)
|
|
40
|
-
JS
|
|
41
|
-
page(:main).wait("[data-test='exit-modal']", timeout: 5)
|
|
42
|
-
page(:main).click("[data-test='modal-no']")
|
|
43
|
-
sleep 0.2
|
|
44
|
-
gone = page(:main).evaluate("!document.querySelector('[data-test=\"exit-modal\"]')")
|
|
45
|
-
assert gone, "expected exit-modal to close after clicking No thanks"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Browserctl.workflow "test_automation_practices/interactions/hover" do
|
|
4
|
-
desc "Hover states page: move mouse over each figure, verify overlay content appears on each"
|
|
5
|
-
|
|
6
|
-
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_interactions_hover.png")
|
|
8
|
-
|
|
9
|
-
step "open hover states page" do
|
|
10
|
-
open_page(:main, url: "#{base_url}/#/hover")
|
|
11
|
-
page(:main).wait("[data-test='hover-example']", timeout: 10)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
step "hover over each figure and verify caption appears" do
|
|
15
|
-
[1, 2, 3].each do |i|
|
|
16
|
-
page(:main).hover("[data-test='hover-figure-#{i}']")
|
|
17
|
-
sleep 0.2
|
|
18
|
-
caption = page(:main).evaluate(
|
|
19
|
-
"!!document.querySelector('[data-test=\"hover-caption-#{i}\"]')"
|
|
20
|
-
)
|
|
21
|
-
assert caption, "expected caption to appear on figure #{i} after hover"
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
step "screenshot the hovered state" do
|
|
26
|
-
page(:main).hover("[data-test='hover-figure-2']")
|
|
27
|
-
sleep 0.2
|
|
28
|
-
page(:main).screenshot(path: screenshot_path)
|
|
29
|
-
end
|
|
30
|
-
end
|