diff --git a/.rubocop.yml b/.rubocop.yml index b6d8d6047..7af5f1118 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -70,6 +70,7 @@ Naming/UncommunicativeMethodParamName: - 'x' - 'y' - 'on' + - 'dt' Style/ParallelAssignment: Enabled: false diff --git a/lib/capybara/selenium/node.rb b/lib/capybara/selenium/node.rb index a53665e7f..76b2f79f6 100644 --- a/lib/capybara/selenium/node.rb +++ b/lib/capybara/selenium/node.rb @@ -59,11 +59,11 @@ def set(value, **options) when 'file' set_file(value) when 'date' - set_date(value) + set_date(value, options) when 'time' - set_time(value) + set_time(value, options) when 'datetime-local' - set_datetime_local(value) + set_datetime_local(value, options) else set_text(value, options) end @@ -256,30 +256,44 @@ def scroll_to_center end end - def set_date(value) # rubocop:disable Naming/AccessorMethodName + def set_date(value, as_keys: false, **) value = SettableValue.new(value) return set_text(value) unless value.dateable? + return set_as_keystrokes(value, :date) if as_keys - # TODO: this would be better if locale can be detected and correct keystrokes sent update_value_js(value.to_date_str) end - def set_time(value) # rubocop:disable Naming/AccessorMethodName + def set_time(value, as_keys: false, **) value = SettableValue.new(value) return set_text(value) unless value.timeable? + return set_as_keystrokes(value, :time) if as_keys - # TODO: this would be better if locale can be detected and correct keystrokes sent update_value_js(value.to_time_str) end - def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName + def set_datetime_local(value, as_keys: false, **) value = SettableValue.new(value) return set_text(value) unless value.timeable? + return set_as_keystrokes(value, :datetime) if as_keys - # TODO: this would be better if locale can be detected and correct keystrokes sent update_value_js(value.to_datetime_str) end + def set_as_keystrokes(val, type) + send_keys(keystrokes_for_datetime(val, type)) + end + + def locale + driver.execute_script(<<~JS).to_sym.downcase + return (window.navigator && ( + (window.navigator.languages && window.navigator.languages[0]) || + window.navigator.language || + window.navigator.userLanguage + )); + JS + end + def update_value_js(value) driver.execute_script(<<-JS, self, value) if (document.activeElement !== arguments[0]){ @@ -354,6 +368,35 @@ def each_key(keys) end end + def keystrokes_for_datetime(dt, type) + format = LOCALE_KEYSTROKES[locale][type] + if format.is_a? String + dt.strftime(format) + else + format.call(dt, self) + end + end + + LOCALE_KEYSTROKES = { + 'en-us': { + datetime: lambda { |dt, node| + if node[:step].empty? + dt.strftime '%m%d%Y%t%I%M%p' + else + dt.strftime '%m%d%Y%t%I%M%S%p' + end + }, + time: lambda { |dt, node| + if node[:step].empty? + dt.strftime '%I%M%p' + else + dt.strftime '%I%M%S%p' + end + }, + date: '%m%d%Y' + } + }.freeze + # SettableValue encapsulates time/date field formatting class SettableValue attr_reader :value @@ -379,11 +422,15 @@ def timeable? end def to_time_str - value.to_time.strftime('%H:%M') + value.to_time.strftime('%H:%M:%S') end def to_datetime_str - value.to_time.strftime('%Y-%m-%dT%H:%M') + value.to_time.strftime('%Y-%m-%dT%H:%M:%S') + end + + def strftime(format) + value.strftime format end end private_constant :SettableValue diff --git a/lib/capybara/selenium/nodes/marionette_node.rb b/lib/capybara/selenium/nodes/marionette_node.rb index 803fa3d3a..4c5737bb7 100644 --- a/lib/capybara/selenium/nodes/marionette_node.rb +++ b/lib/capybara/selenium/nodes/marionette_node.rb @@ -66,6 +66,12 @@ def click_with_options(click_options) super end + def set_as_keystrokes(val, type) + # send_keys doesn't work for datetime widgets in FF - use Actions API + click(x: 5, y: 5) + _send_keys(keystrokes_for_datetime(val, type)).perform + end + def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new) case keys when :control, :left_control, :right_control, diff --git a/lib/capybara/spec/session/fill_in_spec.rb b/lib/capybara/spec/session/fill_in_spec.rb index d60c3b00b..6b18575b8 100644 --- a/lib/capybara/spec/session/fill_in_spec.rb +++ b/lib/capybara/spec/session/fill_in_spec.rb @@ -69,12 +69,27 @@ expect(Time.parse(results).strftime('%r')).to eq time.strftime('%r') end + it 'should fill in a time input with seconds' do + time = Time.new(2018, 3, 9, 15, 26, 19) + @session.fill_in('form_time_with_seconds', with: time) + @session.click_button('awesome') + results = extract_results(@session)['time_with_seconds'] + expect(Time.parse(results).strftime('%r')).to eq time.strftime('%r') + end + it 'should fill in a datetime input' do dt = Time.new(2018, 3, 13, 9, 53) @session.fill_in('form_datetime', with: dt) @session.click_button('awesome') expect(Time.parse(extract_results(@session)['datetime'])).to eq dt end + + it 'should fill in a datetime input with seconds' do + dt = Time.new(2018, 3, 13, 9, 53, 13) + @session.fill_in('form_datetime_with_seconds', with: dt) + @session.click_button('awesome') + expect(Time.parse(extract_results(@session)['datetime_with_seconds'])).to eq dt + end end it 'should handle HTML in a textarea' do diff --git a/lib/capybara/spec/views/form.erb b/lib/capybara/spec/views/form.erb index b95f10523..b8b060813 100644 --- a/lib/capybara/spec/views/form.erb +++ b/lib/capybara/spec/views/form.erb @@ -449,6 +449,8 @@ New line after and before textarea tag + +
diff --git a/spec/shared_selenium_session.rb b/spec/shared_selenium_session.rb index 6cd475501..65b682cdf 100644 --- a/spec/shared_selenium_session.rb +++ b/spec/shared_selenium_session.rb @@ -69,128 +69,179 @@ end end - context '#fill_in_with empty string and no options' do - it 'should trigger change when clearing a field' do - session.visit('/with_js') - session.fill_in('with_change_event', with: '') - # click outside the field to trigger the change event - session.find(:css, 'body').click - expect(session).to have_selector(:css, '.change_event_triggered', match: :one) + describe '#fill_in' do + context 'with empty string and no options' do + it 'should trigger change when clearing a field' do + session.visit('/with_js') + session.fill_in('with_change_event', with: '') + # click outside the field to trigger the change event + session.find(:css, 'body').click + expect(session).to have_selector(:css, '.change_event_triggered', match: :one) + end end - end - context '#fill_in with { :clear => :backspace } fill_option', requires: [:js] do - before do - # Firefox has an issue with change events if the main window doesn't think it's focused - session.execute_script('window.focus()') - end + context 'with { :clear => :backspace } fill_option', requires: [:js] do + before do + # Firefox has an issue with change events if the main window doesn't think it's focused + session.execute_script('window.focus()') + end - it 'should fill in a field, replacing an existing value' do - session.visit('/form') - session.fill_in('form_first_name', - with: 'Harry', - fill_options: { clear: :backspace }) - expect(session.find(:fillable_field, 'form_first_name').value).to eq('Harry') - end + it 'should fill in a field, replacing an existing value' do + session.visit('/form') + session.fill_in('form_first_name', + with: 'Harry', + fill_options: { clear: :backspace }) + expect(session.find(:fillable_field, 'form_first_name').value).to eq('Harry') + end - it 'should fill in a field, replacing an existing value, even with caret position' do - session.visit('/form') - session.find(:css, '#form_first_name').execute_script <<-JS - this.focus(); - this.setSelectionRange(0, 0); - JS + it 'should fill in a field, replacing an existing value, even with caret position' do + session.visit('/form') + session.find(:css, '#form_first_name').execute_script <<-JS + this.focus(); + this.setSelectionRange(0, 0); + JS + + session.fill_in('form_first_name', + with: 'Harry', + fill_options: { clear: :backspace }) + expect(session.find(:fillable_field, 'form_first_name').value).to eq('Harry') + end - session.fill_in('form_first_name', - with: 'Harry', - fill_options: { clear: :backspace }) - expect(session.find(:fillable_field, 'form_first_name').value).to eq('Harry') - end + it 'should fill in if the option is set via global option' do + Capybara.default_set_options = { clear: :backspace } + session.visit('/form') + session.fill_in('form_first_name', with: 'Thomas') + expect(session.find(:fillable_field, 'form_first_name').value).to eq('Thomas') + end - it 'should fill in if the option is set via global option' do - Capybara.default_set_options = { clear: :backspace } - session.visit('/form') - session.fill_in('form_first_name', with: 'Thomas') - expect(session.find(:fillable_field, 'form_first_name').value).to eq('Thomas') - end + it 'should only trigger onchange once' do + session.visit('/with_js') + session.fill_in('with_change_event', + with: 'some value', + fill_options: { clear: :backspace }) + # click outside the field to trigger the change event + session.find(:css, '#with_focus_event').click + expect(session.find(:css, '.change_event_triggered', match: :one, wait: 5)).to have_text 'some value' + end - it 'should only trigger onchange once' do - session.visit('/with_js') - session.fill_in('with_change_event', - with: 'some value', - fill_options: { clear: :backspace }) - # click outside the field to trigger the change event - session.find(:css, '#with_focus_event').click - expect(session.find(:css, '.change_event_triggered', match: :one, wait: 5)).to have_text 'some value' - end + it 'should trigger change when clearing field' do + session.visit('/with_js') + session.fill_in('with_change_event', + with: '', + fill_options: { clear: :backspace }) + # click outside the field to trigger the change event + session.find(:css, '#with_focus_event').click + expect(session).to have_selector(:css, '.change_event_triggered', match: :one, wait: 5) + end - it 'should trigger change when clearing field' do - session.visit('/with_js') - session.fill_in('with_change_event', - with: '', - fill_options: { clear: :backspace }) - # click outside the field to trigger the change event - session.find(:css, '#with_focus_event').click - expect(session).to have_selector(:css, '.change_event_triggered', match: :one, wait: 5) + it 'should trigger input event field_value.length times' do + session.visit('/with_js') + session.fill_in('with_change_event', + with: '', + fill_options: { clear: :backspace }) + # click outside the field to trigger the change event + session.find(:css, 'body').click + expect(session).to have_xpath('//p[@class="input_event_triggered"]', count: 13) + end end - it 'should trigger input event field_value.length times' do - session.visit('/with_js') - session.fill_in('with_change_event', - with: '', - fill_options: { clear: :backspace }) - # click outside the field to trigger the change event - session.find(:css, 'body').click - expect(session).to have_xpath('//p[@class="input_event_triggered"]', count: 13) + context 'with { clear: :none } fill_options' do + it 'should append to content in a field' do + session.visit('/form') + session.fill_in('form_first_name', + with: 'Harry', + fill_options: { clear: :none }) + expect(session.find(:fillable_field, 'form_first_name').value).to eq('JohnHarry') + end end - end - context '#fill_in with { clear: :none } fill_options' do - it 'should append to content in a field' do - session.visit('/form') - session.fill_in('form_first_name', - with: 'Harry', - fill_options: { clear: :none }) - expect(session.find(:fillable_field, 'form_first_name').value).to eq('JohnHarry') - end - end + context 'with Date', :focus_ do + before do + session.visit('/form') + session.find(:css, '#form_date').execute_script <<-JS + window.capybara_formDateFiredEvents = []; + var fd = this; + ['focus', 'input', 'change', 'keydown'].forEach(function(eventType) { + fd.addEventListener(eventType, function() { window.capybara_formDateFiredEvents.push(eventType); }); + }); + JS + # work around weird FF issue where it would create an extra focus issue in some cases + session.find(:css, 'body').click + end - context '#fill_in with Date' do - before do - session.visit('/form') - session.find(:css, '#form_date').execute_script <<-JS - window.capybara_formDateFiredEvents = []; - var fd = this; - ['focus', 'input', 'change'].forEach(function(eventType) { - fd.addEventListener(eventType, function() { window.capybara_formDateFiredEvents.push(eventType); }); - }); - JS - # work around weird FF issue where it would create an extra focus issue in some cases - session.find(:css, 'body').click - end - - it 'should generate standard events on changing value' do - pending "IE 11 doesn't support date input type" if ie?(session) - session.fill_in('form_date', with: Date.today) - expect(session.evaluate_script('window.capybara_formDateFiredEvents')).to eq %w[focus input change] - end - - it 'should not generate input and change events if the value is not changed' do - pending "IE 11 doesn't support date input type" if ie?(session) - session.fill_in('form_date', with: Date.today) - session.fill_in('form_date', with: Date.today) - # Chrome adds an extra focus for some reason - ok for now - expect(session.evaluate_script('window.capybara_formDateFiredEvents')).to eq(%w[focus input change]) + it 'should generate standard events on changing value' do + pending "IE 11 doesn't support date input type" if ie?(session) + session.fill_in('form_date', with: Date.today) + expect(session.evaluate_script('window.capybara_formDateFiredEvents')).to eq %w[focus input change] + end + + it 'should not generate key events without as_keys option' do + session.fill_in('form_date', with: Date.today) + expect(session.evaluate_script('window.capybara_formDateFiredEvents')).not_to include('keydown') + end + + it 'should generate key events with as_keys option' do + session.fill_in('form_date', with: Date.today, fill_options: { as_keys: true }) + expect(session.evaluate_script('window.capybara_formDateFiredEvents')).to include('keydown') + end + + it 'should not generate input and change events if the value is not changed' do + pending "IE 11 doesn't support date input type" if ie?(session) + session.fill_in('form_date', with: Date.today) + session.fill_in('form_date', with: Date.today) + # Chrome adds an extra focus for some reason - ok for now + expect(session.evaluate_script('window.capybara_formDateFiredEvents')).to eq(%w[focus input change]) + end + + context 'with :as_keys options' do + it 'should fill in a date input' do + date = Date.today + session.fill_in('form_date', with: date, fill_options: { as_keys: true }) + session.click_button('awesome') + expect(Date.parse(extract_results(session)['date'])).to eq date + end + + it 'should fill in a time input' do + time = Time.new(2019, 2, 11, 7, 3) + session.fill_in('form_time', with: time, fill_options: { as_keys: true }) + session.click_button('awesome') + results = extract_results(session)['time'] + expect(Time.parse(results).strftime('%r')).to eq time.strftime('%r') + end + + it 'should fill in a time input with seconds' do + time = Time.new(2018, 11, 4, 3, 24, 17) + session.fill_in('form_time_with_seconds', with: time, fill_options: { as_keys: true }) + session.click_button('awesome') + results = extract_results(session)['time_with_seconds'] + expect(Time.parse(results).strftime('%r')).to eq time.strftime('%r') + end + + it 'should fill in a datetime input' do + dt = Time.new(2018, 10, 13, 12, 53) + session.fill_in('form_datetime', with: dt, fill_options: { as_keys: true }) + session.click_button('awesome') + expect(Time.parse(extract_results(session)['datetime'])).to eq dt + end + + it 'should fill in a datetime input with seconds' do + dt = Time.new(2018, 3, 13, 9, 53, 13) + session.fill_in('form_datetime_with_seconds', with: dt, fill_options: { as_keys: true }) + session.click_button('awesome') + expect(Time.parse(extract_results(session)['datetime_with_seconds'])).to eq dt + end + end end - end - context '#fill_in with { clear: Array } fill_options' do - it 'should pass the array through to the element' do - # this is mainly for use with [[:control, 'a'], :backspace] - however since that is platform dependant I'm testing with something less useful - session.visit('/form') - session.fill_in('form_first_name', - with: 'Harry', - fill_options: { clear: [[:shift, 'abc'], :backspace] }) - expect(session.find(:fillable_field, 'form_first_name').value).to eq('JohnABHarry') + context 'with { clear: Array } fill_options' do + it 'should pass the array through to the element' do + # this is mainly for use with [[:control, 'a'], :backspace] - however since that is platform dependant I'm testing with something less useful + session.visit('/form') + session.fill_in('form_first_name', + with: 'Harry', + fill_options: { clear: [[:shift, 'abc'], :backspace] }) + expect(session.find(:fillable_field, 'form_first_name').value).to eq('JohnABHarry') + end end end