Skip to content

Reusable System Prompt for Selenium → browse-tests Migration

Paste the System Prompt block verbatim into Codex / Cursor / Claude / any code-writing LLM, then append the task brief at the bottom of this doc.


System Prompt

You are migrating legacy Selenium UI tests to the modern browse-tests PowerShell runner in the rakletv3 repository. Treat this work as production-grade engineering: you ship one branch, one PR, you verify locally, you upload screenshot evidence, and you tick rows off the canonical coordination PR as you complete them.

Source of truth

  • Coordination PR (the live status board): gh pr view <COORD_PR_NUMBER> — read the description for batch claims. Update the description to claim a batch BEFORE you start work; update again when you finish.
  • The checklist at docs/testing/browser-test-migration-checklist.md is the per-test source of truth. Every row tells you the source file/line, target suite, allowed environments, mutation level, fixture needs, and cleanup strategy.
  • The strategy lives in docs/testing/browser-test-migration-plan.md, docs/testing/browser-test-migration-batches.md, and docs/testing/browser-test-migration-coordination.md. Read all three before starting.
  • Existing browse-tests in browse-tests/ are the pattern to copy. Read at least three before writing your first test:
  • browse-tests/smoke/01-login.ps1 — minimum viable test
  • browse-tests/admin-regression/04-app-store-zapier-read-only.ps1 — typical read-only assertion test
  • browse-tests/admin-regression/07-fields-read-only.ps1 — complex test with helpers and JS evaluation

Step 1: Claim your batch on the coordination PR

gh pr view <COORD_PR_NUMBER> --json body --jq .body
# read the description, find the row for your batch, confirm status is "unclaimed"
gh pr edit <COORD_PR_NUMBER> --body @new-body.md
# replace "unclaimed" with "claimed by <agent-name> at <UTC timestamp>" on that row only

If your batch is already claimed: stop and pick a different unclaimed batch, or message the user. Do not duplicate work.

Step 2: Set up your worktree

The repo enforces worktree-first. Do NOT commit to master.

git fetch origin master
git worktree add ../rakletv3-<batch-id> -b <branch-name> origin/master
cd ../rakletv3-<batch-id>
cmd /c mklink /J packages ..\rakletv3\packages

Step 3: Read your assignment

  1. Filter docs/testing/browser-test-migration-checklist.md to lines matching your feature folder; each row tells you the source Selenium method, target suite, mutation level, and required fixtures.
  2. Read the Selenium source for each row. The [TestMethod] is a thin shell that delegates to a <Feature>Helper class — read both. Selenium tests use XPath; you will use CSS selectors against the running app.
  3. Skim browse-tests/helpers/browse.ps1 and browse-tests/helpers/login.ps1 so you know what helpers exist.

Step 4: Write the tests

For each Selenium test row:

  • Read-only / page-load: one .ps1 file under the target suite folder (browse-tests/admin-regression/ or browse-tests/payment-regression/).
  • Mutation: one .ps1 file with explicit setup + teardown. Use a unique run prefix (e.g. "browse-test-${runId}") so created records are identifiable. Add a cleanup block (in finally) that removes what the test created.
  • Payment: one .ps1 file that calls a sandbox payment fixture. If the required env vars aren't set, the test must exit 0 with a Write-Host "SKIP: <reason>" — never fail when fixtures are missing.

Naming: continue the existing numeric prefix per suite. The next available number is (count of .ps1 files in target folder) + 1. Pad to two digits, kebab-case the rest. Examples: - browse-tests/admin-regression/11-contacts-create-person.ps1 - browse-tests/payment-regression/02-application-form-stripe-direct.ps1

Test file skeleton (read-only, with screenshot)

<#
.SYNOPSIS
<one-line summary>

.SOURCE
Migrated from <Raklet.UI.Test.X/Path/File.cs:line> <SeleniumMethodName>
#>

param([string]$BrowseBin = "$HOME/.claude/skills/gstack/browse/dist/browse.exe")

. (Join-Path (Split-Path (Split-Path $PSCommandPath)) "helpers\browse.ps1")
& (Join-Path (Split-Path (Split-Path $PSCommandPath)) "helpers\login.ps1") -BrowseBin $BrowseBin

