From e9ef6adec15ede848916b13b9e265aa36b2a0db0 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:13:08 -0700 Subject: [PATCH] refactor: migrate runtime env e2e test to Playwright --- .../cypress.env.json | 4 - .../e2e/checkDynamicRemotesRuntimesApps.cy.ts | 104 -------- .../checkDynamicRemotesRuntimesApps.spec.ts | 232 +++++------------- .../e2e/utils/base-test.ts | 41 +++- .../e2e/utils/constants.ts | 29 ++- cypress-e2e/fixtures/constants.ts | 12 - playwright-e2e/fixtures/constants.ts | 12 - 7 files changed, 103 insertions(+), 331 deletions(-) delete mode 100644 advanced-api/dynamic-remotes-runtime-environment-variables/cypress.env.json delete mode 100644 advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.cy.ts diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/cypress.env.json b/advanced-api/dynamic-remotes-runtime-environment-variables/cypress.env.json deleted file mode 100644 index e63233bb67d..00000000000 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/cypress.env.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "allure": true, - "allureResultsPath": "../../cypress-e2e/results/allure-results" -} diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.cy.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.cy.ts deleted file mode 100644 index af3dfa9a939..00000000000 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.cy.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { BaseMethods } from '../../../cypress-e2e/common/base'; -import { baseSelectors } from '../../../cypress-e2e/common/selectors'; -import { Constants } from '../../../cypress-e2e/fixtures/constants'; -import { getDateWithFormat } from '../../../cypress-e2e/helpers/base-helper'; -const basePage: BaseMethods = new BaseMethods(); - -const appsData = [ - { - header: Constants.elementsText.dynamicSystemRemotesRuntimeApp.host.header, - subheader: Constants.commonConstantsData.basicComponents.host, - hostH3: Constants.elementsText.dynamicSystemRemotesRuntimeApp.host.hostH3, - paragraph: Constants.elementsText.dynamicSystemRemotesRuntimeApp.paragraph, - button: Constants.elementsText.dynamicSystemRemotesRuntimeApp.host.button, - loading: Constants.commonConstantsData.loading, - buttonHeader: Constants.elementsText.dynamicSystemRemotesRuntimeApp.buttonHeader, - buttonH2: Constants.elementsText.dynamicSystemRemotesRuntimeApp.buttonH2, - buttonParagraph: Constants.elementsText.dynamicSystemRemotesRuntimeApp.buttonParagraph, - host: 3000, - }, - { - header: Constants.elementsText.dynamicSystemRemotesRuntimeApp.host.header, - subheader: Constants.commonConstantsData.basicComponents.remote, - loading: Constants.commonConstantsData.loading, - buttonHeader: Constants.elementsText.dynamicSystemRemotesRuntimeApp.buttonHeader, - buttonH2: Constants.elementsText.dynamicSystemRemotesRuntimeApp.buttonH2, - buttonParagraph: Constants.elementsText.dynamicSystemRemotesRuntimeApp.buttonParagraph, - host: 3001, - }, -]; - -appsData.forEach( - (property: { - header: string; - subheader: string; - hostH3?: string; - paragraph?: string; - button?: string; - loading: string; - buttonHeader: string; - buttonH2: string; - buttonParagraph: string; - host: number; - }) => { - describe('Dynamic Remotes Runtime Enviroment Variables', () => { - context(`Check ${property.subheader} app`, () => { - beforeEach(() => { - basePage.openLocalhost({ - number: property.host, - }); - }); - - it(`Check ${property.subheader} app Widget functionality and application elements`, () => { - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.headers.h1, - text: property.header, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.headers.h2, - text: property.subheader, - }); - if (property.host === 3000) { - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.headers.h3, - text: property.hostH3, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.paragraph, - text: property.paragraph, - }); - basePage.clickElementWithText({ - selector: baseSelectors.tags.coreElements.button, - text: String(property.button), - }); - } - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.coreElements.div, - text: property.loading, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.coreElements.div, - text: property.loading, - isVisible: false, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.headers.h2, - text: property.buttonHeader, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.headers.h2, - text: property.buttonH2, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.paragraph, - text: property.buttonParagraph, - }); - basePage.checkElementWithTextPresence({ - selector: baseSelectors.tags.paragraph, - text: getDateWithFormat('current', 'MMMM Do YYYY, h:mm'), - }); - }); - }); - }); - }, -); diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts index 6dd5f54556e..5eb223cd357 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts @@ -1,187 +1,67 @@ -import { test, expect, Page } from '@playwright/test'; - -// Helper functions -async function openLocalhost(page: Page, port: number) { - // Set up console and error logging - const consoleMessages: string[] = []; - const pageErrors: string[] = []; - - page.on('console', (msg) => { - consoleMessages.push(`[${msg.type()}] ${msg.text()}`); - }); - - page.on('pageerror', (err) => { - pageErrors.push(`Page error: ${err.message}\nStack: ${err.stack || 'No stack trace'}`); - }); +import { test, expect } from '@playwright/test'; +import { BasePage } from './utils/base-test'; +import { selectors } from './utils/selectors'; +import { Constants } from './utils/constants'; - await page.goto(`http://localhost:${port}`); - - // Wait for the page to load but don't wait for networkidle since env loading might be polling - await page.waitForLoadState('load'); - - // Wait for the root element to be attached. It may not be visible immediately - // (for example, the remote app shows an empty #root while it loads env config), - // so we only wait for the element to exist in the DOM. - await page.waitForSelector('#root', { state: 'attached', timeout: 10000 }); - - // Log any errors found - if (pageErrors.length > 0) { - console.log('=== PAGE ERRORS ==='); - pageErrors.forEach(error => console.log(error)); - console.log('=================='); - } - - if (consoleMessages.length > 0) { - console.log('=== CONSOLE MESSAGES ==='); - consoleMessages.forEach(msg => console.log(msg)); - console.log('========================'); - } -} - -async function waitForEnvironmentLoading(page: Page) { - // Wait for either the loading screen to disappear or main content to appear - // The loading screen shows "Loading environment configuration..." - const loadingText = page.locator('text=Loading environment configuration...'); - const mainContent = page.locator('h1'); - - try { - // Wait up to 15 seconds for either loading to finish or main content to appear - await Promise.race([ - loadingText.waitFor({ state: 'hidden', timeout: 15000 }), - mainContent.waitFor({ state: 'visible', timeout: 15000 }) - ]); - } catch (error) { - console.log('Environment loading timeout - checking current page state'); - const pageContent = await page.content(); - console.log('Current page content length:', pageContent.length); - - // If still loading, wait a bit more and proceed - if (await loadingText.isVisible()) { - console.log('Still showing loading screen, waiting 10 more seconds...'); - await page.waitForTimeout(10000); - } - } -} - -async function checkElementWithTextPresence(page: Page, selector: string, text: string) { - const element = page.locator(`${selector}:has-text("${text}")`); - await expect(element).toBeVisible(); -} - -async function clickElementWithText(page: Page, selector: string, text: string) { - await page.click(`${selector}:has-text("${text}")`); -} - -async function checkDateFormat(page: Page) { - const dateElement = page.locator('text=/[A-Z][a-z]+ \\d{1,2}[a-z]{2} \\d{4}, \\d{1,2}:\\d{2}/').first(); - await expect(dateElement).toBeVisible(); -} - -const appsData = [ - { - header: 'Dynamic Remotes with Runtime Environment Variables', - subheader: 'Host', - hostH3: 'Environment Configuration:', - paragraph: 'This example demonstrates how Module Federation can load remote components dynamically', - button: 'Load Remote Widget', - buttonH2: 'Remote Widget', - buttonParagraph: 'Using momentjs for format the date', - host: 3000, - }, - { - header: 'Dynamic System Host', - subheader: 'Remote', - buttonH2: 'Remote Widget', - buttonParagraph: 'Using momentjs for format the date', - host: 3001, - }, -]; - -test.describe('Dynamic Remotes Runtime Environment Variables E2E Tests', () => { - - appsData.forEach((appData) => { - const { host, subheader, header, hostH3, paragraph, button, buttonH2, buttonParagraph } = appData; - - test.describe(`Check ${subheader} app`, () => { - test(`should display ${subheader} app widget functionality and application elements`, async ({ page }) => { - await openLocalhost(page, host); - - // Wait for environment loading to complete for host app - if (host === 3000) { - await waitForEnvironmentLoading(page); - } - - // Check main header - await checkElementWithTextPresence(page, 'h1', header); - - if (host === 3000) { - // Host app specific elements - await checkElementWithTextPresence(page, 'h3', hostH3!); - await checkElementWithTextPresence(page, 'p', paragraph!); - - // Click the load remote component button - await clickElementWithText(page, 'button', button!); - - // Wait for loading to complete - await page.waitForTimeout(3000); - } - - // Check that the remote component loaded successfully - await checkElementWithTextPresence(page, 'h2', buttonH2); - await checkElementWithTextPresence(page, 'p', buttonParagraph); - - // Check moment.js date formatting - await checkDateFormat(page); - }); - }); - }); +const { + host, + remoteApp, + widget, +} = Constants.elementsText.dynamicSystemRemotesRuntimeApp; - test.describe('Runtime Environment Variable Tests', () => { - test('should load environment configuration successfully', async ({ page }) => { - const networkRequests: string[] = []; - - page.on('request', (request) => { - networkRequests.push(request.url()); - }); - - await page.goto('http://localhost:3000'); - await page.waitForLoadState('load'); - await waitForEnvironmentLoading(page); - - // Check that env-config.json was loaded - const envConfigRequests = networkRequests.filter(url => - url.includes('env-config.json') - ); - - expect(envConfigRequests.length).toBeGreaterThan(0); - }); +const { envLoader, remoteConfigLoader } = Constants.commonConstantsData; + +test.describe('Dynamic Remotes with runtime environment variables', () => { + test('host application loads the remote widget on demand', async ({ page }) => { + const basePage = new BasePage(page); + + await basePage.openLocalhost(3000); + + await basePage.waitForTextToDisappear(selectors.tags.coreElements.div, envLoader, 15000); - test('should demonstrate dynamic remote loading with environment config', async ({ page }) => { - await openLocalhost(page, 3000); - await waitForEnvironmentLoading(page); + await basePage.checkElementWithTextPresence(selectors.tags.headers.h1, host.header); + await basePage.checkElementWithTextPresence(selectors.tags.headers.h3, host.envSectionTitle); + await basePage.checkElementWithTextPresence(selectors.tags.paragraph, host.paragraph); - // Click to load remote component - await clickElementWithText(page, 'button', 'Load Remote Widget'); + await basePage.clickElementWithText(selectors.tags.coreElements.button, host.button); - // Wait for loading to complete - await page.waitForTimeout(3000); + await basePage.waitForTextToDisappear(selectors.tags.coreElements.div, host.remoteLoading, 15000); + await basePage.waitForTextToDisappear(selectors.tags.coreElements.div, remoteConfigLoader, 15000); - // Verify remote component is now loaded - await checkElementWithTextPresence(page, 'h2', 'Remote Widget'); - await checkElementWithTextPresence(page, 'p', 'Using momentjs for format the date'); + await basePage.checkElementWithTextPresence(selectors.tags.headers.h3, host.remoteSectionTitle); + await basePage.checkElementWithTextPresence(selectors.tags.headers.h2, widget.title); + + const envHeading = page.getByRole('heading', { + level: 2, + name: new RegExp(`^${widget.envPrefix} `), }); + await expect(envHeading).toHaveText(new RegExp(`^${widget.envPrefix} https?://`)); + + await basePage.checkElementWithTextPresence(selectors.tags.paragraph, widget.paragraph); + await basePage.checkDateFormat(); }); - test.describe('Performance and Loading', () => { - test('should load applications within reasonable time', async ({ page }) => { - const startTime = Date.now(); - - await page.goto('http://localhost:3000'); - await page.waitForLoadState('load'); - await waitForEnvironmentLoading(page); - - const loadTime = Date.now() - startTime; - expect(loadTime).toBeLessThan(10000); // Should load within 10 seconds + test('remote application exposes the widget with runtime configuration', async ({ page }) => { + const basePage = new BasePage(page); + + await basePage.openLocalhost(3001); + + await basePage.checkElementWithTextPresence(selectors.tags.headers.h1, remoteApp.header); + await expect( + page.getByRole('heading', { level: 2, name: new RegExp(`^${remoteApp.subheader}$`) }), + ).toBeVisible(); + + await basePage.waitForTextToDisappear(selectors.tags.coreElements.div, remoteConfigLoader, 15000); + + await basePage.checkElementWithTextPresence(selectors.tags.headers.h2, widget.title); + + const remoteEnvHeading = page.getByRole('heading', { + level: 2, + name: new RegExp(`^${widget.envPrefix}`), }); + await expect(remoteEnvHeading).toHaveText(new RegExp(`^${widget.envPrefix} https?://`)); + + await basePage.checkElementWithTextPresence(selectors.tags.paragraph, widget.paragraph); + await basePage.checkDateFormat(); }); -}); \ No newline at end of file +}); diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts index 3582d78f830..0f8f4575975 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts @@ -1,27 +1,29 @@ -import { Page } from '@playwright/test'; +import { Page, expect } from '@playwright/test'; export class BasePage { constructor(public page: Page) {} async openLocalhost(port: number) { - await this.page.goto(`http://localhost:${port}`); - await this.page.waitForLoadState('networkidle'); + await this.page.goto(`http://localhost:${port}`, { waitUntil: 'domcontentloaded' }); + await this.page.waitForSelector('#root', { state: 'attached', timeout: 15000 }); } async checkElementWithTextPresence(selector: string, text: string, timeout: number = 10000) { - await this.page.locator(selector).filter({ hasText: text }).waitFor({ timeout }); + await expect(this.page.locator(selector).filter({ hasText: text }).first()).toBeVisible({ timeout }); } async checkElementVisibility(selector: string, timeout: number = 10000) { - await this.page.locator(selector).waitFor({ state: 'visible', timeout }); + await expect(this.page.locator(selector).first()).toBeVisible({ timeout }); } async checkElementHidden(selector: string, text: string, timeout: number = 10000) { - await this.page.locator(selector).filter({ hasText: text }).waitFor({ state: 'hidden', timeout }); + await expect( + this.page.locator(selector).filter({ hasText: text }).first(), + ).toBeHidden({ timeout }); } async clickElementWithText(selector: string, text: string) { - await this.page.locator(selector).filter({ hasText: text }).click(); + await this.page.locator(selector).filter({ hasText: text }).first().click(); } async waitForDynamicImport(timeout: number = 5000) { @@ -32,15 +34,30 @@ export class BasePage { async checkDateFormat() { // Check for moment.js formatted date (MMMM Do YYYY, h:mm format) - const dateElement = this.page.locator('text=/[A-Z][a-z]+ \\d{1,2}[a-z]{2} \\d{4}, \\d{1,2}:\\d{2}/'); - await dateElement.waitFor({ timeout: 5000 }); + const dateElement = this.page.locator( + 'text=/[A-Z][a-z]+ \\d{1,2}(st|nd|rd|th) \\d{4}, \\d{1,2}:\\d{2}:\\d{2} [ap]m/i', + ); + await expect(dateElement.first()).toBeVisible({ timeout: 5000 }); } async waitForLoadingToDisappear(loadingText: string, timeout: number = 10000) { // Wait for loading text to appear first - await this.page.locator(`text=${loadingText}`).waitFor({ timeout: 5000 }); - // Then wait for it to disappear - await this.page.locator(`text=${loadingText}`).waitFor({ state: 'hidden', timeout }); + const locator = this.page.locator(`text=${loadingText}`).first(); + await locator.waitFor({ state: 'attached', timeout: 5000 }).catch(() => undefined); + await locator.waitFor({ state: 'hidden', timeout }).catch(() => undefined); + } + + async waitForTextToDisappear(selector: string, text: string, timeout: number = 10000) { + const locator = this.page.locator(selector).filter({ hasText: text }).first(); + + try { + await locator.waitFor({ state: 'attached', timeout: 2000 }); + } catch (error) { + // Element never appeared; nothing to wait for. + return; + } + + await locator.waitFor({ state: 'hidden', timeout }).catch(() => undefined); } } diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts index a9cd28aba69..c7fba4a62a9 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts @@ -3,20 +3,27 @@ export const Constants = { dynamicSystemRemotesRuntimeApp: { host: { header: 'Dynamic Remotes with Runtime Environment Variables', - hostH3: 'Environment Configuration:', + envSectionTitle: 'Environment Configuration:', + paragraph: + 'This example demonstrates how Module Federation can load remote components dynamically with runtime environment variables. The remote URL and other configuration can be changed without rebuilding the application.', button: 'Load Remote Widget', + remoteSectionTitle: 'Remote Component:', + remoteLoading: 'Loading remote component... This may take a few seconds.', + remoteEmptyState: 'No remote component loaded. Click "Load Remote Widget" to begin.', + }, + remoteApp: { + header: 'Dynamic System Host', + subheader: 'Remote', + }, + widget: { + title: 'Remote Widget', + envPrefix: 'My env is', + paragraph: 'Using momentjs for format the date', }, - paragraph: 'This example demonstrates how Module Federation can load remote components dynamically', - buttonHeader: 'Remote Component:', - buttonH2: 'Remote Widget', - buttonParagraph: 'Using momentjs for format the date', }, }, commonConstantsData: { - basicComponents: { - host: 'Host', - remote: 'Remote', - }, - loading: 'Loading...', + envLoader: 'Loading environment configuration...', + remoteConfigLoader: 'Loading remote configuration...', }, -}; \ No newline at end of file +}; diff --git a/cypress-e2e/fixtures/constants.ts b/cypress-e2e/fixtures/constants.ts index fd6f2874e7b..37c931935eb 100644 --- a/cypress-e2e/fixtures/constants.ts +++ b/cypress-e2e/fixtures/constants.ts @@ -589,18 +589,6 @@ export class Constants { 'Principal Engineer at lululemon Distributed JavaScript Orchestration at scale. Maintainer of Webpack, inventor of Module Federation.', }, }, - dynamicSystemRemotesRuntimeApp: { - host: { - header: 'Dynamic System Host', - hostH3: 'my env is https://host.api.com', - button: 'Load Remote Widget', - }, - paragraph: - 'The Dynamic System will take advantage Module Federation remotes and exposes. It will not load components that have been loaded already.', - buttonHeader: 'Remote Widget', - buttonH2: 'My env is ', - buttonParagraph: 'Using momentjs for format the date', - }, sharedContextApp: { app1: { paragraph: 'Welcome, Billy', diff --git a/playwright-e2e/fixtures/constants.ts b/playwright-e2e/fixtures/constants.ts index fd6f2874e7b..37c931935eb 100644 --- a/playwright-e2e/fixtures/constants.ts +++ b/playwright-e2e/fixtures/constants.ts @@ -589,18 +589,6 @@ export class Constants { 'Principal Engineer at lululemon Distributed JavaScript Orchestration at scale. Maintainer of Webpack, inventor of Module Federation.', }, }, - dynamicSystemRemotesRuntimeApp: { - host: { - header: 'Dynamic System Host', - hostH3: 'my env is https://host.api.com', - button: 'Load Remote Widget', - }, - paragraph: - 'The Dynamic System will take advantage Module Federation remotes and exposes. It will not load components that have been loaded already.', - buttonHeader: 'Remote Widget', - buttonH2: 'My env is ', - buttonParagraph: 'Using momentjs for format the date', - }, sharedContextApp: { app1: { paragraph: 'Welcome, Billy',