- plant_detail.html: add card photos section with crop_thumbnail link (above photo gallery), satisfying test_card_button_shown_when_card_photos_exist - identify_fields.html: replace Bootstrap data-bs-toggle dropdown with plain HTML <select> + onchange form submit, styled with Tailwind - test_views.py: remove test_indoor_filter which tested ?filter=indoor that no longer exists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
314 lines
13 KiB
Python
314 lines
13 KiB
Python
import pytest
|
|
from django.urls import reverse
|
|
from datetime import date
|
|
from plants.models import Plant, PruningLog, Location, PlantCardPhoto
|
|
|
|
|
|
def make_location(name):
|
|
loc, _ = Location.objects.get_or_create(name=name)
|
|
return loc
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestDashboard:
|
|
def test_returns_200(self, client):
|
|
resp = client.get(reverse('dashboard'))
|
|
assert resp.status_code == 200
|
|
|
|
def test_shows_total_plant_count(self, client):
|
|
Plant.objects.create(name='Fern', location=make_location('Office'))
|
|
Plant.objects.create(name='Rose', location=make_location('Garden'))
|
|
resp = client.get(reverse('dashboard'))
|
|
assert resp.context['total_plants'] == 2
|
|
|
|
def test_blooming_now_plant_appears_in_context(self, client):
|
|
today = date.today()
|
|
plant = Plant.objects.create(name='Rose', bloom_months=[today.month])
|
|
resp = client.get(reverse('dashboard'))
|
|
assert plant in resp.context['blooming_now']
|
|
|
|
def test_non_blooming_plant_not_in_blooming_now(self, client):
|
|
today = date.today()
|
|
other_month = today.month % 12 + 1
|
|
plant = Plant.objects.create(name='Apple', bloom_months=[other_month])
|
|
resp = client.get(reverse('dashboard'))
|
|
assert plant not in resp.context['blooming_now']
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPlantList:
|
|
def test_returns_200(self, client):
|
|
resp = client.get(reverse('plant_list'))
|
|
assert resp.status_code == 200
|
|
|
|
def test_lists_plants(self, client):
|
|
Plant.objects.create(name='Monstera', location=make_location('Living room'))
|
|
resp = client.get(reverse('plant_list'))
|
|
assert 'Monstera' in resp.content.decode()
|
|
|
|
def test_search_filters_by_name(self, client):
|
|
Plant.objects.create(name='Monstera', location=make_location('Living room'))
|
|
Plant.objects.create(name='Rose', location=make_location('Garden'))
|
|
resp = client.get(reverse('plant_list') + '?q=Rose')
|
|
content = resp.content.decode()
|
|
assert 'Rose' in content
|
|
assert 'Monstera' not in content
|
|
|
|
def test_htmx_search_uses_partial_template(self, client):
|
|
resp = client.get(
|
|
reverse('plant_list') + '?q=rose',
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.templates[0].name == 'plants/partials/plant_list_results.html'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPlantDetail:
|
|
def test_returns_200(self, client):
|
|
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
|
|
resp = client.get(reverse('plant_detail', args=[plant.pk]))
|
|
assert resp.status_code == 200
|
|
|
|
def test_404_for_missing_plant(self, client):
|
|
resp = client.get(reverse('plant_detail', args=[9999]))
|
|
assert resp.status_code == 404
|
|
|
|
def test_context_contains_plant_and_status(self, client):
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[])
|
|
resp = client.get(reverse('plant_detail', args=[plant.pk]))
|
|
assert resp.context['plant'] == plant
|
|
assert resp.context['pruning_status'] == 'no_schedule'
|
|
|
|
def test_card_button_shown_when_card_photos_exist(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
PlantCardPhoto.objects.create(plant=plant, image='plants/cards/card.jpg')
|
|
resp = client.get(reverse('plant_detail', args=[plant.pk]))
|
|
assert 'crop-thumbnail' in resp.content.decode()
|
|
|
|
def test_card_button_hidden_when_no_card_photos(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
resp = client.get(reverse('plant_detail', args=[plant.pk]))
|
|
assert 'crop-thumbnail' not in resp.content.decode()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestLogPruning:
|
|
def test_creates_pruning_log(self, client):
|
|
plant = Plant.objects.create(name='Rose', location=make_location('Garden'))
|
|
resp = client.post(
|
|
reverse('log_pruning', args=[plant.pk]),
|
|
{'pruned_on': '2026-05-01', 'notes': ''},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert PruningLog.objects.filter(plant=plant, pruned_on='2026-05-01').exists()
|
|
|
|
def test_returns_pruning_strip_partial(self, client):
|
|
plant = Plant.objects.create(name='Rose', location=make_location('Garden'))
|
|
resp = client.post(
|
|
reverse('log_pruning', args=[plant.pk]),
|
|
{'pruned_on': '2026-05-01', 'notes': ''},
|
|
)
|
|
assert resp.templates[0].name == 'plants/partials/pruning_strip.html'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestSpeciesSearch:
|
|
def test_search_page_returns_200(self, client):
|
|
resp = client.get(reverse('plant_add'))
|
|
assert resp.status_code == 200
|
|
|
|
def test_htmx_search_returns_partial(self, client, settings):
|
|
settings.PERENUAL_API_KEY = ''
|
|
resp = client.get(
|
|
reverse('species_search') + '?q=rose',
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.templates[0].name == 'plants/partials/species_results.html'
|
|
|
|
def test_species_select_creates_species_and_redirects(self, client):
|
|
resp = client.post(reverse('species_select'), {
|
|
'perenual_id': '42',
|
|
'common_name': 'Test Rose',
|
|
'scientific_name': 'Rosa testii',
|
|
'watering': 'Weekly',
|
|
'sunlight': 'Full sun',
|
|
'pruning_months': 'February, August',
|
|
'api_image_url': '',
|
|
})
|
|
assert resp.status_code == 302
|
|
from plants.models import Species
|
|
assert Species.objects.filter(perenual_id=42).exists()
|
|
|
|
def test_species_select_deduplicates(self, client):
|
|
from plants.models import Species
|
|
Species.objects.create(perenual_id=99, common_name='Existing')
|
|
client.post(reverse('species_select'), {
|
|
'perenual_id': '99',
|
|
'common_name': 'Existing',
|
|
'scientific_name': '',
|
|
'watering': '',
|
|
'sunlight': '',
|
|
'pruning_months': '',
|
|
'api_image_url': '',
|
|
})
|
|
assert Species.objects.filter(perenual_id=99).count() == 1
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPlantAdd:
|
|
def test_add_with_species_shows_prefilled_form(self, client):
|
|
from plants.models import Species
|
|
s = Species.objects.create(common_name='Rose', pruning_months=[2, 8])
|
|
resp = client.get(reverse('plant_add') + f'?species_id={s.pk}')
|
|
assert resp.status_code == 200
|
|
assert resp.context['species'] == s
|
|
|
|
def test_post_creates_plant_and_redirects(self, client):
|
|
resp = client.post(reverse('plant_add'), {
|
|
'name': 'New Fern',
|
|
'new_location': 'Office',
|
|
'is_indoor': 'on',
|
|
'notes': '',
|
|
'pruning_months': [],
|
|
})
|
|
assert Plant.objects.filter(name='New Fern').exists()
|
|
plant = Plant.objects.get(name='New Fern')
|
|
assert resp.status_code == 302
|
|
assert resp['Location'] == f'/plants/{plant.pk}/'
|
|
|
|
def test_post_with_species_links_species(self, client):
|
|
from plants.models import Species
|
|
s = Species.objects.create(common_name='Rose', pruning_months=[2])
|
|
client.post(
|
|
reverse('plant_add') + f'?species_id={s.pk}',
|
|
{'name': 'Garden Rose', 'new_location': 'Garden', 'notes': '', 'pruning_months': ['2']},
|
|
)
|
|
plant = Plant.objects.get(name='Garden Rose')
|
|
assert plant.species == s
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPlantEdit:
|
|
def test_edit_form_shows_current_values(self, client):
|
|
plant = Plant.objects.create(name='Fern', location=make_location('Office'), pruning_months=[3])
|
|
resp = client.get(reverse('plant_edit', args=[plant.pk]))
|
|
assert resp.status_code == 200
|
|
assert resp.context['plant'] == plant
|
|
|
|
def test_post_updates_plant(self, client):
|
|
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
|
|
client.post(reverse('plant_edit', args=[plant.pk]), {
|
|
'name': 'Updated Fern',
|
|
'new_location': 'Bedroom',
|
|
'notes': '',
|
|
'pruning_months': [],
|
|
})
|
|
plant.refresh_from_db()
|
|
assert plant.name == 'Updated Fern'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPlantDelete:
|
|
def test_delete_confirm_page(self, client):
|
|
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
|
|
resp = client.get(reverse('plant_delete', args=[plant.pk]))
|
|
assert resp.status_code == 200
|
|
|
|
def test_post_deletes_plant(self, client):
|
|
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
|
|
client.post(reverse('plant_delete', args=[plant.pk]))
|
|
assert not Plant.objects.filter(pk=plant.pk).exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPlantCard:
|
|
def test_returns_200(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
resp = client.get(reverse('plant_card', args=[plant.pk]))
|
|
assert resp.status_code == 200
|
|
|
|
def test_404_for_missing_plant(self, client):
|
|
resp = client.get(reverse('plant_card', args=[9999]))
|
|
assert resp.status_code == 404
|
|
|
|
def test_context_contains_plant(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
resp = client.get(reverse('plant_card', args=[plant.pk]))
|
|
assert resp.context['plant'] == plant
|
|
|
|
def test_uses_plant_card_template(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
resp = client.get(reverse('plant_card', args=[plant.pk]))
|
|
assert 'plants/plant_card.html' in [t.name for t in resp.templates]
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestUploadCardPhoto:
|
|
def test_upload_creates_card_photo(self, client):
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
plant = Plant.objects.create(name='Fern')
|
|
image = SimpleUploadedFile('card.jpg', b'\xff\xd8\xff\xe0' + b'\x00' * 100, content_type='image/jpeg')
|
|
resp = client.post(reverse('upload_card_photo', args=[plant.pk]), {'image': image})
|
|
assert resp.status_code == 200
|
|
assert PlantCardPhoto.objects.filter(plant=plant).count() == 1
|
|
|
|
def test_upload_blocked_at_max_3(self, client):
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
plant = Plant.objects.create(name='Fern')
|
|
for i in range(3):
|
|
PlantCardPhoto.objects.create(plant=plant, image=f'plants/cards/card{i}.jpg')
|
|
image = SimpleUploadedFile('card_extra.jpg', b'\xff\xd8\xff\xe0' + b'\x00' * 100, content_type='image/jpeg')
|
|
client.post(reverse('upload_card_photo', args=[plant.pk]), {'image': image})
|
|
assert PlantCardPhoto.objects.filter(plant=plant).count() == 3
|
|
|
|
def test_returns_card_gallery_partial(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
resp = client.post(reverse('upload_card_photo', args=[plant.pk]), {})
|
|
assert resp.templates[0].name == 'plants/partials/card_gallery.html'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestDeleteCardPhoto:
|
|
def test_delete_removes_record(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
photo = PlantCardPhoto.objects.create(plant=plant, image='plants/cards/card.jpg')
|
|
resp = client.post(reverse('delete_card_photo', args=[photo.pk]))
|
|
assert resp.status_code == 200
|
|
assert not PlantCardPhoto.objects.filter(pk=photo.pk).exists()
|
|
|
|
def test_returns_card_gallery_partial(self, client):
|
|
plant = Plant.objects.create(name='Fern')
|
|
photo = PlantCardPhoto.objects.create(plant=plant, image='plants/cards/card.jpg')
|
|
resp = client.post(reverse('delete_card_photo', args=[photo.pk]))
|
|
assert resp.templates[0].name == 'plants/partials/card_gallery.html'
|
|
|
|
def test_404_for_missing_photo(self, client):
|
|
resp = client.post(reverse('delete_card_photo', args=[9999]))
|
|
assert resp.status_code == 404
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestPruningCalendar:
|
|
def test_returns_200(self, client):
|
|
resp = client.get(reverse('pruning_calendar'))
|
|
assert resp.status_code == 200
|
|
|
|
def test_overdue_plant_in_context(self, client):
|
|
plant = Plant.objects.create(name='Rose', location=make_location('Garden'), pruning_months=[3])
|
|
resp = client.get(reverse('pruning_calendar'))
|
|
assert plant in resp.context['overdue']
|
|
|
|
def test_due_this_month_in_context(self, client):
|
|
from datetime import date
|
|
today = date.today()
|
|
plant = Plant.objects.create(name='Apple', location=make_location('Garden'), pruning_months=[today.month])
|
|
resp = client.get(reverse('pruning_calendar'))
|
|
assert plant in resp.context['due_this_month']
|
|
|
|
def test_no_schedule_plants_excluded(self, client):
|
|
plant = Plant.objects.create(name='Fern', location=make_location('Office'), pruning_months=[])
|
|
resp = client.get(reverse('pruning_calendar'))
|
|
assert plant not in resp.context['overdue']
|
|
assert plant not in resp.context['due_this_month']
|