_private/qwestly-private-docs/SOC2/browser-automation.md

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();
}