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.mdis 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, anddocs/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 testbrowse-tests/admin-regression/04-app-store-zapier-read-only.ps1— typical read-only assertion testbrowse-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¶
- Filter
docs/testing/browser-test-migration-checklist.mdto lines matching your feature folder; each row tells you the source Selenium method, target suite, mutation level, and required fixtures. - Read the Selenium source for each row. The
[TestMethod]is a thin shell that delegates to a<Feature>Helperclass — read both. Selenium tests use XPath; you will use CSS selectors against the running app. - Skim
browse-tests/helpers/browse.ps1andbrowse-tests/helpers/login.ps1so you know what helpers exist.
Step 4: Write the tests¶
For each Selenium test row:
- Read-only / page-load: one
.ps1file under the target suite folder (browse-tests/admin-regression/orbrowse-tests/payment-regression/). - Mutation: one
.ps1file with explicit setup + teardown. Use a unique run prefix (e.g."browse-test-${runId}") so created records are identifiable. Add a cleanup block (infinally) that removes what the test created. - Payment: one
.ps1file that calls a sandbox payment fixture. If the required env vars aren't set, the test must exit 0 with aWrite-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-BrowseVisibleandAssert-BrowseVisibleaccept comma-separated lists and try each. - Never use XPath-equivalent CSS gymnastics (
:nth-childchains, deep>paths). If the only stable selector is text-based, use the browse CLI'stext=…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:
- Every row in the batch is either:
- migrated (checklist row updated from
[ ]to[x], status fromtodotomigrated-admin-regression/migrated-payment-regression/migrated-email-regression, Notes column points at the new.ps1), OR - explicitly punted with a one-line reason in the Notes column (e.g. "deferred: requires fixture X not yet available").
- 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. - Each mutating test was run twice to prove cleanup. Second run must not collide with first.
- Screenshots uploaded to
rakletlocalfiles/customcodes/pr-evidence/<your-pr-number>/and linked in the PR body. - No file outside
browse-tests/ordocs/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 withSKIP:. - Never run mutating tests against
*.raklet.netwithout 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.