diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ac4824a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: ransack_demo_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + env: + RAILS_ENV: test + DATABASE_URL: postgres://postgres:postgres@localhost:5432/ransack_demo_test + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2.3' + bundler-cache: true + + - name: Setup Database + run: | + bundle exec rails db:create + bundle exec rails db:migrate + bundle exec rails db:seed + + - name: Run Tests + run: | + bundle exec rails test + + - name: Run RuboCop + run: | + bundle exec rubocop --format github + + system-test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: ransack_demo_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + env: + RAILS_ENV: test + DATABASE_URL: postgres://postgres:postgres@localhost:5432/ransack_demo_test + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2.3' + bundler-cache: true + + - name: Setup Database + run: | + bundle exec rails db:create + bundle exec rails db:migrate + bundle exec rails db:seed + + - name: Install Chrome dependencies + run: | + sudo apt-get update + sudo apt-get install -y google-chrome-stable + + - name: Run System Tests + run: | + bundle exec rails test:system \ No newline at end of file diff --git a/.gitignore b/.gitignore index 923b6976..c898e58c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .bundle +vendor/bundle db/*.sqlite3 log/*.log tmp/ diff --git a/.ruby-version b/.ruby-version index f9892605..b347b11e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.4 +3.2.3 diff --git a/Gemfile b/Gemfile index 660da2f7..926f2c02 100644 --- a/Gemfile +++ b/Gemfile @@ -2,20 +2,24 @@ source "https://rubygems.org" ruby file: ".ruby-version" -gem "rails", "~> 7.1.0" +gem "rails", "~> 8.0.0" # Use PostgreSQL as the database for Active Record gem "pg", "~> 1.0" # Use Puma as the app server -gem "puma", "~> 5.6" +gem "puma", "~> 6.0" +# Use Tailwind CSS for styling +gem "tailwindcss-rails", "~> 2.0" +# Use Importmap for managing JavaScript dependencies +gem "importmap-rails", "~> 2.0" +# Turbo provides partial page replacement and forms without full page reloads +gem "turbo-rails", "~> 2.0" +# Stimulus provides reactive behavior for JavaScript +gem "stimulus-rails", "~> 1.0" # Use SCSS for stylesheets -gem "sass-rails", "~> 5.0" -# Use Uglifier as compressor for JavaScript assets -gem "uglifier", ">= 1.3.0" +gem "sass-rails", "~> 6.0" # Use jquery as the JavaScript library gem "jquery-rails" -# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks -# gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder # gem 'jbuilder', '~> 2.5' # Use ActiveModel has_secure_password @@ -27,6 +31,9 @@ gem "faker" group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem "byebug", platform: :mri + # System testing gems + gem "selenium-webdriver" + gem "webdrivers" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 9a20bd44..b84e563f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,83 +1,77 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.1.5.1) - actionpack (= 7.1.5.1) - activesupport (= 7.1.5.1) + actioncable (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.5.1) - actionpack (= 7.1.5.1) - activejob (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.5.1) - actionpack (= 7.1.5.1) - actionview (= 7.1.5.1) - activejob (= 7.1.5.1) - activesupport (= 7.1.5.1) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) + mail (>= 2.8.0) + actionmailer (8.0.3) + actionpack (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activesupport (= 8.0.3) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.5.1) - actionview (= 7.1.5.1) - activesupport (= 7.1.5.1) + actionpack (8.0.3) + actionview (= 8.0.3) + activesupport (= 8.0.3) nokogiri (>= 1.8.5) - racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.5.1) - actionpack (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + useragent (~> 0.16) + actiontext (8.0.3) + actionpack (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.5.1) - activesupport (= 7.1.5.1) + actionview (8.0.3) + activesupport (= 8.0.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.5.1) - activesupport (= 7.1.5.1) + activejob (8.0.3) + activesupport (= 8.0.3) globalid (>= 0.3.6) - activemodel (7.1.5.1) - activesupport (= 7.1.5.1) - activerecord (7.1.5.1) - activemodel (= 7.1.5.1) - activesupport (= 7.1.5.1) + activemodel (8.0.3) + activesupport (= 8.0.3) + activerecord (8.0.3) + activemodel (= 8.0.3) + activesupport (= 8.0.3) timeout (>= 0.4.0) - activestorage (7.1.5.1) - actionpack (= 7.1.5.1) - activejob (= 7.1.5.1) - activerecord (= 7.1.5.1) - activesupport (= 7.1.5.1) + activestorage (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activesupport (= 8.0.3) marcel (~> 1.0) - activesupport (7.1.5.1) + activesupport (8.0.3) base64 benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) logger (>= 1.4.2) minitest (>= 5.1) - mutex_m securerandom (>= 0.3) - tzinfo (~> 2.0) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) ast (2.4.3) base64 (0.2.0) benchmark (0.4.0) @@ -92,7 +86,6 @@ GEM drb (2.2.1) erb (5.0.1) erubi (1.12.0) - execjs (2.8.1) factory_bot (6.5.1) activesupport (>= 6.1.0) faker (2.20.0) @@ -102,6 +95,10 @@ GEM activesupport (>= 5.0) i18n (1.14.7) concurrent-ruby (~> 1.0) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) io-console (0.8.0) irb (1.15.2) pp (>= 0.6.0) @@ -130,7 +127,6 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.9) minitest (5.25.5) - mutex_m (0.3.0) net-imap (0.3.9) date net-protocol @@ -156,7 +152,7 @@ GEM psych (5.2.6) date stringio - puma (5.6.9) + puma (6.6.1) nio4r (~> 2.0) racc (1.8.1) rack (2.2.14) @@ -167,20 +163,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (7.1.5.1) - actioncable (= 7.1.5.1) - actionmailbox (= 7.1.5.1) - actionmailer (= 7.1.5.1) - actionpack (= 7.1.5.1) - actiontext (= 7.1.5.1) - actionview (= 7.1.5.1) - activejob (= 7.1.5.1) - activemodel (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + rails (8.0.3) + actioncable (= 8.0.3) + actionmailbox (= 8.0.3) + actionmailer (= 8.0.3) + actionpack (= 8.0.3) + actiontext (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activemodel (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) bundler (>= 1.15.0) - railties (= 7.1.5.1) + railties (= 8.0.3) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -188,13 +184,14 @@ GEM rails-html-sanitizer (1.6.1) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.1.5.1) - actionpack (= 7.1.5.1) - activesupport (= 7.1.5.1) - irb + railties (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) @@ -226,17 +223,16 @@ GEM parser (>= 3.3.7.2) prism (~> 1.4) ruby-progressbar (1.13.0) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt securerandom (0.4.1) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -245,17 +241,27 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) stringio (3.1.7) + tailwindcss-rails (2.7.9) + railties (>= 7.0.0) + tailwindcss-rails (2.7.9-x86_64-linux) + railties (>= 7.0.0) thor (1.3.0) tilt (2.0.10) timeout (0.4.3) + tsort (0.2.0) + turbo-rails (2.0.16) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + uri (1.0.3) + useragent (0.16.11) web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -269,24 +275,28 @@ GEM PLATFORMS ruby + x86_64-linux-gnu DEPENDENCIES byebug factory_bot faker + importmap-rails (~> 2.0) jquery-rails listen (~> 3.0.5) pg (~> 1.0) - puma (~> 5.6) - rails (~> 7.1.0) + puma (~> 6.0) + rails (~> 8.0.0) ransack rubocop (~> 1.29) - sass-rails (~> 5.0) - uglifier (>= 1.3.0) + sass-rails (~> 6.0) + stimulus-rails (~> 1.0) + tailwindcss-rails (~> 2.0) + turbo-rails (~> 2.0) web-console RUBY VERSION - ruby 3.4.4p34 + ruby 3.2.3p157 BUNDLED WITH 2.6.9 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4f880601..704476bb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -23,7 +23,8 @@ def setup_search_form(builder) end def button_to_remove_fields - tag.button "Remove", class: "remove_fields btn" + tag.button "Remove", + class: "remove_fields inline-flex items-center px-3 py-1 border border-red-300 shadow-sm text-xs font-medium rounded text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" end def button_to_add_fields(f, type) @@ -32,12 +33,16 @@ def button_to_add_fields(f, type) render(name, f: builder) end - tag.button button_label[type], class: "add_fields btn", 'data-field-type': type, + tag.button button_label[type], + class: "add_fields inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + 'data-field-type': type, 'data-content': "#{fields}" end def button_to_nest_fields(type) - tag.button button_label[type], class: "nest_fields btn", 'data-field-type': type + tag.button button_label[type], + class: "nest_fields inline-flex items-center px-3 py-1 border border-purple-300 shadow-sm text-xs font-medium rounded text-purple-700 bg-purple-50 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500", + 'data-field-type': type end def button_label diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 3b42a17a..4ca632a5 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -10,9 +10,11 @@ def action def link_to_toggle_search_modes if action_name == "advanced_search" - link_to("Go to Simple mode", users_path) + link_to("← Simple Search", users_path, + class: "inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500") else - link_to("Go to Advanced mode", advanced_search_users_path) + link_to("Advanced Search →", advanced_search_users_path, + class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500") end end @@ -51,9 +53,11 @@ def value_fields end def display_distinct_label_and_check_box - tag.section do - check_box_tag(:distinct, "1", user_wants_distinct_results?, class: :cbx) + - label_tag(:distinct, "Return distinct records") + tag.div(class: "flex items-center space-x-2") do + check_box_tag(:distinct, "1", user_wants_distinct_results?, + class: "h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded") + + label_tag(:distinct, "Return distinct records", + class: "text-sm font-medium text-gray-700") end end @@ -75,22 +79,26 @@ def display_results_header(count) def display_sort_column_headers(search) user_column_headers.reduce(String.new) do |string, field| - string << (tag.th sort_link(search, field, method: action)) + string << (tag.th sort_link(search, field, method: action), + class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100") end + post_title_header_labels.reduce(String.new) do |str, i| - str << (tag.th "Post #{i} title") + str << (tag.th "Post #{i} title", + class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider") end end def display_search_results(objects) objects.limit(results_limit).reduce(String.new) do |string, object| - string << (tag.tr display_search_results_row(object)) + string << (tag.tr display_search_results_row(object), + class: "hover:bg-gray-50") end end def display_search_results_row(object) user_column_fields.reduce(String.new) do |string, field| - string << (tag.td object.send(field)) + string << (tag.td object.send(field), + class: "px-6 py-4 whitespace-nowrap text-sm text-gray-900") end .html_safe + display_user_posts(object.posts) @@ -98,7 +106,8 @@ def display_search_results_row(object) def display_user_posts(posts) posts.reduce(String.new) do |string, post| - string << (tag.td truncate(post.title, length: post_title_length)) + string << (tag.td truncate(post.title, length: post_title_length), + class: "px-6 py-4 whitespace-nowrap text-sm text-gray-500") end .html_safe end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 00000000..4974c13d --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,10 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" +import "jquery" + +// Make jQuery available globally +window.$ = window.jQuery = jQuery + +// Import search functionality +import "./search" \ No newline at end of file diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 00000000..cad9feb6 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } \ No newline at end of file diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 00000000..2a17d193 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,7 @@ +// Import and register all your controllers from the importmap under controllers/* + +import { application } from "controllers/application" + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) \ No newline at end of file diff --git a/app/javascript/search.js b/app/javascript/search.js new file mode 100644 index 00000000..44540d66 --- /dev/null +++ b/app/javascript/search.js @@ -0,0 +1,31 @@ +class Search { + constructor(templates = {}) { + this.templates = templates; + } + + remove_fields(button) { + return $(button).closest('.fields').remove(); + } + + add_fields(button, type, content) { + const new_id = new Date().getTime(); + const regexp = new RegExp('new_' + type, 'g'); + return $(button).before(content.replace(regexp, new_id)); + } + + nest_fields(button, type) { + const new_id = new Date().getTime(); + const id_regexp = new RegExp('new_' + type, 'g'); + const template = this.templates[type]; + const object_name = $(button).closest('.fields').attr('data-object-name'); + const sanitized_object_name = object_name.replace(/\]\[|[^-a-zA-Z0-9:.]/g, '_').replace(/_$/, ''); + let updated_template = template.replace(/new_object_name\[/g, object_name + "["); + updated_template = updated_template.replace(/new_object_name_/, sanitized_object_name + '_'); + return $(button).before(updated_template.replace(id_regexp, new_id)); + } +} + +// Make Search available globally +window.Search = Search; + +export default Search; \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 78fefb6d..282f4da2 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,21 +1,48 @@ -<%= tag.html do %> - <%= tag.head do %> - <%= tag.title 'RansackDemo' %> - <%= stylesheet_link_tag 'application' %> - <%= javascript_include_tag 'application' %> + + + Ransack Demo + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + - <% end %> + - <%= tag.body do %> + + +
+
+
+
+

