From 971194034ec326e02ee1603b275c88328352ca45 Mon Sep 17 00:00:00 2001 From: kainacrow <33405173+kainacrow@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:21:48 -0400 Subject: [PATCH 1/2] top news feed + favorites list --- Gemfile | 16 ++++- Gemfile.lock | 38 ++++++++++ app/assets/images/star-empty-icon.png | Bin 0 -> 17832 bytes app/assets/images/star-gold-icon.png | Bin 0 -> 2296 bytes app/assets/stylesheets/application.css | 16 +++++ app/controllers/application_controller.rb | 4 ++ app/controllers/flags_controller.rb | 23 ++++++ app/controllers/stories_controller.rb | 14 ++++ app/models/flag.rb | 4 ++ app/models/story.rb | 7 ++ app/models/user.rb | 6 ++ app/services/hacker_news_service.rb | 25 +++++++ app/views/layouts/application.html.erb | 13 ++++ app/views/stories/_story.html.erb | 14 ++++ app/views/stories/flagged_stories.html.erb | 19 +++++ app/views/stories/index.html.erb | 6 ++ config/application.rb | 3 + config/environments/test.rb | 2 +- config/routes.rb | 6 ++ db/migrate/20240704180117_create_stories.rb | 12 ++++ db/migrate/20240704182024_create_flags.rb | 10 +++ db/schema.rb | 21 +++++- spec/controllers/flags_controller_spec.rb | 74 ++++++++++++++++++++ spec/controllers/stories_controller_spec.rb | 36 ++++++++++ spec/factories/flags.rb | 6 ++ spec/factories/stories.rb | 7 ++ spec/factories/users.rb | 7 ++ spec/models/flag_spec.rb | 8 +++ spec/models/story_spec.rb | 16 +++++ spec/rails_helper.rb | 35 ++++++--- spec/services/hacker_news_service_spec.rb | 47 +++++++++++++ spec/spec_helper.rb | 6 +- spec/views/stories/index.html.erb_spec.rb | 5 ++ 33 files changed, 491 insertions(+), 15 deletions(-) create mode 100644 app/assets/images/star-empty-icon.png create mode 100644 app/assets/images/star-gold-icon.png create mode 100644 app/controllers/flags_controller.rb create mode 100644 app/controllers/stories_controller.rb create mode 100644 app/models/flag.rb create mode 100644 app/models/story.rb create mode 100644 app/services/hacker_news_service.rb create mode 100644 app/views/stories/_story.html.erb create mode 100644 app/views/stories/flagged_stories.html.erb create mode 100644 app/views/stories/index.html.erb create mode 100644 db/migrate/20240704180117_create_stories.rb create mode 100644 db/migrate/20240704182024_create_flags.rb create mode 100644 spec/controllers/flags_controller_spec.rb create mode 100644 spec/controllers/stories_controller_spec.rb create mode 100644 spec/factories/flags.rb create mode 100644 spec/factories/stories.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/models/flag_spec.rb create mode 100644 spec/models/story_spec.rb create mode 100644 spec/services/hacker_news_service_spec.rb create mode 100644 spec/views/stories/index.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index 5a8ffc43..bb55027f 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,6 @@ gem 'pg' gem 'pry-rails' gem 'puma' gem 'rails', '~> 7.0.3' -gem 'rspec-rails' gem 'sass-rails' gem 'selenium-webdriver', group: [:development, :test] gem 'spring', group: :development @@ -20,3 +19,18 @@ gem 'turbolinks' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'uglifier' gem 'web-console', group: :development +gem 'httparty' +gem 'pry-byebug' + +group :test do + gem 'rspec-rails' + gem 'factory_bot_rails' + gem 'faker' + gem 'database_cleaner' + gem 'rails-controller-testing' + gem 'shoulda-matchers', '~> 4.0' +end + +group :test, :development do + gem 'database_cleaner-active_record' +end diff --git a/Gemfile.lock b/Gemfile.lock index 14ec6457..ce9f712a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,6 +69,7 @@ GEM addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) bcrypt (3.1.18) + bigdecimal (3.1.8) bindex (0.8.1) builder (3.2.4) byebug (11.1.3) @@ -92,6 +93,13 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.1.10) crass (1.0.6) + csv (3.3.0) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) devise (4.8.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -102,9 +110,20 @@ GEM digest (3.1.0) erubi (1.11.0) execjs (2.8.1) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + faker (3.4.1) + i18n (>= 1.8.11, < 2) ffi (1.15.5) globalid (1.0.0) activesupport (>= 5.0) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) jbuilder (2.11.5) @@ -124,6 +143,8 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.0) minitest (5.16.3) + multi_xml (0.7.1) + bigdecimal (~> 3.1) net-imap (0.2.3) digest net-protocol @@ -147,6 +168,9 @@ GEM pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) + pry-byebug (3.10.1) + byebug (~> 11.0) + pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (5.0.0) @@ -170,6 +194,10 @@ GEM activesupport (= 7.0.4) bundler (>= 1.15.0) railties (= 7.0.4) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -224,6 +252,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + shoulda-matchers (4.5.1) + activesupport (>= 4.2.0) spring (4.1.0) sprockets (4.1.1) concurrent-ruby (~> 1.0) @@ -265,16 +295,24 @@ DEPENDENCIES byebug capybara coffee-rails + database_cleaner + database_cleaner-active_record devise + factory_bot_rails + faker + httparty jbuilder listen pg + pry-byebug pry-rails puma rails (~> 7.0.3) + rails-controller-testing rspec-rails sass-rails selenium-webdriver + shoulda-matchers (~> 4.0) spring turbolinks tzinfo-data diff --git a/app/assets/images/star-empty-icon.png b/app/assets/images/star-empty-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1c7ad3725e1f17a596d1e20d67ced44f7112099f GIT binary patch literal 17832 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 +
+ <% if current_user %> + <%= link_to 'Sign out', destroy_user_session_path, method: :delete %> + <% else %> + <%= link_to 'Sign in', new_user_session_path %> + <% end %> +
+
+ <%= link_to 'Latest News Stories', stories_path %> +
+
+ <%= link_to 'Team Favorites', flagged_stories_path %> +

