Testing Browser Extensions Before Chrome Web Store Submission

Testing Browser Extensions Before Chrome Web Store Submission

Getting your browser extension rejected from the Chrome Web Store is painful. Google's review can take days to weeks. If your extension gets rejected for a fixable issue — a permissions problem, a policy violation, or a content security issue — you're back to the end of the queue.

The Chrome Web Store has approximately 2,000+ rejected submissions per day, many for avoidable reasons. A thorough pre-submission testing process catches the most common rejection causes before you submit.

Chrome Web Store Rejection Reasons

Understanding why extensions get rejected helps you know what to test for:

Permissions violations (most common): Requesting permissions broader than what the extension actually uses. If your extension requests "<all_urls>" but only needs to run on specific domains, that's a rejection.

Deceptive description/screenshots: The extension's Store listing doesn't accurately match what the extension does.

Remote code execution: Loading JavaScript from external servers (eval(), dynamic Function(), external scripts). Manifest V3 bans most of these patterns.

Privacy policy missing: Any extension that handles user data needs a privacy policy linked in the Store listing.

User data policy violations: Extensions that collect, transmit, or share user data must disclose this prominently and comply with Google's User Data Policy.

Malicious code: Obvious, but includes hidden tracking, ad injection, and behavior that misrepresents what the extension does.

Broken functionality: Extensions that crash, show errors, or don't work as described.

Pre-Submission Testing Checklist

1. Permissions Audit

Audit every permission against actual usage:

// scripts/audit-permissions.js
const manifest = require('./src/manifest.json');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const requestedPermissions = [
  ...(manifest.permissions || []),
  ...(manifest.optional_permissions || []),
  ...(manifest.host_permissions || []),
];

const apiToPermission = {
  'chrome.tabs': 'tabs',
  'chrome.history': 'history',
  'chrome.bookmarks': 'bookmarks',
  'chrome.cookies': 'cookies',
  'chrome.downloads': 'downloads',
  'chrome.geolocation': 'geolocation',
  'chrome.identity': 'identity',
  'chrome.management': 'management',
  'chrome.notifications': 'notifications',
  'chrome.webRequest': 'webRequest',
};

// Scan source files for API usage
function findApiUsage() {
  const sourceDir = './src';
  const jsFiles = execSync(`find ${sourceDir} -name "*.js"`)
    .toString().trim().split('\n');
  
  const usedApis = new Set();
  
  for (const file of jsFiles) {
    const content = fs.readFileSync(file, 'utf8');
    for (const [api, permission] of Object.entries(apiToPermission)) {
      if (content.includes(api)) {
        usedApis.add(permission);
      }
    }
  }
  
  return usedApis;
}

const usedPermissions = findApiUsage();
const unusedPermissions = requestedPermissions.filter(
  p => !usedPermissions.has(p) && !p.includes('://')
);

if (unusedPermissions.length > 0) {
  console.warn('⚠️  Potentially unused permissions:', unusedPermissions.join(', '));
  console.warn('Review and remove if not needed to reduce rejection risk.');
}

Manual verification: For each permission in your manifest, document exactly which features require it. If you can't document a use, remove the permission.

2. Remote Code Execution Check

Chrome Web Store rejects extensions that execute remotely loaded code. Scan for violation patterns:

// scripts/check-remote-code.js
const { execSync } = require('child_process');

const violations = [];

// Check for eval() usage
const evalUsage = execSync("grep -r 'eval(' src/ --include='*.js' -l 2>/dev/null || true")
  .toString().trim();
if (evalUsage) violations.push(`eval() found in: ${evalUsage}`);

// Check for new Function()
const funcUsage = execSync("grep -r 'new Function(' src/ --include='*.js' -l 2>/dev/null || true")
  .toString().trim();
if (funcUsage) violations.push(`new Function() found in: ${funcUsage}`);

// Check for innerHTML with external data
const innerHTMLUsage = execSync("grep -r 'innerHTML' src/ --include='*.js' -l 2>/dev/null || true")
  .toString().trim();
if (innerHTMLUsage) {
  console.warn(`⚠️  innerHTML found in: ${innerHTMLUsage} — review for XSS risk`);
}

// Check for external script loading
const externalScripts = execSync("grep -r 'src.*http' src/ --include='*.html' -l 2>/dev/null || true")
  .toString().trim();
if (externalScripts) violations.push(`External script loading in: ${externalScripts}`);

if (violations.length > 0) {
  console.error('🚫 Remote code execution violations found:');
  violations.forEach(v => console.error(`  ${v}`));
  process.exit(1);
}

3. Content Security Policy Validation

Manifest V3 extensions have CSP enforced automatically, but validate your manifest's CSP if you have custom settings:

// tests/manifest.test.js
const manifest = require('../src/manifest.json');

