├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── capybara-bootstrap-datepicker.gemspec ├── lib ├── capybara-bootstrap-datepicker.rb ├── capybara-bootstrap-datepicker │ ├── rspec.rb │ └── version.rb └── capybara │ └── bootstrap-datepicker.rb └── spec ├── features ├── bootstrap-3.4.html ├── bootstrap-4.4.html ├── bootstrap-5.0.html ├── bootstrap-5.3.html └── bootstrap_datepicker_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.sassc 4 | .sass-cache 5 | capybara-*.html 6 | /.bundle 7 | /vendor/bundle 8 | /log/* 9 | /tmp/* 10 | /db/*.sqlite3 11 | /public/system/* 12 | /coverage/ 13 | /screenshot_* 14 | /spec/tmp/* 15 | **.orig 16 | rerun.txt 17 | pickle-email-*.html 18 | /.yardoc 19 | /doc 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | # The behavior of RuboCop can be controlled via the .rubocop.yml 4 | # configuration file. It makes it possible to enable/disable 5 | # certain cops (checks) and to alter their behavior if they accept 6 | # any parameters. The file can be placed either in your home 7 | # directory or in some project directory. 8 | # 9 | # RuboCop will start looking for the configuration file in the directory 10 | # where the inspected file is and continue its way up to the root directory. 11 | # 12 | # See https://docs.rubocop.org/rubocop/configuration 13 | 14 | plugins: 15 | - rubocop-performance 16 | - rubocop-rspec 17 | - rubocop-capybara 18 | 19 | AllCops: 20 | NewCops: enable 21 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2025-03-30 18:11:21 UTC using RuboCop version 1.75.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 11 | Metrics/AbcSize: 12 | Max: 18 13 | 14 | # Offense count: 1 15 | # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. 16 | Metrics/ParameterLists: 17 | Max: 6 18 | 19 | # Offense count: 2 20 | # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. 21 | # CheckDefinitionPathHierarchyRoots: lib, spec, test, src 22 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 23 | Naming/FileName: 24 | Exclude: 25 | - 'Rakefile.rb' 26 | - 'lib/capybara-bootstrap-datepicker.rb' 27 | - 'lib/capybara/bootstrap-datepicker.rb' 28 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.6.5" 4 | - "2.5.7" 5 | before_install: 6 | - gem install bundler 7 | script: bundle exec rspec spec 8 | addons: 9 | code_climate: 10 | repo_token: 11 | secure: SFpmOHwZ3HBMqMH0NKq0B6DqoQjeDtsr5V9YCRLM7D6LvkoMhbkFYPoNo4nz6EWQ0Ml9JzP7ORumUPv1ujhCcYV+yLQ5eIL/xg+g+3H0Xu1sx6CuzDY4C7mZJO5s84KoH7yu9pBrlIa+UJwezwrCOm7JWURsPhR94YvmKobyOZHEuAyvUbYb+eqAkYfXrl71dC6hywfdlSOP/OEncPpIDZ6EApfWRhqZYHHHoj9EqXuHasNzmr6c2cqAh++7XDIOlHJ8JUEjeZN9ofTrqN4oq4JkGvIVmUHOe62jaj92z4vi3ZH+4lv0GmM5KlxgwRZKcCts5WivaNTyQzUnInUbWlWcIHnBFQMPj17ggjhixb6O+w3uAKlbLUGDYkKRSc+MWEdURpFIhyNCMtesHA1GS9+aU6yuxgz3REJCu7K3s5XkenvqxWucWweCOhNVRYSHhwhXAFO4Atg8ZdgIvGA7gF50TMfvJvQ0UIUvOmCxpVi2dwXDfwCgeks5ghP4Hyf0qXbnkLn3/4CE6khYPjQhb3tDu5AnouzKC3SedVxg9QDKZZYD0XEr7TTB89/LelqDYOjDy1C7/+aLGIi24zgzE7A02W6G8SikX08Er5tUvRw1Fgohkr7Th6YFisIgYqNCyreUeV2co/dhI7lTGk6GrBDqltYPht78EIbto4PVu9s= 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in capybara-bootstrap-datepicker.gemspec 6 | gemspec 7 | 8 | gem 'base64', '~> 0.2.0' 9 | gem 'capybara', '~> 3.29', '>= 3.29.0' 10 | gem 'capybara-screenshot', '~> 1.0', '>= 1.0.22' 11 | gem 'nokogiri', '~> 1.18' 12 | gem 'rack', '~> 3.1' 13 | gem 'rspec', '~> 3.9', '>= 3.9.0' 14 | gem 'rubocop', '~> 1.75', '>= 1.75.1' 15 | gem 'rubocop-capybara', '~> 2.22', '>= 2.22.1' 16 | gem 'rubocop-performance', '~> 1.24' 17 | gem 'rubocop-rspec', '~> 3.5' 18 | gem 'selenium-webdriver', '~> 4.30', '>= 4.30.1' 19 | gem 'timecop', '~> 0.9.10' 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | capybara-bootstrap-datepicker (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | addressable (2.8.7) 10 | public_suffix (>= 2.0.2, < 7.0) 11 | ast (2.4.3) 12 | base64 (0.2.0) 13 | capybara (3.40.0) 14 | addressable 15 | matrix 16 | mini_mime (>= 0.1.3) 17 | nokogiri (~> 1.11) 18 | rack (>= 1.6.0) 19 | rack-test (>= 0.6.3) 20 | regexp_parser (>= 1.5, < 3.0) 21 | xpath (~> 3.2) 22 | capybara-screenshot (1.0.26) 23 | capybara (>= 1.0, < 4) 24 | launchy 25 | childprocess (5.1.0) 26 | logger (~> 1.5) 27 | diff-lcs (1.6.1) 28 | json (2.10.2) 29 | language_server-protocol (3.17.0.4) 30 | launchy (3.1.1) 31 | addressable (~> 2.8) 32 | childprocess (~> 5.0) 33 | logger (~> 1.6) 34 | lint_roller (1.1.0) 35 | logger (1.7.0) 36 | matrix (0.4.2) 37 | mini_mime (1.1.5) 38 | mini_portile2 (2.8.8) 39 | nokogiri (1.18.8) 40 | mini_portile2 (~> 2.8.2) 41 | racc (~> 1.4) 42 | parallel (1.26.3) 43 | parser (3.3.7.3) 44 | ast (~> 2.4.1) 45 | racc 46 | prism (1.4.0) 47 | public_suffix (6.0.1) 48 | racc (1.8.1) 49 | rack (3.1.16) 50 | rack-test (2.2.0) 51 | rack (>= 1.3) 52 | rainbow (3.1.1) 53 | regexp_parser (2.10.0) 54 | rexml (3.4.1) 55 | rspec (3.13.0) 56 | rspec-core (~> 3.13.0) 57 | rspec-expectations (~> 3.13.0) 58 | rspec-mocks (~> 3.13.0) 59 | rspec-core (3.13.3) 60 | rspec-support (~> 3.13.0) 61 | rspec-expectations (3.13.3) 62 | diff-lcs (>= 1.2.0, < 2.0) 63 | rspec-support (~> 3.13.0) 64 | rspec-mocks (3.13.2) 65 | diff-lcs (>= 1.2.0, < 2.0) 66 | rspec-support (~> 3.13.0) 67 | rspec-support (3.13.2) 68 | rubocop (1.75.1) 69 | json (~> 2.3) 70 | language_server-protocol (~> 3.17.0.2) 71 | lint_roller (~> 1.1.0) 72 | parallel (~> 1.10) 73 | parser (>= 3.3.0.2) 74 | rainbow (>= 2.2.2, < 4.0) 75 | regexp_parser (>= 2.9.3, < 3.0) 76 | rubocop-ast (>= 1.43.0, < 2.0) 77 | ruby-progressbar (~> 1.7) 78 | unicode-display_width (>= 2.4.0, < 4.0) 79 | rubocop-ast (1.43.0) 80 | parser (>= 3.3.7.2) 81 | prism (~> 1.4) 82 | rubocop-capybara (2.22.1) 83 | lint_roller (~> 1.1) 84 | rubocop (~> 1.72, >= 1.72.1) 85 | rubocop-performance (1.24.0) 86 | lint_roller (~> 1.1) 87 | rubocop (>= 1.72.1, < 2.0) 88 | rubocop-ast (>= 1.38.0, < 2.0) 89 | rubocop-rspec (3.5.0) 90 | lint_roller (~> 1.1) 91 | rubocop (~> 1.72, >= 1.72.1) 92 | ruby-progressbar (1.13.0) 93 | rubyzip (2.4.1) 94 | selenium-webdriver (4.30.1) 95 | base64 (~> 0.2) 96 | logger (~> 1.4) 97 | rexml (~> 3.2, >= 3.2.5) 98 | rubyzip (>= 1.2.2, < 3.0) 99 | websocket (~> 1.0) 100 | timecop (0.9.10) 101 | unicode-display_width (3.1.4) 102 | unicode-emoji (~> 4.0, >= 4.0.4) 103 | unicode-emoji (4.0.4) 104 | websocket (1.2.11) 105 | xpath (3.2.0) 106 | nokogiri (~> 1.8) 107 | 108 | PLATFORMS 109 | ruby 110 | 111 | DEPENDENCIES 112 | base64 (~> 0.2.0) 113 | capybara (~> 3.29, >= 3.29.0) 114 | capybara-bootstrap-datepicker! 115 | capybara-screenshot (~> 1.0, >= 1.0.22) 116 | nokogiri (~> 1.18) 117 | rack (~> 3.1) 118 | rspec (~> 3.9, >= 3.9.0) 119 | rubocop (~> 1.75, >= 1.75.1) 120 | rubocop-capybara (~> 2.22, >= 2.22.1) 121 | rubocop-performance (~> 1.24) 122 | rubocop-rspec (~> 3.5) 123 | selenium-webdriver (~> 4.30, >= 4.30.1) 124 | timecop (~> 0.9.10) 125 | 126 | BUNDLED WITH 127 | 2.3.3 128 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 François Vantomme 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/capybara-bootstrap-datepicker.svg)](http://badge.fury.io/rb/capybara-bootstrap-datepicker) 2 | [![Travis CI](https://travis-ci.org/akarzim/capybara-bootstrap-datepicker.svg?branch=master)](https://travis-ci.org/akarzim/capybara-bootstrap-datepicker.svg?branch=master) 3 | 4 | [Bootstrap]: https://getbootstrap.com/ 5 | [bootstrap-datepicker]: https://github.com/eternicode/bootstrap-datepicker 6 | [jQuery]: https://jquery.com/ 7 | 8 | # Capybara::BootstrapDatepicker 9 | 10 | Helper for triggering date input with the [bootstrap-datepicker] JavaScript 11 | library. 12 | 13 | This gem does something very simple: it allows you to trigger the [Bootstrap] 14 | date picker to select the date you want. 15 | 16 | ## Supported versions 17 | 18 | This gem has been tested with: 19 | 20 | - [Bootstrap] 5.3.3 + [bootstrap-datepicker] 1.10.0 + [jQuery] 3.7.1 21 | - [Bootstrap] 5.0.1 + [bootstrap-datepicker] 1.9.0 + [jQuery] 3.6.0 22 | - [Bootstrap] 4.4.1 + [bootstrap-datepicker] 1.9.0 + [jQuery] 3.4.1 23 | - [Bootstrap] 3.4.1 + [bootstrap-datepicker] 1.9.0 + [jQuery] 3.4.1 24 | 25 | ## Installation 26 | 27 | Add this line to your application's Gemfile: 28 | 29 | ```ruby 30 | gem 'capybara-bootstrap-datepicker', group: :test 31 | ``` 32 | 33 | Or, add it into your test group 34 | 35 | ```ruby 36 | group :test do 37 | gem 'capybara-bootstrap-datepicker' 38 | ... 39 | end 40 | ``` 41 | 42 | And then execute: 43 | 44 | $ bundle 45 | 46 | Or install it yourself as: 47 | 48 | $ gem install capybara-bootstrap-datepicker 49 | 50 | The gem automatically hooks itself into RSpec helper using `RSpec.configure`. 51 | 52 | ## Usage 53 | 54 | Just use this method inside your Capybara test: 55 | 56 | ```ruby 57 | select_date(2.weeks.ago, from: "Label of the date input") 58 | ``` 59 | 60 | Or in a more advanced way: 61 | 62 | ```ruby 63 | select_date(2.weeks.ago, from: "Date", match: :prefer_exact) 64 | select_date(Date.tomorrow, from: "Label of the date input", format: "%d/%m/%Y") 65 | select_date("2013-05-24", xpath: "//path_to//your_date_input", datepicker: :bootstrap) 66 | ``` 67 | 68 | Available options are: 69 | 70 | - **from**: the label of your date input 71 | - **xpath**: the path to your date input 72 | - **format**: the format used to fill your date input 73 | - **match**: 74 | - **datepicker**: the way to fill your date input 75 | - `:bootstrap` = by clicking the popover using [bootstrap-datepicker] 76 | - `:simple` = just fill the input date 77 | - any extra args to find the input field 78 | 79 | ## Test 80 | 81 | Just run RSpec in your terminal: 82 | 83 | $ rspec 84 | 85 | ## Upgrading from 0.0.x 86 | 87 | RSpec support has been split into a separate file. You'll need to change 88 | `spec_helper.rb` to `require 'capybara-bootstrap-datepicker/rspec'`. 89 | 90 | ## Contributing 91 | 92 | 1. Fork it 93 | 2. Create your feature branch (`git checkout -b my-new-feature`) 94 | 3. Commit your changes (`git commit -am 'Add some feature'`) 95 | 4. Push to the branch (`git push origin my-new-feature`) 96 | 5. Create new Pull Request 97 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | -------------------------------------------------------------------------------- /capybara-bootstrap-datepicker.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | require 'English' 7 | require 'capybara-bootstrap-datepicker/version' 8 | 9 | Gem::Specification.new do |gem| 10 | gem.name = 'capybara-bootstrap-datepicker' 11 | gem.version = Capybara::BootstrapDatepicker::VERSION 12 | gem.authors = ['François Vantomme'] 13 | gem.email = ['akarzim@pm.me'] 14 | gem.summary = 'Bootstrap datepicker helper for Capybara' 15 | gem.description = 'Helper for triggering date input for bootstrap-datepicker javascript library' 16 | gem.homepage = 'https://github.com/akarzim/capybara-bootstrap-datepicker' 17 | gem.license = 'MIT' 18 | 19 | gem.files = `git ls-files`.split($RS) 20 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 21 | gem.require_paths = ['lib'] 22 | 23 | gem.required_ruby_version = '>= 3.2.8' 24 | 25 | gem.metadata['rubygems_mfa_required'] = 'true' 26 | end 27 | -------------------------------------------------------------------------------- /lib/capybara-bootstrap-datepicker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara-bootstrap-datepicker/version' 4 | 5 | module Capybara 6 | # Adds datepicker interaction facilities to {Capybara} 7 | module BootstrapDatepicker 8 | # Selects a date by simulating human interaction with the datepicker or filling the input field 9 | # @param value [#to_date, String] any object that responds to `#to_date` or a parsable date string 10 | # @param datepicker [:bootstrap, :simple] the datepicker to use (are supported: bootstrap or input field) 11 | # @param format [String, nil] a valid date format used to format value 12 | # @param from [String, nil] the path to input field (required if `xpath` is nil) 13 | # @param xpath [String, nil] the xpath to input field (required if `from` is nil) 14 | # @param args [Hash] extra args to find the input field 15 | def select_date(value, datepicker: :bootstrap, format: nil, from: nil, xpath: nil, **args) 16 | raise "Must pass a hash containing 'from' or 'xpath'" if from.nil? && xpath.nil? 17 | 18 | value = value.respond_to?(:to_date) ? value.to_date : Date.parse(value) 19 | date_input = xpath ? find(:xpath, xpath, **args) : find_field(from, **args) 20 | 21 | case datepicker 22 | when :bootstrap 23 | select_bootstrap_date(date_input, value:, format:) 24 | else 25 | select_simple_date(date_input, value:, format:) 26 | first(:xpath, '//body').click 27 | end 28 | end 29 | 30 | # Selects a date by filling the input field 31 | # @param date_input the input field 32 | # @param value [Date] the date to set 33 | # @param format [String, nil] a valid date format used to format value 34 | def select_simple_date(date_input, value:, format: nil) 35 | value = value.strftime format unless format.nil? 36 | 37 | date_input.set "#{value}\e" 38 | end 39 | 40 | # Selects a date by simulating human interaction with the datepicker 41 | # @param (see #select_simple_date) 42 | def select_bootstrap_date(date_input, value:, format: nil) 43 | date_input.click 44 | 45 | picker = Picker.new 46 | 47 | picker.goto_decade_panel 48 | picker.navigate_through_decades value.year 49 | 50 | picker.find_year(value.year).click 51 | picker.find_month(value.month).click 52 | picker.find_day(value.day).click if format.nil? || format.include?('%d') 53 | end 54 | 55 | # The Picker class interacts with the datepicker 56 | class Picker 57 | # Initializes the picker 58 | def initialize 59 | @element = find_picker 60 | end 61 | 62 | # Reveals the decade panel 63 | def goto_decade_panel 64 | current_month.click if days.visible? 65 | current_year.click if months.visible? 66 | end 67 | 68 | # Navigates through the decade panels until the correct one 69 | # @param value [Fixnum] the year of the desired date 70 | def navigate_through_decades(value) 71 | decade_start, decade_end = current_decade_minmax 72 | goto_prev_decade(value, decade_start) if value < decade_start 73 | goto_next_decade(decade_end, value) if value > decade_end 74 | end 75 | 76 | # Get the year we want to click on 77 | # @param value [Fixnum] the year of the desired date 78 | # @return the DOM element to click on 79 | def find_year(value) 80 | years.find '.year', text: value 81 | end 82 | 83 | # Get the month we want to click on 84 | # @param value [Fixnum] the month of the desired date 85 | # @return the DOM element to click on 86 | def find_month(value) 87 | months.find ".month:nth-child(#{value})" 88 | end 89 | 90 | # Get the day we want to click on 91 | # @param value [Fixnum] the day of the desired date 92 | # @return the DOM element to click on 93 | def find_day(value) 94 | day_xpath = <<-XPATH 95 | .//*[contains(concat(' ', @class, ' '), ' day ') 96 | and not(contains(concat(' ', @class, ' '), ' old ')) 97 | and not(contains(concat(' ', @class, ' '), ' new ')) 98 | and normalize-space(text())='#{value}'] 99 | XPATH 100 | 101 | days.find :xpath, day_xpath 102 | end 103 | 104 | private 105 | 106 | # Get the datepicker 107 | # @return the DOM element of the datepicker 108 | def find_picker 109 | Capybara.find(:xpath, '//body').find('.datepicker') 110 | end 111 | 112 | # Get the datepicker panel 113 | # @param period [:years, :months, :days] the panel's period 114 | # @return the DOM element of the panel 115 | def find_period(period) 116 | @element.find(".datepicker-#{period}", visible: false) 117 | end 118 | 119 | # Get the up level panel 120 | # @param (see #find_period) 121 | # @return the DOM element of the switch panel button 122 | def find_switch(period) 123 | send(period).find('th.datepicker-switch', visible: false) 124 | end 125 | 126 | # Get the years panel 127 | # @param (see #find_period) 128 | # @return (see #find_period) 129 | def years 130 | find_period :years 131 | end 132 | 133 | # Get the months panel 134 | # @param (see #find_period) 135 | # @return (see #find_period) 136 | def months 137 | find_period :months 138 | end 139 | 140 | # Get the days panel 141 | # @param (see #find_period) 142 | # @return (see #find_period) 143 | def days 144 | find_period :days 145 | end 146 | 147 | # Get the current decade panel 148 | # @param (see #find_switch) 149 | # @return (see #find_switch) 150 | def current_decade 151 | find_switch :years 152 | end 153 | 154 | # Get the current year panel 155 | # @param (see #find_switch) 156 | # @return (see #find_switch) 157 | def current_year 158 | find_switch :months 159 | end 160 | 161 | # Get the current month panel 162 | # @param (see #find_switch) 163 | # @return (see #find_switch) 164 | def current_month 165 | find_switch :days 166 | end 167 | 168 | # Get the current decade period 169 | # @return [Array] the min and max years of the decade 170 | def current_decade_minmax 171 | current_decade.text.split('-').map(&:to_i) 172 | end 173 | 174 | # Click and display previous decade 175 | def click_prev_decade 176 | years.find('th.prev').click 177 | end 178 | 179 | # Click and display next decade 180 | def click_next_decade 181 | years.find('th.next').click 182 | end 183 | 184 | # Calculates the distance in decades between min and max 185 | # @param min [Fixnum] a 4 digits year 186 | # @param max [Fixnum] a 4 digits year 187 | # @return [Fixnum] the distance in decades between min and max 188 | def gap(min, max) 189 | return 0 if min >= max 190 | 191 | ((max / 10) - (min / 10)) 192 | end 193 | 194 | # Go backward to the wanted decade 195 | # @param value [Fixnum] the year of the desired date 196 | # @param decade_start [Fixnum] the first year of a decade 197 | def goto_prev_decade(value, decade_start) 198 | gap(value, decade_start).times { click_prev_decade } 199 | end 200 | 201 | # Go forward to the wanted decade 202 | # @param decade_end [Fixnum] the last year of a decade 203 | # @param value [Fixnum] the year of the desired date 204 | def goto_next_decade(decade_end, value) 205 | gap(decade_end, value).times { click_next_decade } 206 | end 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /lib/capybara-bootstrap-datepicker/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara-bootstrap-datepicker' 4 | 5 | RSpec.configure do |c| 6 | c.include Capybara::BootstrapDatepicker 7 | end 8 | -------------------------------------------------------------------------------- /lib/capybara-bootstrap-datepicker/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The gem's version 4 | module Capybara 5 | module BootstrapDatepicker 6 | # the current version of the gem 7 | VERSION = '1.0.0' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/capybara/bootstrap-datepicker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require '../capybara-bootstrap-datepicker' 4 | -------------------------------------------------------------------------------- /spec/features/bootstrap-3.4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

Bootstrap 3.4 Datepicker

9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 | 29 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 | 59 | 60 | 61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/features/bootstrap-4.4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

Bootstrap 4.4 Datepicker

9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 | 29 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 | 59 | 60 | 61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/features/bootstrap-5.0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

Bootstrap 5.0 Datepicker

9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 | 29 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 | 59 | 60 | 61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/features/bootstrap-5.3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

Bootstrap 5.3 Datepicker

9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 | 29 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 | 59 | 60 | 61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/features/bootstrap_datepicker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'timecop' 4 | 5 | RSpec.shared_examples 'a datepicker' do 6 | before do 7 | Timecop.travel(Time.local(2023, 3, 31)) 8 | end 9 | 10 | context 'with default date' do 11 | subject { Date.parse(find_field(label).value) } 12 | 13 | let(:label) { 'Label of my date input' } 14 | 15 | it 'fills in an input without using the datepicker', :js do 16 | select_date Date.today, from: label 17 | expect(subject).to eq Date.today 18 | end 19 | 20 | it 'fills in a standard datepicker', :js do 21 | select_date Date.today, from: label, datepicker: :simple 22 | expect(subject).to eq Date.today 23 | end 24 | 25 | it 'fills in a datepicker based on Bootstrap', :js do 26 | select_date Date.today, from: label, datepicker: :bootstrap 27 | expect(subject).to eq Date.today 28 | end 29 | 30 | it 'fills in an input with DateTime object', :js do 31 | select_date DateTime.now, from: label 32 | expect(subject).to eq Date.today 33 | end 34 | end 35 | 36 | context 'when decade discovery' do 37 | subject { Date.parse(find_field(label).value) } 38 | 39 | let(:label) { 'Label of my date input' } 40 | 41 | it 'fills in date in previous decade', :js do 42 | date = Date.new(2018) 43 | 44 | select_date date, from: label 45 | expect(subject).to eq date 46 | end 47 | 48 | it 'fills in date in current decade', :js do 49 | date = Date.new(2021) 50 | 51 | select_date date, from: label 52 | expect(subject).to eq date 53 | end 54 | 55 | it 'fills in date in next decade', :js do 56 | date = Date.new(2032) 57 | 58 | select_date date, from: label 59 | expect(subject).to eq date 60 | end 61 | 62 | it 'fills in date 3 decades in the past', :js do 63 | date = Date.new(1998) 64 | 65 | select_date date, from: label 66 | expect(subject).to eq date 67 | end 68 | 69 | it 'fills in date 3 decades in the future', :js do 70 | date = Date.new(2052) 71 | 72 | select_date date, from: label 73 | expect(subject).to eq date 74 | end 75 | end 76 | 77 | context 'with dialog callback' do 78 | subject { Date.parse(find_field(label).value) } 79 | 80 | let(:label) { 'with dialog callback' } 81 | 82 | it 'fills in a datepicker while passing alert dialog', :js do 83 | accept_alert 'Date has changed' do 84 | select_date Date.today, from: label, datepicker: :simple 85 | end 86 | 87 | expect(subject).to eq Date.today 88 | end 89 | 90 | it 'fills in a datepicker while passing alert dialog on Bootstrap', :js do 91 | accept_alert 'Date has changed' do 92 | select_date Date.today, from: label, datepicker: :bootstrap 93 | end 94 | 95 | expect(subject).to eq Date.today 96 | end 97 | end 98 | 99 | context 'with locale date' do 100 | subject { Date.parse(find_field('localized date input').value) } 101 | 102 | let(:label) { 'localized date input' } 103 | 104 | it 'fills in a localized datepicker based on Bootstrap', :js do 105 | select_date Date.today, from: label, datepicker: :bootstrap 106 | expect(subject).to eq Date.today 107 | end 108 | end 109 | 110 | context 'with required date' do 111 | subject { Date.parse(find_field(label).value) } 112 | 113 | let(:label) { 'Start date' } 114 | 115 | it 'fills in a required datepicker based on Bootstrap', :js do 116 | select_date Date.today, from: label, datepicker: :bootstrap 117 | expect(subject).to eq Date.today 118 | end 119 | end 120 | 121 | context 'with yyyy-mm format' do 122 | subject { Date.strptime(find_field(label).value, '%Y-%m') } 123 | 124 | let(:label) { 'with YYYY-MM format' } 125 | 126 | it 'fills in a date without day', :js do 127 | select_date Date.today, from: label, datepicker: :simple, format: '%Y-%m' 128 | expect(subject).to eq Date.new(2023, 3) 129 | end 130 | 131 | it 'fills in a date without day on Bootstrap', :js do 132 | select_date Date.today, from: label, datepicker: :bootstrap, format: '%Y-%m' 133 | expect(subject).to eq Date.new(2023, 3) 134 | end 135 | end 136 | end 137 | 138 | RSpec.describe 'Bootstrap Datepicker', type: :feature do 139 | browsers = %i[firefox chrome] 140 | bootstrap_versions = ['3.4', '4.4', '5.0', '5.3'] 141 | 142 | browsers.each do |browser| 143 | describe "Driven by #{browser}" do 144 | before do 145 | Capybara.current_driver = browser 146 | Capybara.javascript_driver = browser 147 | end 148 | 149 | bootstrap_versions.each do |version| 150 | describe "Boostrap #{version}" do 151 | before do 152 | Capybara.current_session.driver.visit "#{Capybara.app_host}/bootstrap-#{version}.html" 153 | end 154 | 155 | it 'loads the page correctly', :js do 156 | expect(page).to have_content "Bootstrap #{version} Datepicker" 157 | end 158 | 159 | it_behaves_like 'a datepicker' 160 | end 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 4 | require 'bundler/setup' 5 | Bundler.setup 6 | 7 | require 'capybara' 8 | require 'capybara/rspec' 9 | require 'capybara-bootstrap-datepicker/rspec' 10 | require 'capybara-screenshot/rspec' 11 | require 'selenium-webdriver' 12 | 13 | Capybara.configure do |config| 14 | config.app_host = "file://#{File.expand_path('features', __dir__)}" 15 | end 16 | 17 | Capybara.register_driver :chrome do |app| 18 | options = Selenium::WebDriver::Chrome::Options.new 19 | options.add_argument('headless') 20 | options.add_argument('disable-gpu') 21 | options.add_argument('--disable-translate') 22 | options.add_argument('--window-size=1200,2000') 23 | Capybara::Selenium::Driver.new(app, browser: :chrome, options:) 24 | end 25 | 26 | Capybara.register_driver :firefox do |app| 27 | options = Selenium::WebDriver::Firefox::Options.new 28 | options.add_argument('--headless') 29 | options.browser_version = 'esr' 30 | 31 | Capybara::Selenium::Driver.new(app, browser: :firefox, options:) 32 | end 33 | 34 | RSpec.configure do |config| 35 | config.expect_with :rspec do |expectations| 36 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 37 | end 38 | 39 | config.mock_with :rspec do |mocks| 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # These two settings work together to allow you to limit a spec run 44 | # to individual examples or groups you care about by tagging them with 45 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 46 | # get run. 47 | config.filter_run :focus 48 | config.run_all_when_everything_filtered = true 49 | 50 | # Limits the available syntax to the non-monkey patched syntax that is 51 | # recommended. For more details, see: 52 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 53 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 54 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 55 | config.disable_monkey_patching! 56 | 57 | # This setting enables warnings. It's recommended, but in some cases may 58 | # be too noisy due to issues in dependencies. 59 | # config.warnings = true 60 | 61 | # Many RSpec users commonly either run the entire suite or an individual 62 | # file, and it's useful to allow more verbose output when running an 63 | # individual spec file. 64 | if config.files_to_run.one? 65 | # Use the documentation formatter for detailed output, 66 | # unless a formatter has already been configured 67 | # (e.g. via a command-line flag). 68 | config.default_formatter = 'doc' 69 | end 70 | 71 | # Print the 10 slowest examples and example groups at the 72 | # end of the spec run, to help surface which specs are running 73 | # particularly slow. 74 | config.profile_examples = 10 75 | 76 | # Run specs in random order to surface order dependencies. If you find an 77 | # order dependency and want to debug it, you can fix the order by providing 78 | # the seed, which is printed after each run. 79 | # --seed 1234 80 | config.order = :random 81 | 82 | # Seed global randomization in this process using the `--seed` CLI option. 83 | # Setting this allows you to use `--seed` to deterministically reproduce 84 | # test failures related to randomization by passing the same `--seed` value 85 | # as the one that triggered the failure. 86 | Kernel.srand config.seed 87 | end 88 | --------------------------------------------------------------------------------