Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions advanced-api/automatic-vendor-sharing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
4 changes: 0 additions & 4 deletions advanced-api/automatic-vendor-sharing/cypress.env.json

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,176 +1,97 @@
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;
};

const headerText = Constants.commonConstantsData.headerText;

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,
},
{
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,
},
];

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);

// Check header and subheader exist
await checkElementWithTextPresence(page, 'h1', headerText);
await checkElementWithTextPresence(page, 'h2', appNameText);
await basePage.openLocalhost(app.host);

// 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.locator(selectors.tags.headers.h1)).toContainText(headerText);
await expect(page.locator(selectors.tags.headers.h2)).toContainText(app.appDisplayName);

test(`should display ${appNameText} button correctly`, 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`;

// Check button exists with correct text
await checkElementWithTextPresence(page, 'button', buttonText);
});
if (error.includes('dynamic-remote-type-hints-plugin')) {
return false;
}

test(`should handle ${appNameText} button interactions`, async ({ page }) => {
await openLocalhost(page, host);
return true;
});

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());
expect(relevantErrors, 'Unexpected console errors detected in the browser console').toHaveLength(0);
});

// 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);
});
test(`exposes the styled local button for ${app.appDisplayName}`, async ({ page }) => {
const basePage = new BasePage(page);

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()}`);
}
});
await basePage.openLocalhost(app.host);

// Test cross-origin requests work properly
await page.goto('http://localhost:3001');
await page.waitForLoadState('networkidle');
const localButton = page.getByRole('button', { name: app.localButtonText });
await expect(localButton).toBeVisible();
await expect(localButton).toHaveCSS('background-color', app.localButtonColor);

// Should have no CORS errors
expect(corsErrors).toHaveLength(0);
});
await localButton.click();
await expect(localButton).toBeVisible();
});

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
});
});
test(`loads the remote button for ${app.appDisplayName}`, async ({ page }) => {
const basePage = new BasePage(page);

test.describe('AutomaticVendorFederation Features', () => {
test('should demonstrate shared vendor optimization', async ({ page }) => {
await page.goto('http://localhost:3001');
await page.waitForLoadState('networkidle');
await basePage.openLocalhost(app.host);
await basePage.waitForDynamicImport();

// 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)');
});
const remoteButton = page.getByRole('button', { name: app.remoteButtonText });
await expect(remoteButton).toBeVisible();
await expect(remoteButton).toHaveCSS('background-color', app.remoteButtonColor);

test('should handle error boundaries correctly', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
await remoteButton.click();
await expect(remoteButton).toBeVisible();
});

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);
});
});
});
}
});
13 changes: 8 additions & 5 deletions advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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)',
},
};
8 changes: 2 additions & 6 deletions advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
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',
Expand Down
Loading