<%= notice %>

diff --git a/app/views/stories/_story.html.erb b/app/views/stories/_story.html.erb new file mode 100644 index 00000000..3f19858b --- /dev/null +++ b/app/views/stories/_story.html.erb @@ -0,0 +1,14 @@ +
+

+ <% if current_user.flagged_stories.exists?(story['id']) %> + <%= form_with url: story_flag_path(story['id']), method: :delete, local: true, class: 'inline-form' do %> + <%= image_tag 'star-gold-icon.png', alt: 'Unflag', data: { confirm: 'Are you sure you want to unflag this story?' }, class: 'flag-icon', onclick: 'this.closest("form").submit()' %> + <% end %> + <% else %> + <%= form_with url: story_flag_path(story['id']), method: :post, local: true, class: 'inline-form' do %> + <%= image_tag 'star-empty-icon.png', alt: 'Flag', class: 'flag-icon', onclick: 'this.closest("form").submit()' %> + <% end %> + <% end %> + <%= story['title'] %> +

+
diff --git a/app/views/stories/flagged_stories.html.erb b/app/views/stories/flagged_stories.html.erb new file mode 100644 index 00000000..924428f1 --- /dev/null +++ b/app/views/stories/flagged_stories.html.erb @@ -0,0 +1,19 @@ +

Flagged Stories

+
    + <% @flagged_stories.each do |story_id, flags| %> +
    +

    + <% if current_user.flagged_stories.exists?(story_id) %> + <%= form_with url: story_flag_path(Story.find(story_id)), method: :delete, local: true, class: 'inline-form' do |form| %> + <%= image_tag 'star-gold-icon.png', alt: 'Unflag', data: { confirm: 'Are you sure you want to unflag this story?' }, class: 'flag-icon', onclick: 'this.closest("form").submit()' %> + <% end %> + <% else %> + <%= form_with url: story_flag_path(Story.find(story_id)), method: :post, local: true, class: 'inline-form' do %> + <%= image_tag 'star-empty-icon.png', alt: 'Flag', class: 'flag-icon', onclick: 'this.closest("form").submit()' %> + <% end %> + <% end %> + <%= flags.first.story.title %>   - flagged by <%= flags.map(&:user).map(&:name).to_sentence %> +

    +
    + <% end %> +
