How to Debug Browser Tests That Fail Only When Content Security Policy Blocks a Script, Frame, or Inline Handler
By Markus Gasser · August 25, 2026
A practical guide to debugging browser test failures caused by Content Security Policy, including console signals, blocked script and frame patterns, inline handler issues, and a workflow to separate app bugs from policy misconfigurations.
When a browser test passes in dev but fails in QA, CSP is one of the first things I check. A page can look healthy while the browser quietly refuses to execute a script, load a frame, or honor an inline event handler. The result is usually a confusing failure: a missing button action, an empty modal, a never-loading widget, or a test that times out waiting for DOM that should have appeared.
The key distinction is simple: CSP is a browser-enforced policy, not an app exception. Your code may be present, but the browser blocks it before it runs. That is why the fastest path to diagnosis is to capture the browser’s own CSP violation signals, then confirm whether the blocked resource is actually required for the test, or whether the policy is doing exactly what it was designed to do.
If the console says a script, frame, or inline handler was refused by CSP, treat that as evidence first, not noise.
The failure shape you are looking for
CSP-related browser test failures usually fall into one of three buckets:
- Blocked script execution, a
<script>tag, injected test helper, analytics snippet, or bundled asset is refused. - Blocked frame loading, an iframe never renders because
frame-srcorchild-srcdisallows it, or embedding is refused because offrame-ancestors. - Blocked inline handler or inline script, a click handler defined as
onclick="...", a string-basedsetTimeout, or inline script is blocked because the policy does not allow'unsafe-inline', a nonce, or a hash.
These are not identical problems, and they need different checks. The browser may emit a clear console message for each one. Start there.
For the policy syntax itself, the Content Security Policy Level 3 specification is the canonical reference. For a developer-oriented summary of directives and console behavior, the MDN CSP guide is also useful.
First question: is the test broken, or is the policy blocking it?
Before changing code or relaxing policy, answer three questions:
- Is the failure reproducible only in environments with a different CSP header or meta tag?
- Does the browser console show a CSP violation message at the same time the test fails?
- Does the same action work when the policy is removed or replaced with a known-safe test policy?
If the answer to the first two is yes, the issue is likely policy-related. If the browser logs are clean and only the test assertion fails, then you may be looking at a separate app bug, timing problem, or selector issue.
A good debugging habit is to compare the exact response headers between environments. CSP is often set in one of two places:
- The
Content-Security-PolicyHTTP response header - A
<meta http-equiv="Content-Security-Policy" ...>tag in the page
Headers win for the full document response. A meta tag can still surprise you in local or staged builds if the app injects one conditionally.
What to capture from the browser console
When a test fails, collect the console text and network context together. The most useful CSP messages usually include:
- The directive that blocked the action, such as
script-src,frame-src,child-src,default-src,style-src, orscript-src-elem - The blocked URL or a note that
inlinewas refused - The document URL where the violation happened
- A source line or stack trace, when the browser provides one
A typical script error might look like this:
text Refused to load the script ‘https://example-cdn.test/widget.js’ because it violates the following Content Security Policy directive: “script-src ‘self’”.
A frame-related message often looks like this:
text Refused to frame ‘https://checkout.example.test/’ because it violates the following Content Security Policy directive: “frame-src ‘self’”.
An inline handler failure may appear like this:
text Refused to execute inline event handler because it violates the following Content Security Policy directive: “script-src ‘self’”.
The exact wording varies by browser, but the directive name is the important part.
Playwright example, capture console and page errors
If you are debugging in Playwright, wire console events into the test so the violation is visible in the failure log:
page.on('console', msg => {
if (msg.type() === 'error') {
console.log('console error:', msg.text());
}
});
page.on(‘pageerror’, err => { console.log(‘page error:’, err.message); });
This does not replace browser devtools, but it gives you a reproducible trail in CI.
How to separate script, frame, and inline-handler problems
1) Script blocked by CSP in automation
This usually shows up when the page depends on a script bundle, third-party widget, or test helper that is allowed in one environment but not another. Common causes include:
- A CDN host missing from
script-src - A nonce or hash mismatch after a deployment change
- The test injecting JavaScript into the page in a way the browser policy does not allow
- A third-party script loading in dev but not in QA because the QA policy is stricter
Your job here is to identify whether the script is essential to the app flow or incidental to the test. If the test only breaks because a helper script cannot run, the issue may be in the test design, not the product.
2) Frame-related failures
Frames are easy to confuse because there are two different policy ideas in play:
frame-srcandchild-srcaffect what the page is allowed to embedframe-ancestorsaffects which parents are allowed to embed the page
That distinction matters. If your test opens a page that contains an iframe, you are usually dealing with frame-src or child-src. If your app is trying to embed a page inside another page and the browser refuses, frame-ancestors may be the blocker.
frame-ancestorsis about who may embed the page, not what the page may embed.
This often becomes visible in payment flows, identity providers, embedded help centers, and sandboxed preview environments.
3) Inline handler or inline script failures
Inline handlers are one of the most common surprise failures in security-hardened apps. Examples include onclick, onchange, inline <script>, and some string-based DOM APIs. These are often tolerated in older apps or dev environments, then blocked in QA or production once the CSP is tightened.
If a test clicks a button and nothing happens, check whether the behavior depends on an inline handler that CSP now blocks. The same is true for framework-generated templates that still emit inline code in edge cases.
A safer pattern is to bind behavior in external JavaScript and avoid inline event attributes entirely. If inline code is unavoidable, the policy needs a nonce or hash that matches the exact content the browser sees. The spec is precise here, and browser behavior is unforgiving.
A reproducible debugging workflow
Use this sequence when a test failure smells like CSP:
- Confirm the policy
- Inspect the response headers or page meta tag.
- Compare the QA policy to dev.
- Re-run with console capture on
- Save browser console errors, page errors, and failed network requests.
- Look specifically for the blocked directive name.
- Identify the blocked resource
- Is it a script URL, an iframe URL, or inline code?
- Is the blocked item required for the app, or only for the test?
- Check whether the test itself depends on forbidden behavior
- Does the test inject inline JavaScript?
- Does it expect a third-party frame that QA blocks?
- Does it rely on a browser extension or bookmarklet-style helper?
- Verify the app in a minimal browser session
- Open the same page with devtools open.
- Disable the test harness and manually repeat the action.
- If the browser still logs the violation, the issue is not the automation layer.
- Decide whether to fix the app, the policy, or the test
- App bug: script should not be inline, host should be allowed, or frame should be permitted.
- Policy misconfiguration: approved resource was omitted from CSP.
- Test bug: the test uses a technique that the production policy correctly blocks.
A minimal header diff can save hours
Many CSP failures turn out to be one of these changes:
- Added a new CDN host but forgot
script-src - Swapped a script tag from external to inline during a build step
- Tightened
frame-ancestorsand broke an embedded preview - Removed
'unsafe-inline'without moving event handlers out of HTML - Added a meta CSP tag in one environment but not another
If your CI can log response headers, keep them in the test artifact. That makes drift visible. For a small number of endpoints, even a simple text comparison is enough to show the difference between a passing dev policy and a failing QA policy.
When the browser test is right and the app is wrong
Sometimes the browser failure is the useful signal. A policy block can reveal a real security bug or a deploy regression that dev accidentally masked.
Examples:
- A login page still depends on an inline handler, but production policy disallows it
- A checkout widget now loads from a new domain that was never added to CSP
- An embedded admin tool is no longer allowed to appear in a frame, but the app still assumes it can
In these cases, do not “fix” the test by weakening it first. Fix the policy or the app so the browser can enforce the intended security boundary and the user flow still works.
When the browser test is wrong
The test may be the problem if it relies on behavior the app never promised to support:
- Injecting inline code into a page with a strict policy
- Using a script bookmarklet style approach in a locked-down environment
- Depending on a frame that the product intentionally blocks with
frame-ancestors - Clicking a button that only works because dev mode still allows inline handlers
That kind of test may pass in local development and fail by design in QA. In that case, rewrite the test to interact through supported UI paths or approved application hooks.
A short checklist for each CSP failure
- Capture the exact browser console error
- Note the directive name, not just the blocked URL
- Compare dev and QA response headers
- Check whether the blocked resource is script, frame, or inline code
- Decide whether the test, app, or policy owns the fix
- Re-run after the smallest possible change
Not all automation failures are CSP failures
It is easy to over-attribute. A timeout waiting for a button can also come from:
- A selector change
- Slow rendering or hydration
- Network failures unrelated to CSP
- A cross-origin frame issue that is not policy-related
- A test environment missing the right cookies or auth state
If the console does not show a CSP violation, keep looking. CSP is a useful suspect, not a universal answer.
Bottom line
If you need to debug browser tests when content security policy blocks scripts, frames, or inline handlers, start with the browser’s own violation message, then trace the blocked resource back to either the app, the policy, or the test harness. The fastest diagnosis comes from three artifacts together: console errors, response headers, and a clear understanding of which CSP directive is doing the blocking.
Once you separate those three, most “mysterious” QA-only failures become ordinary configuration drift or test-design issues, which is exactly where they belong.
FAQ
Why does the test pass locally but fail in QA?
Local environments often use a looser CSP, no CSP, or different host allowlists. QA may have stricter script-src, frame-src, or frame-ancestors rules that the browser enforces before the code runs.
What is the quickest way to confirm CSP is the cause?
Check the browser console for a CSP violation message and compare the QA response headers with the working environment. If the directive name matches the missing behavior, you have a strong signal.
Why do inline handlers fail more often than external scripts?
Inline handlers depend on policy exceptions such as nonces, hashes, or 'unsafe-inline'. If those are removed or never added, the browser blocks the handler even though the HTML still contains it.
What is the difference between frame-src and frame-ancestors?
frame-src controls what the page may embed. frame-ancestors controls who may embed the page. They solve opposite sides of the framing problem.
Should I disable CSP to make tests pass?
Usually no. Disabling CSP removes the signal that reveals a real compatibility or security issue. The better fix is to align the app, policy, and test so the intended flow works under the real policy.
Can a browser automation tool bypass CSP?
The browser still enforces CSP on the page. Automation can drive the browser, but it does not turn off the browser’s policy checks unless you deliberately change the test environment or browser settings.