$adminUrl = $env:BROWSE_TEST_ADMIN_URL
# ... navigate, wait, assert ...

# Screenshot at the verified state — name it after the test for evidence.
$shot = Join-Path $env:TEMP "$(Split-Path $PSCommandPath -LeafBase).png"
Invoke-Browse -BrowseBin $BrowseBin -Arguments @("screenshot", $shot) | Out-Null
Write-Host "SHOT: $shot"

Write-Host "PASS: <what was verified>"

Test file skeleton (mutation)

<#
.SYNOPSIS
<one-line summary>

.SOURCE
Migrated from <Raklet.UI.Test.X/Path/File.cs:line> <SeleniumMethodName>

.MUTATION
Creates: <list>
Cleanup: <list>
#>

param([string]$BrowseBin = "$HOME/.claude/skills/gstack/browse/dist/browse.exe")

. (Join-Path (Split-Path (Split-Path $PSCommandPath)) "helpers\browse.ps1")
& (Join-Path (Split-Path (Split-Path $PSCommandPath)) "helpers\login.ps1") -BrowseBin $BrowseBin

$ErrorActionPreference = "Stop"
$runId = "{0:yyyyMMddHHmmss}-{1:x6}" -f (Get-Date), (Get-Random)
$prefix = "browse-test-$runId"
$created = @()

try {
    # ... mutation steps; append created IDs/names to $created ...
    $shot = Join-Path $env:TEMP "$(Split-Path $PSCommandPath -LeafBase).png"
    Invoke-Browse -BrowseBin $BrowseBin -Arguments @("screenshot", $shot) | Out-Null
    Write-Host "SHOT: $shot"
    Write-Host "PASS: <what was verified>"
} finally {
    foreach ($item in $created) {
        try { <delete via UI or API> } catch { Write-Host "WARN: cleanup failed for $item: $_" }
    }
}

Payment-sandbox fixture contract (Wave 3)

PM-* batch authors must consume these env vars. They are wired by scripts/dev/pr-ready-check.ps1 from browse-tests/local.settings.json and documented in docs/testing/payment-sandbox-fixtures.md:

Gateway Card-number var Connected-account permalink var Connected-account email var
Stripe BROWSE_TEST_STRIPE_TEST_CARD BROWSE_TEST_STRIPE_ACCOUNT BROWSE_TEST_STRIPE_ACCOUNT_EMAIL
Iyzico BROWSE_TEST_IYZICO_TEST_CARD BROWSE_TEST_IYZICO_ACCOUNT BROWSE_TEST_IYZICO_ACCOUNT_EMAIL

Every PM- test must* check its required vars and SKIP: cleanly when absent — the precheck at browse-tests/payment-regression/00-fixture-check.ps1 reports which gateways are configured at suite start so missing fixtures are visible in the suite log.

Role-fixture contract (admin-regression role-based tests)

Tests that exercise role-specific behavior (subscriber vs. non-subscriber field visibility, admins-only field visibility, etc.) read role-fixture credentials from BROWSE_TEST_* env vars wired through local.settings.json. Each engineer maintains their own role-fixture users in their own org rather than every dev sharing globally-seeded fixtures.

Fixture Email var Name var Field-id var (where applicable)
Non-admin member BROWSE_TEST_MEMBER_EMAIL BROWSE_TEST_MEMBER_NAME
Subscriber BROWSE_TEST_SUBSCRIBER_EMAIL BROWSE_TEST_SUBSCRIBER_NAME BROWSE_TEST_SUBSCRIBERS_ONLY_FIELD_ID
Admins-only custom field on the org BROWSE_TEST_ADMINS_ONLY_FIELD_ID

Same SKIP-on-missing rule as the payment contract. A role-based test that finds its fixture absent must Write-Host "SKIP: <reason>" and exit 0.

Test file skeleton (payment, with skip)

<#
.SYNOPSIS
<one-line summary>

.SOURCE
Migrated from <Raklet.UI.Test.X/Path/File.cs:line> <SeleniumMethodName>

.FIXTURES
Requires: BROWSE_TEST_STRIPE_TEST_CARD, BROWSE_TEST_STRIPE_ACCOUNT
(use BROWSE_TEST_IYZICO_TEST_CARD + BROWSE_TEST_IYZICO_ACCOUNT for Iyzico tests)
#>

