{"record":{"id":"cf2f66a7682b3260","repo":"TryGhost/Ghost","slug":"portal-popup-did-not-open","errorCode":null,"errorMessage":"Portal popup did not open","messagePattern":"Portal popup did not open","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"e2e/helpers/pages/public/public-page.ts","lineNumber":48,"sourceCode":"     * Playwright's retry mechanism to handle the race condition deterministically.\n     */\n    async clickLinkAndWaitForPopup(link: Locator): Promise<void> {\n        await this.portalScript.waitFor({state: 'attached'});\n        await this.portalRoot.waitFor({state: 'attached'});\n\n        // Retry until Portal finishes async init and can react to the click.\n        for (let attempt = 0; attempt < 5; attempt++) {\n            await link.click();\n\n            try {\n                await this.portalIframe.waitFor({state: 'visible', timeout: 1000});\n                return;\n            } catch {\n                // Keep retrying until Portal's hashchange listener is ready.\n            }\n        }\n\n        throw new Error('Portal popup did not open');\n    }\n\n    async isPortalOpen(): Promise<boolean> {\n        const count = await this.portalFrame.count();\n        if (count === 0) {\n            return false;\n        }\n        return await this.portalFrame.isVisible();\n    }\n}\n\nexport class PublicPage extends BasePage {\n    public readonly portalRoot: Locator;\n    public readonly portalIframe: Locator;\n    public readonly portalPopupFrame: Locator;\n    public readonly portalScript: Locator;\n    private readonly subscribeLink: Locator;\n    private readonly signInLink: Locator;","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/helpers/pages/public/public-page.ts#L30-L66","documentation":"openPortalIfNotOpen() clicks the trigger link up to 5 times, each time waiting up to 1s for the portal iframe to become visible. If all 5 attempts fail it throws. The retry exists specifically because Portal's hashchange listener initializes asynchronously and may drop the first clicks. The throw means Portal never became interactive within ~5s.","triggerScenarios":"Portal JS bundle failed to load (CDN/script error) so the iframe never renders. The trigger link selector changed. Portal opened in a mode/location the iframe selector doesn't match. Heavy page where Portal init took >5s.","commonSituations":"Public app bundle (portal) missing or misbuilt; theme missing {{ghost_head}} so the portal script isn't injected; iframe selector drift after a Portal UI change; slow CI machine.","solutions":["Check browser console / network for the portal script failing to load; confirm {{ghost_head}} is in the theme.","Increase attempts or the per-attempt timeout for slow environments.","Verify the portalIframe selector still matches the current Portal DOM.","Ensure the public page fully loaded before clicking (waitForLoadState)."],"exampleFix":"// before\nfor (let attempt = 0; attempt < 5; attempt++) {\n    await link.click();\n    try { await this.portalIframe.waitFor({state:'visible', timeout: 1000}); return; } catch {}\n}\nthrow new Error('Portal popup did not open');\n\n// after\nfor (let attempt = 0; attempt < 10; attempt++) {\n    await link.click();\n    try { await this.portalIframe.waitFor({state:'visible', timeout: 2000}); return; } catch {}\n}\nthrow new Error('Portal popup did not open after 10 attempts');","handlingStrategy":"retry","validationCode":"// Confirm the portal script is injected before clicking:\nconst hasPortal = await page.evaluate(() => !!document.querySelector('script[src*=\"portal\"]'));\nif (!hasPortal) throw new Error('Portal script not present on page');","typeGuard":null,"tryCatchPattern":"for (let attempt = 0; attempt < 10; attempt++) {\n    await link.click();\n    try { await this.portalIframe.waitFor({state:'visible', timeout: 2000}); return; }\n    catch { /* Portal init is async; retry */ }\n}\nthrow new Error('Portal popup did not open');","preventionTips":["Ensure the theme includes {{ghost_head}} so the portal script loads.","Wait for page load (waitForLoadState) before clicking the trigger.","Keep the iframe selector in sync with Portal DOM changes."],"tags":["playwright","e2e","portal","flaky","async-init"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}