Skip to content

Commit 1379ded

Browse files
test: harden automatic vendor sharing playwright coverage (#4402)
1 parent 53e568c commit 1379ded

File tree

7 files changed

+132
-252
lines changed

7 files changed

+132
-252
lines changed

advanced-api/automatic-vendor-sharing/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,10 +449,10 @@ jest.mock('app2/Button', () => {
449449

450450
```bash
451451
# Interactive mode
452-
npm run cypress:debug
452+
pnpm exec playwright test --ui
453453

454454
# Headless mode
455-
npm run e2e:ci
455+
pnpm run e2e:ci
456456
```
457457

458458
### Unit Tests
@@ -470,7 +470,7 @@ npm test
470470
- [Module Federation Documentation](https://module-federation.io/)
471471
- [Webpack Module Federation](https://webpack.js.org/concepts/module-federation/)
472472
- [Module Federation Enhanced](https://github.com/module-federation/enhanced)
473-
- [Best Practices Guide](../../cypress-e2e/README.md)
473+
- [Playwright Testing Guide](https://playwright.dev/docs/test-intro)
474474

475475
## Contributing
476476

advanced-api/automatic-vendor-sharing/cypress.env.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.cy.ts

Lines changed: 0 additions & 87 deletions
This file was deleted.
Lines changed: 95 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,127 @@
1-
import { test, expect, Page } from '@playwright/test';
2-
3-
// Helper functions
4-
async function openLocalhost(page: Page, port: number) {
5-
await page.goto(`http://localhost:${port}`);
6-
await page.waitForLoadState('networkidle');
7-
}
8-
9-
async function checkElementWithTextPresence(page: Page, selector: string, text: string) {
10-
const element = page.locator(`${selector}:has-text("${text}")`);
11-
await expect(element).toBeVisible();
12-
}
13-
14-
async function clickElementWithText(page: Page, selector: string, text: string) {
15-
await page.click(`${selector}:has-text("${text}")`);
16-
}
17-
18-
19-
20-
const appsData = [
1+
import { test, expect } from '@playwright/test';
2+
import { BasePage } from './utils/base-test';
3+
import { Constants } from './utils/constants';
4+
import { selectors } from './utils/selectors';
5+
6+
type AppInfo = {
7+
host: number;
8+
appDisplayName: string;
9+
localButtonText: string;
10+
localButtonColor: string;
11+
remoteButtonText: string;
12+
remoteButtonColor: string;
13+
localSectionHeading: string;
14+
localSectionDescription: string;
15+
remoteSectionHeading: string;
16+
remoteSectionDescription: string;
17+
};
18+
19+
const headerText = Constants.commonConstantsData.headerText;
20+
const localSectionHeading = Constants.sections.localHeading;
21+
const infoSection = Constants.infoSection;
22+
23+
const apps: AppInfo[] = [
2124
{
22-
headerText: 'Module Federation with Automatic Vendor Sharing',
23-
appNameText: 'App 1 (Host & Remote)',
24-
buttonColor: 'rgb(255, 0, 0)',
2525
host: 3001,
26+
appDisplayName: Constants.commonConstantsData.appDisplayNames.app1,
27+
localButtonText: Constants.commonConstantsData.buttonLabels.app1,
28+
localButtonColor: Constants.color.app1Button,
29+
remoteButtonText: Constants.commonConstantsData.buttonLabels.app2,
30+
remoteButtonColor: Constants.color.app2Button,
31+
localSectionHeading,
32+
localSectionDescription: Constants.sections.descriptions.app1Local,
33+
remoteSectionHeading: Constants.sections.remoteHeadings.app1,
34+
remoteSectionDescription: Constants.sections.descriptions.app1Remote,
2635
},
2736
{
28-
headerText: 'Module Federation with Automatic Vendor Sharing',
29-
appNameText: 'App 2 (Host & Remote)',
30-
buttonColor: 'rgb(0, 0, 139)',
3137
host: 3002,
38+
appDisplayName: Constants.commonConstantsData.appDisplayNames.app2,
39+
localButtonText: Constants.commonConstantsData.buttonLabels.app2,
40+
localButtonColor: Constants.color.app2Button,
41+
remoteButtonText: Constants.commonConstantsData.buttonLabels.app1,
42+
remoteButtonColor: Constants.color.app1Button,
43+
localSectionHeading,
44+
localSectionDescription: Constants.sections.descriptions.app2Local,
45+
remoteSectionHeading: Constants.sections.remoteHeadings.app2,
46+
remoteSectionDescription: Constants.sections.descriptions.app2Remote,
3247
},
3348
];
3449

35-
test.describe('Automatic Vendor Sharing E2E Tests', () => {
36-
37-
appsData.forEach((appData) => {
38-
const { host, appNameText, headerText } = appData;
39-
40-
test.describe(`Check ${appNameText}`, () => {
41-
test(`should display ${appNameText} header and subheader correctly`, async ({ page }) => {
50+
test.describe('Automatic Vendor Sharing example', () => {
51+
for (const app of apps) {
52+
test.describe(app.appDisplayName, () => {
53+
test(`renders the shell for ${app.appDisplayName}`, async ({ page }) => {
54+
const basePage = new BasePage(page);
4255
const consoleErrors: string[] = [];
56+
4357
page.on('console', (msg) => {
4458
if (msg.type() === 'error') {
4559
consoleErrors.push(msg.text());
4660
}
4761
});
4862

49-
await openLocalhost(page, host);
63+
await basePage.openLocalhost(app.host);
5064

51-
// Check header and subheader exist
52-
await checkElementWithTextPresence(page, 'h1', headerText);
53-
await checkElementWithTextPresence(page, 'h2', appNameText);
65+
await expect(page.locator(selectors.tags.headers.h1)).toContainText(headerText);
66+
await expect(page.locator(selectors.tags.headers.h2)).toContainText(app.appDisplayName);
5467

55-
// Verify no critical console errors
56-
const criticalErrors = consoleErrors.filter(error =>
57-
error.includes('Failed to fetch') ||
58-
error.includes('ChunkLoadError') ||
59-
error.includes('Module not found') ||
60-
error.includes('TypeError')
61-
);
62-
expect(criticalErrors).toHaveLength(0);
63-
});
68+
await expect(page.getByRole('heading', { level: 3, name: app.localSectionHeading })).toBeVisible();
69+
await expect(page.getByText(app.localSectionDescription)).toBeVisible();
6470

65-
test(`should display ${appNameText} button correctly`, async ({ page }) => {
66-
await openLocalhost(page, host);
71+
await expect(page.getByRole('heading', { level: 3, name: app.remoteSectionHeading })).toBeVisible();
72+
await expect(page.getByText(app.remoteSectionDescription, { exact: false })).toBeVisible();
6773

68-
const buttonText = `${appNameText.split(' ')[0]} ${appNameText.split(' ')[1]} Button`;
69-
70-
// Check button exists with correct text
71-
await checkElementWithTextPresence(page, 'button', buttonText);
72-
});
74+
await expect(page.getByRole('heading', { level: 3, name: infoSection.heading })).toBeVisible();
75+
await expect(page.getByText(infoSection.summary)).toBeVisible();
76+
await expect(page.getByText(infoSection.sharedDependencies)).toBeVisible();
77+
await expect(page.getByText(infoSection.loadStrategy)).toBeVisible();
78+
await expect(page.getByText(infoSection.benefits)).toBeVisible();
7379

74-
test(`should handle ${appNameText} button interactions`, async ({ page }) => {
75-
await openLocalhost(page, host);
80+
const relevantErrors = consoleErrors.filter((error) => {
81+
if (error.includes('WebSocket connection to') && error.includes('WEB_SOCKET_CONNECT_MAGIC_ID')) {
82+
return false;
83+
}
7684

77-
const buttonText = `${appNameText.split(' ')[0]} ${appNameText.split(' ')[1]} Button`;
78-
79-
// Click the button and verify it responds
80-
await clickElementWithText(page, 'button', buttonText);
81-
82-
// Verify button is still visible and functional after click
83-
await checkElementWithTextPresence(page, 'button', buttonText);
84-
});
85-
});
86-
});
87-
88-
test.describe('Cross-App Integration Tests', () => {
89-
test('should demonstrate automatic vendor sharing between apps', async ({ page }) => {
90-
const networkRequests: string[] = [];
91-
92-
page.on('request', (request) => {
93-
networkRequests.push(request.url());
94-
});
85+
if (error.includes('dynamic-remote-type-hints-plugin')) {
86+
return false;
87+
}
9588

96-
// Visit both apps to trigger vendor sharing
97-
await page.goto('http://localhost:3001');
98-
await page.waitForLoadState('networkidle');
99-
100-
await page.goto('http://localhost:3002');
101-
await page.waitForLoadState('networkidle');
102-
103-
// Verify shared dependencies are loaded efficiently
104-
const reactRequests = networkRequests.filter(url =>
105-
url.includes('react') && !url.includes('react-dom')
106-
);
107-
108-
// Should not load React multiple times due to vendor sharing
109-
expect(reactRequests.length).toBeLessThanOrEqual(10);
110-
});
89+
return true;
90+
});
11191

112-
test('should handle CORS correctly for federated modules', async ({ page }) => {
113-
const corsErrors: string[] = [];
114-
page.on('response', (response) => {
115-
if (response.status() >= 400 && response.url().includes('localhost:300')) {
116-
corsErrors.push(`${response.status()} - ${response.url()}`);
117-
}
92+
expect(relevantErrors, 'Unexpected console errors detected in the browser console').toHaveLength(0);
11893
});
11994

120-
// Test cross-origin requests work properly
121-
await page.goto('http://localhost:3001');
122-
await page.waitForLoadState('networkidle');
95+
test(`exposes the styled local button for ${app.appDisplayName}`, async ({ page }) => {
96+
const basePage = new BasePage(page);
12397

124-
// Should have no CORS errors
125-
expect(corsErrors).toHaveLength(0);
126-
});
98+
await basePage.openLocalhost(app.host);
12799

128-
test('should load applications within reasonable time', async ({ page }) => {
129-
const startTime = Date.now();
130-
131-
await page.goto('http://localhost:3001');
132-
await page.waitForLoadState('networkidle');
133-
134-
const loadTime = Date.now() - startTime;
135-
expect(loadTime).toBeLessThan(10000); // Should load within 10 seconds
136-
});
137-
});
100+
const localButton = page.getByRole('button', { name: app.localButtonText });
101+
await expect(localButton).toBeVisible();
102+
await expect(localButton.locator('span')).toHaveCount(1);
103+
await expect(localButton).toHaveCSS('background-color', app.localButtonColor);
138104

139-
test.describe('AutomaticVendorFederation Features', () => {
140-
test('should demonstrate shared vendor optimization', async ({ page }) => {
141-
await page.goto('http://localhost:3001');
142-
await page.waitForLoadState('networkidle');
105+
await localButton.click();
106+
await expect(localButton.locator('span')).toHaveCount(2);
107+
await expect(localButton.locator('span').nth(1)).toHaveText('1');
108+
});
143109

144-
// Check that the main elements are present
145-
await checkElementWithTextPresence(page, 'h1', 'Module Federation with Automatic Vendor Sharing');
146-
await checkElementWithTextPresence(page, 'h2', 'App 1 (Host & Remote)');
147-
});
110+
test(`loads the remote button for ${app.appDisplayName}`, async ({ page }) => {
111+
const basePage = new BasePage(page);
148112

149-
test('should handle error boundaries correctly', async ({ page }) => {
150-
const consoleErrors: string[] = [];
151-
page.on('console', (msg) => {
152-
if (msg.type() === 'error') {
153-
consoleErrors.push(msg.text());
154-
}
155-
});
113+
await basePage.openLocalhost(app.host);
114+
await basePage.waitForDynamicImport();
156115

157-
await page.goto('http://localhost:3001');
158-
await page.waitForLoadState('networkidle');
159-
160-
// Click button to test functionality
161-
const buttonExists = await page.locator('button').first().isVisible();
162-
if (buttonExists) {
163-
await page.locator('button').first().click();
164-
await page.waitForTimeout(1000);
165-
}
166-
167-
// Should handle any errors gracefully
168-
const criticalErrors = consoleErrors.filter(error =>
169-
error.includes('Uncaught') &&
170-
!error.includes('webpack-dev-server') &&
171-
!error.includes('DevTools')
172-
);
173-
expect(criticalErrors).toHaveLength(0);
116+
const remoteButton = page.getByRole('button', { name: app.remoteButtonText });
117+
await expect(remoteButton).toBeVisible();
118+
await expect(page.getByText(app.remoteSectionDescription, { exact: false })).toBeVisible();
119+
await expect(remoteButton).toHaveCSS('background-color', app.remoteButtonColor);
120+
121+
await remoteButton.click();
122+
await expect(remoteButton.locator('span')).toHaveCount(2);
123+
await expect(remoteButton.locator('span').nth(1)).toHaveText('1');
124+
});
174125
});
175-
});
176-
});
126+
}
127+
});

0 commit comments

Comments
 (0)