Ransack Demo

+
+
+
+
- <%= yield %> + +
+ <%= yield %> +
- <%= tag.footer app_info %> - <% end %> -<% end %> + + + + diff --git a/app/views/users/_results.erb b/app/views/users/_results.erb index 81e75699..3495ee0e 100644 --- a/app/views/users/_results.erb +++ b/app/views/users/_results.erb @@ -1,6 +1,47 @@ -<%= display_query_sql(@users) %> -<%= tag.h2 display_results_header(@users.size) %> -<%= tag.table do %> - <%= tag.thead display_sort_column_headers(@search), escape: false %> - <%= tag.tbody display_search_results(@users), escape: false %> +<% if @users.present? %> + +
+
+

Generated SQL Query

+ +
+ +
+ + +
+
+

<%= display_results_header(@users.size) %>

+
+ + +
+
+ + + <%= display_sort_column_headers(@search), escape: false %> + + + <%= display_search_results(@users), escape: false %> + +
+
+
+
+<% else %> +
+
+ + + +

No users found

+

Try adjusting your search criteria.

+
+
<% end %> diff --git a/app/views/users/advanced_search.erb b/app/views/users/advanced_search.erb index 1913d994..946c7919 100644 --- a/app/views/users/advanced_search.erb +++ b/app/views/users/advanced_search.erb @@ -1,32 +1,75 @@ -<%= tag.h1 'Advanced User Search' %> -<%= tag.header link_to_toggle_search_modes %> +
+ +
+
+

