diff --git a/spec/models/concerns/taggable_spec.rb b/spec/models/concerns/taggable_spec.rb new file mode 100644 index 00000000..22a5ab32 --- /dev/null +++ b/spec/models/concerns/taggable_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Taggable do + # Use Place as the test model since it includes Taggable + let(:user) { create(:user) } + let(:tag1) { create(:tag, user: user, name: 'Home') } + let(:tag2) { create(:tag, user: user, name: 'Work') } + let(:tag3) { create(:tag, user: user, name: 'Gym') } + + describe 'associations' do + it { expect(Place.new).to have_many(:taggings).dependent(:destroy) } + it { expect(Place.new).to have_many(:tags).through(:taggings) } + end + + describe 'scopes' do + let!(:place1) { create(:place, user: user) } + let!(:place2) { create(:place, user: user) } + let!(:place3) { create(:place, user: user) } + + before do + place1.tags << [tag1, tag2] + place2.tags << tag1 + # place3 has no tags + end + + describe '.with_tags' do + it 'returns places with any of the specified tag IDs' do + results = Place.with_tags([tag1.id]) + expect(results).to contain_exactly(place1, place2) + end + + it 'returns places with multiple tag IDs' do + results = Place.with_tags([tag1.id, tag2.id]) + expect(results).to contain_exactly(place1, place2) + end + + it 'returns distinct results when place has multiple matching tags' do + results = Place.with_tags([tag1.id, tag2.id]) + expect(results.count).to eq(2) + expect(results).to contain_exactly(place1, place2) + end + + it 'returns empty when no places have the specified tags' do + results = Place.with_tags([tag3.id]) + expect(results).to be_empty + end + + it 'accepts a single tag ID' do + results = Place.with_tags(tag1.id) + expect(results).to contain_exactly(place1, place2) + end + end + + describe '.without_tags' do + it 'returns only places without any tags' do + results = Place.without_tags + expect(results).to contain_exactly(place3) + end + + it 'returns empty when all places have tags' do + place3.tags << tag3 + results = Place.without_tags + expect(results).to be_empty + end + + it 'returns all places when none have tags' do + place1.tags.clear + place2.tags.clear + results = Place.without_tags + expect(results).to contain_exactly(place1, place2, place3) + end + end + + describe '.tagged_with' do + it 'returns places tagged with the specified tag name' do + results = Place.tagged_with('Home', user) + expect(results).to contain_exactly(place1, place2) + end + + it 'returns distinct results' do + results = Place.tagged_with('Home', user) + expect(results.count).to eq(2) + end + + it 'returns empty when no places have the tag name' do + results = Place.tagged_with('NonExistent', user) + expect(results).to be_empty + end + + it 'filters by user' do + other_user = create(:user) + other_tag = create(:tag, user: other_user, name: 'Home') + other_place = create(:place, user: other_user) + other_place.tags << other_tag + + results = Place.tagged_with('Home', user) + expect(results).to contain_exactly(place1, place2) + expect(results).not_to include(other_place) + end + end + end + + describe 'instance methods' do + let(:place) { create(:place, user: user) } + + describe '#add_tag' do + it 'adds a tag to the record' do + expect { + place.add_tag(tag1) + }.to change { place.tags.count }.by(1) + end + + it 'does not add duplicate tags' do + place.add_tag(tag1) + expect { + place.add_tag(tag1) + }.not_to change { place.tags.count } + end + + it 'adds the correct tag' do + place.add_tag(tag1) + expect(place.tags).to include(tag1) + end + + it 'can add multiple different tags' do + place.add_tag(tag1) + place.add_tag(tag2) + expect(place.tags).to contain_exactly(tag1, tag2) + end + end + + describe '#remove_tag' do + before do + place.tags << [tag1, tag2] + end + + it 'removes a tag from the record' do + expect { + place.remove_tag(tag1) + }.to change { place.tags.count }.by(-1) + end + + it 'removes the correct tag' do + place.remove_tag(tag1) + expect(place.tags).not_to include(tag1) + expect(place.tags).to include(tag2) + end + + it 'does nothing when tag is not present' do + expect { + place.remove_tag(tag3) + }.not_to change { place.tags.count } + end + end + + describe '#tag_names' do + it 'returns an empty array when no tags' do + expect(place.tag_names).to eq([]) + end + + it 'returns array of tag names' do + place.tags << [tag1, tag2] + expect(place.tag_names).to contain_exactly('Home', 'Work') + end + + it 'returns tag names in database order' do + place.tags << tag2 + place.tags << tag1 + # Order depends on taggings created_at + expect(place.tag_names).to be_an(Array) + expect(place.tag_names.size).to eq(2) + end + end + + describe '#tagged_with?' do + before do + place.tags << tag1 + end + + it 'returns true when tagged with the specified tag' do + expect(place.tagged_with?(tag1)).to be true + end + + it 'returns false when not tagged with the specified tag' do + expect(place.tagged_with?(tag2)).to be false + end + + it 'returns false when place has no tags' do + place.tags.clear + expect(place.tagged_with?(tag1)).to be false + end + end + end +end diff --git a/spec/models/place_spec.rb b/spec/models/place_spec.rb index 915ab0ca..184221b9 100644 --- a/spec/models/place_spec.rb +++ b/spec/models/place_spec.rb @@ -18,6 +18,100 @@ RSpec.describe Place, type: :model do it { is_expected.to define_enum_for(:source).with_values(%i[manual photon]) } end + describe 'scopes' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let!(:place1) { create(:place, user: user1, name: 'Zoo') } + let!(:place2) { create(:place, user: user1, name: 'Airport') } + let!(:place3) { create(:place, user: user2, name: 'Museum') } + + describe '.for_user' do + it 'returns places for the specified user' do + expect(Place.for_user(user1)).to contain_exactly(place1, place2) + end + + it 'does not return places for other users' do + expect(Place.for_user(user1)).not_to include(place3) + end + + it 'returns empty when user has no places' do + new_user = create(:user) + expect(Place.for_user(new_user)).to be_empty + end + end + + describe '.ordered' do + it 'orders places by name alphabetically' do + expect(Place.for_user(user1).ordered).to eq([place2, place1]) + end + + it 'handles case-insensitive ordering' do + place_lower = create(:place, user: user1, name: 'airport') + place_upper = create(:place, user: user1, name: 'BEACH') + + ordered = Place.for_user(user1).ordered + # The ordered scope orders by name alphabetically (case-sensitive in most DBs) + expect(ordered.map(&:name)).to include('airport', 'BEACH') + end + end + end + + describe 'Taggable concern integration' do + let(:user) { create(:user) } + let(:place) { create(:place, user: user) } + let(:tag1) { create(:tag, user: user, name: 'Restaurant') } + let(:tag2) { create(:tag, user: user, name: 'Favorite') } + + it 'can add tags to a place' do + place.add_tag(tag1) + expect(place.tags).to include(tag1) + end + + it 'can remove tags from a place' do + place.tags << tag1 + place.remove_tag(tag1) + expect(place.tags).not_to include(tag1) + end + + it 'returns tag names' do + place.tags << [tag1, tag2] + expect(place.tag_names).to contain_exactly('Restaurant', 'Favorite') + end + + it 'checks if tagged with a specific tag' do + place.tags << tag1 + expect(place.tagged_with?(tag1)).to be true + expect(place.tagged_with?(tag2)).to be false + end + + describe 'scopes' do + let!(:tagged_place) { create(:place, user: user) } + let!(:untagged_place) { create(:place, user: user) } + + before do + tagged_place.tags << tag1 + end + + it 'filters places with specific tags' do + results = Place.with_tags([tag1.id]) + expect(results).to include(tagged_place) + expect(results).not_to include(untagged_place) + end + + it 'filters places without tags' do + results = Place.without_tags + expect(results).to include(untagged_place) + expect(results).not_to include(tagged_place) + end + + it 'filters places by tag name and user' do + results = Place.tagged_with('Restaurant', user) + expect(results).to include(tagged_place) + expect(results).not_to include(untagged_place) + end + end + end + describe 'methods' do let(:place) { create(:place, :with_geodata) }