Discover a new web app and bootstrap an app-specific Selenium test skill for it. $ARGUMENTS

The input may include a URL and optionally login credentials (username and password).
This skill is a bootstrapper — its output is a new app-specific skill file, not a test script.

---

## Steps to follow

1. **Identify the app name**
   - Extract a short lowercase name from the URL subdomain or path (e.g. `elroderick` from `elroderick.experimental.algrthmz.com`, `dashboard` from `myapp.com/dashboard`).

2. **Build the discovery script**
   - Do NOT use WebFetch, browser plugins, or any Chrome extension to inspect the app.
   - Do NOT use the Claude for Chrome plugin or any browser-based tool.
   - The only permitted discovery method is a **Python Selenium script**.
   - Write a single script named `discover_<appname>.py` that:
     - Logs in using the provided credentials
     - Navigates every internal page it can find
     - For each page, prints: all headings, nav links, buttons, form fields, and table structures
     - For form fields: dump **all** inputs including hidden ones (skip only `_csrf` and flatpickr calendar sub-inputs like `flatpickr-monthDropdown-months` and `numInput cur-year`). Hidden inputs often carry the actual field name for date pickers and pre-assigned record IDs.
     - For list/table pages: check for pagination controls (`[aria-label*="page"]`, `.pagination`, `[class*="page"]`, Next/Previous links). Record the visible row count and whether more pages exist.
     - For new-record forms (e.g. `/view/ledger_edit` with no id param): note whether a hidden `input[name="id"]` is present — this is a pre-assigned record ID that can be used for direct verification after save.
     - Writes all output to both the console (`print`) and a file named `discover_<appname>_result.txt` using a `log(line, f)` helper that calls `print`, `f.write`, and `f.flush`
     - Opens the result file in **append mode** (`"a"`) with a `=== RUN <timestamp> ===` header at the start
     - Uses the standard headless Chrome setup (see boilerplate below)
     - Derives the result filename from `__file__` using `SCRIPT_STEM`
   - Write the script, then stop — do not run it via Bash. The user will upload and run it on the web app runner.

3. **Discover the app's pages and forms**
   - Read the result file the user provides after running the discovery script on the web app runner.
   - If the output is incomplete, write a follow-up targeted discovery script for specific pages or forms and repeat until you have a complete picture of the app.

4. **Build the app knowledge document**
   - From the discovery output, compile:
     - Authentication flow (selectors, post-login URL check)
     - Full URL structure (list, create, edit, dashboard paths)
     - Every form's fields — include hidden inputs; note which fields appear in the table but have NO form input (these are calculated server-side and must not be filled by test scripts)
     - For date pickers: the **hidden input name** (e.g. `name="date"`) that flatpickr updates — this is what the server reads, not the visible display input (which has no `name`)
     - Whether new-record forms pre-assign a record ID in a hidden `input[name="id"]` — if so, this ID can be used to navigate directly to the saved record for verification
     - Whether list pages are paginated and at what approximate row limit — this determines how verification must be done (direct record URL, not row count)
     - Any UI quirks (flatpickr, styled buttons that need JS click, modals, inline forms)
     - Table column layouts (index, field name, format — currency with €/comma, date locale, raw decimal)
     - Which columns are safe for numeric comparison vs need special parsing

