from io import BytesIO from pathlib import Path import app.db as db_module import app.images as images_module from PIL import Image from fastapi import UploadFile from app.models import Box, Item, SubItem def create_box(client, name="Box A", note="Packed", room="Bedroom", status="ready", image=None): data = {"name": name, "note": note, "room": room, "status": status} files = {"image_file": image} if image is not None else None return client.post("/boxes", data=data, files=files, follow_redirects=False) def create_item( client, box_id, name="Item A", note="Note", quantity="2", is_container=False, image=None, submit_action="save", ): data = {"name": name, "note": note, "quantity": quantity, "submit_action": submit_action} if is_container: data["is_container"] = "on" files = {"image_file": image} if image is not None else None return client.post(f"/boxes/{box_id}/items", data=data, files=files, follow_redirects=False) def create_subitem( client, item_id, name="SubItem A", note="Small", quantity="3", image=None, submit_action="save", ): files = {"image_file": image} if image is not None else None return client.post( f"/items/{item_id}/subitems", data={"name": name, "note": note, "quantity": quantity, "submit_action": submit_action}, files=files, follow_redirects=False, ) def make_image_upload( filename="sample.png", image_format="PNG", size=(2000, 1000), color=(255, 0, 0), mode="RGB", ): image = Image.new(mode, size, color) output = BytesIO() image.save(output, format=image_format) return (filename, output.getvalue(), "image/png") def make_oriented_jpeg_upload( filename="portrait.jpg", size=(1600, 900), orientation=6, ): image = Image.new("RGB", size, (20, 120, 220)) exif = Image.Exif() exif[274] = orientation output = BytesIO() image.save(output, format="JPEG", exif=exif) return (filename, output.getvalue(), "image/jpeg") def read_jpeg_size(image_bytes): with Image.open(BytesIO(image_bytes)) as image: return image.format, image.size def test_app_uses_isolated_sqlite_database(client): assert db_module.engine is not None database_name = Path(db_module.engine.url.database) assert database_name.name == "test.db" assert "data/app.db" not in str(database_name) def test_root_redirects_to_boxes(client): response = client.get("/", follow_redirects=False) assert response.status_code == 302 assert response.headers["location"] == "/boxes" def test_boxes_page_returns_200(client): response = client.get("/boxes") assert response.status_code == 200 assert "箱子" in response.text def test_boxes_page_uses_relative_stylesheet_path(client): response = client.get("/boxes") assert response.status_code == 200 assert 'href="/static/style.css"' in response.text assert "http://" not in response.text.split("/static/style.css")[0] def test_base_template_includes_pwa_metadata(client): response = client.get("/boxes") assert response.status_code == 200 assert 'rel="manifest" href="/manifest.webmanifest"' in response.text assert 'name="theme-color" content="#0b57d0"' in response.text assert 'rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png"' in response.text assert 'navigator.serviceWorker.register("/service-worker.js")' in response.text def test_manifest_is_served_with_pwa_fields(client): response = client.get("/manifest.webmanifest") assert response.status_code == 200 assert response.headers["content-type"].startswith("application/manifest+json") manifest = response.json() assert manifest["name"] == "搬家助手" assert manifest["short_name"] == "搬家助手" assert manifest["start_url"] == "/boxes" assert manifest["display"] == "standalone" assert any(icon["sizes"] == "192x192" for icon in manifest["icons"]) assert any(icon["purpose"] == "maskable" for icon in manifest["icons"]) def test_service_worker_is_served_from_root(client): response = client.get("/service-worker.js") assert response.status_code == 200 assert response.headers["content-type"].startswith("application/javascript") assert 'self.addEventListener("install"' in response.text def test_boxes_overview_card_shows_note_and_item_count_without_room_or_status(client, db_session): box = Box( name="Kitchen Box", note="易碎餐具和杯子", room="Kitchen", status="packed", ) box.items.append(Item(name="Plate", is_container=False)) db_session.add(box) db_session.commit() response = client.get("/boxes") assert response.status_code == 200 assert "Kitchen Box" in response.text assert "物品数:1" in response.text assert "易碎餐具和杯子" in response.text assert "房间:" not in response.text assert "状态:" not in response.text def test_boxes_overview_renders_cleanly_when_note_is_empty(client, db_session): box = Box(name="No Note Box", note=None, room="Office", status="open") db_session.add(box) db_session.commit() response = client.get("/boxes") assert response.status_code == 200 assert "No Note Box" in response.text assert "房间:" not in response.text assert "状态:" not in response.text def test_can_create_box(client, db_session): response = create_box(client, name="Kitchen Box") assert response.status_code == 303 box = db_session.query(Box).one() assert box.name == "Kitchen Box" assert box.room == "Bedroom" def test_can_edit_box(client, db_session): box = Box(name="Old Box") db_session.add(box) db_session.commit() response = client.post( f"/boxes/{box.id}/update", data={"name": "Updated Box", "note": "Updated", "room": "Office", "status": "open"}, follow_redirects=False, ) assert response.status_code == 303 db_session.refresh(box) assert box.name == "Updated Box" assert box.room == "Office" assert box.status == "open" def test_can_delete_box(client, db_session): box = Box(name="Delete Me") db_session.add(box) db_session.commit() box_id = box.id response = client.post(f"/boxes/{box_id}/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() assert db_session.get(Box, box_id) is None def test_deleting_box_cascades_to_items_and_subitems(client, db_session): box = Box(name="Cascade Box") item = Item(name="Container", is_container=True, box=box) subitem = SubItem(name="Cable", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.post(f"/boxes/{box.id}/delete", follow_redirects=False) assert response.status_code == 303 assert db_session.query(Box).count() == 0 assert db_session.query(Item).count() == 0 assert db_session.query(SubItem).count() == 0 def test_missing_box_returns_404(client): response = client.get("/boxes/9999") assert response.status_code == 404 def test_box_detail_returns_200_when_box_exists(client, db_session): box = Box(name="Visible Box") db_session.add(box) db_session.commit() response = client.get(f"/boxes/{box.id}") assert response.status_code == 200 assert "Visible Box" in response.text def test_box_detail_item_cards_show_notes_without_note_placeholder_text(client, db_session): box = Box(name="Overview Box") box.items.append(Item(name="Accessory Pouch", note="充电器和转换头", is_container=True, quantity=2)) db_session.add(box) db_session.commit() response = client.get(f"/boxes/{box.id}") assert response.status_code == 200 assert "Accessory Pouch" in response.text assert "数量:2" in response.text assert "充电器和转换头" in response.text assert "有备注" not in response.text def test_can_create_regular_item_under_box(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="Book", is_container=False) assert response.status_code == 303 item = db_session.query(Item).one() assert item.name == "Book" assert item.is_container is False def test_can_create_container_item_under_box(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="Accessory Pouch", is_container=True) assert response.status_code == 303 item = db_session.query(Item).one() assert item.is_container is True def test_can_edit_item(client, db_session): box = Box(name="Main Box") item = Item(name="Old Item", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.post( f"/items/{item.id}/update", data={"name": "New Item", "note": "Changed", "quantity": "7", "is_container": "on"}, follow_redirects=False, ) assert response.status_code == 303 db_session.refresh(item) assert item.name == "New Item" assert item.quantity == 7 assert item.is_container is True def test_can_delete_item(client, db_session): box = Box(name="Main Box") item = Item(name="Delete Item", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() item_id = item.id response = client.post(f"/items/{item_id}/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() assert db_session.get(Item, item_id) is None def test_deleting_container_item_cascades_to_subitems(client, db_session): box = Box(name="Main Box") item = Item(name="Container", box=box, is_container=True) subitem = SubItem(name="Clip", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.post(f"/items/{item.id}/delete", follow_redirects=False) assert response.status_code == 303 assert db_session.query(Item).count() == 0 assert db_session.query(SubItem).count() == 0 def test_missing_item_returns_404(client): response = client.get("/items/9999") assert response.status_code == 404 def test_item_detail_returns_200_when_item_exists(client, db_session): box = Box(name="Main Box") item = Item(name="Visible Item", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.get(f"/items/{item.id}") assert response.status_code == 200 assert "Visible Item" in response.text def test_can_create_subitem_under_container_item(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = create_subitem(client, item.id, name="USB Cable") assert response.status_code == 303 subitem = db_session.query(SubItem).one() assert subitem.name == "USB Cable" def test_cannot_create_subitem_under_non_container_item(client, db_session): box = Box(name="Main Box") item = Item(name="Book", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = create_subitem(client, item.id, name="Should Fail") assert response.status_code == 400 assert response.json()["detail"] == "Only container items can have sub-items" assert db_session.query(SubItem).count() == 0 def test_can_edit_subitem(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) subitem = SubItem(name="Old Sub-item", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.post( f"/subitems/{subitem.id}/update", data={"name": "New Sub-item", "note": "Updated", "quantity": "9"}, follow_redirects=False, ) assert response.status_code == 303 db_session.refresh(subitem) assert subitem.name == "New Sub-item" assert subitem.quantity == 9 def test_can_delete_subitem(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) subitem = SubItem(name="Delete Sub-item", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() subitem_id = subitem.id response = client.post(f"/subitems/{subitem_id}/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() assert db_session.get(SubItem, subitem_id) is None def test_missing_subitem_returns_404(client): response = client.get("/subitems/9999/edit") assert response.status_code == 404 def test_post_redirects_are_reasonable(client, db_session): box = Box(name="Redirect Box") db_session.add(box) db_session.commit() item_response = create_item(client, box.id, name="Lamp") item = db_session.query(Item).one() item.is_container = True db_session.commit() subitem_response = create_subitem(client, item.id, name="Bulb") assert item_response.headers["location"] == f"/boxes/{box.id}" assert subitem_response.headers["location"] == f"/items/{item.id}" def test_can_upload_image_for_box_and_process_it(client, db_session): response = create_box(client, name="Photo Box", image=make_image_upload()) assert response.status_code == 303 box = db_session.query(Box).one() assert box.image_blob is not None assert box.image_mime_type == "image/jpeg" assert box.image_width == 1600 assert box.image_height == 800 image_format, image_size = read_jpeg_size(box.image_blob) assert image_format == "JPEG" assert image_size == (1600, 800) def test_image_pipeline_applies_exif_orientation_before_saving(client, db_session): response = create_box(client, name="Portrait Box", image=make_oriented_jpeg_upload()) assert response.status_code == 303 box = db_session.query(Box).filter_by(name="Portrait Box").one() assert box.image_blob is not None assert box.image_width == 900 assert box.image_height == 1600 image_format, image_size = read_jpeg_size(box.image_blob) assert image_format == "JPEG" assert image_size == (900, 1600) def test_can_upload_image_for_item(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="Book", image=make_image_upload()) assert response.status_code == 303 item = db_session.query(Item).one() assert item.image_blob is not None assert item.image_mime_type == "image/jpeg" def test_can_upload_image_for_subitem(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = create_subitem(client, item.id, name="Cable", image=make_image_upload()) assert response.status_code == 303 subitem = db_session.query(SubItem).one() assert subitem.image_blob is not None assert subitem.image_mime_type == "image/jpeg" def test_box_image_route_returns_jpeg(client, db_session): box = Box(name="Photo Box") db_session.add(box) db_session.commit() client.post( f"/boxes/{box.id}/update", data={"name": box.name, "note": "", "room": "", "status": ""}, files={"image_file": make_image_upload()}, follow_redirects=False, ) db_session.expire_all() response = client.get(f"/boxes/{box.id}/image") assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" def test_item_image_route_returns_jpeg(client, db_session): box = Box(name="Main Box") item = Item(name="Lamp", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() client.post( f"/items/{item.id}/update", data={"name": item.name, "note": "", "quantity": "", "is_container": ""}, files={"image_file": make_image_upload()}, follow_redirects=False, ) response = client.get(f"/items/{item.id}/image") assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" def test_subitem_image_route_returns_jpeg(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) subitem = SubItem(name="Cable", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() client.post( f"/subitems/{subitem.id}/update", data={"name": subitem.name, "note": "", "quantity": ""}, files={"image_file": make_image_upload()}, follow_redirects=False, ) response = client.get(f"/subitems/{subitem.id}/image") assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" def test_image_routes_return_404_when_no_image(client, db_session): box = Box(name="Main Box") item = Item(name="Lamp", box=box, is_container=False) subitem = SubItem(name="Cable", parent_item=Item(name="Pouch", box=box, is_container=True)) db_session.add_all([box, item, subitem.parent_item, subitem]) db_session.commit() assert client.get(f"/boxes/{box.id}/image").status_code == 404 assert client.get(f"/items/{item.id}/image").status_code == 404 assert client.get(f"/subitems/{subitem.id}/image").status_code == 404 def test_image_routes_return_404_for_missing_objects(client): assert client.get("/boxes/9999/image").status_code == 404 assert client.get("/items/9999/image").status_code == 404 assert client.get("/subitems/9999/image").status_code == 404 def test_can_replace_existing_image(client, db_session): box = Box(name="Photo Box") db_session.add(box) db_session.commit() client.post( f"/boxes/{box.id}/update", data={"name": box.name, "note": "", "room": "", "status": ""}, files={"image_file": make_image_upload(color=(255, 0, 0))}, follow_redirects=False, ) db_session.expire_all() first_blob = db_session.get(Box, box.id).image_blob client.post( f"/boxes/{box.id}/update", data={"name": box.name, "note": "", "room": "", "status": ""}, files={"image_file": make_image_upload(color=(0, 255, 0))}, follow_redirects=False, ) db_session.expire_all() updated_box = db_session.get(Box, box.id) assert updated_box.image_blob is not None assert updated_box.image_blob != first_blob def test_can_delete_existing_image_and_clear_metadata(client, db_session): box = Box(name="Photo Box") db_session.add(box) db_session.commit() client.post( f"/boxes/{box.id}/update", data={"name": box.name, "note": "", "room": "", "status": ""}, files={"image_file": make_image_upload()}, follow_redirects=False, ) response = client.post(f"/boxes/{box.id}/image/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() updated_box = db_session.get(Box, box.id) assert updated_box.image_blob is None assert updated_box.image_mime_type is None assert updated_box.image_width is None assert updated_box.image_height is None assert client.get(f"/boxes/{box.id}/image").status_code == 404 def test_deleting_image_when_none_exists_is_safe(client, db_session): item = Item(name="No Image Item", box=Box(name="Main Box"), is_container=False) db_session.add(item) db_session.commit() response = client.post(f"/items/{item.id}/image/delete", follow_redirects=False) assert response.status_code == 303 def test_uploading_non_image_file_returns_400_and_does_not_write_data(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = client.post( f"/items/{box.id}/subitems", data={"name": "Should Fail", "note": "", "quantity": ""}, files={"image_file": ("bad.txt", b"not an image", "text/plain")}, follow_redirects=False, ) assert response.status_code in {400, 404} item = Item(name="Pouch", box=box, is_container=True) db_session.add(item) db_session.commit() response = create_subitem( client, item.id, name="Should Fail", image=("bad.txt", b"not an image", "text/plain"), ) assert response.status_code == 400 assert response.json()["detail"] == "上传的文件不是合法图片" assert db_session.query(SubItem).count() == 0 def test_broken_image_processing_returns_400_and_keeps_image_fields_empty(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = client.post( f"/boxes/{box.id}/update", data={"name": box.name, "note": "", "room": "", "status": ""}, files={"image_file": ("broken.jpg", b"\xff\xd8\xff\xdbbroken", "image/jpeg")}, follow_redirects=False, ) assert response.status_code == 400 db_session.expire_all() updated_box = db_session.get(Box, box.id) assert updated_box.image_blob is None assert updated_box.image_mime_type is None assert updated_box.image_width is None assert updated_box.image_height is None def test_heic_upload_returns_clear_error_if_heif_support_is_unavailable(monkeypatch): heic_file = UploadFile(filename="sample.heic", file=BytesIO(b"not-a-real-heic")) def fake_open(*args, **kwargs): raise images_module.UnidentifiedImageError("cannot identify image file") monkeypatch.setattr(images_module, "HEIF_SUPPORT_ENABLED", False) monkeypatch.setattr(images_module.Image, "open", fake_open) monkeypatch.setattr(images_module.shutil, "which", lambda command: None) try: images_module.process_upload(heic_file) assert False, "Expected HEIC upload to raise HTTPException" except Exception as exc: assert getattr(exc, "status_code", None) == 400 assert "HEIC/HEIF" in getattr(exc, "detail", "") def test_can_search_box_by_name(client, db_session): box = Box(name="冬季衣物箱") db_session.add(box) db_session.commit() response = client.get("/search?q=衣物") assert response.status_code == 200 assert "冬季衣物箱" in response.text assert "类型:Box" in response.text def test_can_search_box_by_note(client, db_session): box = Box(name="普通箱子", note="里面放厨房锅具") db_session.add(box) db_session.commit() response = client.get("/search?q=锅具") assert response.status_code == 200 assert "普通箱子" in response.text def test_can_search_item_by_name(client, db_session): box = Box(name="书房箱") item = Item(name="电源延长线", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.get("/search?q=延长线") assert response.status_code == 200 assert "电源延长线" in response.text assert "位于箱子:书房箱" in response.text def test_can_search_item_by_note(client, db_session): box = Box(name="工具箱") item = Item(name="杂项", note="放备用螺丝刀", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.get("/search?q=螺丝刀") assert response.status_code == 200 assert "杂项" in response.text def test_can_search_subitem_by_name(client, db_session): box = Box(name="文件箱") item = Item(name="文件袋", box=box, is_container=True) subitem = SubItem(name="护照复印件", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.get("/search?q=护照") assert response.status_code == 200 assert "护照复印件" in response.text assert "位于物品:文件袋 / 箱子:文件箱" in response.text def test_can_search_subitem_by_note(client, db_session): box = Box(name="电子箱") item = Item(name="配件盒", box=box, is_container=True) subitem = SubItem(name="接口", note="备用转接头", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.get("/search?q=转接头") assert response.status_code == 200 assert "接口" in response.text def test_search_result_shows_item_box_info(client, db_session): box = Box(name="客厅箱") item = Item(name="相机", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = client.get("/search?q=相机") assert response.status_code == 200 assert "位于箱子:客厅箱" in response.text assert "容器" in response.text assert "是" in response.text def test_search_result_shows_subitem_item_and_box_info(client, db_session): box = Box(name="卧室箱") item = Item(name="收纳袋", box=box, is_container=True) subitem = SubItem(name="袜子", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.get("/search?q=袜子") assert response.status_code == 200 assert "位于物品:收纳袋 / 箱子:卧室箱" in response.text def test_search_page_handles_empty_query(client): response = client.get("/search") assert response.status_code == 200 assert "输入关键词后,可以跨 Box、Item、SubItem 进行搜索。" in response.text def test_search_page_handles_no_results(client): response = client.get("/search?q=不存在的关键词") assert response.status_code == 200 assert "没有找到匹配结果。" in response.text def test_search_result_renders_thumbnail_link_when_image_exists(client, db_session): box = Box(name="图片箱") db_session.add(box) db_session.commit() client.post( f"/boxes/{box.id}/update", data={"name": box.name, "note": "", "room": "", "status": ""}, files={"image_file": make_image_upload()}, follow_redirects=False, ) response = client.get("/search?q=图片箱") assert response.status_code == 200 assert f'/boxes/{box.id}/image' in response.text def test_search_result_without_image_does_not_break_template(client, db_session): item = Item(name="无图物品", box=Box(name="普通箱"), is_container=False) db_session.add(item) db_session.commit() response = client.get("/search?q=无图物品") assert response.status_code == 200 assert "无图物品" in response.text def test_new_box_page_shows_clear_context(client): response = client.get("/boxes/new") assert response.status_code == 200 assert "新建 Box" in response.text assert "创建顶层箱子" in response.text def test_new_item_page_shows_clear_context_and_default_quantity(client, db_session): box = Box(name="主卧箱") db_session.add(box) db_session.commit() response = client.get(f"/boxes/{box.id}/items/new") assert response.status_code == 200 assert "新建 Item" in response.text assert "主卧箱" in response.text assert 'name="quantity"' in response.text assert 'value="1"' in response.text assert "这个物品本身是一个小容器" in response.text assert "保存并添加下一个" in response.text def test_new_subitem_page_shows_clear_context_and_default_quantity(client, db_session): box = Box(name="客厅箱") item = Item(name="文件袋", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = client.get(f"/items/{item.id}/subitems/new") assert response.status_code == 200 assert "新建 SubItem" in response.text assert "客厅箱" in response.text assert "文件袋" in response.text assert 'name="quantity"' in response.text assert 'value="1"' in response.text assert "保存并添加下一个" in response.text def test_box_detail_page_renders_clear_hierarchy_and_dense_list_structure(client, db_session): box = Box(name="厨房箱") item = Item(name="锅", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.get(f"/boxes/{box.id}") assert response.status_code == 200 assert "Box" in response.text assert "厨房箱" in response.text assert "overview-grid" in response.text assert f'data-href="/items/{item.id}"' in response.text assert "是否容器" not in response.text def test_item_detail_page_renders_clear_hierarchy(client, db_session): box = Box(name="书房箱") item = Item(name="配件盒", box=box, is_container=True) subitem = SubItem(name="转接头", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.get(f"/items/{item.id}") assert response.status_code == 200 assert "容器型 Item" in response.text assert "书房箱" in response.text assert "SubItem" in response.text assert f'data-href="/subitems/{subitem.id}/edit"' in response.text assert f'action="/subitems/{subitem.id}/delete"' in response.text assert "overview-grid" in response.text def test_box_detail_page_shows_primary_and_secondary_cta_buttons(client, db_session): box = Box(name="操作箱") db_session.add(box) db_session.commit() response = client.get(f"/boxes/{box.id}") assert response.status_code == 200 assert "button button-primary" in response.text assert "button button-secondary" in response.text assert "删除箱子" in response.text def test_boxes_overview_uses_clickable_cards_without_detail_edit_buttons(client, db_session): box = Box(name="可点击箱子") db_session.add(box) db_session.commit() response = client.get("/boxes") assert response.status_code == 200 assert f'data-href="/boxes/{box.id}"' in response.text assert "查看详情" not in response.text assert "编辑" not in response.text def test_item_detail_page_shows_primary_action_for_adding_subitems(client, db_session): box = Box(name="容器箱") item = Item(name="收纳盒", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = client.get(f"/items/{item.id}") assert response.status_code == 200 assert "添加子物品" in response.text assert "button button-primary" in response.text def test_creating_regular_item_redirects_back_to_parent_box(client, db_session): box = Box(name="连续录入箱") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="剪刀", is_container=False) assert response.status_code == 303 assert response.headers["location"] == f"/boxes/{box.id}" def test_creating_regular_subitem_redirects_back_to_parent_item(client, db_session): box = Box(name="配件箱") item = Item(name="线材袋", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = create_subitem(client, item.id, name="USB头") assert response.status_code == 303 assert response.headers["location"] == f"/items/{item.id}" def test_creating_box_redirects_to_new_box_detail(client): response = create_box(client, name="新箱子") assert response.status_code == 303 assert response.headers["location"].startswith("/boxes/") assert not response.headers["location"].endswith("/items/new") def test_creating_container_item_redirects_to_item_detail(client, db_session): box = Box(name="子容器箱") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="小收纳盒", is_container=True) created_item = db_session.query(Item).one() assert response.status_code == 303 assert response.headers["location"] == f"/items/{created_item.id}" def test_creating_item_with_save_and_add_next_returns_to_same_new_item_context(client, db_session): box = Box(name="快速录入箱") db_session.add(box) db_session.commit() response = create_item( client, box.id, name="袜子", is_container=False, submit_action="save_and_add_next", ) assert response.status_code == 303 assert response.headers["location"] == f"/boxes/{box.id}/items/new" def test_creating_subitem_with_save_and_add_next_returns_to_same_new_subitem_context(client, db_session): box = Box(name="电子箱") item = Item(name="配件袋", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = create_subitem( client, item.id, name="转接头", submit_action="save_and_add_next", ) assert response.status_code == 303 assert response.headers["location"] == f"/items/{item.id}/subitems/new"