Advanced User Search

+

Build complex search queries with custom conditions and sorting

+
+
+ <%= link_to_toggle_search_modes %> +
+
-<%= search_form_for( - @search, - url: advanced_search_users_path, - html: { method: :post } - ) do |f| %> + +
+
+ <%= search_form_for( + @search, + url: advanced_search_users_path, + html: { method: :post, class: "space-y-8" } + ) do |f| %> + + <% setup_search_form(f) %> - <% setup_search_form(f) %> + +
+
+

Sorting Options

+ <%= button_to_add_fields(f, :sort) %> +
+ +
+ <%= f.sort_fields do |s| %> +
+ <%= render 'sort_fields', f: s %> +
+ <% end %> +
+
- <%= tag.fieldset do %> - <%= tag.legend 'Sorting' %> - <%= f.sort_fields do |s| %> - <%= render 'sort_fields', f: s %> - <% end %> - <%= button_to_add_fields(f, :sort) %> - <% end %> + +
+
+

Search Conditions

+ <%= button_to_add_fields(f, :grouping) %> +
+ +
+ <%= f.grouping_fields do |g| %> +
+ <%= render 'grouping_fields', f: g %> +
+ <% end %> +
+
- <%= tag.fieldset do %> - <%= tag.legend 'Condition Groups' %> - <%= f.grouping_fields do |g| %> - <%= render 'grouping_fields', f: g %> - <% end %> - <%= button_to_add_fields(f, :grouping) %> - <% end %> + +
+
+ <%= display_distinct_label_and_check_box %> +
+
+ + <%= f.submit "Execute Search", + class: "inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> +
+
+ <% end %> +
+
- <%= display_distinct_label_and_check_box %> - <%= f.submit class: 'btn btn--invert' %> -<% end %> - -<%= render 'results' %> + + <%= render 'results' %> +
diff --git a/app/views/users/index.erb b/app/views/users/index.erb index 9b9d6bb8..875d8f8c 100644 --- a/app/views/users/index.erb +++ b/app/views/users/index.erb @@ -1,39 +1,75 @@ -<%= tag.h1 'Search Users' %> -<%= tag.header link_to_toggle_search_modes %> +
+ +
+
+

