charai 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +169 -0
- data/Rakefile +3 -0
- data/bin/console +8 -0
- data/charai.gemspec +33 -0
- data/lib/charai/action_queue.rb +41 -0
- data/lib/charai/agent.rb +97 -0
- data/lib/charai/browser.rb +142 -0
- data/lib/charai/browser_launcher.rb +170 -0
- data/lib/charai/browser_process.rb +26 -0
- data/lib/charai/browsing_context.rb +201 -0
- data/lib/charai/driver.rb +147 -0
- data/lib/charai/input_tool.rb +393 -0
- data/lib/charai/openai_chat.rb +163 -0
- data/lib/charai/openai_configuration.rb +58 -0
- data/lib/charai/spline_deceleration.rb +129 -0
- data/lib/charai/util.rb +11 -0
- data/lib/charai/version.rb +5 -0
- data/lib/charai/web_socket.rb +156 -0
- data/lib/charai.rb +18 -0
- metadata +109 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
module Charai
|
2
|
+
class BrowserLauncher
|
3
|
+
def initialize
|
4
|
+
if ::Charai::Util.macos?
|
5
|
+
if File.exist?("/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox")
|
6
|
+
@firefox_executable_path = "/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox"
|
7
|
+
end
|
8
|
+
elsif ::Charai::Util.linux?
|
9
|
+
if File.exist?("/usr/bin/firefox-devedition")
|
10
|
+
@firefox_executable_path = "/usr/bin/firefox-devedition"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
raise 'Firefox Developer Edition is not found.' unless @firefox_executable_path
|
15
|
+
end
|
16
|
+
|
17
|
+
def launch(headless: false, debug_protocol: false)
|
18
|
+
tmpdir = Dir.mktmpdir('charai')
|
19
|
+
create_user_profile(tmpdir)
|
20
|
+
|
21
|
+
args = [
|
22
|
+
"--remote-debugging-port=0",
|
23
|
+
"--profile",
|
24
|
+
tmpdir,
|
25
|
+
"--no-remote",
|
26
|
+
]
|
27
|
+
if ::Charai::Util.macos?
|
28
|
+
args << "--foreground"
|
29
|
+
end
|
30
|
+
if headless
|
31
|
+
args << "--headless"
|
32
|
+
end
|
33
|
+
|
34
|
+
proc = BrowserProcess.new(
|
35
|
+
@firefox_executable_path,
|
36
|
+
*args,
|
37
|
+
"about:blank",
|
38
|
+
)
|
39
|
+
at_exit do
|
40
|
+
proc.kill
|
41
|
+
FileUtils.remove_entry(tmpdir)
|
42
|
+
end
|
43
|
+
trap(:INT) { proc.kill ; exit 130 }
|
44
|
+
trap(:TERM) { proc.kill ; proc.dispose }
|
45
|
+
trap(:HUP) { proc.kill ; proc.dispose }
|
46
|
+
|
47
|
+
endpoint = wait_for_ws_endpoint(proc)
|
48
|
+
|
49
|
+
web_socket = WebSocket.new(url: "#{endpoint}/session")
|
50
|
+
Browser.new(web_socket, debug_protocol: debug_protocol)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def create_user_profile(profile_dir)
|
56
|
+
open(File.join(profile_dir, 'user.js'), 'w') do |f|
|
57
|
+
f.write(template_for_user_profile)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def template_for_user_profile
|
62
|
+
# execute the code below to create a template of user profile for Firefox.
|
63
|
+
# -----------
|
64
|
+
# import { createProfile } from '@puppeteer/browsers';
|
65
|
+
#
|
66
|
+
# await createProfile('firefox', {
|
67
|
+
# path: './my_prefs',
|
68
|
+
# preferences: {
|
69
|
+
# 'remote.active-protocols': 1,
|
70
|
+
# 'fission.webContentIsolationStrategy': 0,
|
71
|
+
# }
|
72
|
+
# })
|
73
|
+
<<~JS
|
74
|
+
user_pref("app.normandy.api_url", "");
|
75
|
+
user_pref("app.update.checkInstallTime", false);
|
76
|
+
user_pref("app.update.disabledForTesting", true);
|
77
|
+
user_pref("apz.content_response_timeout", 60000);
|
78
|
+
user_pref("browser.contentblocking.features.standard", "-tp,tpPrivate,cookieBehavior0,-cm,-fp");
|
79
|
+
user_pref("browser.dom.window.dump.enabled", true);
|
80
|
+
user_pref("browser.newtabpage.activity-stream.feeds.system.topstories", false);
|
81
|
+
user_pref("browser.newtabpage.enabled", false);
|
82
|
+
user_pref("browser.pagethumbnails.capturing_disabled", true);
|
83
|
+
user_pref("browser.safebrowsing.blockedURIs.enabled", false);
|
84
|
+
user_pref("browser.safebrowsing.downloads.enabled", false);
|
85
|
+
user_pref("browser.safebrowsing.malware.enabled", false);
|
86
|
+
user_pref("browser.safebrowsing.phishing.enabled", false);
|
87
|
+
user_pref("browser.search.update", false);
|
88
|
+
user_pref("browser.sessionstore.resume_from_crash", false);
|
89
|
+
user_pref("browser.shell.checkDefaultBrowser", false);
|
90
|
+
user_pref("browser.startup.homepage", "about:blank");
|
91
|
+
user_pref("browser.startup.homepage_override.mstone", "ignore");
|
92
|
+
user_pref("browser.startup.page", 0);
|
93
|
+
user_pref("browser.tabs.disableBackgroundZombification", false);
|
94
|
+
user_pref("browser.tabs.warnOnCloseOtherTabs", false);
|
95
|
+
user_pref("browser.tabs.warnOnOpen", false);
|
96
|
+
user_pref("browser.translations.automaticallyPopup", false);
|
97
|
+
user_pref("browser.uitour.enabled", false);
|
98
|
+
user_pref("browser.urlbar.suggest.searches", false);
|
99
|
+
user_pref("browser.usedOnWindows10.introURL", "");
|
100
|
+
user_pref("browser.warnOnQuit", false);
|
101
|
+
user_pref("datareporting.healthreport.documentServerURI", "http://dummy.test/dummy/healthreport/");
|
102
|
+
user_pref("datareporting.healthreport.logging.consoleEnabled", false);
|
103
|
+
user_pref("datareporting.healthreport.service.enabled", false);
|
104
|
+
user_pref("datareporting.healthreport.service.firstRun", false);
|
105
|
+
user_pref("datareporting.healthreport.uploadEnabled", false);
|
106
|
+
user_pref("datareporting.policy.dataSubmissionEnabled", false);
|
107
|
+
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
|
108
|
+
user_pref("devtools.jsonview.enabled", false);
|
109
|
+
user_pref("dom.disable_open_during_load", false);
|
110
|
+
user_pref("dom.file.createInChild", true);
|
111
|
+
user_pref("dom.ipc.reportProcessHangs", false);
|
112
|
+
user_pref("dom.max_chrome_script_run_time", 0);
|
113
|
+
user_pref("dom.max_script_run_time", 0);
|
114
|
+
user_pref("extensions.autoDisableScopes", 0);
|
115
|
+
user_pref("extensions.enabledScopes", 5);
|
116
|
+
user_pref("extensions.getAddons.cache.enabled", false);
|
117
|
+
user_pref("extensions.installDistroAddons", false);
|
118
|
+
user_pref("extensions.screenshots.disabled", true);
|
119
|
+
user_pref("extensions.update.enabled", false);
|
120
|
+
user_pref("extensions.update.notifyUser", false);
|
121
|
+
user_pref("extensions.webservice.discoverURL", "http://dummy.test/dummy/discoveryURL");
|
122
|
+
user_pref("focusmanager.testmode", true);
|
123
|
+
user_pref("general.useragent.updates.enabled", false);
|
124
|
+
user_pref("geo.provider.testing", true);
|
125
|
+
user_pref("geo.wifi.scan", false);
|
126
|
+
user_pref("hangmonitor.timeout", 0);
|
127
|
+
user_pref("javascript.options.showInConsole", true);
|
128
|
+
user_pref("media.gmp-manager.updateEnabled", false);
|
129
|
+
user_pref("media.sanity-test.disabled", true);
|
130
|
+
user_pref("network.cookie.sameSite.laxByDefault", false);
|
131
|
+
user_pref("network.http.prompt-temp-redirect", false);
|
132
|
+
user_pref("network.http.speculative-parallel-limit", 0);
|
133
|
+
user_pref("network.manage-offline-status", false);
|
134
|
+
user_pref("network.sntp.pools", "dummy.test");
|
135
|
+
user_pref("plugin.state.flash", 0);
|
136
|
+
user_pref("privacy.trackingprotection.enabled", false);
|
137
|
+
user_pref("remote.enabled", true);
|
138
|
+
user_pref("security.certerrors.mitm.priming.enabled", false);
|
139
|
+
user_pref("security.fileuri.strict_origin_policy", false);
|
140
|
+
user_pref("security.notification_enable_delay", 0);
|
141
|
+
user_pref("services.settings.server", "http://dummy.test/dummy/blocklist/");
|
142
|
+
user_pref("signon.autofillForms", false);
|
143
|
+
user_pref("signon.rememberSignons", false);
|
144
|
+
user_pref("startup.homepage_welcome_url", "about:blank");
|
145
|
+
user_pref("startup.homepage_welcome_url.additional", "");
|
146
|
+
user_pref("toolkit.cosmeticAnimations.enabled", false);
|
147
|
+
user_pref("toolkit.startup.max_resumed_crashes", -1);
|
148
|
+
user_pref("remote.active-protocols", 1);
|
149
|
+
user_pref("fission.webContentIsolationStrategy", 0);
|
150
|
+
JS
|
151
|
+
end
|
152
|
+
|
153
|
+
def wait_for_ws_endpoint(browser_process)
|
154
|
+
lines = []
|
155
|
+
Timeout.timeout(30) do
|
156
|
+
loop do
|
157
|
+
line = browser_process.stderr.readline
|
158
|
+
/^WebDriver BiDi listening on (ws:\/\/.*)$/.match(line) do |m|
|
159
|
+
return m[1].gsub(/\r/, '')
|
160
|
+
end
|
161
|
+
lines << line
|
162
|
+
end
|
163
|
+
end
|
164
|
+
rescue EOFError
|
165
|
+
raise lines.join("\n")
|
166
|
+
rescue Timeout::Error
|
167
|
+
raise "Timed out after 30 seconds while trying to connect to the browser."
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Charai
|
4
|
+
class BrowserProcess
|
5
|
+
def initialize(*command)
|
6
|
+
stdin, @stdout, @stderr, @thread = Open3.popen3(*command, { pgroup: true })
|
7
|
+
stdin.close
|
8
|
+
@pid = @thread.pid
|
9
|
+
rescue Errno::ENOENT => err
|
10
|
+
raise LaunchError.new("Failed to launch browser process: #{err}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def kill
|
14
|
+
Process.kill(:KILL, @pid)
|
15
|
+
rescue Errno::ESRCH
|
16
|
+
# already killed
|
17
|
+
end
|
18
|
+
|
19
|
+
def dispose
|
20
|
+
[@stdout, @stderr].each { |io| io.close unless io.closed? }
|
21
|
+
@thread.terminate
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :stdout, :stderr
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Charai
|
4
|
+
class BrowsingContext
|
5
|
+
def initialize(browser, context_id)
|
6
|
+
@browser = browser
|
7
|
+
@context_id = context_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def navigate(url, wait: :interactive)
|
11
|
+
bidi_call_async('browsingContext.navigate', {
|
12
|
+
url: url,
|
13
|
+
wait: wait,
|
14
|
+
}.compact).value!
|
15
|
+
end
|
16
|
+
|
17
|
+
def realms
|
18
|
+
result = bidi_call_async('script.getRealms').value!
|
19
|
+
result['realms'].map do |realm|
|
20
|
+
Realm.new(
|
21
|
+
browsing_context: self,
|
22
|
+
id: realm['realm'],
|
23
|
+
origin: realm['origin'],
|
24
|
+
type: realm['type'],
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_realm
|
30
|
+
realms.find { |realm| realm.type == 'window' }
|
31
|
+
end
|
32
|
+
|
33
|
+
def activate
|
34
|
+
bidi_call_async('browsingContext.activate').value!
|
35
|
+
end
|
36
|
+
|
37
|
+
def capture_screenshot(origin: nil, format: nil, clip: nil)
|
38
|
+
result = bidi_call_async('browsingContext.captureScreenshot', {
|
39
|
+
origin: origin,
|
40
|
+
format: format,
|
41
|
+
clip: clip,
|
42
|
+
}.compact).value!
|
43
|
+
|
44
|
+
Base64.strict_decode64(result['data'])
|
45
|
+
end
|
46
|
+
|
47
|
+
def close(prompt_unload: nil)
|
48
|
+
bidi_call_async('browsingContext.close', {
|
49
|
+
promptUnload: prompt_unload,
|
50
|
+
}.compact).value!
|
51
|
+
end
|
52
|
+
|
53
|
+
def perform_keyboard_actions(&block)
|
54
|
+
q = ActionQueue.new
|
55
|
+
block.call(q)
|
56
|
+
perform_actions([{
|
57
|
+
type: 'key',
|
58
|
+
id: '__charai_keyboard',
|
59
|
+
actions: q.to_a,
|
60
|
+
}])
|
61
|
+
end
|
62
|
+
|
63
|
+
def perform_mouse_actions(&block)
|
64
|
+
q = ActionQueue.new
|
65
|
+
block.call(q)
|
66
|
+
perform_actions([{
|
67
|
+
type: 'pointer',
|
68
|
+
id: '__charai_mouse',
|
69
|
+
actions: q.to_a,
|
70
|
+
}])
|
71
|
+
end
|
72
|
+
|
73
|
+
def perform_mouse_wheel_actions(&block)
|
74
|
+
q = ActionQueue.new
|
75
|
+
block.call(q)
|
76
|
+
perform_actions([{
|
77
|
+
type: 'wheel',
|
78
|
+
id: '__charai_wheel',
|
79
|
+
actions: q.to_a,
|
80
|
+
}])
|
81
|
+
end
|
82
|
+
|
83
|
+
def reload(ignore_cache: nil, wait: nil)
|
84
|
+
bidi_call_async('browsingContext.reload', {
|
85
|
+
ignoreCache: ignore_cache,
|
86
|
+
wait: wait,
|
87
|
+
}.compact).value!
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_viewport(width:, height:, device_pixel_ratio: nil)
|
91
|
+
bidi_call_async('browsingContext.setViewport', {
|
92
|
+
viewport: {
|
93
|
+
width: width,
|
94
|
+
height: height,
|
95
|
+
},
|
96
|
+
devicePixelRatio: device_pixel_ratio,
|
97
|
+
}.compact).value!
|
98
|
+
end
|
99
|
+
|
100
|
+
def traverse_history(delta)
|
101
|
+
bidi_call_async('browsingContext.traverseHistory', {
|
102
|
+
delta: delta,
|
103
|
+
}).value!
|
104
|
+
end
|
105
|
+
|
106
|
+
def url
|
107
|
+
@url
|
108
|
+
end
|
109
|
+
|
110
|
+
def _update_url(url)
|
111
|
+
@url = url
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def bidi_call_async(method_, params = {})
|
117
|
+
@browser.bidi_call_async(method_, params.merge({ context: @context_id }))
|
118
|
+
end
|
119
|
+
|
120
|
+
def perform_actions(actions)
|
121
|
+
bidi_call_async('input.performActions', {
|
122
|
+
actions: actions,
|
123
|
+
}).value!
|
124
|
+
end
|
125
|
+
|
126
|
+
class Realm
|
127
|
+
def initialize(browsing_context:, id:, origin:, type: nil)
|
128
|
+
@browsing_context = browsing_context
|
129
|
+
@id = id
|
130
|
+
@origin = origin
|
131
|
+
@type = type
|
132
|
+
end
|
133
|
+
|
134
|
+
class ScriptEvaluationError < StandardError; end
|
135
|
+
|
136
|
+
def script_evaluate(expression)
|
137
|
+
result = @browsing_context.send(:bidi_call_async, 'script.evaluate', {
|
138
|
+
expression: expression,
|
139
|
+
target: { realm: @id },
|
140
|
+
awaitPromise: true,
|
141
|
+
}).value!
|
142
|
+
|
143
|
+
if result['type'] == 'exception'
|
144
|
+
raise ScriptEvaluationError, result['exceptionDetails']['text']
|
145
|
+
end
|
146
|
+
|
147
|
+
deserialize(result['result'])
|
148
|
+
end
|
149
|
+
|
150
|
+
attr_reader :type
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# ref: https://github.com/puppeteer/puppeteer/blob/puppeteer-v23.5.3/packages/puppeteer-core/src/bidi/Deserializer.ts#L21
|
155
|
+
# Converted using ChatGPT 4o
|
156
|
+
def deserialize(result)
|
157
|
+
case result["type"]
|
158
|
+
when 'array'
|
159
|
+
result['value']&.map { |value| deserialize(value) }
|
160
|
+
when 'set'
|
161
|
+
result['value']&.each_with_object(Set.new) do |value, acc|
|
162
|
+
acc.add(deserialize(value))
|
163
|
+
end
|
164
|
+
when 'object'
|
165
|
+
result['value']&.each_with_object({}) do |tuple, acc|
|
166
|
+
key, value = tuple
|
167
|
+
acc[key] = deserialize(value)
|
168
|
+
end
|
169
|
+
when 'map'
|
170
|
+
result['value']&.each_with_object({}) do |tuple, acc|
|
171
|
+
key, value = tuple
|
172
|
+
acc[key] = deserialize(value)
|
173
|
+
end
|
174
|
+
when 'promise'
|
175
|
+
{}
|
176
|
+
when 'regexp'
|
177
|
+
flags = 0
|
178
|
+
result['value']['flags']&.each_char do |flag|
|
179
|
+
case flag
|
180
|
+
when 'm'
|
181
|
+
flags |= Regexp::MULTILINE
|
182
|
+
when 'i'
|
183
|
+
flags |= Regexp::IGNORECASE
|
184
|
+
end
|
185
|
+
end
|
186
|
+
Regexp.new(result['value']['pattern'], flags)
|
187
|
+
when 'date'
|
188
|
+
Date.parse(result['value'])
|
189
|
+
when 'undefined'
|
190
|
+
nil
|
191
|
+
when 'null'
|
192
|
+
nil
|
193
|
+
when 'number', 'bigint', 'boolean', 'string'
|
194
|
+
result['value']
|
195
|
+
else
|
196
|
+
raise ArgumentError, "Unknown type: #{result['type']}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charai
|
4
|
+
class Driver < ::Capybara::Driver::Base
|
5
|
+
def initialize(_app, **options)
|
6
|
+
@openai_configuration = options[:openai_configuration]
|
7
|
+
unless @openai_configuration
|
8
|
+
raise ArgumentError, "driver_options[:openai_configuration] is required"
|
9
|
+
end
|
10
|
+
@headless = options[:headless]
|
11
|
+
@callback = options[:callback]
|
12
|
+
@introduction = options[:introduction]
|
13
|
+
@debug_protocol = %w[1 true].include?(ENV['DEBUG'])
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_writer :callback, :introduction, :additional_instruction
|
17
|
+
|
18
|
+
def wait?; false; end
|
19
|
+
def needs_server?; true; end
|
20
|
+
|
21
|
+
def <<(text)
|
22
|
+
agent << text
|
23
|
+
end
|
24
|
+
|
25
|
+
def last_message
|
26
|
+
agent.last_message
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset!
|
30
|
+
@browsing_context&.close
|
31
|
+
@browsing_context = nil
|
32
|
+
@browser&.close
|
33
|
+
@browser = nil
|
34
|
+
@openai_chat&.clear
|
35
|
+
@agent = nil
|
36
|
+
@additional_instruction = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit(path)
|
40
|
+
host = Capybara.app_host || Capybara.default_host
|
41
|
+
|
42
|
+
url =
|
43
|
+
if host
|
44
|
+
Addressable::URI.parse(host) + path
|
45
|
+
else
|
46
|
+
path
|
47
|
+
end
|
48
|
+
|
49
|
+
browsing_context.navigate(url)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def browser
|
55
|
+
@browser ||= Browser.launch(headless: @headless, debug_protocol: @debug_protocol)
|
56
|
+
end
|
57
|
+
|
58
|
+
def browsing_context
|
59
|
+
@browsing_context ||= browser.create_browsing_context.tap do |context|
|
60
|
+
context.set_viewport(width: 1024, height: 800)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def openai_chat
|
65
|
+
@openai_chat ||= OpenaiChat.new(
|
66
|
+
@openai_configuration,
|
67
|
+
introduction: @introduction || default_introduction,
|
68
|
+
callback: @callback,
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def agent
|
73
|
+
@agent ||= Agent.new(
|
74
|
+
input_tool: InputTool.new(browsing_context, callback: @callback),
|
75
|
+
openai_chat: openai_chat,
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def default_introduction
|
80
|
+
<<~MARKDOWN
|
81
|
+
あなたはWebサイトの試験が得意なテスターです。Rubyのコードを使ってブラウザを自動操作する方法にも詳しいです。
|
82
|
+
|
83
|
+
ブラウザを操作する方法は以下の内容です。
|
84
|
+
|
85
|
+
* 画面の左上から (10ピクセル, 20ピクセル)の位置をクリックしたい場合には `driver.click(x: 10, y: 20)`
|
86
|
+
* キーボードで "hogeHoge!!" と入力したい場合には `driver.type_text("hogeHoge!!")`
|
87
|
+
* Enterキーを押したい場合には `driver.press_key("Enter")`
|
88
|
+
* コピー&ペーストをしたい場合には `driver.on_pressing_key("CtrlOrMeta") { driver.press_key("c") ; driver.press_key("v") }`
|
89
|
+
* 画面の左上から (10ピクセル, 20ピクセル)の位置にマウスを置いてスクロール操作で下に向かってスクロールをしたい場合には `driver.scroll_down(x: 10, y: 20, velocity: 1500)`
|
90
|
+
* 同様に、上に向かってスクロールをしたい場合には `driver.scroll_up(x: 10, y: 20, velocity: 1500)`
|
91
|
+
* 画面が切り替わるまで2秒待ちたい場合には `driver.sleep_seconds(2)`
|
92
|
+
* 現在の画面を一旦確認したい場合には `driver.capture_screenshot`
|
93
|
+
* DOM要素の位置を確認するために、JavaScriptの実行結果を取得したい場合は `driver.execute_script('JSON.stringify(document.querySelector("#some").getBoundingClientRect())')`
|
94
|
+
* テスト項目1がOKの場合には `driver.assertion_ok("テスト項目1")` 、テスト項目2がNGの場合には `driver.assertion_fail("テスト項目2")`
|
95
|
+
|
96
|
+
例えば、class="login"のテキストボックスの場所を特定したい場合には
|
97
|
+
|
98
|
+
```
|
99
|
+
driver.execute_script('JSON.stringify(document.querySelector("input.login").getBoundingClientRect())')
|
100
|
+
```
|
101
|
+
|
102
|
+
そうすると、私が以下のように実行結果を返します。
|
103
|
+
|
104
|
+
```
|
105
|
+
{"top":396.25,"right":638.4140625,"bottom":422.25,"left":488.4140625,"width":150,"height":26,"x":488.4140625,"y":396.25}
|
106
|
+
```
|
107
|
+
|
108
|
+
これで、要素の真ん中をクリックしたい場合には `driver.click(x: 563, y: 409)` のように実行できます。
|
109
|
+
|
110
|
+
また、画面の (100, 200) の位置にあるテキストボックスに"admin"というログイン名を入力して、画面の (100, 200) の位置にあるテキストボックスに "Passw0rd!" という文字列を入力して、Submitした結果、ログイン後のダッシュボード画面が表示されていることを確認する場合には、
|
111
|
+
|
112
|
+
```
|
113
|
+
driver.click(x: 100, y: 200)
|
114
|
+
driver.type_text("admin")
|
115
|
+
driver.click(x: 100, y: 320)
|
116
|
+
driver.type_text("Passw0rd!")
|
117
|
+
driver.press_key("Enter")
|
118
|
+
driver.sleep_seconds(2)
|
119
|
+
driver.capture_screenshot
|
120
|
+
```
|
121
|
+
|
122
|
+
のような指示だけを出力してください。 `driver.capture_screenshot` を呼ぶと、その後、私が画像をアップロードします。その画像を見て、ログイン画面のままであれば、再度上記のようなログイン手順を、ログインを完了できるように指示だけ出力してください。
|
123
|
+
|
124
|
+
```
|
125
|
+
driver.click(x: 100, y: 320)
|
126
|
+
driver.type_text("Passw0rd!")
|
127
|
+
driver.press_key("Enter")
|
128
|
+
driver.sleep_seconds(2)
|
129
|
+
driver.capture_screenshot
|
130
|
+
```
|
131
|
+
|
132
|
+
### 注意点
|
133
|
+
* ログイン後のダッシュボード画面に遷移したと判断したら `driver.assertion_ok("ログイン後のダッシュボード画面に遷移すること")` のような指示だけ出力してください。5回やってもうまくいかない場合には `driver.assertion_fail("ログイン後のダッシュボード画面に遷移すること")` のような指示だけ出力してください。
|
134
|
+
* 必ず、画像を見てクリックする場所がどこかを判断して `driver.click` を実行するようにしてください。場所がわからない場合には `driver.execute_script` を活用して、要素の場所を確認してください。 `driver.execute_script` を呼ぶと、私がJavaScriptの実行結果をアップロードします。現在のDOMの内容を確認したいときにも `driver.execute_script` は使用できます。例えば `driver.execute_script('document.body.innerHTML')` を実行すると現在のDOMのBodyのHTMLを取得することができます。
|
135
|
+
* 何も変化がない場合には、正しい場所をクリックできていない可能性が高いです。その場合には上記のgetBoundingClientRectを使用する手順で、クリックまたはスクロールする位置を必ず確かめてください。
|
136
|
+
* 画面外の要素はクリックできないので、getBoundingClientRectの結果、画面外にあることが判明したら、画面内に表示されるようにスクロールしてからクリックしてください。
|
137
|
+
* 一覧画面などでは、画面の一部だけがスクロールすることもあります。その場合には、スクロールする要素を特定して、その要素の位置を取得してからスクロール操作を行ってください。
|
138
|
+
* `driver.execute_script` を複数実行した場合には、私は最後の結果だけをアップロードしますので、getBoundingClientRectを複数回使用する場合には、1回ずつ分けて指示してください。
|
139
|
+
* 最後に実行された内容が `driver.capture_screenshot` または `driver.execute_script` ではない場合には、会話が強制終了してしまいますので、操作を続ける必要がある場合には `driver.execute_script` または `driver.capture_screenshot` を最後に実行してください。
|
140
|
+
|
141
|
+
#{@additional_instruction ? "### 補足説明\n#{@additional_instruction}" : ""}
|
142
|
+
|
143
|
+
それでは始めます。テストしたい手順は以下の内容です。
|
144
|
+
MARKDOWN
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|