{"id":8847,"date":"2026-01-27T12:31:44","date_gmt":"2026-01-27T07:01:44","guid":{"rendered":"https:\/\/www.testleaf.com\/blog\/?p=8847"},"modified":"2026-01-27T12:34:25","modified_gmt":"2026-01-27T07:04:25","slug":"your-playwright-tests-fail-for-this-reason-frames-popups-downloads","status":"publish","type":"post","link":"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/","title":{"rendered":"Your Playwright Tests Fail for This Reason (Frames, Popups, Downloads)"},"content":{"rendered":"<div style=\"margin-top: 0px; margin-bottom: 0px;\" class=\"sharethis-inline-share-buttons\" ><\/div><!--[if lt IE 9]><script>document.createElement('audio');<\/script><![endif]-->\n<audio class=\"wp-audio-shortcode\" id=\"audio-8847-1\" preload=\"none\" style=\"width: 100%;\" controls=\"controls\"><source type=\"audio\/mpeg\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Your-Playwright-Tests-Fail-for-This-Reason-Frames-Popups-Downloads.mp3?_=1\" \/><a href=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Your-Playwright-Tests-Fail-for-This-Reason-Frames-Popups-Downloads.mp3\">https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Your-Playwright-Tests-Fail-for-This-Reason-Frames-Popups-Downloads.mp3<\/a><\/audio>\n<p>&nbsp;<\/p>\n<p>Modern web apps don\u2019t \u201cbreak tests\u201d because selectors are bad.<\/p>\n<p>They break tests because the UI keeps changing <em>context<\/em>:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.testleaf.com\/blog\/selenium-context-switching-iframes-windows-file-uploads\/\"><strong>iFrames<\/strong><\/a> (payments, chat widgets, embedded dashboards)<\/li>\n<li><strong>Popups \/ new tabs<\/strong> (SSO providers, help centers, previews)<\/li>\n<li><strong>Uploads &amp; downloads<\/strong> (reports, resumes, exports)<\/li>\n<\/ul>\n<p>In older tools, these were classic flake zones: you\u2019d switch context manually, race the DOM, and hope nothing refreshed mid-step.<\/p>\n<p>Playwright takes a different approach.<br \/>\nYou don\u2019t \u201cswitch\u201d the old-school way\u2014you use <strong>first-class APIs<\/strong> built for exactly these scenarios.<\/p>\n<p>In this guide, we\u2019ll cover:<\/p>\n<ul>\n<li>frameLocator() for stable iframe interactions<\/li>\n<li>Uploads with setInputFiles() and the filechooser pattern<\/li>\n<li>Downloads using Promise.all() (and why saveAs() matters for CI)<\/li>\n<li>Popups and handling multiple pages predictably<\/li>\n<\/ul>\n<p><img fetchpriority=\"high\" decoding=\"async\" class=\"aligncenter size-full wp-image-8850\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6.webp\" alt=\"Playwright test style\" width=\"2048\" height=\"1143\" srcset=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6.webp 2048w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6-300x167.webp 300w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6-1024x572.webp 1024w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6-768x429.webp 768w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6-1536x857.webp 1536w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Untitled-design-6-150x84.webp 150w\" sizes=\"(max-width: 2048px) 100vw, 2048px\" \/><\/p>\n<p>Code examples below use Playwright Test style.<\/p>\n<p>import { test, expect } from &#8216;@playwright\/test&#8217;;<\/p>\n<h2><span class=\"ez-toc-section\" id=\"1_iFrames_stop_switching_start_%E2%80%9Ctunneling%E2%80%9D_with_frameLocator\"><\/span><strong>1) iFrames: stop switching, start \u201ctunneling\u201d with <\/strong><strong>frameLocator()<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2><div id=\"ez-toc-container\" class=\"ez-toc-v2_0_82_2 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/#1_iFrames_stop_switching_start_%E2%80%9Ctunneling%E2%80%9D_with_frameLocator\" >1) iFrames: stop switching, start \u201ctunneling\u201d with frameLocator()<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/#2_Uploads_bypass_the_OS_dialog_two_patterns_youll_actually_use\" >2) Uploads: bypass the OS dialog (two patterns you\u2019ll actually use)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/#3_Downloads_catch_the_event_and_save_the_file_CI-safe\" >3) Downloads: catch the event and save the file (CI-safe)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/#4_Popups_new_tab_new_Page_catch_it_intentionally\" >4) Popups: new tab = new Page (catch it intentionally)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/#5_Robust_recipes_common_patterns_youll_hit_in_the_real_world\" >5) Robust recipes (common patterns you\u2019ll hit in the real world)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/#FAQs\" >FAQs<\/a><\/li><\/ul><\/nav><\/div>\n\n<p>You <em>can<\/em> use page.frame(&#8230;), but in most testing workflows, frameLocator() is simpler and safer. It gives you a clean \u201ctunnel\u201d into the frame while you keep using locators normally.<\/p>\n<pre>test('pay inside iframe using frameLocator', async ({ page }) =&gt; {\r\n\r\n  await page.goto('https:\/\/your-app-url.com');\r\n\r\n\u00a0 const frame = page.frameLocator('iframe[data-testid=\"payment-frame\"]');\r\n\r\n\r\n\u00a0 await expect(frame.getByLabel('Card Number')).toBeVisible();\r\n\r\n\r\n\u00a0 await frame.getByLabel('Card Number').fill('4111111111111111');\r\n\r\n\u00a0 await frame.getByLabel('Expiry Date').fill('12\/30');\r\n\r\n\u00a0 await frame.getByRole('button', { name: 'Pay Now' }).click();\r\n\r\n\r\n\u00a0 await expect(page.getByText('Payment successful')).toBeVisible();\r\n\r\n});<\/pre>\n<p><strong>Why <\/strong><strong>frameLocator()<\/strong><strong> is a game changer<\/strong><\/p>\n<ul>\n<li><strong>No context switching<\/strong>: you don\u2019t go \u201cin\/out\u201d of frames manually.<\/li>\n<li><strong><a href=\"https:\/\/www.testleaf.com\/blog\/ai-powered-element-locators-selenium-automation\/\">Locator<\/a>-first mental model<\/strong>: the code reads like normal page automation.<\/li>\n<li><strong>Works well for nesting<\/strong>: frames inside frames stay manageable.<\/li>\n<\/ul>\n<p><strong>Pro tip:<\/strong> Waiting for the iframe element isn\u2019t always enough.<br \/>\nThe most stable wait is a real UI signal <em>inside<\/em> the frame:<\/p>\n<p>await expect(frame.getByLabel(&#8216;Card Number&#8217;)).toBeVisible();<\/p>\n<h2><span class=\"ez-toc-section\" id=\"2_Uploads_bypass_the_OS_dialog_two_patterns_youll_actually_use\"><\/span><strong>2) Uploads: bypass the OS dialog (two patterns you\u2019ll actually use)<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Playwright doesn\u2019t deal with the OS file picker like a human does. It sets files at the <a href=\"https:\/\/www.testleaf.com\/blog\/selenium-dom-properties-explained-fix-hidden-error-messages\/\">DOM<\/a> level.<\/p>\n<p><strong>Method A: <\/strong><strong>setInputFiles()<\/strong><strong> (best practice)<\/strong><\/p>\n<p>Most apps have an &lt;input type=&#8221;file&#8221;&gt;\u2014even if it\u2019s hidden and triggered by a fancy button. If you can locate the input, this is the cleanest approach.<\/p>\n<pre>test('upload resume with setInputFiles', async ({ page }) =&gt; {\r\n\r\n\u00a0 await page.goto('https:\/\/your-app-url.com');\r\n\r\n\r\n\u00a0 await page.getByTestId('resume-upload').setInputFiles('tests\/data\/resume.pdf');\r\n\r\n\r\n\u00a0 await page.getByRole('button', { name: 'Submit' }).click();\r\n\r\n\u00a0 await expect(page.getByTestId('toast-success')).toBeVisible();\r\n\r\n});<\/pre>\n<p><strong>Multiple files?<\/strong><\/p>\n<pre>await page.getByTestId('attachments-upload').setInputFiles([\r\n\r\n\u00a0 'tests\/data\/file1.pdf',\r\n\r\n\u00a0 'tests\/data\/file2.png',\r\n\r\n]);<\/pre>\n<p><strong>Method B: <\/strong><strong>filechooser<\/strong><strong> event (when the input is created dynamically)<\/strong><\/p>\n<p>Some apps create the input only when you click \u201c<a href=\"https:\/\/www.testleaf.com\/blog\/selenium-file-upload-download-automation-guide-2025-step-by-step-for-chrome-firefox-edge\/\">Upload<\/a>\u201d. In that case, listen for the chooser event <em>before<\/em> clicking.<\/p>\n<pre>test('upload using filechooser event', async ({ page }) =&gt; {\r\n\r\n\u00a0 await page.goto('https:\/\/your-app-url.com');\r\n\r\n\u00a0 const [fileChooser] = await Promise.all([\r\n\r\n\u00a0\u00a0\u00a0 page.waitForEvent('filechooser'),\r\n\r\n\u00a0\u00a0\u00a0 page.getByRole('button', { name: 'Upload' }).click(),\r\n\r\n  ]);\r\n\r\n\r\n\u00a0 await fileChooser.setFiles('tests\/data\/resume.pdf');\r\n\r\n\u00a0 await expect(page.getByTestId('toast-success')).toBeVisible();\r\n\r\n});<\/pre>\n<p><strong>Practical rule:<\/strong> Try Method A first.<br \/>\nUse Method B only when you truly can\u2019t access the input.<\/p>\n<p><img decoding=\"async\" class=\"aligncenter size-full wp-image-8852\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload.webp\" alt=\"CI safe file upload\" width=\"2048\" height=\"1143\" srcset=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload.webp 2048w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload-300x167.webp 300w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload-1024x572.webp 1024w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload-768x429.webp 768w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload-1536x857.webp 1536w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/CI-safe-file-upload-150x84.webp 150w\" sizes=\"(max-width: 2048px) 100vw, 2048px\" \/><\/p>\n<h2><span class=\"ez-toc-section\" id=\"3_Downloads_catch_the_event_and_save_the_file_CI-safe\"><\/span><strong>3) Downloads: catch the event and save the file (CI-safe)<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In automation, we don\u2019t click and then search a Downloads folder.<\/p>\n<p>We intercept the download event and control where the file goes.<\/p>\n<pre>test('download report and save it reliably', async ({ page }) =&gt; {\r\n\r\n  await page.goto('https:\/\/your-app-url.com');\r\n\r\n\u00a0 const [download] = await Promise.all([\r\n\r\n\u00a0\u00a0\u00a0 page.waitForEvent('download'),\r\n\r\n\u00a0\u00a0\u00a0 page.getByRole('button', { name: 'Export as PDF' }).click(),\r\n\r\n  ]);\r\n\r\n\u00a0 const filename = download.suggestedFilename();\r\n\r\n\r\n\u00a0 \/\/ CI-friendly: persist the artifact\r\n\r\n\u00a0 await download.saveAs(`downloads\/${filename}`);\r\n\r\n\u00a0 expect(filename).toContain('report');\r\n\r\n});<\/pre>\n<p><strong>Why <\/strong><strong>saveAs()<\/strong><strong> matters<\/strong><\/p>\n<ul>\n<li>Downloads can be stored in <strong>temporary locations<\/strong><\/li>\n<li>CI systems (<a href=\"https:\/\/en.wikipedia.org\/wiki\/Jenkins_(software)\">Jenkins<\/a>\/<a href=\"https:\/\/github.com\/features\/actions\">GitHub Actions<\/a>) need <strong>real artifacts<\/strong> for debugging<\/li>\n<li>saveAs() gives you an explicit file you can archive<\/li>\n<\/ul>\n<p><a href=\"https:\/\/playwright-webinar.testleaf.com\/?utm_source=Playwright_Webinar&amp;utm_medium=Organic&amp;utm_campaign=Playwright_Webinar_blog\"><img decoding=\"async\" class=\"aligncenter wp-image-7702 size-full\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass.png\" alt=\"Playwright Masterclass\" width=\"2048\" height=\"512\" srcset=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass.png 2048w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass-300x75.png 300w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass-1024x256.png 1024w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass-768x192.png 768w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass-1536x384.png 1536w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/11\/Playwright-Masterclass-150x38.png 150w\" sizes=\"(max-width: 2048px) 100vw, 2048px\" \/><\/a><\/p>\n<h2><span class=\"ez-toc-section\" id=\"4_Popups_new_tab_new_Page_catch_it_intentionally\"><\/span><strong>4) <a href=\"https:\/\/www.testleaf.com\/blog\/selenium-explicit-waits-spinners-modals-popups\/\">Popups<\/a>: new tab = new <\/strong><strong>Page<\/strong><strong> (catch it intentionally)<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When your app opens a new tab\/window (SSO, Help Center, Preview), Playwright creates a new Page object.<\/p>\n<p>The reliable approach is the same pattern again: <strong>listen + click together<\/strong>.<\/p>\n<pre>test('handle popup and validate content', async ({ page }) =&gt; {\r\n\r\n\u00a0 await page.goto('https:\/\/your-app-url.com');\r\n\r\n\r\n\u00a0 const [popup] = await Promise.all([\r\n\r\n\u00a0\u00a0\u00a0 page.waitForEvent('popup'),\r\n\r\n\u00a0\u00a0\u00a0 page.getByRole('link', { name: 'Open in new tab' }).click(),\r\n\r\n\u00a0 ]);\r\n\r\n\r\n\u00a0 await popup.waitForLoadState();\r\n\r\n\r\n\u00a0 await expect(popup).toHaveTitle(\/Help Center\/i);\r\n\r\n\u00a0 await popup.close();\r\n\r\n\r\n\u00a0 await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();\r\n\r\n});<\/pre>\n<p><strong>Note:<\/strong> In rare cases where the tab is opened outside the immediate click chain, you can listen at the <strong>context level<\/strong>:<\/p>\n<pre>test('popup fallback using context page event', async ({ page, context }) =&gt; {\r\n\r\n\u00a0 const [newPage] = await Promise.all([\r\n\r\n\u00a0\u00a0\u00a0 context.waitForEvent('page'),\r\n\r\n\u00a0\u00a0\u00a0 page.getByRole('link', { name: 'Open in new tab' }).click(),\r\n\r\n  ]);\r\n\r\n\u00a0 await expect(newPage).toHaveTitle(\/Help Center\/i);\r\n\r\n});<\/pre>\n<h2><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-8851\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow.webp\" alt=\"Playwright popup and new tab handling flow\" width=\"2048\" height=\"1143\" srcset=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow.webp 2048w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow-300x167.webp 300w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow-1024x572.webp 1024w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow-768x429.webp 768w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow-1536x857.webp 1536w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2026\/01\/Playwright-popup-and-new-tab-handling-flow-150x84.webp 150w\" sizes=\"(max-width: 2048px) 100vw, 2048px\" \/><\/h2>\n<h2><span class=\"ez-toc-section\" id=\"5_Robust_recipes_common_patterns_youll_hit_in_the_real_world\"><\/span><strong>5) Robust recipes (<a href=\"https:\/\/www.testleaf.com\/blog\/selenium-anti-pattern-what-not-to-do-in-automation\/\">common patterns<\/a> you\u2019ll hit in the real world)<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h5><strong>Pattern 1: \u201cCross-boundary\u201d popup (iframe click \u2192 popup opens)<\/strong><\/h5>\n<pre>Example: payment iframe opens a bank\/auth page in a new tab.\r\n\r\ntest('iframe button opens popup', async ({ page }) =&gt; {\r\n\r\n  await page.goto('https:\/\/your-app-url.com');\r\n\r\n  const frame = page.frameLocator('iframe[data-testid=\"payment-frame\"]');\r\n\r\n\u00a0 const [popup] = await Promise.all([\r\n\r\n\u00a0\u00a0\u00a0 page.waitForEvent('popup'),\r\n\r\n\u00a0\u00a0\u00a0 frame.getByRole('button', { name: 'Pay with Bank' }).click(),\r\n\r\n\u00a0 ]);\r\n\r\n\u00a0 await popup.waitForLoadState();\r\n\r\n\u00a0 await expect(popup).toHaveURL(\/bank-auth\/i);\r\n\r\n});<\/pre>\n<h5><strong>Pattern 2: Download verification (filename guardrail)<\/strong><\/h5>\n<pre>test('download csv - verify filename', async ({ page }) =&gt; {\r\n\r\n  await page.goto('https:\/\/your-app-url.com');\r\n\r\n\u00a0 const [download] = await Promise.all([\r\n\r\n\u00a0\u00a0\u00a0 page.waitForEvent('download'),\r\n\r\n\u00a0\u00a0\u00a0 page.getByRole('button', { name: 'Get CSV' }).click(),\r\n\r\n\u00a0 ]);\r\n\r\n\r\n\u00a0 expect(download.suggestedFilename()).toMatch(\/^monthly_report_\\d{4}\\.csv$\/);\r\n\r\n\u00a0 await download.saveAs(`downloads\/${download.suggestedFilename()}`);\r\n\r\n});<\/pre>\n<h3><strong>Conclusion<\/strong><\/h3>\n<p>Frames, uploads, downloads, and popups aren\u2019t \u201cedge cases\u201d. They\u2019re daily reality in modern web apps.<\/p>\n<p>Playwright makes them predictable if you stick to a few rules:<\/p>\n<ul>\n<li>Use <strong>frameLocator()<\/strong> to work inside iFrames without stateful switching<\/li>\n<li>Prefer <strong>setInputFiles()<\/strong> on the file input; use <strong>filechooser<\/strong> only when required<\/li>\n<li>Use <strong>Promise.all()<\/strong> to set the event trap <em>before<\/em> you click<\/li>\n<li>Always <strong>saveAs()<\/strong> downloads so CI has real artifacts<\/li>\n<\/ul>\n<p>When you apply these patterns consistently, your \u201ccomplex UI\u201d tests often become the <strong>most reliable<\/strong> ones.<\/p>\n<p>If you want, I can also add a short \u201cTroubleshooting Cheat Sheet\u201d section (common failures \u2192 root cause \u2192 fix) to make this even more shareable on LinkedIn.<\/p>\n<p>&nbsp;<\/p>\n<h2 data-start=\"421\" data-end=\"458\"><span class=\"ez-toc-section\" id=\"FAQs\"><\/span><strong>FAQs<\/strong><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"459\" data-end=\"506\">1) Why do Playwright tests fail in iframes?<\/strong><br data-start=\"506\" data-end=\"509\" \/>Because the UI is in a different context. Use <code data-start=\"555\" data-end=\"571\">frameLocator()<\/code> to \u201ctunnel\u201d into the iframe and interact with elements normally.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"676\" data-end=\"739\">2) What is the most stable way to automate iframe elements?<\/strong><br data-start=\"739\" data-end=\"742\" \/>Use a real UI signal inside the frame (like a visible label or field) before interacting, not just waiting for the iframe tag.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"908\" data-end=\"970\">3) How do I upload files in Playwright without OS dialogs?<\/strong><br data-start=\"970\" data-end=\"973\" \/>Use <code data-start=\"977\" data-end=\"994\">setInputFiles()<\/code> on the file input (even if it\u2019s visually hidden). It\u2019s the cleanest, most reliable approach.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"1127\" data-end=\"1203\">4) When should I use the <code data-start=\"1154\" data-end=\"1167\">filechooser<\/code> event instead of setInputFiles()?<\/strong><br data-start=\"1203\" data-end=\"1206\" \/>Use <code data-start=\"1210\" data-end=\"1223\">filechooser<\/code> only when the file input is created dynamically after clicking \u201cUpload.\u201d Listen for the event and click together using <code data-start=\"1343\" data-end=\"1358\">Promise.all()<\/code>.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"1399\" data-end=\"1479\">5) What\u2019s the safest pattern for downloads in Playwright (especially in CI)?<\/strong><br data-start=\"1479\" data-end=\"1482\" \/>Trap the <code data-start=\"1491\" data-end=\"1501\">download<\/code> event first using <code data-start=\"1520\" data-end=\"1535\">Promise.all()<\/code>, then save the file explicitly using <code data-start=\"1573\" data-end=\"1592\">download.saveAs()<\/code> so CI retains the artifact.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"1660\" data-end=\"1712\">6) Why is <code data-start=\"1672\" data-end=\"1682\">saveAs()<\/code> important for <a href=\"https:\/\/www.testleaf.com\/blog\/stage-by-stage-ci-cd-pipeline-dev-qa-preprod-prod\/\">CI pipelines<\/a>?<\/strong><br data-start=\"1712\" data-end=\"1715\" \/>Downloads may land in temporary locations. <code data-start=\"1758\" data-end=\"1768\">saveAs()<\/code> creates a real file you can archive in CI tools like Jenkins or GitHub Actions.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"1888\" data-end=\"1950\">7) How do I handle popups\/new tabs reliably in Playwright?<\/strong><br data-start=\"1950\" data-end=\"1953\" \/>Treat the popup as a new <code data-start=\"1978\" data-end=\"1984\">Page<\/code>. Use the \u201clisten + click\u201d pattern: wait for <code data-start=\"2029\" data-end=\"2036\">popup<\/code>, click the link, then work on the new page.<\/p>\n<p data-start=\"459\" data-end=\"674\"><strong data-start=\"2120\" data-end=\"2203\">8) What\u2019s the single rule that makes context changes predictable in Playwright?<\/strong><br data-start=\"2203\" data-end=\"2206\" \/>Set the event trap before the action: use <code data-start=\"2248\" data-end=\"2263\">Promise.all()<\/code> to listen for <code data-start=\"2278\" data-end=\"2285\">popup<\/code>, <code data-start=\"2287\" data-end=\"2297\">download<\/code>, or <code data-start=\"2302\" data-end=\"2315\">filechooser<\/code> and click in the same step.<\/p>\n<h5><strong>We Also Provide Training In:<\/strong><\/h5>\n<ul>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/selenium-automation-certification-training-course.html?utm_source=blog_post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>Advanced Selenium Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/playwright.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>Playwright Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/genai-qa-engineers-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>Gen AI Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/aws-cloud-architect-certification-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>AWS Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/rest-api-testing-certification-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>REST API Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/full-stack-developer-certification-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>Full Stack Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/appium-mobile-automation-certification-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>Appium Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/dev-ops-master-certification-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>DevOps Training<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.testleaf.com\/course\/apache-jmeter-testing-training-course.html?utm_source=blog-post&amp;utm_medium=Organic&amp;utm_campaign=Blog_Post\"><strong>JMeter Performance Training<\/strong><\/a><\/li>\n<\/ul>\n<h6><strong>Author\u2019s Bio<\/strong>:<\/h6>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-6744 size-full alignleft\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/09\/Kadhir.png\" sizes=\"(max-width: 200px) 100vw, 200px\" srcset=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/09\/Kadhir.png 200w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/09\/Kadhir-150x150.png 150w, https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/09\/Kadhir-96x96.png 96w\" alt=\"Kadhir\" width=\"200\" height=\"200\" \/><\/p>\n<p>Content Writer at Testleaf, specializing in SEO-driven content for test automation, software development, and cybersecurity. I turn complex technical topics into clear, engaging stories that educate, inspire, and drive digital transformation.<\/p>\n<p><strong>Ezhirkadhir Raja<\/strong><\/p>\n<p>Content Writer \u2013 Testleaf<\/p>\n<p><a href=\"http:\/\/linkedin.com\/in\/ezhirkadhir\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.testleaf.com\/blog\/wp-content\/uploads\/2025\/07\/linkedin.png\" alt=\"LinkedIn Logo\" width=\"28\" height=\"28\" \/><\/a><\/p>\n<p data-start=\"459\" data-end=\"674\">\n","protected":false},"excerpt":{"rendered":"<p>&nbsp; Modern web apps don\u2019t \u201cbreak tests\u201d because selectors are bad. They break tests because the UI keeps changing context: iFrames (payments, chat widgets, embedded dashboards) Popups \/ new tabs (SSO providers, help centers, previews) Uploads &amp; downloads (reports, resumes, exports) In older tools, these were classic flake zones: you\u2019d switch context manually, race the &hellip;<\/p>\n<p class=\"read-more\"> <a class=\"\" href=\"https:\/\/www.testleaf.com\/blog\/your-playwright-tests-fail-for-this-reason-frames-popups-downloads\/\"> <span class=\"screen-reader-text\">Your Playwright Tests Fail for This Reason (Frames, Popups, Downloads)<\/span> Read More &raquo;<\/a><\/p>\n","protected":false},"author":1,"featured_media":8849,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"site-sidebar-layout":"default","site-content-layout":"default","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"default","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","footnotes":""},"categories":[345],"tags":[995,994,839,150,776,346,975,993],"class_list":["post-8847","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-playwright","tag-downloads","tag-frames","tag-locators","tag-playwright","tag-playwright-auto-waits","tag-playwright-java-script","tag-playwright-mcp","tag-pop-ups"],"acf":[],"aioseo_notices":[],"amp_enabled":true,"_links":{"self":[{"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/posts\/8847","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/comments?post=8847"}],"version-history":[{"count":5,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/posts\/8847\/revisions"}],"predecessor-version":[{"id":8857,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/posts\/8847\/revisions\/8857"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/media\/8849"}],"wp:attachment":[{"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/media?parent=8847"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/categories?post=8847"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.testleaf.com\/blog\/wp-json\/wp\/v2\/tags?post=8847"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}