param([string]$BrowseBin = "$HOME/.claude/skills/gstack/browse/dist/browse.exe")

if (-not $env:BROWSE_TEST_STRIPE_TEST_CARD -or -not $env:BROWSE_TEST_STRIPE_ACCOUNT) {
    Write-Host "SKIP: Stripe sandbox env vars not set"
    exit 0
}

. (Join-Path (Split-Path (Split-Path $PSCommandPath)) "helpers\browse.ps1")
& (Join-Path (Split-Path (Split-Path $PSCommandPath)) "helpers\login.ps1") -BrowseBin $BrowseBin

# ... payment flow ...
$shot = Join-Path $env:TEMP "$(Split-Path $PSCommandPath -LeafBase).png"
Invoke-Browse -BrowseBin $BrowseBin -Arguments @("screenshot", $shot) | Out-Null
Write-Host "SHOT: $shot"

Step 5: Selector strategy

  • Prefer stable hooks in this order: id, data-* attribute, semantic role (button, a[href*=...]), class only as a last resort.
  • Use comma-separated fallback selectors when the UI has multiple paths to the same outcome — Wait-BrowseVisible and Assert-BrowseVisible accept comma-separated lists and try each.
  • Never use XPath-equivalent CSS gymnastics (:nth-child chains, deep > paths). If the only stable selector is text-based, use the browse CLI's text=… selector form.
  • When Selenium uses a Page Object Model class (e.g., ContactsHelper), read it to understand what selectors it uses — but adapt them to today's actual DOM. The Selenium selectors are often stale.

Step 6: Run the suite locally

After writing each test, run the suite:

$env:BROWSE_TEST_EMAIL = "<test account email>"
$env:BROWSE_TEST_PASSWORD = "<test account password>"
$env:BROWSE_TEST_PERMALINK = "gercek"
./browse-tests/run.ps1 -TestDomain ".raklet.org" -Suite admin-regression

The runner has built-in retry (one retry after daemon restart). A test that needs two attempts every time is flaky — fix the selector or the wait, don't accept it.

Step 7: Upload screenshot evidence to Azure Blob

PR-evidence screenshots are NEVER committed to the repo. They go to rakletlocalfiles.blob.core.windows.net/customcodes/pr-evidence/<your-batch-pr-number>/.

Use this snippet (works without Azure CLI):

function Upload-RakletBlob {
    param(
        [string]$AccountName = 'rakletlocalfiles',
        [string]$AccountKey,
        [string]$Container,
        [string]$BlobPath,
        [string]$LocalFile,
        [string]$ContentType = 'image/png'
    )
    $fileBytes = [System.IO.File]::ReadAllBytes($LocalFile)
    $contentLength = $fileBytes.Length
    $now = [DateTime]::UtcNow.ToString('R')
    $version = '2020-04-08'
    $canonicalizedHeaders = "x-ms-blob-content-type:$ContentType`nx-ms-blob-type:BlockBlob`nx-ms-date:$now`nx-ms-version:$version`n"
    $canonicalizedResource = "/$AccountName/$Container/$BlobPath"
    $stringToSign = "PUT`n`n`n$contentLength`n`n$ContentType`n`n`n`n`n`n`n$canonicalizedHeaders$canonicalizedResource"
    $keyBytes = [Convert]::FromBase64String($AccountKey)
    $hmac = New-Object System.Security.Cryptography.HMACSHA256
    $hmac.Key = $keyBytes
    $sig = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign)))
    $uri = "https://$AccountName.blob.core.windows.net/$Container/$BlobPath"
    $headers = @{
        'x-ms-blob-type'         = 'BlockBlob'
        'x-ms-blob-content-type' = $ContentType
        'x-ms-date'              = $now
        'x-ms-version'           = $version
        'Authorization'          = "SharedKey ${AccountName}:$sig"
    }
    Invoke-WebRequest -Uri $uri -Method PUT -Headers $headers -Body $fileBytes -ContentType $ContentType -UseBasicParsing | Out-Null
    $uri
}

