intent/tests/test_phase4_exit.py
Alec Perkins f6b7374be7 Add feature files, image pins and implementations (Phase 4)
Files: prototype and documentation files on features, with versions.
Adding, replacing or removing a file is a feature revision with a
change note. Types come from the extension; uploads are capped at
25 MB and stored under opaque paths. Every file URL checks project
access, then redirects to a short-lived signed URL on the bucket, or
streams with nosniff and a CSP sandbox when no bucket is configured.
HTML prototypes run in a sandboxed iframe without same-origin access.

Threads: whole-file threads and pins at a point on an image, drawn as
an SVG overlay in percentages. Pins are placed with number fields, or
by clicking the image with JS.

Implementations: name, link and status, plus a per-feature status grid
stamped with the feature revision, marked behind when the feature
changes, and confirmable without changing the status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:15:30 -04:00

72 lines
2.9 KiB
Python

"""Phase 4 exit: a feature with a prototype image has a pinned thread; an implementation shows
one feature shipped and one behind. Driven through the views."""
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from intent.comments.models import CommentThread, ThreadKind
from intent.features.models import Feature
P = {"org_slug": "catbriar", "project_slug": "intent"}
PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
def test_prototype_pin_and_implementation_status(client, open_project, editor, outsider):
client.force_login(editor)
for title in ("Checkout", "Receipts"):
client.post(reverse("features:create", kwargs=P), {"title": title, "note": ""})
checkout, receipts = Feature.objects.order_by("number")
# A prototype image on the feature.
client.post(
reverse("files:add", kwargs={**P, "feature_key": checkout.key}),
{
"kind": "prototype",
"upload": SimpleUploadedFile("checkout.png", PNG),
"note": "First mockup",
},
)
file = checkout.files.get()
# Someone pins a comment to a point on it.
client.force_login(outsider)
client.post(
reverse(
"files:thread_create", kwargs={**P, "feature_key": checkout.key, "file_id": file.pk}
),
{"body": "Pay button is hard to find", "across": "80", "down": "90"},
)
pin = CommentThread.objects.get(kind=ThreadKind.IMAGE)
file_page = client.get(
reverse("files:detail", kwargs={**P, "feature_key": checkout.key, "file_id": file.pk})
).content.decode()
assert '<circle cx="80.0%" cy="90.0%"' in file_page
assert f'href="#thread-{pin.pk}"' in file_page
# An implementation ships both features, then one feature changes.
client.force_login(editor)
client.post(
reverse("implementations:create", kwargs=P),
{"name": "Storefront", "url": "https://forge.example/shop/storefront"},
)
impl = open_project.implementations.get()
statuses = reverse("implementations:statuses", kwargs={**P, "impl_id": impl.pk})
client.post(statuses, {f"status-{checkout.pk}": "shipped", f"status-{receipts.pk}": "shipped"})
client.post(
reverse("features:edit", kwargs={**P, "feature_key": receipts.key}),
{
"title": "Receipts",
"description": "Email a receipt.",
"status": "active",
"note": "Email",
},
)
page = client.get(
reverse("implementations:detail", kwargs={**P, "impl_id": impl.pk})
).content.decode()
checkout_row = page[page.index(f'id="{checkout.key}"') : page.index(f'id="{receipts.key}"')]
receipts_row = page[page.index(f'id="{receipts.key}"') :]
assert "behind" not in checkout_row
assert "<strong>behind</strong> (now 2)" in receipts_row
assert "Shipped: 2" in page and "Behind the current revision: 1" in page