Widgets
+Widgets
+ +Drag a widget to move it, drag its bottom-right corner to resize it -- like arranging widgets on a phone's home screen. Widgets can't overlap. Click a widget's gear icon for its own diff --git a/server/tests/test_widget_placement.py b/server/tests/test_widget_placement.py index 1b36162..2f1cde0 100644 --- a/server/tests/test_widget_placement.py +++ b/server/tests/test_widget_placement.py @@ -15,7 +15,7 @@ from __future__ import annotations from app.models import Frame, FrameButtonAction, Widget -from .conftest import csrf_headers +from .conftest import csrf_headers, link_user, login, make_user def _widget_id(db_session, widget_type="photos") -> int: @@ -162,6 +162,64 @@ def test_delete_unknown_widget_404s(client, db_session): assert resp.status_code == 404 +def test_clear_all_removes_every_widget_and_cascades_button_actions(client, db_session): + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + photos_id = _shrink_default_widget(client, db_session, w=4, h=5) + create_resp = client.post("/api/frames/1/widgets", json={"widget_type": "whiteboard"}, + headers=csrf_headers(client)) + assert create_resp.status_code == 200, create_resp.text + db_session.add(FrameButtonAction(frame_id=1, button="next", widget_id=photos_id, action="advance", sort_order=0)) + db_session.commit() + + resp = client.delete("/api/frames/1/widgets", headers=csrf_headers(client)) + assert resp.status_code == 200, resp.text + assert resp.json() == {"status": "cleared", "count": 2} + + assert db_session.query(Widget).filter_by(frame_id=1).all() == [] + assert db_session.query(FrameButtonAction).filter_by(frame_id=1).all() == [] + + list_resp = client.get("/api/frames/1/widgets") + assert list_resp.json()["widgets"] == [] + + +def test_clear_all_on_an_already_empty_frame_is_a_no_op(client, db_session): + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + widget_id = _widget_id(db_session) + client.delete(f"/api/frames/1/widgets/{widget_id}", headers=csrf_headers(client)) + + resp = client.delete("/api/frames/1/widgets", headers=csrf_headers(client)) + assert resp.status_code == 200 + assert resp.json() == {"status": "cleared", "count": 0} + + +def test_clear_all_unrelated_user_404s(client, db_session): + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + make_user(db_session, "mallory") + + client.cookies.clear() + login(client, "mallory") + resp = client.delete("/api/frames/1/widgets", headers=csrf_headers(client)) + assert resp.status_code == 404 + # Nothing was touched -- alice's widget is still there. + assert db_session.query(Widget).filter_by(frame_id=1).count() == 1 + + +def test_clear_all_linked_but_not_controlling_user_409s(client, db_session): + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + bob = make_user(db_session, "bob") + frame = db_session.get(Frame, 1) + link_user(db_session, bob, frame) + # alice (via /setup) already holds control -- bob is linked (can view) + # but not the controller. + + client.cookies.clear() + login(client, "bob") + resp = client.delete("/api/frames/1/widgets", headers=csrf_headers(client)) + assert resp.status_code == 409 + assert resp.json()["detail"]["error"] == "not_controller" + assert db_session.query(Widget).filter_by(frame_id=1).count() == 1 + + def test_widgets_scoped_to_their_own_frame(client, db_session): """A widget id from a different frame must 404, not silently operate cross-frame -- same posture as photo_widget_config_or_404 and every