diff --git a/app/views/stories/index.html.erb b/app/views/stories/index.html.erb new file mode 100644 index 00000000..8905a179 --- /dev/null +++ b/app/views/stories/index.html.erb @@ -0,0 +1,6 @@ +

Top Hacker News Stories

+
    + <% @stories.each do |story| %> + <%= render partial: 'story', locals: { story: story } %> + <% end %> +
diff --git a/config/application.rb b/config/application.rb index dab4cec6..eb28291e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,9 @@ class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.1 + config.active_record.legacy_connection_handling = false + config.action_controller.urlsafe_csrf_tokens = true + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/config/environments/test.rb b/config/environments/test.rb index 8e5cbde5..7b6dc4c0 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -5,7 +5,7 @@ # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + config.cache_classes = false # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that diff --git a/config/routes.rb b/config/routes.rb index c12ef082..929e59d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,10 @@ Rails.application.routes.draw do devise_for :users root to: 'pages#home' + + get 'flagged_stories', to: 'stories#flagged_stories', as: 'flagged_stories' + + resources :stories do + resource :flag, only: [:create, :destroy] + end end diff --git a/db/migrate/20240704180117_create_stories.rb b/db/migrate/20240704180117_create_stories.rb new file mode 100644 index 00000000..725c5b75 --- /dev/null +++ b/db/migrate/20240704180117_create_stories.rb @@ -0,0 +1,12 @@ +class CreateStories < ActiveRecord::Migration[7.0] + def change + create_table :stories do |t| + t.string :title + t.string :url + + t.timestamps + end + + add_index :stories, [:title, :url], unique: true + end +end diff --git a/db/migrate/20240704182024_create_flags.rb b/db/migrate/20240704182024_create_flags.rb new file mode 100644 index 00000000..3e72d59d --- /dev/null +++ b/db/migrate/20240704182024_create_flags.rb @@ -0,0 +1,10 @@ +class CreateFlags < ActiveRecord::Migration[6.1] + def change + create_table :flags do |t| + t.references :user, null: false, foreign_key: true + t.references :story, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index acc34f3b..71ad1a14 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,27 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2018_02_28_212101) do +ActiveRecord::Schema[7.0].define(version: 2024_07_04_182024) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "flags", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "story_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["story_id"], name: "index_flags_on_story_id" + t.index ["user_id"], name: "index_flags_on_user_id" + end + + create_table "stories", force: :cascade do |t| + t.string "title" + t.string "author" + t.string "url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "users", force: :cascade do |t| t.string "first_name" t.string "last_name" @@ -33,4 +50,6 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "flags", "stories" + add_foreign_key "flags", "users" end diff --git a/spec/controllers/flags_controller_spec.rb b/spec/controllers/flags_controller_spec.rb new file mode 100644 index 00000000..6b9a4173 --- /dev/null +++ b/spec/controllers/flags_controller_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +RSpec.describe FlagsController, type: :controller do + let(:user) { create(:user) } + let(:story) { create(:story, title: 'New Story', url: 'www.example.com') } + + before do + sign_in user + end + + describe 'POST #create' do + context 'with a valid story' do + it 'does not create a new story if it already exists' do + expect(story).to be_persisted + expect { + post :create, params: { story_id: story.id } + }.to change(Story, :count).by(0) + end + + it 'creates a new flag record for the story' do + expect { + post :create, params: { story_id: story.id } + }.to change(Flag, :count).by(1) + end + + it 'redirects to the stories index page' do + post :create, params: { story_id: story.id } + expect(response).to redirect_to(stories_path) + end + + it 'sets a flash notice on success' do + post :create, params: { story_id: story.id } + expect(flash[:notice]).to eq('Story flagged successfully.') + end + end + + context 'with an invalid story' do + it 'does not create a new flag record' do + expect { + post :create, params: { story_id: 999000000000000 } + }.to_not change(Flag, :count) + end + + it 'redirects to the stories index page with an alert message' do + post :create, params: { story_id: 999000000000000 } + expect(response).to redirect_to(stories_path) + expect(flash[:alert]).to eq('Story not found.') + end + end + end + + describe 'DELETE #destroy' do + before do + current_user = user + current_user.flagged_stories << story + end + + it 'removes a flag for the story' do + expect { + delete :destroy, params: { story_id: story.id } + }.to change(Flag, :count).by(-1) + end + + it 'redirects to the stories index page' do + delete :destroy, params: { story_id: story.id } + expect(response).to redirect_to(stories_path) + end + + it 'sets a flash notice on success' do + delete :destroy, params: { story_id: story.id } + expect(flash[:notice]).to eq('Story unflagged successfully.') + end + end +end diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb new file mode 100644 index 00000000..7b27671c --- /dev/null +++ b/spec/controllers/stories_controller_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe StoriesController, type: :controller do + let(:user) { create(:user) } + let(:story) { create(:story) } + + before do + sign_in user + allow(HackerNewsService).to receive_message_chain(:new, :fetch_top_stories).and_return([story]) + end + + describe 'before actions' do + it 'calls #load_stories' do + expect(controller).to receive(:load_stories) + get :index + end + + it 'calls #flagged_stories' do + expect(controller).to receive(:flagged_stories) + get :index + end + end + + describe 'private methods' do + it 'correctly sets @stories' do + get :index + expect(assigns(:stories)).to include(story) + end + + it 'correctly sets @flagged_stories' do + Flag.create(user: user, story: story) + get :index + expect(assigns(:flagged_stories)).to eq({ story.id => [Flag.last] }) + end + end +end diff --git a/spec/factories/flags.rb b/spec/factories/flags.rb new file mode 100644 index 00000000..0ddfc123 --- /dev/null +++ b/spec/factories/flags.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :flag do + association :user + association :story + end +end diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb new file mode 100644 index 00000000..55a34d42 --- /dev/null +++ b/spec/factories/stories.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :story do + sequence(:id) { |n| n } + title { 'Example Story' } + url { 'https://example.com' } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 00000000..a38fda71 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :user do + email { 'testuser@example.com' } + password { 'password123' } + password_confirmation { 'password123' } + end +end diff --git a/spec/models/flag_spec.rb b/spec/models/flag_spec.rb new file mode 100644 index 00000000..eb6106a8 --- /dev/null +++ b/spec/models/flag_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +RSpec.describe Flag, type: :model do + describe 'associations' do + it { should belong_to(:user) } + it { should belong_to(:story) } + end +end diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb new file mode 100644 index 00000000..045a6398 --- /dev/null +++ b/spec/models/story_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe Story, type: :model do + subject { build(:story) } + + describe 'associations' do + it { should have_many(:flags).dependent(:destroy) } + it { should have_many(:flagged_by_users).through(:flags).source(:user) } + end + + describe 'validations' do + it { should validate_presence_of(:title) } + it { should validate_uniqueness_of(:title).scoped_to(:url) } + it { should validate_presence_of(:url) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index bbe1ba57..a9c81561 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,10 +1,14 @@ # This file is copied to spec/ when you run 'rails generate rspec:install' require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' -require File.expand_path('../../config/environment', __FILE__) +require_relative '../config/environment' # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' + +require 'rails-controller-testing' + +Rails::Controller::Testing.install # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in @@ -20,21 +24,29 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +# Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } # Checks for pending migrations and applies them before tests are run. -# If you are not using ActiveRecord, you can remove this line. -ActiveRecord::Migration.maintain_test_schema! - +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods + config.include Devise::Test::ControllerHelpers, type: :controller # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_path = Rails.root.join('spec/fixtures') # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = true + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and # `post` in specs under `spec/controllers`. @@ -42,12 +54,12 @@ # You can disable this behaviour by removing the line below, and instead # explicitly tag your specs with their type, e.g.: # - # RSpec.describe UsersController, :type => :controller do + # RSpec.describe UsersController, type: :controller do # # ... # end # # The different available types are documented in the features, such as in - # https://relishapp.com/rspec/rspec-rails/docs + # https://rspec.info/features/6-0/rspec-rails config.infer_spec_type_from_file_location! # Filter lines from Rails gems in backtraces. @@ -55,3 +67,10 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/spec/services/hacker_news_service_spec.rb b/spec/services/hacker_news_service_spec.rb new file mode 100644 index 00000000..25b2913f --- /dev/null +++ b/spec/services/hacker_news_service_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +RSpec.describe HackerNewsService, type: :service do + let(:service) { described_class.new } + + before do + allow(service).to receive(:fetch_story_ids).and_return([1, 2, 3]) + allow(service).to receive(:fetch_story).with(1).and_return({ 'id' => 1, 'title' => 'Story 1', 'url' => 'https://example.com/1' }) + allow(service).to receive(:fetch_story).with(2).and_return({ 'id' => 2, 'title' => 'Story 2', 'url' => 'https://example.com/2' }) + allow(service).to receive(:fetch_story).with(3).and_return({ 'id' => 3, 'title' => 'Story 3', 'url' => 'https://example.com/3' }) + end + + describe '#fetch_top_stories' do + it 'fetches the top stories from the API' do + stories = service.fetch_top_stories + + expect(stories.size).to eq(3) + expect(stories.first).to include('id' => 1, 'title' => 'Story 1', 'url' => 'https://example.com/1') + expect(stories.second).to include('id' => 2, 'title' => 'Story 2', 'url' => 'https://example.com/2') + expect(stories.third).to include('id' => 3, 'title' => 'Story 3', 'url' => 'https://example.com/3') + end + + it 'fetches only the number of stories specified by the limit' do + stories = service.fetch_top_stories(2) + + expect(stories.size).to eq(2) + expect(stories.first['id']).to eq(1) + expect(stories.second['id']).to eq(2) + end + end + + describe '#fetch_story_ids' do + it 'fetches the story IDs' do + ids = service.send(:fetch_story_ids) + + expect(ids).to eq([1, 2, 3]) + end + end + + describe '#fetch_story' do + it 'fetches a single story' do + story = service.send(:fetch_story, 1) + + expect(story).to include('id' => 1, 'title' => 'Story 1', 'url' => 'https://example.com/1') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce33d66d..327b58ea 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,7 +12,7 @@ # the additional setup, and require it from the spec files that actually need # it. # -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest @@ -61,9 +61,7 @@ # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ config.disable_monkey_patching! # Many RSpec users commonly either run the entire suite or an individual diff --git a/spec/views/stories/index.html.erb_spec.rb b/spec/views/stories/index.html.erb_spec.rb new file mode 100644 index 00000000..7227ec6d --- /dev/null +++ b/spec/views/stories/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "stories/index.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end From aae6fc6590cd3b6e6ee41206fcf7e02f82308038 Mon Sep 17 00:00:00 2001 From: kainacrow <33405173+kainacrow@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:50:13 -0400 Subject: [PATCH 2/2] clean up --- spec/spec_helper.rb | 6 ++++-- spec/views/stories/index.html.erb_spec.rb | 5 ----- 2 files changed, 4 insertions(+), 7 deletions(-) delete mode 100644 spec/views/stories/index.html.erb_spec.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 327b58ea..ce33d66d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,7 +12,7 @@ # the additional setup, and require it from the spec files that actually need # it. # -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest @@ -61,7 +61,9 @@ # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: - # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode config.disable_monkey_patching! # Many RSpec users commonly either run the entire suite or an individual diff --git a/spec/views/stories/index.html.erb_spec.rb b/spec/views/stories/index.html.erb_spec.rb deleted file mode 100644 index 7227ec6d..00000000 --- a/spec/views/stories/index.html.erb_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe "stories/index.html.erb", type: :view do - pending "add some examples to (or delete) #{__FILE__}" -end