diff --git a/scripts/chrome-reddit-verify-phase17b.py b/scripts/chrome-reddit-verify-phase17b.py
index e97d7a8..419610c 100644
--- a/scripts/chrome-reddit-verify-phase17b.py
+++ b/scripts/chrome-reddit-verify-phase17b.py
@@ -135,21 +135,27 @@ async def click_overflow_menu_item(page: Page, label: str) -> bool:
async def dump_form(page: Page) -> dict:
"""Snapshot the visible Devvit form (faceplate-form). Returns shape:
- { title, description, fields: [{name, type, defaultValue, options}] }
+ { title, description, dialogText, fields: [{name, type, defaultValue, options}] }
+ Devvit's modal markup doesn't expose a stable `
` for the title or
+ `[slot="description"]`, so the test logic prefers `dialogText` (full
+ dialog text-content) + field-name signals over the formal title/desc.
"""
return await page.evaluate(
"""() => {
const form = document.querySelector('faceplate-form');
if (!form) return { error: 'no faceplate-form' };
- const out = { title: '', description: '', acceptLabel: '', cancelLabel: '', fields: [] };
- // Best-effort title/description extraction — Devvit renders these as
- // h2/p inside the modal.
- const root = form.closest('faceplate-dialog') || form.parentElement;
+ const out = { title: '', description: '', dialogText: '', acceptLabel: '', cancelLabel: '', fields: [] };
+ const root = form.closest('faceplate-dialog') || form.closest('shreddit-dialog') || form.parentElement;
if (root) {
- const h2 = root.querySelector('h2');
- if (h2) out.title = h2.innerText.trim();
- const desc = root.querySelector('[slot="description"], .description, p');
- if (desc) out.description = desc.innerText.trim().slice(0, 600);
+ // Best-effort title — try a handful of common selectors.
+ const h2 = root.querySelector('h2, [slot="title"], [data-testid="dialog-title"]');
+ if (h2) out.title = (h2.innerText || h2.textContent || '').trim();
+ const desc = root.querySelector('[slot="description"], .description');
+ if (desc) out.description = (desc.innerText || desc.textContent || '').trim().slice(0, 600);
+ // Always capture the full dialog text — onboarding cards / token
+ // cost lines / "Welcome" greetings show up here even when there's
+ // no formal description slot.
+ out.dialogText = (root.innerText || root.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 2000);
const accept = root.querySelector('button[type="submit"], [data-form-accept], faceplate-button[type="submit"]');
if (accept) out.acceptLabel = (accept.innerText || accept.textContent || '').trim();
const cancel = root.querySelectorAll('button, faceplate-button');
@@ -287,44 +293,105 @@ async def shot(name: str) -> str:
await shot("04-after-submit")
# ── 3 · expect Clarify (with select field) ──────────────────────
- title = (clarify_form.get('title') or '').lower()
- is_clarify = 'clarify' in title or 'round' in (clarify_form.get('description') or '').lower()
+ # Devvit doesn't expose a stable title node, so detect by field-name
+ # signals: clarificationTurn (state-carrier added by Phase 1.7b) is
+ # present iff the server returned the clarify modal.
+ field_names = {f.get('name', '') for f in clarify_form.get('fields', [])}
+ is_clarify = 'clarificationTurn' in field_names
select_fields = [f for f in clarify_form.get('fields', []) if 'select' in (f.get('type') or '')]
clarify_pass = is_clarify and len(select_fields) > 0
+ # Bonus: the dialog text should contain the "(Round X of N)" prefix
+ # from the prompt.
+ dlg = (clarify_form.get('dialogText') or '').lower()
+ round_visible = 'round' in dlg and 'of' in dlg
report.add(StepResult(
"clarify-renders-select",
clarify_pass,
- f"is_clarify={is_clarify} select_fields={[f['name'] for f in select_fields]} all_fields={[f['name'] for f in clarify_form.get('fields', [])]}",
- extra={"form": clarify_form},
+ f"is_clarify={is_clarify} select_fields={[f['name'] for f in select_fields]} round_visible={round_visible}",
+ extra={"form": clarify_form, "round_visible": round_visible},
))
if not clarify_pass:
# The model didn't ask for clarification on this input — the
# Confirm form should have opened directly. Fall through to
# confirm-form check.
- print(f"[verify] no clarify modal — assuming confirm path. title={title!r}")
+ print(f"[verify] no clarify modal — assuming confirm path.")
else:
- # Pick the first option in the select, then Re-compile.
+ # Pick a suggested option via direct DOM mutation (the Devvit
+ # `faceplate-select` Lit element doesn't respond to a vanilla
+ # locator.click, but assigning .value + dispatching change/input
+ # events drives the same code path the human click would).
+ picker_result = {}
try:
- select = page.locator('faceplate-form').first.locator('select, faceplate-select').first
- await select.click(timeout=3_000)
- await page.wait_for_timeout(500)
- # Pick the first non-default value.
- opts = select_fields[0].get('options', [])
- if len(opts) > 0:
- await page.evaluate(
- """val => {
- const sel = document.querySelector('faceplate-form select, faceplate-form faceplate-select');
- if (!sel) return false;
- if (sel.tagName === 'SELECT') sel.value = val;
- else sel.setAttribute('value', val);
- sel.dispatchEvent(new Event('change', { bubbles: true }));
- return true;
- }""",
- opts[0]['value'],
- )
+ picker_result = await page.evaluate(
+ """() => {
+ const form = document.querySelector('faceplate-form');
+ if (!form) return { ok: false, reason: 'no faceplate-form' };
+ // Find the named clarificationAnswer wrapper, then any
+ // control inside it.
+ const wrap = form.querySelector('[data-field-name="clarificationAnswer"], faceplate-form-field[name="clarificationAnswer"], [name="clarificationAnswer"]');
+ const inspect = (el) => ({
+ tag: el.tagName.toLowerCase(),
+ name: el.getAttribute('name') || '',
+ role: el.getAttribute('role') || '',
+ valueAttr: el.getAttribute('value') || '',
+ valueProp: el.value !== undefined ? String(el.value) : '',
+ });
+ const found = [];
+ // Scan inside the wrap (or form root if wrap not found)
+ // for ANY interactive control.
+ const scope = wrap || form;
+ const ctls = scope.querySelectorAll('select, faceplate-select, faceplate-radio-group, faceplate-listbox-button, [role="combobox"], [role="listbox"], [role="radiogroup"], button, faceplate-radio-input');
+ for (const c of ctls) found.push(inspect(c));
+ // Also list any visible options in the document (Devvit may
+ // render them as siblings of the select once expanded).
+ const opts = [...document.querySelectorAll('option, faceplate-listbox-item, faceplate-radio-input, [role="option"]')]
+ .map(o => ({ tag: o.tagName.toLowerCase(), text: (o.textContent || '').trim().slice(0, 60), value: o.getAttribute('value') || '' }));
+ // Attempt 1 — pick the SECOND option of the first
+ // matching native