browserctl 0.6.0 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/bin/browserctl +29 -0
- data/examples/test_automation_practices/advanced/ab_testing.rb +38 -0
- data/examples/test_automation_practices/advanced/broken_images.rb +25 -0
- data/examples/test_automation_practices/advanced/file_download.rb +40 -0
- data/examples/test_automation_practices/advanced/iframes.rb +37 -0
- data/examples/test_automation_practices/advanced/shadow_dom.rb +35 -0
- data/examples/test_automation_practices/{login.rb → auth/login.rb} +4 -4
- data/examples/test_automation_practices/{login_negative.rb → auth/login_negative.rb} +4 -4
- data/examples/test_automation_practices/dialogs/alerts.rb +45 -0
- data/examples/test_automation_practices/{notifications.rb → dialogs/notifications.rb} +7 -7
- data/examples/test_automation_practices/{dynamic_elements.rb → dynamic/dynamic_elements.rb} +9 -8
- data/examples/test_automation_practices/dynamic/tables.rb +47 -0
- data/examples/test_automation_practices/{checkboxes.rb → forms/checkboxes.rb} +6 -6
- data/examples/test_automation_practices/forms/file_upload.rb +30 -0
- data/examples/test_automation_practices/forms/forms.rb +47 -0
- data/examples/test_automation_practices/forms/slider.rb +51 -0
- data/examples/test_automation_practices/interactions/context_menu.rb +54 -0
- data/examples/test_automation_practices/interactions/drag_drop.rb +41 -0
- data/examples/test_automation_practices/interactions/exit_intent.rb +47 -0
- data/examples/test_automation_practices/interactions/hover.rb +30 -0
- data/examples/test_automation_practices/interactions/key_press.rb +38 -0
- data/lib/browserctl/client.rb +42 -0
- data/lib/browserctl/commands/ask.rb +20 -0
- data/lib/browserctl/commands/dialog.rb +33 -0
- data/lib/browserctl/commands/page.rb +1 -1
- data/lib/browserctl/errors.rb +3 -0
- data/lib/browserctl/secret_resolver_registry.rb +39 -0
- data/lib/browserctl/secret_resolvers/base.rb +17 -0
- data/lib/browserctl/secret_resolvers/env.rb +13 -0
- data/lib/browserctl/secret_resolvers/macos_keychain.rb +29 -0
- data/lib/browserctl/secret_resolvers/one_password.rb +22 -0
- data/lib/browserctl/secret_resolvers.rb +14 -0
- data/lib/browserctl/server/command_dispatcher.rb +8 -0
- data/lib/browserctl/server/handlers/interaction.rb +87 -0
- data/lib/browserctl/server/handlers/page_lifecycle.rb +4 -0
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow.rb +36 -9
- data/lib/browserctl.rb +1 -0
- metadata +31 -10
- data/examples/smoke/params_file.rb +0 -36
- data/examples/smoke/store_fetch.rb +0 -39
- data/examples/test_automation_practices/key_press.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e63d2425e4bbd57beefebf31e3f616a3183823f467b08c89ba6a1e2bb32cca0b
|
|
4
|
+
data.tar.gz: 8f12b646d805237a46b2bba2c194fb91c21834dc78648db97a4f74b0cc672de6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50521d3c938c009818d85c93b793c55f5ec2b6d7ae64ec0daae5bf337d793f795189f870d1451d5dca18b285c428a2dd48ec1c0b6af210e6e7c3b41a49c14989
|
|
7
|
+
data.tar.gz: fe973906dbfe35134aaa32c2e3080196febaa99dc4c8858683ceb9a9e41579f9a40b7ca7865af67ba946a6b2acc68dd7bafa31036daafeebe96e23edbd3f53c6
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ 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.8.0](https://github.com/patrick204nqh/browserctl/compare/v0.7.0...v0.8.0) (2026-04-29)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **v0.8:** secret resolver plugin system + load_session fallback ([#54](https://github.com/patrick204nqh/browserctl/issues/54)) ([69737bf](https://github.com/patrick204nqh/browserctl/commit/69737bf10528ad691a31abf916953325637af597))
|
|
19
|
+
|
|
20
|
+
## [0.7.0](https://github.com/patrick204nqh/browserctl/compare/v0.6.0...v0.7.0) (2026-04-28)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* v0.7 interaction primitives (press/hover/upload/select/dialog/ask) + page_focus fix ([#47](https://github.com/patrick204nqh/browserctl/issues/47)) ([63daadc](https://github.com/patrick204nqh/browserctl/commit/63daadcfbd4e967fc539b714393d163fabfc86b2))
|
|
26
|
+
|
|
13
27
|
## [0.6.0](https://github.com/patrick204nqh/browserctl/compare/v0.5.0...v0.6.0) (2026-04-28)
|
|
14
28
|
|
|
15
29
|
|
data/bin/browserctl
CHANGED
|
@@ -27,6 +27,8 @@ require "browserctl/commands/storage"
|
|
|
27
27
|
require "browserctl/commands/session"
|
|
28
28
|
require "browserctl/commands/daemon"
|
|
29
29
|
require "browserctl/commands/workflow"
|
|
30
|
+
require "browserctl/commands/dialog"
|
|
31
|
+
require "browserctl/commands/ask"
|
|
30
32
|
|
|
31
33
|
def print_result(res)
|
|
32
34
|
if res.is_a?(Hash) && res[:error]
|
|
@@ -62,7 +64,14 @@ def usage
|
|
|
62
64
|
wait <page> <selector> [--timeout N]
|
|
63
65
|
pause <page> [--message MSG]
|
|
64
66
|
resume <page>
|
|
67
|
+
ask <prompt>
|
|
65
68
|
devtools <page>
|
|
69
|
+
press <page> <key>
|
|
70
|
+
hover <page> <selector>
|
|
71
|
+
upload <page> <selector> <file>
|
|
72
|
+
select <page> <selector> <value>
|
|
73
|
+
dialog accept <page> [text]
|
|
74
|
+
dialog dismiss <page>
|
|
66
75
|
|
|
67
76
|
Cookie:
|
|
68
77
|
cookie list <page>
|
|
@@ -127,6 +136,7 @@ case cmd
|
|
|
127
136
|
when "workflow" then Browserctl::Commands::Workflow.run(runner, args)
|
|
128
137
|
when "record" then Browserctl::Commands::Record.run(args)
|
|
129
138
|
when "init" then Browserctl::Commands::Init.run(args)
|
|
139
|
+
when "ask" then Browserctl::Commands::Ask.run(args)
|
|
130
140
|
|
|
131
141
|
else
|
|
132
142
|
client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
|
|
@@ -166,6 +176,25 @@ else
|
|
|
166
176
|
puts "(#{opts[:message]})" if opts[:message]
|
|
167
177
|
puts "When done: browserctl resume #{name}"
|
|
168
178
|
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
179
|
+
when "press"
|
|
180
|
+
name = args.shift or abort "usage: browserctl press <page> <key>"
|
|
181
|
+
key = args.shift or abort "usage: browserctl press <page> <key>"
|
|
182
|
+
print_result(client.press(name, key))
|
|
183
|
+
when "hover"
|
|
184
|
+
name = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
185
|
+
selector = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
186
|
+
print_result(client.hover(name, selector))
|
|
187
|
+
when "upload"
|
|
188
|
+
name = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
189
|
+
selector = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
190
|
+
path = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
191
|
+
print_result(client.upload(name, selector, path))
|
|
192
|
+
when "select"
|
|
193
|
+
name = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
194
|
+
selector = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
195
|
+
value = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
196
|
+
print_result(client.select(name, selector, value))
|
|
197
|
+
when "dialog" then Browserctl::Commands::Dialog.run(client, args)
|
|
169
198
|
when "devtools"
|
|
170
199
|
name = args.shift or abort "usage: browserctl devtools <page>"
|
|
171
200
|
res = client.devtools(name)
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
|
@@ -0,0 +1,35 @@
|
|
|
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,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Browserctl.workflow "test_automation_practices/login" do
|
|
3
|
+
Browserctl.workflow "test_automation_practices/auth/login" do
|
|
4
4
|
desc "Auth page: fill credentials, verify success message, logout"
|
|
5
5
|
|
|
6
6
|
param :username, default: "admin"
|
|
7
7
|
param :password, default: "admin", secret: true
|
|
8
8
|
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
9
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/
|
|
9
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_auth_login.png")
|
|
10
10
|
|
|
11
11
|
step "open auth page" do
|
|
12
12
|
open_page(:main, url: "#{base_url}/#/auth")
|
|
@@ -20,7 +20,7 @@ Browserctl.workflow "test_automation_practices/login" do
|
|
|
20
20
|
|
|
21
21
|
step "verify successful login" do
|
|
22
22
|
page(:main).wait("[data-test='auth-success']", timeout: 10)
|
|
23
|
-
msg =
|
|
23
|
+
msg = page(:main).evaluate("document.querySelector('[data-test=\"auth-success\"]')?.innerText?.trim()")
|
|
24
24
|
assert msg&.length&.positive?, "expected a success message, got: #{msg.inspect}"
|
|
25
25
|
page(:main).screenshot(path: screenshot_path)
|
|
26
26
|
end
|
|
@@ -28,7 +28,7 @@ Browserctl.workflow "test_automation_practices/login" do
|
|
|
28
28
|
step "logout and verify form reappears" do
|
|
29
29
|
page(:main).click("[data-test='logout-button']")
|
|
30
30
|
page(:main).wait("[data-test='login-button']", timeout: 5)
|
|
31
|
-
visible =
|
|
31
|
+
visible = page(:main).evaluate("!!document.querySelector('[data-test=\"login-button\"]')")
|
|
32
32
|
assert visible, "expected login form to reappear after logout"
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Browserctl.workflow "test_automation_practices/login_negative" do
|
|
3
|
+
Browserctl.workflow "test_automation_practices/auth/login_negative" do
|
|
4
4
|
desc "Auth page: invalid credentials show an error message"
|
|
5
5
|
|
|
6
6
|
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
7
|
param :screenshot_path,
|
|
8
|
-
default: File.expand_path(".browserctl/screenshots/
|
|
8
|
+
default: File.expand_path(".browserctl/screenshots/tap_auth_login_negative.png")
|
|
9
9
|
|
|
10
10
|
step "open auth page" do
|
|
11
11
|
open_page(:main, url: "#{base_url}/#/auth")
|
|
@@ -19,9 +19,9 @@ Browserctl.workflow "test_automation_practices/login_negative" do
|
|
|
19
19
|
|
|
20
20
|
step "verify error is shown and success is absent" do
|
|
21
21
|
page(:main).wait("[data-test='auth-error']", timeout: 10)
|
|
22
|
-
error =
|
|
22
|
+
error = page(:main).evaluate("document.querySelector('[data-test=\"auth-error\"]')?.innerText?.trim()")
|
|
23
23
|
assert error&.length&.positive?, "expected an error message, got: #{error.inspect}"
|
|
24
|
-
no_success =
|
|
24
|
+
no_success = page(:main).evaluate("!document.querySelector('[data-test=\"auth-success\"]')")
|
|
25
25
|
assert no_success, "expected no success element when credentials are invalid"
|
|
26
26
|
page(:main).screenshot(path: screenshot_path)
|
|
27
27
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
|
@@ -6,36 +6,36 @@ NOTIFICATION_ITEMS_JS = <<~JS
|
|
|
6
6
|
document.querySelectorAll('[data-test^="notification-"]:not([data-test="notification-container"])').length
|
|
7
7
|
JS
|
|
8
8
|
|
|
9
|
-
Browserctl.workflow "test_automation_practices/notifications" do
|
|
9
|
+
Browserctl.workflow "test_automation_practices/dialogs/notifications" do
|
|
10
10
|
desc "Notifications: trigger success, error, and info toasts — verify count increases on each trigger"
|
|
11
11
|
|
|
12
12
|
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
13
13
|
param :screenshot_path,
|
|
14
|
-
default: File.expand_path(".browserctl/screenshots/
|
|
14
|
+
default: File.expand_path(".browserctl/screenshots/tap_dialogs_notifications.png")
|
|
15
15
|
|
|
16
16
|
step "open notifications page and record baseline" do
|
|
17
17
|
open_page(:main, url: "#{base_url}/#/notifications")
|
|
18
18
|
page(:main).wait("[data-test='notification-container']", timeout: 5)
|
|
19
19
|
# Let any delayed on-load notifications finish appearing before we snapshot the baseline
|
|
20
20
|
sleep 1
|
|
21
|
-
store(:baseline,
|
|
21
|
+
store(:baseline, page(:main).evaluate(NOTIFICATION_ITEMS_JS))
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
step "trigger success notification and verify count increased by 1" do
|
|
25
25
|
base = fetch(:baseline)
|
|
26
26
|
page(:main).click("[data-test='add-success']")
|
|
27
27
|
page(:main).wait("[data-test^='notification-']:not([data-test='notification-container'])", timeout: 5)
|
|
28
|
-
count =
|
|
28
|
+
count = page(:main).evaluate(NOTIFICATION_ITEMS_JS)
|
|
29
29
|
assert count == base + 1, "expected #{base + 1} notifications after success, got: #{count}"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
step "dismiss one notification and verify count returns to baseline" do
|
|
33
33
|
base = fetch(:baseline)
|
|
34
|
-
|
|
34
|
+
page(:main).evaluate("document.querySelector('[data-test^=\"close-notification-\"]')?.click()")
|
|
35
35
|
deadline = Time.now + 5
|
|
36
36
|
remaining = nil
|
|
37
37
|
loop do
|
|
38
|
-
remaining =
|
|
38
|
+
remaining = page(:main).evaluate(NOTIFICATION_ITEMS_JS)
|
|
39
39
|
break if remaining <= base || Time.now > deadline
|
|
40
40
|
|
|
41
41
|
sleep 0.2
|
|
@@ -50,7 +50,7 @@ Browserctl.workflow "test_automation_practices/notifications" do
|
|
|
50
50
|
page(:main).click("[data-test='add-error']")
|
|
51
51
|
page(:main).click("[data-test='add-info']")
|
|
52
52
|
page(:main).wait("[data-test^='notification-']:not([data-test='notification-container'])", timeout: 5)
|
|
53
|
-
count =
|
|
53
|
+
count = page(:main).evaluate(NOTIFICATION_ITEMS_JS)
|
|
54
54
|
assert count == base + 2, "expected #{base + 2} notifications (error + info), got: #{count}"
|
|
55
55
|
page(:main).screenshot(path: screenshot_path)
|
|
56
56
|
end
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Browserctl.workflow "test_automation_practices/dynamic_elements" do
|
|
3
|
+
Browserctl.workflow "test_automation_practices/dynamic/dynamic_elements" do
|
|
4
4
|
desc "Dynamic elements: trigger reload, wait for items, toggle hidden content"
|
|
5
5
|
|
|
6
6
|
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
7
|
param :screenshot_path,
|
|
8
|
-
default: File.expand_path(".browserctl/screenshots/
|
|
8
|
+
default: File.expand_path(".browserctl/screenshots/tap_dynamic_dynamic_elements.png")
|
|
9
9
|
|
|
10
10
|
step "open dynamic elements page" do
|
|
11
11
|
open_page(:main, url: "#{base_url}/#/dynamic-elements")
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
step "assert no dynamic items exist before reload" do
|
|
15
|
-
absent =
|
|
15
|
+
absent = page(:main).evaluate("!document.querySelector('[data-test=\"dynamic-item-0\"]')")
|
|
16
16
|
assert absent, "expected no dynamic items before triggering reload"
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -23,10 +23,9 @@ Browserctl.workflow "test_automation_practices/dynamic_elements" do
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
step "verify all three dynamic items are present" do
|
|
26
|
-
count =
|
|
27
|
-
"main",
|
|
26
|
+
count = page(:main).evaluate(
|
|
28
27
|
"[0,1,2].filter(i => !!document.querySelector(`[data-test=\"dynamic-item-${i}\"]`)).length"
|
|
29
|
-
)
|
|
28
|
+
)
|
|
30
29
|
assert count == 3, "expected 3 dynamic items, got: #{count}"
|
|
31
30
|
page(:main).screenshot(path: screenshot_path)
|
|
32
31
|
end
|
|
@@ -34,7 +33,9 @@ Browserctl.workflow "test_automation_practices/dynamic_elements" do
|
|
|
34
33
|
step "toggle hidden content on" do
|
|
35
34
|
page(:main).click("[data-test='toggle-hidden-button']")
|
|
36
35
|
page(:main).wait("[data-test='hidden-content']", timeout: 5)
|
|
37
|
-
visible =
|
|
38
|
-
|
|
36
|
+
visible = page(:main).evaluate(
|
|
37
|
+
"!document.querySelector('[data-test=\"hidden-content\"]')?.classList.contains('hidden')"
|
|
38
|
+
)
|
|
39
|
+
assert visible, "expected hidden-content to become visible after toggle (hidden class should be absent)"
|
|
39
40
|
end
|
|
40
41
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Browserctl.workflow "test_automation_practices/checkboxes" do
|
|
3
|
+
Browserctl.workflow "test_automation_practices/forms/checkboxes" do
|
|
4
4
|
desc "Checkboxes: toggle individual, check all, uncheck all — assert state each time"
|
|
5
5
|
|
|
6
6
|
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
-
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/
|
|
7
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_forms_checkboxes.png")
|
|
8
8
|
|
|
9
9
|
step "open checkboxes page" do
|
|
10
10
|
open_page(:main, url: "#{base_url}/#/checkboxes")
|
|
@@ -12,20 +12,20 @@ Browserctl.workflow "test_automation_practices/checkboxes" do
|
|
|
12
12
|
|
|
13
13
|
step "read initial state — checkbox 2 pre-checked" do
|
|
14
14
|
js = "[1,2,3].map(i => document.querySelector(`[data-test=\"checkbox-checkbox${i}\"]`)?.checked)"
|
|
15
|
-
states =
|
|
15
|
+
states = page(:main).evaluate(js)
|
|
16
16
|
assert states == [false, true, false], "expected initial state [false, true, false], got: #{states.inspect}"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
step "toggle checkbox 1 on" do
|
|
20
20
|
page(:main).click("[data-test='checkbox-checkbox1']")
|
|
21
|
-
checked =
|
|
21
|
+
checked = page(:main).evaluate("document.querySelector('[data-test=\"checkbox-checkbox1\"]').checked")
|
|
22
22
|
assert checked, "expected checkbox 1 to be checked after click"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
step "check all and verify" do
|
|
26
26
|
page(:main).click("[data-test='check-all-button']")
|
|
27
27
|
js = "[1,2,3].map(i => document.querySelector(`[data-test=\"checkbox-checkbox${i}\"]`)?.checked)"
|
|
28
|
-
states =
|
|
28
|
+
states = page(:main).evaluate(js)
|
|
29
29
|
assert states.all?, "expected all checkboxes checked, got: #{states.inspect}"
|
|
30
30
|
page(:main).screenshot(path: screenshot_path)
|
|
31
31
|
end
|
|
@@ -33,7 +33,7 @@ Browserctl.workflow "test_automation_practices/checkboxes" do
|
|
|
33
33
|
step "uncheck all and verify" do
|
|
34
34
|
page(:main).click("[data-test='uncheck-all-button']")
|
|
35
35
|
js = "[1,2,3].map(i => document.querySelector(`[data-test=\"checkbox-checkbox${i}\"]`)?.checked)"
|
|
36
|
-
states =
|
|
36
|
+
states = page(:main).evaluate(js)
|
|
37
37
|
assert states.none?, "expected all checkboxes unchecked, got: #{states.inspect}"
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|