Skip to content

Commit 2864701

Browse files
test(e2e): un-skip add_row + bulk_remove flakes — root cause was test bugs
Previous "state-machine anomaly" skips (logged in qa-findings.jsonl 2026-05-24) were actually three stacked TEST bugs, not app bugs: 1. **Pagination trap (test_wl_save.py + test_crud_workflow.py)**: `wl_table.js:776` jumps `currentPage` to the new last page after add_row. The new row is the LAST visible row on that page (not the first). Tests filling `set_cell_value(0, ...)` instead overwrote existing rows on the new page, and the empty new row got filtered out by `wl_save.js:505` (drops rows where all visible cols are empty). Net effect: zero row growth + one silent edit of an unrelated row. 2. **Comment-column gate (test_crud_workflow.py)**: `getAuditComment` at `wl_save.js:410` does NOT show an audit-comment modal when the CSV has a `Comment` column. Instead it scans EVERY visible row's Comment cell — if any cell is empty (including the new row), the entire save aborts client-side with "Comment field cannot be empty". Tests must fill the Comment cell on the new row. 3. **Read-only Expires cell (test_wl_save.py)**: the `Expires` column uses a date-picker overlay; the underlying input is read-only. `textarea.fill()` fails with "element is not editable". Tests must skip `Expires` (and any `_`-prefixed reserved cols). 4. **Paginated DOM count assertions (both files)**: assertions like `final < initial` or `new_count == initial + 1` on paginated tables are unreliable — `reloadCsvQuiet` after save returns to page 0, which still shows ROWS_PER_PAGE rows even when the total decreased by 2. Replaced with REST `get_csv_content` verification of the true row count. Fixed tests (all four were skip-marked, now active): - tests/e2e/test_crud_workflow.py::test_add_row - tests/e2e/test_crud_workflow.py::test_crud_workflow_end_to_end - tests/e2e/test_wl_save.py::TestAddRowSave::test_add_row_and_save - tests/e2e/test_wl_save.py::TestBulkRemoval::test_bulk_remove Full Python E2E baseline after this commit (playwright 1.60 + chromium 148): 30 passed, 9 skipped, 0 errors (was: 26 passed, 11 skipped, 2 failed) Notes: - The DR55_brute_force_users.csv had 2 polluted rows from prior REST probe sessions (empty-Comment rows that triggered the getAuditComment gate). Cleanup was a one-time REST call; the fixed tests use `backup_csv`/`restore_csv` and won't reintroduce pollution. - The 9 remaining skips are unchanged: multi-tab CP iteration flake (2), multi-user approval flow deferred to .cjs (4), revert UI flow (3) — the last 3 can be implemented now that save-persistence works, but that's separate scope.
1 parent 24b7bde commit 2864701

2 files changed

Lines changed: 92 additions & 45 deletions

File tree

tests/e2e/test_crud_workflow.py

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -81,24 +81,30 @@ def test_edit_cell_and_save(browser, rest_client):
8181

