- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.common.keys import Keys
|
|
|
|
from .base import wait
|
|
|
|
|
|
class PostPage:
|
|
def __init__(self, test):
|
|
self.test = test
|
|
|
|
def get_table_rows(self):
|
|
# Post-May08b: #id_post_table is now a <ul> of <li class="post-line">
|
|
# rows (no <ol> numbering). The CSS selector still works since the
|
|
# id is preserved.
|
|
return self.test.browser.find_elements(By.CSS_SELECTOR, "#id_post_table .post-line")
|
|
|
|
@wait
|
|
def wait_for_row_in_post_table(self, line_text, line_number=None):
|
|
# `line_number` retained for backwards compat with callers that
|
|
# passed a position counter — ignored now (lines order by created_at,
|
|
# numbering is gone). Match by text containment instead.
|
|
rows = self.get_table_rows()
|
|
row_texts = [row.text for row in rows]
|
|
self.test.assertTrue(
|
|
any(line_text in t for t in row_texts),
|
|
f"Line text {line_text!r} not in any row: {row_texts!r}",
|
|
)
|
|
|
|
def get_line_input_box(self):
|
|
# /billboard/ new-post applet uses #id_text (creates a fresh Post);
|
|
# post.html aperture uses #id_post_line_text (appends to existing).
|
|
# `add_post_line` is called from both contexts, so probe in order.
|
|
boxes = self.test.browser.find_elements(By.ID, "id_post_line_text")
|
|
if boxes:
|
|
return boxes[0]
|
|
return self.test.browser.find_element(By.ID, "id_text")
|
|
|
|
def add_post_line(self, line_text):
|
|
new_line_no = len(self.get_table_rows()) + 1
|
|
self.get_line_input_box().send_keys(line_text)
|
|
self.get_line_input_box().send_keys(Keys.ENTER)
|
|
self.wait_for_row_in_post_table(line_text, new_line_no)
|
|
return self
|
|
|
|
def get_share_box(self):
|
|
return self.test.browser.find_element(
|
|
By.CSS_SELECTOR,
|
|
'input[name="recipient"]',
|
|
)
|
|
|
|
def get_shared_with_list(self):
|
|
return self.test.browser.find_elements(
|
|
By.CSS_SELECTOR,
|
|
".post-recipient"
|
|
)
|
|
|
|
def share_post_with(self, email):
|
|
# Bud-btn flow (post-Brief sprint): click bottom-left handshake,
|
|
# type the email in the slide-out, click the .btn-confirm OK, wait
|
|
# for the recipient chip.
|
|
bud_btn = self.test.browser.find_element(By.ID, "id_bud_btn")
|
|
bud_btn.click()
|
|
recipient = self.test.wait_for(
|
|
lambda: self.test.browser.find_element(By.ID, "id_recipient")
|
|
)
|
|
recipient.send_keys(email)
|
|
ok = self.test.browser.find_element(
|
|
By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm"
|
|
)
|
|
ok.click()
|
|
self.test.wait_for(
|
|
lambda: self.test.assertIn(
|
|
email, [item.text for item in self.get_shared_with_list()]
|
|
)
|
|
)
|
|
|
|
def get_post_owner(self):
|
|
# `<span id="id_post_owner" hidden>` — Selenium .text returns "" for
|
|
# hidden elements, so read textContent attribute instead.
|
|
return self.test.browser.find_element(
|
|
By.ID, "id_post_owner"
|
|
).get_attribute("textContent")
|