_private/qwestly-private-docs/SOC2/browser-automation.md
Table of Contents
Vanta Browser Automation Patterns
Last Updated: 2026-05-20
A reference for Playwright MCP automation patterns specific to Vanta workflows. This supplements the general guidance in CLAUDE.md.
Quick Reference
| Pattern | Approach |
|---|---|
| Element interaction | Always use browser_run_code_unsafe with raw Playwright API |
| File uploads | Click button → find input[type="file"] in dialog → setInputFiles() |
| Dropdown selection | Click combobox → wait → click option |
| Recommendation radios | Click the generic container by text content |
| Scrolling | Use page.evaluate(() => window.scrollTo(0, N)) before locating off-screen elements |
Why browser_run_code_unsafe?
The Playwright MCP's built-in tools (browser_click, browser_type, browser_fill_form) all require an element target parameter that comes from the accessibility snapshot. These [ref=e19] references cannot be used as CSS selectors — they fail with "Unexpected token while parsing CSS selector". Additionally, Playwright role-based locators like getByRole('button', { name: 'Upload' }) can hang indefinitely in some Vanta pages, likely due to React re-render loops or overlapping elements.
browser_run_code_unsafe bypasses both issues by executing raw Playwright API calls in the server process.
Element Locator Strategies
When Role Locators Work
Simple elements usually resolve fine:
// Textboxes
await page.getByRole('textbox', { name: 'Identifier' }).fill('value');
// Comboboxes
await page.getByRole('combobox').click();
// Dropdown options
await page.getByRole('option', { name: 'exact text' }).click();
When Role Locators Hang
getByRole('button', { name: 'Upload' }) may hang forever. Fall back to iterating locators:
// Find button by text content
const buttons = page.locator('button');
const count = await buttons.count();
let targetBtn = null;
for (let i = 0; i < count; i++) {
const text = await buttons.nth(i).textContent();
if (text && text.includes('Upload files')) {
targetBtn = buttons.nth(i);
break;
}
}
if (targetBtn) await targetBtn.click();
Elements Off-Screen
Elements above the viewport get negative y coordinates from boundingBox(). Scroll first:
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
Workflow: Upload a Security Assessment / Vendor Review
This is the process for the Security review tab on a vendor page (/c/qwestly.com/vendors/{id}/security-review).
Step 1: Open the Upload Dialog
The "Upload files" button is inside the Assessment section, below the recommendation radios and above the Summary of Findings. It opens a modal dialog.
// Use text-content iteration — role selectors may hang
const buttons = page.locator('button');
for (let i = 0; i < await buttons.count(); i++) {
const text = await buttons.nth(i).textContent();
if (text && text.includes('Upload files')) {
await buttons.nth(i).click();
break;
}
}
await page.waitForTimeout(1500);
Step 2: Upload the File
The dialog contains a hidden file input. Do NOT try page.waitForEvent('filechooser') — this pattern doesn't trigger reliably for Vanta's dialog. Instead, find the file input directly and use setInputFiles():
const dialog = page.getByRole('dialog', { name: /upload/i });
// Upload file via the hidden input
const fileInput = dialog.locator('input[type="file"]');
await fileInput.setInputFiles('/path/to/document.pdf');
await page.waitForTimeout(2000);
Step 3: Fill Required Fields
After the file is attached, the dialog shows three fields:
| Field | Type | Required | Notes |
|---|---|---|---|
| Filename | textbox | Pre-filled | Auto-filled from file name, can override |
| Select (File type) | combobox | Yes | Must pick before Upload enables |
| Description | textbox | No | Optional contextual note |
// Fill description
const desc = dialog.locator('input[name="description"], textarea[name="description"]');
await desc.fill('Description text here.');
// Open the file type dropdown
await dialog.getByRole('combobox').click();
await page.waitForTimeout(500);
// Select the appropriate type
await page.getByRole('option', { name: 'Other Security Assessment' }).click();
await page.waitForTimeout(500);
Available file types (as of 2026-05-20):
- Business Association Agreement
- Contract
- Data Processing Agreement
- ISO 27001 report
- Other
- Other Security Assessment (use for custom security reviews)
- PCI Attestation of Compliance
- Penetration Test report
- Public portal or website
- SOC 1 report
- SOC 2 bridge letter
- SOC 2 report
- SOC 3 report
Step 4: Submit
// Upload button is now enabled
const uploadBtn = dialog.getByRole('button', { name: 'Upload' });
await uploadBtn.click();
await page.waitForTimeout(3000);
Step 5: Fill Summary of Findings
Back on the main page, find the "Summary of findings" textbox:
const summaryBox = page.getByRole('textbox', { name: /summary/i });
await summaryBox.fill(`Key findings bullet points here.`);
Step 6: Select Recommendation
The recommendation options (Approved / Conditionally approved / Not approved) are generic containers with cursor:pointer — not native radio buttons. Click the text element:
await page.getByText('Approved').first().click();
After clicking, the Approved option shows an [active] attribute.
Workflow: Adding Custom Inventory Items
See CLAUDE.md — the existing guidance is still accurate.
Troubleshooting
Browser Already in Use
pkill -f "mcp-chrome"
Then retry.
Button Click Hangs Indefinitely
Vanta pages can trigger React re-render loops that block Playwright role-based locator resolution. Fall back to text-content iteration (see "When Role Locators Hang" above).
Negative Bounding Box Coordinates
Element is above the viewport. Scroll to top before interacting:
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
Dialog Not Appearing
The "Upload files" button is nested deep in the page. If clicking seems to do nothing, wait longer (Vanta's UI can be slow) and check:
const dialogs = page.locator('[role="dialog"]');
const count = await dialogs.count();
Upload Button Stays Disabled
The "Upload" button in the dialog remains disabled until the file type (combobox) is selected. The Description field is optional and doesn't gate the button.
Full Example: Security Review Upload
async (page) => {
// 1. Navigate to the security review page
await page.goto('https://app.vanta.com/c/qwestly.com/vendors/{id}/security-review');
await page.waitForTimeout(2000);
// 2. Open upload dialog by finding button text
const buttons = page.locator('button');
for (let i = 0; i < await buttons.count(); i++) {
const text = await buttons.nth(i).textContent();
if (text && text.includes('Upload files')) {
await buttons.nth(i).click();
break;
}
}
await page.waitForTimeout(1500);
// 3. Upload file
const dialog = page.getByRole('dialog', { name: /upload/i });
await dialog.locator('input[type="file"]')
.setInputFiles('/path/to/assessment.pdf');
await page.waitForTimeout(1000);
// 4. Fill description
await dialog.locator('input[name="description"]')
.fill('Security assessment for...');
// 5. Select file type
await dialog.getByRole('combobox').click();
await page.waitForTimeout(500);
await page.getByRole('option', { name: 'Other Security Assessment' }).click();
await page.waitForTimeout(500);
// 6. Click Upload
await dialog.getByRole('button', { name: 'Upload' }).click();
await page.waitForTimeout(3000);
// 7. Fill summary
await page.getByRole('textbox', { name: /summary/i })
.fill(`Key findings:\n- Point 1\n- Point 2`);
// 8. Select Approved
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
await page.getByText('Approved').first().click();
}