How to Test Apps Hosted on CodePen With Cypress
Cypress can visit a CodePen page, but its embedded cross-origin preview is not a reliable target for Cypress commands. Here are the dependable ways to test the app or verify its links.
Short answer: Cypress can visit a CodePen page, but it cannot normally automate the CodePen preview when that preview is embedded in a cross-origin iframe. For dependable behavior tests, run the app at a local or deployed URL you control and point Cypress there. If you only need to confirm a link to another site, assert its href without navigating to it. cy.origin() handles top-level navigation between origins; it does not reach into an embedded cross-origin iframe.
This distinction matters because old CodePen Cypress examples are no longer reliable. A 2017 Cypress post explicitly warns that its approach stopped working after CodePen added CAPTCHA. Treat that code as historical, not as a current recipe. Cypress’s CodePen article and its cross-origin testing guide explain the relevant limitations.
1. Choose the right test target
Start by deciding what the test must prove. The preview frame, the Pen page, and the app itself are different targets.
| What you need to verify | Recommended target | Why |
|---|---|---|
| App behavior, such as buttons, forms, or rendered state | A local or deployed app URL you control | You control the origin and can make the test deterministic. |
| A link on your page points to the right destination | Your page; assert the link’s href |
You can check the destination without automating a third-party site. |
| Commands after navigation to a second, top-level origin | Use cy.origin() for the second origin |
This is Cypress’s supported cross-origin navigation model. |
| DOM inside a CodePen preview iframe | Do not rely on Cypress iframe access | The embedded preview is cross-origin and Cypress cannot normally communicate with it. |
If you control the app, run it separately in a local development server or controlled deployment. The key requirement is that Cypress visits the app at an origin you control; the exact way you serve it depends on your project. Avoid assuming that a CodePen preview URL behaves like a stable application test environment.
2. Set up Cypress for an app URL you control
Install Cypress using your project’s package manager, then configure the base URL to the local or deployed app. This example assumes the app is already available at http://localhost:8080.
npm install --save-dev cypress
Add a script to package.json:
{
"scripts": {
"test:e2e": "cypress run",
"test:e2e:open": "cypress open"
}
}
Create cypress.config.js:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:8080',
specPattern: 'cypress/e2e/**/*.cy.js'
}
})
For an app with a different framework or server, replace the base URL and arrange for that server to be running before Cypress starts. The Cypress configuration reference documents the available configuration fields: Cypress configuration.
3. Test the app’s behavior at its controlled URL
For a concrete example, suppose the app has a button with the accessible name “Show result” and displays a result in an element with data-testid="result". Create cypress/e2e/app.cy.js:
describe('CodePen app behavior', () => {
it('shows a result when the user activates the button', () => {
cy.visit('/')
cy.findByRole('button', { name: 'Show result' }).click()
cy.get('[data-testid="result"]').should('be.visible')
cy.get('[data-testid="result"]').should('contain.text', 'Ready')
})
})
This is JavaScript test code; replace the example selectors and expected text with elements from your app. Cypress retries queries and assertions while waiting for the UI to reach the expected state, so prefer state-based assertions over fixed sleeps. To use Testing Library commands such as findByRole, install and configure the Cypress Testing Library package; otherwise use a selector available in your project, for example cy.get('[data-testid="show-result"]').click().
Run the test with:
npm run test:e2e
Run the interactive runner with npm run test:e2e:open. Keep the tested app available at the configured base URL for either command.
4. Verify an external link without opening the destination
If the requirement is “the app links to the right place,” test the link itself. This is more stable than visiting a site you do not control.
describe('outbound link', () => {
it('points to the expected destination', () => {
cy.visit('/')
cy.get('[data-testid="documentation-link"]')
.should('have.attr', 'href', 'https://example.com/docs')
})
})
Use the actual selector and expected URL from your app. If you need to assert a URL assembled from a base and path, assert the resulting complete href. Cypress discusses this approach in its cross-origin guide.
You can also use cy.request() when you need to inspect an HTTP response rather than exercise browser behavior. It is not a substitute for testing the destination’s client-side interactions.
5. Use cy.origin() only for top-level navigation
When a test clicks a link and the browser navigates to another origin as a top-level page, place commands for that destination inside cy.origin(). The origin must match the destination, including scheme, hostname, and port when present.
describe('top-level navigation', () => {
it('checks a page after navigating to another origin', () => {
cy.visit('https://your-controlled-app.example')
cy.get('[data-testid="external-link"]').click()
cy.origin('https://second-origin.example', () => {
cy.get('h1').should('be.visible')
})
})
})
This only applies when the browser has navigated to the second origin as the page under test. It does not grant access to a cross-origin iframe embedded in the first page. Current Cypress behavior also requires cy.origin() for cross-origin navigation under documented defaults; Cypress v14 stopped injecting document.domain by default. See the cy.origin() API.
6. What if the iframe is same-origin?
Cypress can query a same-origin iframe because the browser permits the parent page to access its document. A simple helper can wait until the frame body exists and then wrap it for normal Cypress queries:
function getSameOriginIframeBody(selector) {
return cy
.get(selector)
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
}
describe('same-origin iframe', () => {
it('finds content in a same-origin frame', () => {
cy.visit('/page-with-frame')
getSameOriginIframeBody('iframe[data-testid="app-frame"]')
.find('[data-testid="status"]')
.should('have.text', 'Ready')
})
})
Use this only when the iframe document is genuinely same-origin with the page. A CodePen preview hosted on a different origin does not become same-origin because it is visible on the page. Cypress’s FAQ describes the browser same-origin restriction and same-origin iframe pattern.
7. Why the old CodePen Cypress recipe fails
The historical approach inspected CodePen’s page and reached through its iframe to the Pen’s app. That depended on access to the iframe and on CodePen’s page continuing to expose the same structure. Cypress’s 2017 article now warns that the example no longer works, citing CodePen’s CAPTCHA protections against scrapers and third-party tools. Separately, current Cypress documentation states that it cannot automate or communicate with a cross-origin embedded iframe using the normal Cypress approach.
Do not paste the old iframe-query recipe into a new suite expecting it to work. Cypress’s recipes index may still list older demonstrations; a listing is not evidence that a third-party flow remains supported. CodePen published a 2019 post suggesting Debug Mode with CodePen Pro as an easier route at that time. That post is historical, so verify current availability and account terms directly with CodePen before building a workflow around it: CodePen’s “Test Pens with Cypress” post.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
contentDocument is null or iframe queries fail |
The iframe is cross-origin; browser same-origin policy blocks the parent test context. | Run the app at a controlled URL, assert the containing page’s link, or make the frame same-origin if you control both sides. |
cy.origin() does not find elements in the Pen preview |
cy.origin() supports top-level origin changes, not iframe access. |
Use it only after top-level navigation. For embedded preview behavior, move the test to an app URL you control. |
| Old CodePen example stops at a CAPTCHA or page challenge | CodePen added CAPTCHA protections, and the old Cypress article says its method no longer works. | Do not automate the challenge. Test the app independently or verify the external link without following it. |
cy.origin() reports a mismatch |
The origin string differs from the actual destination, such as a different scheme, hostname, or port. | Use the exact origin shown in the navigated URL, without a path. |
| Test cannot connect to localhost | The app server is not running, the port differs, or the base URL is wrong. | Start the app server before Cypress, check the port, and align baseUrl with the server URL. |
| Element query times out on the controlled app | The selector does not match, the app has not reached the expected state, or the target is not rendered. | Confirm the selector in the app, wait on an observable state, and assert the expected content. Avoid arbitrary fixed delays where an assertion can express readiness. |
Setting chromeWebSecurity: false is sometimes described as a workaround, but Cypress documents it as constrained and browser-dependent. It should not be the default design for a dependable CodePen test. Prefer testing at the app’s own controlled URL.
9. Performance, reliability, and cost considerations
- Reliability: A test against an origin you control avoids dependence on CodePen’s preview markup, CAPTCHA behavior, and third-party availability. External link assertions are particularly durable because they do not load the destination.
- Performance: Avoid adding fixed waits to compensate for a blocked iframe. They make a test slower without removing the origin restriction. Wait for application state with Cypress assertions instead.
- CI: Ensure the app server starts before Cypress and use the same configured URL in local and CI runs. Keep test data and dependencies deterministic where the app allows it.
- Cost: Cypress test execution has no CodePen automation fee implied by these examples. A controlled local run uses your existing development environment; hosting and CI costs depend on the services you choose. No current CodePen Debug Mode pricing or availability is established by the historical source, so check CodePen’s current documentation for those terms.
10. Or skip the browser setup
If your goal is to inspect how a CodePen page or your app looks, rather than interact with controls and assert behavior, a screenshot API can avoid browser automation setup. ScreenshotNeo takes a screenshot or PDF from one GET request; it is separate from Cypress and does not test application behavior.
See the ScreenshotNeo API documentation for request options. This complete cURL example captures a page as WebP:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://codepen.io/ --output shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://codepen.io/"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://codepen.io/'
})
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`)
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`)
const bytes = new Uint8Array(await res.arrayBuffer())
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes))
ScreenshotNeo removes known cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.
11. Frequently asked questions
Can Cypress test a CodePen at all?
It can visit a CodePen page and test parts that are accessible in the page’s origin. The embedded cross-origin preview is the limitation; use a controlled app URL for app behavior.
Does cy.origin() fix a cross-origin iframe?
No. It is for commands after top-level navigation to another origin, not for reaching into an embedded frame.
Can I use Cypress to check what a CodePen looks like?
You can capture a screenshot of a publicly reachable page, but a screenshot does not establish that its controls or application logic work. Use behavioral tests at a controlled URL for that.
Is CodePen Debug Mode still available with Pro?
The cited CodePen post dates from 2019. It describes Debug Mode with CodePen Pro at that time, but this article does not establish current availability or plan terms. Check CodePen’s current documentation and account access.