8282
@pytest.mark.crud
8383
@pytest.mark.e2e
84-
@pytest.mark.skip(reason=(
85-
"Known pre-existing state-machine issue: after add_row + set_cell_value(new_value) "
86-
"+ save_changes, REST shows row count unchanged. Same failure mode exists in "
87-
"test_wl_save.py::test_add_row_and_save (10 -> 2 / 10 -> 1 outcomes). Suspected "
88-
"interaction with content_hash optimistic locking when the only change is a new "
89-
"row with mostly-empty fields; needs dedicated investigation against the save "
90-
"pipeline. Not a playwright 1.60 regression (also fails on 1.40 per the same "
91-
"smoke). See qa-findings.jsonl 2026-05-24 entry."
92-
))
9384
def test_add_row(browser, rest_client):
94-
"""Add a row, fill the first column, save. Verify row added via REST.
95-
96-
NOTE on pagination: `add_row` jumps the table to the LAST page (where
97-
the new empty row lives — see wl_table.js:776
98-
`currentPage = ceil(currentRows.length / ROWS_PER_PAGE) - 1`).
99-
With ROWS_PER_PAGE=10, a CSV with 10 rows ends up on a new page 2
100-
showing only the new row, so `tbody tr` count drops from 10 → 1.
101-
Verifying via REST after save sidesteps the pagination DOM trap.
85+
"""Add a row, fill the first column + the Comment column, save.
86+
87+
Two non-obvious traps documented here because both blocked this test
88+
on every playwright version until 2026-05-24:
89+
90+
1) PAGINATION TRAP: after `add_row`, wl_table.js:776 jumps to the
91+
new last page. The new row is the LAST visible row on that page
92+
(not the first). Calling `set_cell_value(0, ...)` would fill an
93+
existing row instead, and the empty new row gets filtered out by
94+
wl_save.js:505 (`filter` keeps rows where SOME visible col is
95+
non-empty). Net effect on the bad path: 1 silent edit + 1 dropped
96+
row = no row growth.
97+
98+
2) COMMENT-COLUMN GATE: `getAuditComment` (wl_save.js:410) does
99+
NOT show an audit-comment modal when the CSV has a `Comment`
100+
column. Instead it scans EVERY row's Comment cell; if ANY cell
101+
is empty (including the new row we just added), the entire save
102+
aborts client-side with "Comment field cannot be empty." — NO
103+
save_csv POST is sent.
104+
105+
Fix: fill the LAST visible row's col 0 AND its Comment cell. The
106+
Comment column index varies per CSV; we find it dynamically by
107+
looking at the `data-header` attribute of each cell in the new row.
102108
"""
103109
bak = setup_clean(rest_client)
104110
initial_total = len(bak.get("rows", []))
@@ -111,9 +117,25 @@ def test_add_row(browser, rest_client):
111117
page.add_row()
112118
time.sleep(0.5)
113119

114-
# The new row is the only one visible on the current (last)
115-
# page; index 0 IS the new row.
116-
page.set_cell_value(0, 0, new_value)
120+
# The new row is the LAST visible row on the current (last) page.
121+
visible_after = page.get_row_count()
122+
assert visible_after > 0, "Expected at least one visible row after add_row"
123+
new_row_idx = visible_after - 1
124+
125+
# Fill col 0
126+
page.set_cell_value(new_row_idx, 0, new_value)
127+
128+
# Find the Comment col index in the new row by data-header attr
129+
new_row = page.page.locator("#csv-table-container table tbody tr").nth(new_row_idx)
130+
cells = new_row.locator("td textarea.wl-input, td input.wl-input")
131+
comment_idx = None
132+
for j in range(cells.count()):
133+
if cells.nth(j).get_attribute("data-header") == "Comment":
134+
comment_idx = j
135+
break
136+
if comment_idx is not None:
137+
page.set_cell_value(new_row_idx, comment_idx, f"E2E add row test row note")
138+
117139
page.save_changes(comment="E2E add row test")
118140

119141
# REST verification: total row count grew by 1, new value present
@@ -227,17 +249,13 @@ def test_horizontal_scroll_wide_csv(browser, rest_client):
227249

228250
@pytest.mark.crud
229251
@pytest.mark.e2e
230-
@pytest.mark.skip(reason=(
231-
"Depends on add_row+save persisting a new row, which is the same path that "
232-
"test_add_row hits and fails on. Skip linked to test_add_row's skip reason — "
233-
"fix both together in the dedicated investigation session."
234-
))
235252
def test_crud_workflow_end_to_end(browser, rest_client):
236253
"""End-to-end: load, edit, add, save, verify all via REST.
237254
238255
Edit happens on visible page 1; add_row jumps to last page (per
239-
wl_table.js pagination semantics). After add, the new row is at
240-
visible index 0 (the only row on the new page).
256+
wl_table.js pagination semantics). The new row is the LAST visible
257+
row on the new last page (see test_add_row docstring for the
258+
pagination-trap explanation).
241259
"""
242260
bak = setup_clean(rest_client)
243261
initial_total = len(bak.get("rows", []))
@@ -252,10 +270,21 @@ def test_crud_workflow_end_to_end(browser, rest_client):
252270
assert page.set_cell_value(0, 0, edit_value)
253271

254272
# Add a row; this jumps to last page where the new row is the
255-
# only visible one (visible index 0).
273+
# LAST visible one. Fill col 0 AND Comment (see test_add_row
274+
# docstring for why Comment is required).
256275
page.add_row()
257276
time.sleep(0.5)
258-
page.set_cell_value(0, 0, add_value)
277+
visible_after_add = page.get_row_count()
278+
assert visible_after_add > 0, "Expected at least one visible row after add_row"
279+
new_row_idx = visible_after_add - 1
280+
page.set_cell_value(new_row_idx, 0, add_value)
281+
# Fill Comment col by data-header lookup
282+
new_row = page.page.locator("#csv-table-container table tbody tr").nth(new_row_idx)
283+
cells = new_row.locator("td textarea.wl-input, td input.wl-input")
284+
for j in range(cells.count()):
285+
if cells.nth(j).get_attribute("data-header") == "Comment":
286+
page.set_cell_value(new_row_idx, j, "E2E e2e row note")
287+
break
259288

260289
# Single save commits both changes
261290
page.save_changes(comment="E2E e2e: edit + add + save")

tests/e2e/test_wl_save.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,20 @@ def test_undo_restores_row(self, page: Page):
318318
class TestBulkRemoval:
319319

320320
def test_bulk_remove(self, page: Page):
321+
"""Bulk-remove 2 rows, save, verify via REST.
322+
323+
NOTE: the old assertion `final < initial` used DOM tbody tr
324+
count, which is paginated and unreliable. After bulk remove +
325+
save + reloadCsvQuiet, if total rows > ROWS_PER_PAGE, page 0
326+
still shows ROWS_PER_PAGE rows (so DOM count stays at 10 even
327+
though we removed 2). Verify via REST instead.
328+
"""
321329
bak = _backup()
330+
initial_total = len(bak.get("rows", []))
331+
if initial_total < 2:
332+
pytest.skip("Need >= 2 rows in CSV")
322333
try:
323334
nav_to_csv(page)
324-
initial = page.locator("#csv-table-container tbody tr").count()
325-
if initial < 2:
326-
pytest.skip("Need >= 2 rows")
327335

328336
cbs = page.locator("#csv-table-container tbody input.wl-row-check")
329337
cbs.nth(0).check()
@@ -343,8 +351,9 @@ def test_bulk_remove(self, page: Page):
343351
modal.locator(".btn-danger, .btn-primary").first.click()
344352
page.wait_for_timeout(3000)
345353

346-
final = page.locator("#csv-table-container tbody tr").count()
347-
assert final < initial, f"Bulk remove failed: {initial} -> {final}"
354+
after = _rest_get("get_csv_content", {"csv_file": TEST_CSV, "app": TEST_APP})
355+
assert len(after.get("rows", [])) < initial_total, \
356+
f"REST row count: {initial_total} -> {len(after.get('rows', []))}, expected decrease"
348357
finally:
349358
_restore(bak)
350359

@@ -484,25 +493,35 @@ def test_comment_column_skips_modal(self, page: Page):
484493
class TestAddRowSave:
485494

486495
def test_add_row_and_save(self, page: Page):
496+
"""Add a row, fill all visible cols, save. Verify via REST.
497+
498+
NOTE: the old assertion `new_count == initial + 1` was wrong on
499+
a paginated table. `add_row` jumps currentPage to the new last
500+
page (wl_table.js:776), where the visible row count is just the
501+
last-page slice — not initial+1. Verify via REST instead, where
502+
we see the true total row count.
503+
"""
487504
bak = _backup()
505+
initial_total = len(bak.get("rows", []))
488506
try:
489507
nav_to_csv(page)
490-
initial = page.locator("#csv-table-container tbody tr").count()
491508

492509
page.locator("#btn-add-row").click()
493510
page.wait_for_timeout(500)
494511

495-
new_count = page.locator("#csv-table-container tbody tr").count()
496-
assert new_count == initial + 1, f"Add row: {initial} -> {new_count}"
497-
498-
# Fill the new row (last row)
512+
# The new row is the LAST visible row on the new last page.
499513
last_row = page.locator("#csv-table-container tbody tr").last
500514
textareas = last_row.locator("textarea.wl-input")
501515
for i in range(textareas.count()):
502516
ta = textareas.nth(i)
503517
hdr = ta.get_attribute("data-header") or ""
504518
if hdr.startswith("_"):
505519
continue
520+
# Expires uses a date-picker overlay and the underlying
521+
# input is read-only — skip it. Comment must be non-empty
522+
# (wl_save.js:410 getAuditComment will abort otherwise).
523+
if hdr == "Expires":
524+
continue
506525
if hdr == "Comment":
507526
ta.fill("E2E new row")
508527
elif hdr == "user":
@@ -516,12 +535,11 @@ def test_add_row_and_save(self, page: Page):
516535
page.locator("#btn-save").click()
517536
page.wait_for_timeout(4000)
518537

519-
msg = page.locator("#message-container")
520-
msg_text = msg.inner_text()
521-
msg_class = msg.get_attribute("class") or ""
522-
assert "success" in msg_class or "Saved" in msg_text or "Added" in msg_text, \
523-
f"Expected save success, got: {msg_text}"
524-
538+
# Verify via REST: total row count grew by 1 (DOM is paginated
539+
# and unreliable for total-row assertions).
540+
after = _rest_get("get_csv_content", {"csv_file": TEST_CSV, "app": TEST_APP})
541+
assert len(after.get("rows", [])) == initial_total + 1, \
542+
f"REST row count: {initial_total} -> {len(after.get('rows', []))}, expected +1"
525543
finally:
526544
_restore(bak)
527545

0 commit comments

Comments
 (0)