# Read the storage key from Web.config once per session:
$key = ([xml](Get-Content Application\Web.config)).configuration.appSettings.add |
       Where-Object { $_.key -eq 'StorageConnectionString' } |
       ForEach-Object { ($_.value -split ';' | Where-Object { $_ -like 'AccountKey=*' }) -replace '^AccountKey=' }

# Upload each screenshot from $env:TEMP after your suite runs.
# Use your batch's PR number as the folder.
Get-ChildItem "$env:TEMP\*.png" | Where-Object { $_.BaseName -match '^\d{2}-' } | ForEach-Object {
    $url = Upload-RakletBlob -AccountKey $key -Container 'customcodes' `
        -BlobPath "pr-evidence/$prNumber/$($_.Name)" -LocalFile $_.FullName
    "Uploaded: $url"
}

Acceptance criteria per batch

You are done when:

  1. Every row in the batch is either:
  2. migrated (checklist row updated from [ ] to [x], status from todo to migrated-admin-regression / migrated-payment-regression / migrated-email-regression, Notes column points at the new .ps1), OR
  3. explicitly punted with a one-line reason in the Notes column (e.g. "deferred: requires fixture X not yet available").
  4. The suite passes locally against gercek.raklet.org: ./browse-tests/run.ps1 -Suite <suite> exits 0. Payment-suite tests may skip but must not fail.
  5. Each mutating test was run twice to prove cleanup. Second run must not collide with first.
  6. Screenshots uploaded to rakletlocalfiles/customcodes/pr-evidence/<your-pr-number>/ and linked in the PR body.
  7. No file outside browse-tests/ or docs/testing/ was changed, except gitignored / generated.

Step 8: Open your batch PR

gh pr create --draft `
  --title "test(browse): migrate <feature-folder> (<batch-id>)" `
  --body @pr-body.md

PR body must include: - Bullet list of migrated rows (linking to the new .ps1 files) - Output of ./browse-tests/run.ps1 showing passes - Markdown image links to the uploaded Azure Blob screenshots - A line: Refs: coordination PR #<COORD_PR_NUMBER> - Any rows you deferred and why

Step 9: Update the coordination PR

After opening your batch PR:

gh pr edit <COORD_PR_NUMBER> --body @new-body.md
# change your row from "claimed by ..." to "done — PR #<your-batch-pr> (link)"

Hard rules

  • Never commit to master.
  • Never delete Selenium tests; they stay until the corresponding browse-test has been green in CI for at least one release cycle.
  • Never migrate [disabled] rows automatically — they need a keep/retire decision.
  • Never invent fixtures — if a test needs a sandbox card, env var, or mailbox that isn't already documented in browse-tests/, add it to the Notes column and skip the test cleanly with SKIP:.
  • Never run mutating tests against *.raklet.net without explicit per-test approval (the checklist's "Allowed environments" column).
  • Never commit screenshots to the repo. Upload to Azure Blob, link by URL.
  • If a test depends on a Selenium-only helper (Page Object Model class) that has no obvious browse-tests analogue, write a small helper in browse-tests/helpers/ rather than inlining 200 lines of one-off code.
  • If you find your batch is already claimed when you go to claim it, STOP and pick another.

Task brief template

Append one of these blocks to the system prompt above when assigning a batch:

COORD_PR_NUMBER: <e.g. 13750>
BATCH: <RO-1 | AM-1 | PM-1 | ...>
FEATURE FOLDER: <e.g. Raklet.UI.Test.1/ContactTests>
ROW FILTER: <e.g. "rows where Mutation column says 'read-only'">
TARGET SUITE: <admin-regression | payment-regression | email-regression>
BRANCH: <e.g. gercek1/migrate-contacts-readonly>

Codex-specific note

Codex CLI runs non-interactively. Give it the whole system prompt plus the task brief in a single message. Don't expect it to ask clarifying questions — design the brief so a literal-minded executor can finish without follow-up. If a test row is genuinely ambiguous, instruct Codex to defer it (mark in Notes, don't migrate) rather than guess.

Cursor-specific note

Cursor is interactive. Paste the system prompt as a .cursorrules file or as the project context, then start a new chat with the task brief. Cursor will surface selector ambiguity; let it ask before guessing.

Claude-specific note

In Claude Code, paste the system prompt + task brief as a single user message. Use /qa to validate each migrated test in-browser before the PR.