Search Users

+

Find users by name, email, or their posts

+
+
+ <%= link_to_toggle_search_modes %> +
+
-<%= search_form_for @search do |f| %> + +
+
+ <%= search_form_for @search, local: true, class: "space-y-6" do |f| %> + + +
+

User Information

+
+
+ <%= f.label :first_name_or_last_name_cont, "Name", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= f.text_field :first_name_or_last_name_cont, + class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", + placeholder: "Search by first or last name" %> +
+
+ <%= f.label :email_cont, "Email", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= f.text_field :email_cont, + class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", + placeholder: "Search by email address" %> +
+
+
- <%= tag.fieldset do %> - <%= tag.legend 'User' %> - <%= tag.ul do %> - <%= tag.li do %> - <%= f.label :first_name_or_last_name_cont %> - <%= f.text_field :first_name_or_last_name_cont %> - <% end %> - <%= tag.li do %> - <%= f.label :email_cont %> - <%= f.text_field :email_cont %> - <% end %> - <% end %> - <% end %> + +
+

User's Posts

+
+
+ <%= f.label :posts_title_cont, "Post Title", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= f.text_field :posts_title_cont, + class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", + placeholder: "Search by post title" %> +
+
+ <%= f.label :other_posts_title_cont, "Other Posts Title", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= f.text_field :other_posts_title_cont, + class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", + placeholder: "Search by other post titles" %> +
+
+
- <%= tag.fieldset do %> - <%= tag.legend "User's Posts" %> - <%= tag.ul do %> - <%= tag.li do %> - <%= f.label :posts_title_cont %> - <%= f.text_field :posts_title_cont %> + +
+
+ <%= display_distinct_label_and_check_box %> +
+
+ + <%= f.submit "Search Users", + class: "inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> +
+
<% end %> +
+
- <%= tag.li do %> - <%= f.label :other_posts_title_cont %> - <%= f.text_field :other_posts_title_cont %> - <% end %> - <% end %> - <% end %> - - <%= display_distinct_label_and_check_box %> - <%= f.submit class: 'btn btn--invert' %> -<% end %> - -<%= render 'results' %> + + <%= render 'results' %> +
diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 00000000..9f14511f --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,8 @@ +# Pin npm packages by running ./bin/importmap + +pin "application", preload: true +pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.7.1/dist/jquery.js" +pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:@hotwired/stimulus@3.2.2/dist/stimulus.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true +pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 00000000..9c50e1c8 --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] +end \ No newline at end of file diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml index 78577cd2..f4a3beac 100644 --- a/test/fixtures/posts.yml +++ b/test/fixtures/posts.yml @@ -1,11 +1,21 @@ # Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html -one: - user: - title: MyString - body: MyText - -two: - user: - title: MyString - body: MyText +ruby_basics: + user: john + title: Ruby Basics + body: An introduction to Ruby programming language + +rails_intro: + user: jane + title: Rails Introduction + body: Getting started with Ruby on Rails framework + +advanced_tips: + user: john + title: Advanced Tips + body: Advanced techniques for Ruby and Rails development + +design_patterns: + user: bob + title: Design Patterns + body: Common design patterns in Ruby applications diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index f3af21e5..1247e936 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,13 +1,19 @@ # Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html -one: - first_name: MyString - last_name: MyString - email: MyString +john: + first_name: John + last_name: Doe + email: john.doe@example.com password_digest: MyString -two: - first_name: MyString - last_name: MyString - email: MyString +jane: + first_name: Jane + last_name: Smith + email: jane.smith@example.com + password_digest: MyString + +bob: + first_name: Bob + last_name: Johnson + email: bob.johnson@example.com password_digest: MyString diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index eee25744..a367205d 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -4,10 +4,75 @@ class UsersControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success + assert_not_nil assigns(:search) + assert_not_nil assigns(:users) end test "should get advanced search" do get :advanced_search assert_response :success + assert_not_nil assigns(:search) + assert_not_nil assigns(:users) + assert assigns(:search).groupings.any?, "Should have at least one grouping" + end + + test "should handle search with name parameter" do + get :index, params: { q: { first_name_or_last_name_cont: "John" } } + assert_response :success + assert_not_nil assigns(:users) + # The search should work and return results + assert assigns(:users).count >= 0 + end + + test "should handle search with email parameter" do + get :index, params: { q: { email_cont: "example.com" } } + assert_response :success + assert_not_nil assigns(:users) + end + + test "should handle search with posts title parameter" do + get :index, params: { q: { posts_title_cont: "Ruby" } } + assert_response :success + assert_not_nil assigns(:users) + end + + test "should handle distinct parameter" do + get :index, params: { distinct: "1", q: { first_name_cont: "John" } } + assert_response :success + assert_not_nil assigns(:users) + end + + test "should handle advanced search with complex parameters" do + post :advanced_search, params: { + q: { + groupings: { + "0" => { + conditions: { + "0" => { + a: { "0" => { name: "first_name" } }, + p: "cont", + v: { "0" => { value: "John" } } + } + } + } + } + } + } + assert_response :success + assert_not_nil assigns(:search) + assert_not_nil assigns(:users) + end + + test "should handle empty search parameters gracefully" do + get :index, params: { q: {} } + assert_response :success + assert_not_nil assigns(:users) + end + + test "should handle POST to advanced search" do + post :advanced_search + assert_response :success + assert_not_nil assigns(:search) + assert_not_nil assigns(:users) end end diff --git a/test/integration/users_integration_test.rb b/test/integration/users_integration_test.rb new file mode 100644 index 00000000..7cdc21d8 --- /dev/null +++ b/test/integration/users_integration_test.rb @@ -0,0 +1,97 @@ +require "test_helper" + +class UsersIntegrationTest < ActionDispatch::IntegrationTest + test "can access index page" do + get users_path + assert_response :success + assert_select "h1", "Search Users" + end + + test "can access advanced search page" do + get advanced_search_users_path + assert_response :success + assert_select "h1", "Advanced User Search" + end + + test "can post to advanced search" do + post advanced_search_users_path + assert_response :success + assert_select "h1", "Advanced User Search" + end + + test "search with parameters works" do + get users_path, params: { q: { first_name_cont: "John" } } + assert_response :success + assert_select "table" # Should show results table + end + + test "search with distinct parameter works" do + get users_path, params: { + q: { first_name_cont: "John" }, + distinct: "1" + } + assert_response :success + assert_select "table" + end + + test "advanced search with complex parameters works" do + post advanced_search_users_path, params: { + q: { + groupings: { + "0" => { + conditions: { + "0" => { + a: { "0" => { name: "first_name" } }, + p: "cont", + v: { "0" => { value: "John" } } + } + } + } + } + } + } + assert_response :success + assert_select "table" + end + + test "root path redirects to users index" do + get root_path + assert_response :success + assert_select "h1", "Search Users" + end + + test "page contains required elements" do + get users_path + assert_response :success + + # Check for form elements + assert_select "form" + assert_select "input[type=text]", minimum: 2 + assert_select "input[type=checkbox]" + assert_select "input[type=submit]" + + # Check for navigation + assert_select "a", text: /Advanced Search/ + + # Check for footer + assert_select "footer" + assert_select "a[href*='github.com']" + end + + test "advanced search page contains required elements" do + get advanced_search_users_path + assert_response :success + + # Check for form elements + assert_select "form" + assert_select "input[type=submit]" + assert_select "input[type=checkbox]" + + # Check for navigation + assert_select "a", text: /Simple Search/ + + # Check for dynamic sections + assert_select "button", text: /Add Sort/ + assert_select "button", text: /Add Condition Group/ + end +end \ No newline at end of file diff --git a/test/system/users_test.rb b/test/system/users_test.rb new file mode 100644 index 00000000..432779a7 --- /dev/null +++ b/test/system/users_test.rb @@ -0,0 +1,93 @@ +require "application_system_test_case" + +class UsersTest < ApplicationSystemTestCase + test "visiting the index" do + visit users_url + + assert_selector "h1", text: "Search Users" + assert_selector "form" + assert_selector "input[placeholder*='Search by first or last name']" + assert_selector "input[placeholder*='Search by email address']" + end + + test "performing a simple search" do + visit users_url + + # Search by name + fill_in "Search by first or last name", with: "John" + click_on "Search Users" + + # Should stay on the same page and show results + assert_current_path users_path + assert_selector "table" # Results table should be present + end + + test "navigating to advanced search" do + visit users_url + + click_on "Advanced Search" + + assert_current_path advanced_search_users_path + assert_selector "h1", text: "Advanced User Search" + assert_selector ".bg-green-50", text: "Sorting Options" + assert_selector ".bg-purple-50", text: "Search Conditions" + end + + test "performing an advanced search" do + visit advanced_search_users_path + + # Should have default condition groups + assert_selector ".bg-white.rounded-md", minimum: 1 + + # Execute search + click_on "Execute Search" + + # Should show results + assert_selector "table" # Results table should be present + end + + test "toggling between search modes" do + # Start at simple search + visit users_url + assert_selector "h1", text: "Search Users" + + # Go to advanced search + click_on "Advanced Search" + assert_selector "h1", text: "Advanced User Search" + + # Go back to simple search + click_on "Simple Search" + assert_selector "h1", text: "Search Users" + end + + test "distinct checkbox functionality" do + visit users_url + + # Checkbox should be present + assert_selector "input[type=checkbox]" + assert_text "Return distinct records" + + # Check the box and submit + check "Return distinct records" + click_on "Search Users" + + # Should stay on the same page + assert_current_path users_path + end + + test "page layout and styling" do + visit users_url + + # Check for modern layout elements + assert_selector "header.bg-white.shadow" + assert_selector "main.max-w-7xl" + assert_selector "footer.bg-white.border-t" + + # Check for TailwindCSS classes in form + assert_selector ".bg-gray-50.rounded-lg" # User Information section + assert_selector ".bg-blue-50.rounded-lg" # User's Posts section + + # Check for modern button styling + assert_selector ".bg-indigo-600", text: "Advanced Search" + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 0ef9c58b..e5bd2435 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -3,6 +3,9 @@ require "rails/test_help" class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests @@ -10,4 +13,21 @@ class ActiveSupport::TestCase fixtures :all # Add more helper methods to be used by all tests here... + + private + + # Helper method to simulate controller action name for helper tests + def stub_action_name(name) + controller = UsersController.new + controller.action_name = name + @controller = controller + end +end + +class ActionController::TestCase + include Devise::Test::ControllerHelpers if defined?(Devise) +end + +class ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers if defined?(Devise) end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb new file mode 100644 index 00000000..40c25c36 --- /dev/null +++ b/test/unit/helpers/application_helper_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +class ApplicationHelperTest < ActionView::TestCase + include ApplicationHelper + + test "button_to_remove_fields generates correct button" do + button = button_to_remove_fields + assert_includes button, "Remove" + assert_includes button, "remove_fields" + assert_includes button, "border-red-300" + assert_includes button, "text-red-700" + end + + test "button_to_nest_fields generates correct button" do + button = button_to_nest_fields(:grouping) + assert_includes button, "Add Condition Group" + assert_includes button, "nest_fields" + assert_includes button, "border-purple-300" + assert_includes button, "text-purple-700" + assert_includes button, 'data-field-type="grouping"' + end + + test "button_label returns correct labels" do + labels = button_label + assert_equal "Add Value", labels[:value] + assert_equal "Add Condition", labels[:condition] + assert_equal "Add Sort", labels[:sort] + assert_equal "Add Condition Group", labels[:grouping] + end + + test "app_info contains correct information" do + info = app_info + assert_includes info, "Ransack demo app" + assert_includes info, "Rails" + assert_includes info, "Ruby" + assert_includes info, "Source code for this demo available on GitHub" + end + + test "source_code_link returns correct link" do + link = source_code_link + assert_includes link, "Source code for this demo available on GitHub" + assert_includes link, "https://github.com/activerecord-hackery/ransack_demo" + end +end \ No newline at end of file diff --git a/test/unit/helpers/users_helper_test.rb b/test/unit/helpers/users_helper_test.rb index 774d33fe..e452dc51 100644 --- a/test/unit/helpers/users_helper_test.rb +++ b/test/unit/helpers/users_helper_test.rb @@ -1,4 +1,78 @@ require "test_helper" class UsersHelperTest < ActionView::TestCase + include UsersHelper + + def setup + @controller = UsersController.new + @controller.action_name = "index" + end + + test "link_to_toggle_search_modes shows advanced link for index" do + @controller.action_name = "index" + link = link_to_toggle_search_modes + assert_includes link, "Advanced Search" + assert_includes link, "bg-indigo-600" + end + + test "link_to_toggle_search_modes shows simple link for advanced_search" do + @controller.action_name = "advanced_search" + link = link_to_toggle_search_modes + assert_includes link, "Simple Search" + assert_includes link, "border-gray-300" + end + + test "action returns correct method" do + @controller.action_name = "index" + assert_equal :get, action + + @controller.action_name = "advanced_search" + assert_equal :post, action + end + + test "display_distinct_label_and_check_box renders correctly" do + result = display_distinct_label_and_check_box + assert_includes result, 'type="checkbox"' + assert_includes result, "Return distinct records" + assert_includes result, "text-indigo-600" + end + + test "user_wants_distinct_results? works with params" do + params[:distinct] = "1" + assert user_wants_distinct_results? + + params[:distinct] = "0" + assert_not user_wants_distinct_results? + + params[:distinct] = nil + assert_not user_wants_distinct_results? + end + + test "display_results_header handles different counts" do + header = display_results_header(5) + assert_equal "Your 5 results", header + + header = display_results_header(1) + assert_equal "Your 1 result", header + + header = display_results_header(15) + assert_equal "Your first 10 results out of 15 total", header + end + + test "display_query_sql formats SQL correctly" do + users = User.limit(5) + result = display_query_sql(users) + assert_includes result, "SQL:" + assert_includes result, users.to_sql + end + + private + + def params + @params ||= {} + end + + def action_name + @controller.action_name + end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 5c07f490..0e6e55ad 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -1,7 +1,74 @@ require "test_helper" class UserTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end + def setup + @user = users(:john) + end + + test "should have valid fixtures" do + assert @user.valid? + assert_equal "John", @user.first_name + assert_equal "Doe", @user.last_name + assert_equal "john.doe@example.com", @user.email + end + + test "should have associations" do + assert_respond_to @user, :posts + assert_respond_to @user, :other_posts + assert_respond_to @user, :comments + assert_respond_to @user, :roles + end + + test "should format datetime correctly" do + assert_match %r{\d{2}/\d{2}/\d{2} \d{2}:\d{2}}, @user.created + assert_match %r{\d{2}/\d{2}/\d{2} \d{2}:\d{2}}, @user.updated + end + + test "should have ransackable attributes" do + ransackable_attrs = User.ransackable_attributes + assert_includes ransackable_attrs, "first_name" + assert_includes ransackable_attrs, "last_name" + assert_includes ransackable_attrs, "email" + assert_includes ransackable_attrs, "full_name" + assert_not_includes ransackable_attrs, "password_digest" + end + + test "should have ransortable attributes" do + ransortable_attrs = User.ransortable_attributes + assert_includes ransortable_attrs, "first_name" + assert_includes ransortable_attrs, "last_name" + assert_includes ransortable_attrs, "email" + assert_not_includes ransortable_attrs, "password_digest" + end + + test "should have ransackable associations" do + ransackable_assocs = User.ransackable_associations + assert_includes ransackable_assocs, "posts" + assert_includes ransackable_assocs, "other_posts" + end + + test "should search by full name using ransacker" do + search = User.ransack(full_name_cont: "John Doe") + results = search.result + assert_includes results, @user + end + + test "should search by first or last name" do + search = User.ransack(first_name_or_last_name_cont: "John") + results = search.result + assert_includes results, @user + end + + test "should search by email" do + search = User.ransack(email_cont: "john.doe") + results = search.result + assert_includes results, @user + end + + test "should get postgres version" do + version = User.postgres_version + assert_not_nil version + assert_kind_of String, version + assert version.length > 0 + end end