Claude
Skills
Sign in
Back

form-debugger

Included with Lifetime
$97 forever

Analyze all forms on a page: validation states, autocomplete attribute coverage, label/input association, required field audit, submission behavior interception, password field security, and ARIA attributes on custom controls.

Security

What this skill does


# Form Debugger

Perform a comprehensive audit of all forms on a page covering HTML5 validation,
accessibility, autocomplete, security, and submission behavior.

## When to Use

- Auditing form accessibility before launch (labels, ARIA, focus order).
- Debugging validation issues (mismatched patterns, missing required attributes).
- Verifying autocomplete attributes for browser autofill compatibility.
- Checking password field security (autocomplete, visibility toggle, strength).
- Understanding form submission behavior (action, method, encoding, JS intercept).
- Reviewing custom form controls for ARIA compliance.

## Prerequisites

- **Playwright MCP server** connected and responding (all `mcp__playwright__browser_*` tools available).
- Target page must contain one or more `<form>` elements or form controls.

## Workflow

### Step 1 -- Navigate to the Target Page

```
browser_navigate({ url: "<target_url>" })
```

### Step 2 -- Enumerate All Forms and Inputs

Collect detailed metadata about every form and its inputs.

```javascript
browser_evaluate({
  function: `() => {
    const forms = Array.from(document.querySelectorAll('form'));
    const orphanInputs = Array.from(document.querySelectorAll('input:not(form input), select:not(form select), textarea:not(form textarea)'));

    function getInputInfo(input) {
      const labels = [];
      // Explicit label via for attribute
      if (input.id) {
        document.querySelectorAll('label[for="' + input.id + '"]').forEach(l => labels.push(l.textContent.trim()));
      }
      // Implicit label (input inside label)
      const parentLabel = input.closest('label');
      if (parentLabel) labels.push(parentLabel.textContent.trim().substring(0, 100));
      // aria-label and aria-labelledby
      const ariaLabel = input.getAttribute('aria-label');
      const ariaLabelledBy = input.getAttribute('aria-labelledby');
      let ariaLabelText = null;
      if (ariaLabelledBy) {
        ariaLabelText = ariaLabelledBy.split(' ').map(id => {
          const el = document.getElementById(id);
          return el ? el.textContent.trim() : null;
        }).filter(Boolean).join(' ');
      }

      const validity = input.validity ? {
        valid: input.validity.valid,
        valueMissing: input.validity.valueMissing,
        typeMismatch: input.validity.typeMismatch,
        patternMismatch: input.validity.patternMismatch,
        tooLong: input.validity.tooLong,
        tooShort: input.validity.tooShort,
        rangeUnderflow: input.validity.rangeUnderflow,
        rangeOverflow: input.validity.rangeOverflow,
        stepMismatch: input.validity.stepMismatch,
        customError: input.validity.customError,
        validationMessage: input.validationMessage || null
      } : null;

      return {
        tag: input.tagName,
        type: input.type || null,
        name: input.name || null,
        id: input.id || null,
        required: input.required || input.getAttribute('aria-required') === 'true',
        disabled: input.disabled,
        readOnly: input.readOnly || false,
        placeholder: input.placeholder || null,
        autocomplete: input.autocomplete || 'NOT SET',
        pattern: input.pattern || null,
        minLength: input.minLength > -1 ? input.minLength : null,
        maxLength: input.maxLength > -1 ? input.maxLength : null,
        min: input.min || null,
        max: input.max || null,
        value: input.type === 'password' ? '[REDACTED]' : (input.value || '').substring(0, 50),
        labels: labels,
        ariaLabel: ariaLabel,
        ariaLabelText: ariaLabelText,
        hasLabel: labels.length > 0 || !!ariaLabel || !!ariaLabelText,
        role: input.getAttribute('role'),
        ariaInvalid: input.getAttribute('aria-invalid'),
        ariaDescribedBy: input.getAttribute('aria-describedby'),
        validity: validity,
        tabIndex: input.tabIndex
      };
    }

    const formData = forms.map((form, idx) => ({
      index: idx,
      id: form.id || null,
      name: form.name || null,
      action: form.action || null,
      method: form.method || 'GET',
      enctype: form.enctype || null,
      noValidate: form.noValidate,
      target: form.target || null,
      inputCount: form.elements.length,
      inputs: Array.from(form.elements).map(getInputInfo)
    }));

    const orphans = orphanInputs.map(getInputInfo);

    return {
      formCount: forms.length,
      forms: formData,
      orphanInputCount: orphans.length,
      orphanInputs: orphans.slice(0, 20)
    };
  }`
})
```

