From ebdf0f0bd6edcb5007fe378e9fa60257560f295f Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:44:32 -0700 Subject: [PATCH] test: harden automatic vendor sharing playwright coverage --- .../automatic-vendor-sharing/README.md | 6 +- .../automatic-vendor-sharing/cypress.env.json | 4 - .../e2e/checkAutomaticVendorApps.cy.ts | 87 ------- .../e2e/checkAutomaticVendorApps.spec.ts | 239 +++++++----------- .../e2e/utils/constants.ts | 36 ++- .../e2e/utils/selectors.ts | 10 +- .../playwright.config.ts | 2 +- 7 files changed, 132 insertions(+), 252 deletions(-) delete mode 100644 advanced-api/automatic-vendor-sharing/cypress.env.json delete mode 100644 advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.cy.ts diff --git a/advanced-api/automatic-vendor-sharing/README.md b/advanced-api/automatic-vendor-sharing/README.md index 49573321cbe..a1c1a4d8374 100644 --- a/advanced-api/automatic-vendor-sharing/README.md +++ b/advanced-api/automatic-vendor-sharing/README.md @@ -449,10 +449,10 @@ jest.mock('app2/Button', () => { ```bash # Interactive mode -npm run cypress:debug +pnpm exec playwright test --ui # Headless mode -npm run e2e:ci +pnpm run e2e:ci ``` ### Unit Tests @@ -470,7 +470,7 @@ npm test - [Module Federation Documentation](https://module-federation.io/) - [Webpack Module Federation](https://webpack.js.org/concepts/module-federation/) - [Module Federation Enhanced](https://github.com/module-federation/enhanced) -- [Best Practices Guide](../../cypress-e2e/README.md) +- [Playwright Testing Guide](https://playwright.dev/docs/test-intro) ## Contributing diff --git a/advanced-api/automatic-vendor-sharing/cypress.env.json b/advanced-api/automatic-vendor-sharing/cypress.env.json deleted file mode 100644 index e63233bb67d..00000000000 --- a/advanced-api/automatic-vendor-sharing/cypress.env.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "allure": true, - "allureResultsPath": "../../cypress-e2e/results/allure-results" -} diff --git a/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.cy.ts b/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.cy.ts deleted file mode 100644 index 013f88e73b9..00000000000 --- a/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.cy.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { BaseMethods } from '../../../cypress-e2e/common/base'; -import { baseSelectors } from '../../../cypress-e2e/common/selectors'; -import { Constants } from '../../../cypress-e2e/fixtures/constants'; -import { CssAttr } from '../../../cypress-e2e/types/cssAttr'; - -const basePage: BaseMethods = new BaseMethods(); - -const appButtonPosition: number = 0; - -const appsData = [ - { - headerSelector: baseSelectors.tags.headers.h1, - subHeaderSelector: baseSelectors.tags.headers.h2, - buttonSelector: baseSelectors.tags.coreElements.button, - headerText: Constants.commonConstantsData.biDirectional, - appNameText: Constants.commonConstantsData.commonCountAppNames.app1, - buttonColor: Constants.color.red, - host: 3001, - }, - { - headerSelector: baseSelectors.tags.headers.h1, - subHeaderSelector: baseSelectors.tags.headers.h2, - buttonSelector: baseSelectors.tags.coreElements.button, - headerText: Constants.commonConstantsData.biDirectional, - appNameText: Constants.commonConstantsData.commonCountAppNames.app2, - buttonColor: Constants.color.deepBlue, - host: 3002, - }, -]; - -appsData.forEach( - (property: { - headerSelector: string; - subHeaderSelector: string; - buttonSelector: string; - headerText: string; - appNameText: string; - buttonColor: string; - host: number; - }) => { - const host = property.host === 3002 ? appsData[1].host : appsData[0].host; - const appName = property.host === 3002 ? appsData[1].appNameText : appsData[0].appNameText; - const color = property.host === 3002 ? appsData[1].buttonColor : appsData[0].buttonColor; - - describe(`Automatic Vendor Sharing`, () => { - context(`Check ${appName}`, () => { - beforeEach(() => { - basePage.openLocalhost({ - number: host, - }); - }); - - it(`Check ${appName} header and subheader exist on the page`, () => { - basePage.checkElementWithTextPresence({ - selector: property.headerSelector, - text: property.headerText, - }); - basePage.checkElementWithTextPresence({ - selector: property.subHeaderSelector, - text: `${appName}`, - }); - }); - - it(`Check buttons in ${appName} exist`, () => { - basePage.checkElementWithTextPresence({ - selector: property.buttonSelector, - text: `${appName} ${Constants.commonConstantsData.button}`, - }); - }); - - it(`Check button property in ${appName}`, () => { - basePage.checkElementContainText({ - selector: property.buttonSelector, - text: `${appName} ${Constants.commonConstantsData.button}`, - index: appButtonPosition, - }); - basePage.checkElementHaveProperty({ - selector: property.buttonSelector, - text: `${appName} ${Constants.commonConstantsData.button}`, - prop: CssAttr.background, - value: color, - }); - }); - }); - }); - }, -); diff --git a/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts b/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts index 6bd6ba97b75..df99430301b 100644 --- a/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts +++ b/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts @@ -1,176 +1,127 @@ -import { test, expect, Page } from '@playwright/test'; - -// Helper functions -async function openLocalhost(page: Page, port: number) { - await page.goto(`http://localhost:${port}`); - await page.waitForLoadState('networkidle'); -} - -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}")`); -} - - - -const appsData = [ +import { test, expect } from '@playwright/test'; +import { BasePage } from './utils/base-test'; +import { Constants } from './utils/constants'; +import { selectors } from './utils/selectors'; + +type AppInfo = { + host: number; + appDisplayName: string; + localButtonText: string; + localButtonColor: string; + remoteButtonText: string; + remoteButtonColor: string; + localSectionHeading: string; + localSectionDescription: string; + remoteSectionHeading: string; + remoteSectionDescription: string; +}; + +const headerText = Constants.commonConstantsData.headerText; +const localSectionHeading = Constants.sections.localHeading; +const infoSection = Constants.infoSection; + +const apps: AppInfo[] = [ { - headerText: 'Module Federation with Automatic Vendor Sharing', - appNameText: 'App 1 (Host & Remote)', - buttonColor: 'rgb(255, 0, 0)', host: 3001, + appDisplayName: Constants.commonConstantsData.appDisplayNames.app1, + localButtonText: Constants.commonConstantsData.buttonLabels.app1, + localButtonColor: Constants.color.app1Button, + remoteButtonText: Constants.commonConstantsData.buttonLabels.app2, + remoteButtonColor: Constants.color.app2Button, + localSectionHeading, + localSectionDescription: Constants.sections.descriptions.app1Local, + remoteSectionHeading: Constants.sections.remoteHeadings.app1, + remoteSectionDescription: Constants.sections.descriptions.app1Remote, }, { - headerText: 'Module Federation with Automatic Vendor Sharing', - appNameText: 'App 2 (Host & Remote)', - buttonColor: 'rgb(0, 0, 139)', host: 3002, + appDisplayName: Constants.commonConstantsData.appDisplayNames.app2, + localButtonText: Constants.commonConstantsData.buttonLabels.app2, + localButtonColor: Constants.color.app2Button, + remoteButtonText: Constants.commonConstantsData.buttonLabels.app1, + remoteButtonColor: Constants.color.app1Button, + localSectionHeading, + localSectionDescription: Constants.sections.descriptions.app2Local, + remoteSectionHeading: Constants.sections.remoteHeadings.app2, + remoteSectionDescription: Constants.sections.descriptions.app2Remote, }, ]; -test.describe('Automatic Vendor Sharing E2E Tests', () => { - - appsData.forEach((appData) => { - const { host, appNameText, headerText } = appData; - - test.describe(`Check ${appNameText}`, () => { - test(`should display ${appNameText} header and subheader correctly`, async ({ page }) => { +test.describe('Automatic Vendor Sharing example', () => { + for (const app of apps) { + test.describe(app.appDisplayName, () => { + test(`renders the shell for ${app.appDisplayName}`, async ({ page }) => { + const basePage = new BasePage(page); const consoleErrors: string[] = []; + page.on('console', (msg) => { if (msg.type() === 'error') { consoleErrors.push(msg.text()); } }); - await openLocalhost(page, host); + await basePage.openLocalhost(app.host); - // Check header and subheader exist - await checkElementWithTextPresence(page, 'h1', headerText); - await checkElementWithTextPresence(page, 'h2', appNameText); + await expect(page.locator(selectors.tags.headers.h1)).toContainText(headerText); + await expect(page.locator(selectors.tags.headers.h2)).toContainText(app.appDisplayName); - // Verify no critical console errors - const criticalErrors = consoleErrors.filter(error => - error.includes('Failed to fetch') || - error.includes('ChunkLoadError') || - error.includes('Module not found') || - error.includes('TypeError') - ); - expect(criticalErrors).toHaveLength(0); - }); + await expect(page.getByRole('heading', { level: 3, name: app.localSectionHeading })).toBeVisible(); + await expect(page.getByText(app.localSectionDescription)).toBeVisible(); - test(`should display ${appNameText} button correctly`, async ({ page }) => { - await openLocalhost(page, host); + await expect(page.getByRole('heading', { level: 3, name: app.remoteSectionHeading })).toBeVisible(); + await expect(page.getByText(app.remoteSectionDescription, { exact: false })).toBeVisible(); - const buttonText = `${appNameText.split(' ')[0]} ${appNameText.split(' ')[1]} Button`; - - // Check button exists with correct text - await checkElementWithTextPresence(page, 'button', buttonText); - }); + await expect(page.getByRole('heading', { level: 3, name: infoSection.heading })).toBeVisible(); + await expect(page.getByText(infoSection.summary)).toBeVisible(); + await expect(page.getByText(infoSection.sharedDependencies)).toBeVisible(); + await expect(page.getByText(infoSection.loadStrategy)).toBeVisible(); + await expect(page.getByText(infoSection.benefits)).toBeVisible(); - test(`should handle ${appNameText} button interactions`, async ({ page }) => { - await openLocalhost(page, host); + const relevantErrors = consoleErrors.filter((error) => { + if (error.includes('WebSocket connection to') && error.includes('WEB_SOCKET_CONNECT_MAGIC_ID')) { + return false; + } - const buttonText = `${appNameText.split(' ')[0]} ${appNameText.split(' ')[1]} Button`; - - // Click the button and verify it responds - await clickElementWithText(page, 'button', buttonText); - - // Verify button is still visible and functional after click - await checkElementWithTextPresence(page, 'button', buttonText); - }); - }); - }); - - test.describe('Cross-App Integration Tests', () => { - test('should demonstrate automatic vendor sharing between apps', async ({ page }) => { - const networkRequests: string[] = []; - - page.on('request', (request) => { - networkRequests.push(request.url()); - }); + if (error.includes('dynamic-remote-type-hints-plugin')) { + return false; + } - // Visit both apps to trigger vendor sharing - await page.goto('http://localhost:3001'); - await page.waitForLoadState('networkidle'); - - await page.goto('http://localhost:3002'); - await page.waitForLoadState('networkidle'); - - // Verify shared dependencies are loaded efficiently - const reactRequests = networkRequests.filter(url => - url.includes('react') && !url.includes('react-dom') - ); - - // Should not load React multiple times due to vendor sharing - expect(reactRequests.length).toBeLessThanOrEqual(10); - }); + return true; + }); - test('should handle CORS correctly for federated modules', async ({ page }) => { - const corsErrors: string[] = []; - page.on('response', (response) => { - if (response.status() >= 400 && response.url().includes('localhost:300')) { - corsErrors.push(`${response.status()} - ${response.url()}`); - } + expect(relevantErrors, 'Unexpected console errors detected in the browser console').toHaveLength(0); }); - // Test cross-origin requests work properly - await page.goto('http://localhost:3001'); - await page.waitForLoadState('networkidle'); + test(`exposes the styled local button for ${app.appDisplayName}`, async ({ page }) => { + const basePage = new BasePage(page); - // Should have no CORS errors - expect(corsErrors).toHaveLength(0); - }); + await basePage.openLocalhost(app.host); - test('should load applications within reasonable time', async ({ page }) => { - const startTime = Date.now(); - - await page.goto('http://localhost:3001'); - await page.waitForLoadState('networkidle'); - - const loadTime = Date.now() - startTime; - expect(loadTime).toBeLessThan(10000); // Should load within 10 seconds - }); - }); + const localButton = page.getByRole('button', { name: app.localButtonText }); + await expect(localButton).toBeVisible(); + await expect(localButton.locator('span')).toHaveCount(1); + await expect(localButton).toHaveCSS('background-color', app.localButtonColor); - test.describe('AutomaticVendorFederation Features', () => { - test('should demonstrate shared vendor optimization', async ({ page }) => { - await page.goto('http://localhost:3001'); - await page.waitForLoadState('networkidle'); + await localButton.click(); + await expect(localButton.locator('span')).toHaveCount(2); + await expect(localButton.locator('span').nth(1)).toHaveText('1'); + }); - // Check that the main elements are present - await checkElementWithTextPresence(page, 'h1', 'Module Federation with Automatic Vendor Sharing'); - await checkElementWithTextPresence(page, 'h2', 'App 1 (Host & Remote)'); - }); + test(`loads the remote button for ${app.appDisplayName}`, async ({ page }) => { + const basePage = new BasePage(page); - test('should handle error boundaries correctly', async ({ page }) => { - const consoleErrors: string[] = []; - page.on('console', (msg) => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); - } - }); + await basePage.openLocalhost(app.host); + await basePage.waitForDynamicImport(); - await page.goto('http://localhost:3001'); - await page.waitForLoadState('networkidle'); - - // Click button to test functionality - const buttonExists = await page.locator('button').first().isVisible(); - if (buttonExists) { - await page.locator('button').first().click(); - await page.waitForTimeout(1000); - } - - // Should handle any errors gracefully - const criticalErrors = consoleErrors.filter(error => - error.includes('Uncaught') && - !error.includes('webpack-dev-server') && - !error.includes('DevTools') - ); - expect(criticalErrors).toHaveLength(0); + const remoteButton = page.getByRole('button', { name: app.remoteButtonText }); + await expect(remoteButton).toBeVisible(); + await expect(page.getByText(app.remoteSectionDescription, { exact: false })).toBeVisible(); + await expect(remoteButton).toHaveCSS('background-color', app.remoteButtonColor); + + await remoteButton.click(); + await expect(remoteButton.locator('span')).toHaveCount(2); + await expect(remoteButton.locator('span').nth(1)).toHaveText('1'); + }); }); - }); -}); \ No newline at end of file + } +}); diff --git a/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts b/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts index 96b1b49f2ed..2c842375c78 100644 --- a/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts +++ b/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts @@ -1,14 +1,38 @@ export const Constants = { commonConstantsData: { - biDirectional: 'Module Federation with Automatic Vendor Sharing', - button: 'Button', - commonCountAppNames: { + headerText: 'Module Federation with Automatic Vendor Sharing', + appDisplayNames: { app1: 'App 1 (Host & Remote)', app2: 'App 2 (Host & Remote)', }, + buttonLabels: { + app1: 'App 1 Button', + app2: 'App 2 Button', + }, }, color: { - red: 'rgb(255, 0, 0)', - deepBlue: 'rgb(0, 0, 139)', + app1Button: 'rgb(136, 0, 0)', + app2Button: 'rgb(0, 0, 204)', + }, + sections: { + localHeading: 'Local Component', + remoteHeadings: { + app1: 'Remote Component (App 2)', + app2: 'Remote Component (App 1)', + }, + descriptions: { + app1Local: "This button is served from App 1's local bundle", + app2Local: "This button is served from App 2's local bundle", + app1Remote: 'This button is loaded from App 2 via Module Federation', + app2Remote: 'This button is loaded from App 1 via Module Federation', + }, + }, + infoSection: { + heading: 'Automatic Vendor Sharing Info', + summary: + 'This example demonstrates AutomaticVendorFederation, which intelligently shares dependencies between microfrontends to optimize bundle sizes and prevent duplicate code.', + sharedDependencies: 'Shared Dependencies: react, react-dom', + loadStrategy: 'Load Strategy: loaded-first (uses the first loaded version)', + benefits: 'Benefits: Reduced bundle size, faster loading, consistent dependency versions', }, -}; \ No newline at end of file +}; diff --git a/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts b/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts index 6761574198a..1ad9bdea892 100644 --- a/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts +++ b/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts @@ -1,15 +1,11 @@ export const selectors = { - dataTestIds: { - app1Button: '[data-e2e="APP_1__BUTTON"]', - app2Button: '[data-e2e="APP_2__BUTTON"]', - }, tags: { headers: { - h1: 'h1', - h2: 'h2', + h1: 'header h1', + h2: 'header h2', }, coreElements: { button: 'button', }, }, -}; \ No newline at end of file +}; diff --git a/advanced-api/automatic-vendor-sharing/playwright.config.ts b/advanced-api/automatic-vendor-sharing/playwright.config.ts index a8727741bd5..f77e8c98740 100644 --- a/advanced-api/automatic-vendor-sharing/playwright.config.ts +++ b/advanced-api/automatic-vendor-sharing/playwright.config.ts @@ -43,4 +43,4 @@ export default defineConfig({ timeout: 120000, }, ], -}); \ No newline at end of file +});