5. **Write the app-specific skill file**
   - Create `<appname>/generate-<appname>-test.md` in the project folder (not in `.claude/commands/`) with the following structure:
     - A header describing the app (name, URL, credentials)
     - An **App knowledge** section containing all compiled information from step 4 — laid out so future test generation needs no discovery at all
     - A **Steps to follow** section that instructs the skill to:
       - Ask what scenario to test if not provided
       - Use this folder structure — the script will be uploaded manually to the web app runner, so write it as a standalone file with no subfolder assumptions:
         - Script filename: `test_<appname>_<scenario>.py`
         - Result file: derived automatically from `__file__` — always use this pattern at the top of every script:
           ```python
           SCRIPT_DIR  = os.path.dirname(os.path.abspath(__file__))
           SCRIPT_STEM = os.path.splitext(os.path.basename(__file__))[0]
           RESULT_FILE = os.path.join(SCRIPT_DIR, f"{SCRIPT_STEM}_result.txt")
           ```
       - Never overwrite an existing script — append a number to the filename if it already exists
       - Write the Selenium script using only the embedded knowledge
       - Always randomise test data using `random` and `datetime`
       - **Number inputs — always use the JS setter, never `clear()` + `send_keys()`:**
         `clear()` is unreliable on `type=number` inputs in headless Chrome; the value may not clear or the browser may reject the interaction. Use this helper instead:
         ```python
         def set_number_field(driver, f, name, value):
             el = driver.find_element(By.CSS_SELECTOR, f'input[name="{name}"]')
             driver.execute_script(
                 "arguments[0].value = ''; arguments[0].value = arguments[1];"
                 "arguments[0].dispatchEvent(new Event('input', {bubbles:true}));"
                 "arguments[0].dispatchEvent(new Event('change', {bubbles:true}));",
                 el, str(value)
             )
             log(f"  Set {name!r} = {value}", f)
         ```
       - **Flatpickr date fields — two steps required:**
         The visible flatpickr input has no `name` attribute and is never submitted. The server reads a **hidden** input (e.g. `input[name="date"]`). After calling the flatpickr JS API, always also set the hidden input directly:
         ```python
         def set_flatpickr(driver, f, name, y, m, d):
             date_iso = f"{y}-{m:02d}-{d:02d}"
             try:
                 driver.execute_script(f"""
                     var fp = document.querySelector('.flatpickr-input')._flatpickr;
                     fp.setDate(new Date({y}, {m-1}, {d}), true);
                     fp.close();
                 """)
             except Exception as e:
                 log(f"  flatpickr JS failed ({e})", f)
             result = driver.execute_script(f"""
                 var hidden = document.querySelector('input[name="{name}"]');
                 if (hidden) {{
                     hidden.value = '{date_iso}';
                     hidden.dispatchEvent(new Event('input', {{bubbles:true}}));
                     hidden.dispatchEvent(new Event('change', {{bubbles:true}}));
                     return 'set: ' + hidden.value;
                 }}
                 return 'WARNING: hidden input not found';
             """)
             log(f"  Date hidden input ({name!r}) → {{result}}", f)
         ```
         The `name` argument (e.g. `"date"`) comes from the app knowledge document.
       - **Verifying a create — never count rows in a list page:**
         List pages are often paginated. A new record at the end of the list may not appear on the current page, causing a row-count check to always report "Expected N+1, got N". Instead:
         1. Before submitting the new-record form, capture the pre-assigned ID: `driver.execute_script("var el = document.querySelector('input[name=\"id\"]'); return el ? el.value : null;")`
         2. After submit, navigate directly to the edit URL: `driver.get(f"{BASE_URL}/view/<edit_path>?id={new_id}")`
         3. Read back the saved field values from the edit form and assert they match the submitted values
       - **Always log pre-submit field values** — right before clicking Save, read and log each form field value (including hidden date input) to confirm the values are actually in the DOM. This catches JS-set values being silently ignored.
       - **Do not try to fill calculated fields** — some columns visible in the list table (e.g. "Transaction Total") are computed server-side and have no form input. Attempting to fill them wastes time and produces warnings. Only fill fields that appear in the form dump.
       - Always call `.clear()` before `.send_keys()` on plain **text** inputs (not number inputs — use the JS setter for those)
       - Handle `UnexpectedAlertPresentException` by printing the alert, dismissing it, and exiting cleanly
       - **Credentials** — never hardcode usernames or passwords. Always use empty string fallbacks:
         ```python
         USERNAME = os.environ.get('APP_USERNAME', '')
         PASSWORD = os.environ.get('APP_PASSWORD', '')
         ```
         The web app runner prompts for credentials before running and injects `APP_USERNAME` and `APP_PASSWORD` as environment variables at run time. Users can also save credentials to a `.env` file in the app folder for automatic injection without prompting.
       - **Always use this exact Chrome setup** — the script runs headlessly on a Linux server:
         ```python
         import tempfile
         from selenium import webdriver
         from selenium.webdriver.chrome.service import Service
         from webdriver_manager.chrome import ChromeDriverManager

         options = webdriver.ChromeOptions()
         options.add_argument("--headless=new")
         options.add_argument("--no-sandbox")
         options.add_argument("--disable-setuid-sandbox")
         options.add_argument("--disable-dev-shm-usage")
         options.add_argument("--disable-gpu")
         options.add_argument("--disable-extensions")
         options.add_argument("--remote-debugging-port=0")
         options.add_argument(f"--user-data-dir={tempfile.mkdtemp()}")

         driver = webdriver.Chrome(
             service=Service(ChromeDriverManager().install()),
             options=options,
         )
         ```
       - **Result file rules** — the web app runner reads this file to determine PASS/FAIL:
         - Open in **append mode** (`"a"`) so each run adds to the history — the runner displays the full file and detects PASS/FAIL from the last run block
         - Use a `log(line, f)` helper that calls `print(line)`, `f.write(line + "\n")`, and `f.flush()`
         - Every run must start with `=== RUN <timestamp> ===` so runs are clearly separated in the output
         - The last meaningful line of each run must be either `PASS` or `FAIL: <reason>` — the UI banner and sidebar dot are determined by the last occurrence of PASS or FAIL in the file
         - Wrap the entire test body in `try/except` inside the `with open(...)` block so the file is always written even if the script crashes
         - Do **not** call `input()` — the script runs non-interactively on the server. Remove any keep-browser-open logic entirely.
       - **Boilerplate template** every generated script must follow:
         ```python
         import os, tempfile, random
         from datetime import datetime, timedelta, timezone
         from selenium import webdriver
         from selenium.webdriver.chrome.service import Service
         from selenium.webdriver.common.by import By
         from selenium.webdriver.support.ui import WebDriverWait, Select
         from selenium.webdriver.support import expected_conditions as EC
         from selenium.common.exceptions import UnexpectedAlertPresentException
         from webdriver_manager.chrome import ChromeDriverManager

         SCRIPT_DIR  = os.path.dirname(os.path.abspath(__file__))
         SCRIPT_STEM = os.path.splitext(os.path.basename(__file__))[0]
         RESULT_FILE = os.path.join(SCRIPT_DIR, f"{SCRIPT_STEM}_result.txt")

         def log(line, f):
             print(line)
             f.write(line + "\n")
             f.flush()

         def set_number_field(driver, f, name, value):
             el = driver.find_element(By.CSS_SELECTOR, f'input[name="{name}"]')
             driver.execute_script(
                 "arguments[0].value = ''; arguments[0].value = arguments[1];"
                 "arguments[0].dispatchEvent(new Event('input', {bubbles:true}));"
                 "arguments[0].dispatchEvent(new Event('change', {bubbles:true}));",
                 el, str(value)
             )
             log(f"  Set {name!r} = {value}", f)

         def set_flatpickr(driver, f, name, y, m, d):
             date_iso = f"{y}-{m:02d}-{d:02d}"
             try:
                 driver.execute_script(f"""
                     var fp = document.querySelector('.flatpickr-input')._flatpickr;
                     fp.setDate(new Date({y}, {m-1}, {d}), true);
                     fp.close();
                 """)
             except Exception as e:
                 log(f"  flatpickr JS failed ({{e}})", f)
             result = driver.execute_script(f"""
                 var hidden = document.querySelector('input[name="{name}"]');
                 if (hidden) {{{{
                     hidden.value = '{date_iso}';
                     hidden.dispatchEvent(new Event('input', {{{{bubbles:true}}}}));
                     hidden.dispatchEvent(new Event('change', {{{{bubbles:true}}}}));
                     return 'set: ' + hidden.value;
                 }}}}
                 return 'WARNING: hidden input not found';
             """)
             log(f"  Date ({name!r}) → {{result}}", f)

         with open(RESULT_FILE, "a") as f:
             try:
                 log(f"=== RUN {datetime.now(timezone.utc).isoformat()} ===", f)

                 options = webdriver.ChromeOptions()
                 options.add_argument("--headless=new")
                 options.add_argument("--no-sandbox")
                 options.add_argument("--disable-setuid-sandbox")
                 options.add_argument("--disable-dev-shm-usage")
                 options.add_argument("--disable-gpu")
                 options.add_argument("--disable-extensions")
                 options.add_argument("--remote-debugging-port=0")
                 options.add_argument(f"--user-data-dir={tempfile.mkdtemp()}")

                 driver = webdriver.Chrome(
                     service=Service(ChromeDriverManager().install()),
                     options=options,
                 )
                 wait = WebDriverWait(driver, 10)

                 # --- test body goes here ---
                 # Pattern for create-and-verify:
                 #   new_id = driver.execute_script("var el = document.querySelector('input[name=\"id\"]'); return el ? el.value : null;")
                 #   ... fill form ...
                 #   log("[PRE-SUBMIT]", f)
                 #   for field in ("date", "market_value", ...):
                 #       val = driver.execute_script(f"var el = document.querySelector('[name=\"{field}\"]'); return el ? el.value : 'NOT FOUND';")
                 #       log(f"  {field} = {val!r}", f)
                 #   submit_btn.click(); time.sleep(2)
                 #   driver.get(f"{BASE_URL}/view/<edit_path>?id={new_id}")
                 #   ... assert saved values ...

                 log("PASS", f)

             except UnexpectedAlertPresentException as e:
                 try:
                     alert = driver.switch_to.alert
                     log(f"ALERT: {alert.text}", f)
                     alert.dismiss()
                 except Exception:
                     pass
                 log(f"FAIL: UnexpectedAlertPresentException — {e}", f)
                 driver.quit()

             except AssertionError as e:
                 log(f"FAIL: {e}", f)
                 driver.quit()

             except Exception as e:
                 import traceback
                 log(f"FAIL (unexpected): {e}", f)
                 log(traceback.format_exc(), f)
                 driver.quit()
         ```

6. **Add discovery scripts and output files to `.gitignore`**
   - Ensure `discover_<appname>.py` and `discover_<appname>_output.txt` are listed in `.gitignore` so credentials are never committed.