### Step 3 -- Capture Accessibility Tree for Form Structure

Use the accessibility snapshot to verify how assistive technologies see the
form structure.

```
browser_snapshot()
```

### Step 4 -- Audit Autocomplete Coverage

Check that interactive fields have appropriate `autocomplete` values.

```javascript
browser_evaluate({
  function: `() => {
    const inputs = Array.from(document.querySelectorAll('input, select, textarea'));
    const autocompleteAudit = { covered: 0, missing: 0, incorrect: [], recommendations: [] };

    const typeToAutocomplete = {
      email: 'email',
      tel: 'tel',
      url: 'url',
      password: 'current-password',
      text: null // depends on context
    };

    const nameHints = {
      'first': 'given-name', 'firstname': 'given-name', 'fname': 'given-name',
      'last': 'family-name', 'lastname': 'family-name', 'lname': 'family-name',
      'name': 'name', 'fullname': 'name',
      'email': 'email', 'mail': 'email',
      'phone': 'tel', 'tel': 'tel', 'mobile': 'tel',
      'address': 'street-address', 'street': 'street-address',
      'city': 'address-level2', 'state': 'address-level1',
      'zip': 'postal-code', 'postal': 'postal-code', 'postcode': 'postal-code',
      'country': 'country-name',
      'cc-number': 'cc-number', 'cardnumber': 'cc-number',
      'cc-exp': 'cc-exp', 'expiry': 'cc-exp',
      'cc-csc': 'cc-csc', 'cvv': 'cc-csc', 'cvc': 'cc-csc',
      'username': 'username', 'user': 'username',
      'organization': 'organization', 'company': 'organization'
    };

    for (const input of inputs) {
      if (input.type === 'hidden' || input.type === 'submit' || input.type === 'button' || input.type === 'reset') continue;
      if (input.disabled || input.readOnly) continue;

      const ac = input.autocomplete;
      if (ac && ac !== 'on' && ac !== 'off') {
        autocompleteAudit.covered++;
      } else {
        autocompleteAudit.missing++;
        // Try to recommend based on name/id
        const identifier = (input.name || input.id || '').toLowerCase();
        for (const [hint, value] of Object.entries(nameHints)) {
          if (identifier.includes(hint)) {
            autocompleteAudit.recommendations.push({
              element: input.tagName + (input.id ? '#' + input.id : '') + (input.name ? '[name=' + input.name + ']' : ''),
              current: ac || 'NOT SET',
              recommended: value,
              reason: 'Name/ID contains "' + hint + '"'
            });
            break;
          }
        }
        // Type-based recommendation
        if (typeToAutocomplete[input.type] && !autocompleteAudit.recommendations.find(r => r.element.includes(input.id || input.name || ''))) {
          autocompleteAudit.recommendations.push({
            element: input.tagName + (input.id ? '#' + input.id : '') + (input.name ? '[name=' + input.name + ']' : ''),
            current: ac || 'NOT SET',
            recommended: typeToAutocomplete[input.type],
            reason: 'Input type is "' + input.type + '"'
          });
        }
      }
    }

    return autocompleteAudit;
  }`
})
```

### Step 5 -- Audit Password Field Security

Check password inputs for security best practices.

```javascript
browser_evaluate({
  function: `() => {
    const passwordInputs = Array.from(document.querySelectorAll('input[type="password"]'));
    if (passwordInputs.length === 0) return { found: false, message: 'No password fields found' };

    r

Related in Security