describe('Content Security Policy', () => {
  it('does not allow unsafe-eval in extension CSP', () => {
    const csp = manifest.content_security_policy?.extension_pages;
    if (csp) {
      expect(csp).not.toContain('unsafe-eval');
      expect(csp).not.toContain('unsafe-inline');
    }
  });
  
  it('does not load scripts from external origins', () => {
    const csp = manifest.content_security_policy?.extension_pages;
    if (csp) {
      // Should only reference 'self'
      expect(csp).not.toMatch(/script-src.*https?:\/\//);
    }
  });
});

4. Data Collection Disclosure Check

If your extension collects any user data, verify your manifest and listing disclose it:

describe('privacy compliance', () => {
  it('privacy policy URL is specified if data is collected', () => {
    // If extension collects user data, this must be in manifest or Store listing
    const collectsData = manifest.permissions?.some(p => 
      ['cookies', 'history', 'identity', 'tabs'].includes(p)
    );
    
    if (collectsData) {
      // At minimum, document this requirement — actual privacy policy is in Store listing
      const hasPrivacyNote = require('../PRIVACY.md');
      expect(hasPrivacyNote).toBeDefined();
    }
  });
  
  it('does not send user data without disclosure', () => {
    // Scan for fetch/XHR calls to external domains
    const externalCalls = execSync(
      "grep -r 'fetch\\|XMLHttpRequest\\|chrome.runtime.sendMessage' src/ --include='*.js' -n 2>/dev/null || true"
    ).toString();
    
    // Manual review required — log for inspection
    console.log('External communications to review:', externalCalls);
  });
});

5. Functional Testing Before Submission

Load the packed extension (not unpacked) for final testing:

# Pack the extension
# Chrome: Extensions > Pack extension (or via CLI)
npx web-ext build --source-dir ./src --artifacts-dir ./dist

# Load the .crx or .zip in Chrome (as unpacked doesn't test packaging)

Test with the packed version:

// tests/e2e/pre-submission.spec.js
test('extension loads without errors from packed format', async () => {
  const errors = [];
  const browser = await chromium.launch({
    args: [
      `--load-extension=${PACKED_EXTENSION_PATH}`,
      '--disable-extensions-except=' + PACKED_EXTENSION_PATH,
    ],
  });
  
  const context = browser.contexts()[0];
  context.on('weberror', err => errors.push(err));
  
  // Test all extension pages
  const extensionId = await getExtensionId(browser);
  const pages = ['popup.html', 'options.html'];
  
  for (const page of pages) {
    const p = await context.newPage();
    await p.goto(`chrome-extension://${extensionId}/${page}`);
    await p.waitForLoadState('networkidle');
    await p.close();
  }
  
  expect(errors).toHaveLength(0);
  await browser.close();
});

6. Performance Testing

Google's review checks for extensions that impact browser performance:

test('extension popup loads within 500ms', async () => {
  const browser = await chromium.launchPersistentContext('', {
    args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],
  });
  
  const extensionId = await getExtensionId(browser);
  const page = await browser.newPage();
  
  const startTime = Date.now();
  await page.goto(`chrome-extension://${extensionId}/popup.html`);
  await page.waitForLoadState('networkidle');
  const loadTime = Date.now() - startTime;
  
  expect(loadTime).toBeLessThan(500);
  await browser.close();
});

test('content script does not significantly increase page load time', async () => {
  // Baseline: page load without extension
  const baselineTime = await measurePageLoad('https://example.com', { noExtension: true });
  
  // With extension
  const extensionTime = await measurePageLoad('https://example.com', { withExtension: true });
  
  const overhead = extensionTime - baselineTime;
  expect(overhead).toBeLessThan(200); // Extension adds less than 200ms
});

7. Store Listing Validation

Before submission, verify your Store listing assets:

  • Extension name ≤ 75 characters
  • Short description ≤ 132 characters (Chrome requirement)
  • Detailed description: accurate, no misleading claims
  • Screenshots: 1280×800 or 640×400 pixels, show actual extension UI
  • Promotional images match extension category (if provided)
  • Privacy policy URL accessible and describes actual data handling
  • Extension version follows semantic versioning

Automated Pre-Submission Pipeline

# .github/workflows/pre-submission.yml
name: Pre-Submission Checks

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  pre-submission:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      
      - run: npm ci
      
      - name: Lint manifest
        run: npx web-ext lint --source-dir ./src
      
      - name: Audit permissions
        run: node scripts/audit-permissions.js
      
      - name: Check remote code execution
        run: node scripts/check-remote-code.js
      
      - name: Build extension
        run: npm run build
      
      - name: Run unit tests
        run: npm test
      
      - name: Run E2E tests
        run: xvfb-run npx playwright test --project=chrome
      
      - name: Run performance tests
        run: xvfb-run npx playwright test --project=performance
      
      - name: Generate submission report
        run: node scripts/generate-submission-report.js
      
      - uses: actions/upload-artifact@v4
        with:
          name: submission-package
          path: |
            dist/
            submission-report.md

What Google Actually Reviews

The review process is primarily automated with human review for flagged extensions. The automated checks focus on:

  • Manifest validation (permissions, CSP)
  • Static code analysis for policy violations
  • Network request analysis (what external services does it call?)
  • Store listing accuracy

Human reviewers look at:

  • Extensions with broad permissions
  • Extensions requesting sensitive permissions (downloads, nativeMessaging)
  • Extensions with significant user data access
  • Extensions in sensitive categories

Summary

Pre-submission testing should verify:

  1. Permissions: Only request permissions you actually use
  2. Remote code: No eval(), no external script loading
  3. CSP: Extension CSP doesn't allow unsafe patterns
  4. Privacy: Data collection is disclosed if present
  5. Functionality: Extension works from packed format, not just unpacked
  6. Performance: Popup loads fast, content script overhead is minimal
  7. Store listing: Accurate description, correct screenshot dimensions

The most impactful checks are permissions audit and remote code scanning — these catch the two most common rejection reasons. Run these automatically in CI so you catch violations before they reach submission.

Read more

Start now free