Skip to content

Commit 4b0a608

Browse files
committed
feat: e2e HMR tests
1 parent 8797784 commit 4b0a608

File tree

3 files changed

+98
-2
lines changed

3 files changed

+98
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"eslint-plugin-unicorn": "^50.0.1",
4040
"esmock": "^2.7.3",
4141
"oclif": "^4.22.44",
42+
"playwright": "^1.48.0",
4243
"ts-node": "^10.9.2",
4344
"typescript": "^5.5.4"
4445
},

test/commands/lightning/dev/componentLocalPreview.nut.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { expect } from 'chai';
2020
import { TestSession } from '@salesforce/cli-plugins-testkit';
2121
import axios from 'axios';
2222
import * as dotenv from 'dotenv';
23+
import { chromium, Browser, Page } from 'playwright';
2324
import { toKebabCase } from './helpers/utils.js';
2425
import { createSfdxProject, createLwcComponent } from './helpers/projectSetup.js';
2526
import { startLightningDevServer } from './helpers/devServerUtils.js';
@@ -31,6 +32,7 @@ const INSTANCE_URL = process.env.TESTKIT_HUB_INSTANCE;
3132
const TEST_TIMEOUT_MS = 60_000;
3233
const STARTUP_DELAY_MS = 5000;
3334
const DEV_SERVER_PORT = 3000;
35+
const HMR_WAIT_MS = 3000; // Time to wait for HMR to apply changes
3436

3537
// Skip this test in CI environment - run only locally
3638
const shouldSkipTest = process.env.CI === 'true' || process.env.CI === '1';
@@ -104,6 +106,88 @@ const shouldSkipTest = process.env.CI === 'true' || process.env.CI === '1';
104106
componentHttpSuccess = false;
105107
}
106108

109+
// Launch browser and test HMR
110+
let browser: Browser | null = null;
111+
let page: Page | null = null;
112+
let hmrTestPassed = false;
113+
114+
try {
115+
browser = await chromium.launch({ headless: true });
116+
page = await browser.newPage();
117+
118+
// Navigate to component URL
119+
await page.goto(componentUrl, { waitUntil: 'networkidle' });
120+
121+
// Get initial content - check for the greeting text
122+
const initialGreeting = await page.locator('h1').textContent();
123+
expect(initialGreeting).to.include('Hello, World!');
124+
125+
// Get the component file path
126+
const componentJsPath = path.join(
127+
projectDir,
128+
'force-app',
129+
'main',
130+
'default',
131+
'lwc',
132+
componentName,
133+
`${componentName}.js`
134+
);
135+
136+
// Read current component file
137+
const originalJsContent = await fs.promises.readFile(componentJsPath, 'utf8');
138+
139+
// Modify the component - change greeting text
140+
const modifiedJsContent = originalJsContent.replace(
141+
"greeting = 'Hello, World!';",
142+
"greeting = 'Hello, HMR Test!';"
143+
);
144+
await fs.promises.writeFile(componentJsPath, modifiedJsContent);
145+
146+
// Wait for HMR to detect and apply changes
147+
await new Promise((r) => setTimeout(r, HMR_WAIT_MS));
148+
149+
// Wait for the page content to update (HMR should update without full reload)
150+
try {
151+
// Wait for the h1 element to contain the new text (HMR should update without full reload)
152+
// eslint-disable-next-line unicorn/numeric-separators-style
153+
await page.locator('h1').waitFor({ state: 'visible', timeout: 10000 });
154+
155+
// Poll for the updated content with retries
156+
let retries = 20;
157+
let foundUpdatedContent = false;
158+
while (retries > 0 && !foundUpdatedContent) {
159+
// eslint-disable-next-line no-await-in-loop
160+
const currentGreeting = await page.locator('h1').textContent();
161+
if (currentGreeting?.includes('Hello, HMR Test!')) {
162+
foundUpdatedContent = true;
163+
} else {
164+
// eslint-disable-next-line no-await-in-loop
165+
await new Promise((r) => setTimeout(r, 500));
166+
retries--;
167+
}
168+
}
169+
170+
// Verify the change is reflected
171+
const updatedGreeting = await page.locator('h1').textContent();
172+
expect(updatedGreeting).to.include('Hello, HMR Test!');
173+
expect(foundUpdatedContent, 'HMR did not update the component within the timeout period').to.be.true;
174+
hmrTestPassed = true;
175+
} catch (hmrError) {
176+
stderrOutput += `HMR test failed: ${String(hmrError)}\n`;
177+
hmrTestPassed = false;
178+
}
179+
180+
// Restore original content
181+
await fs.promises.writeFile(componentJsPath, originalJsContent);
182+
} catch (browserError) {
183+
const err = browserError as { message?: string };
184+
stderrOutput += `Browser automation error: ${err.message ?? 'Unknown error'}\n`;
185+
hmrTestPassed = false;
186+
} finally {
187+
if (page) await page.close();
188+
if (browser) await browser.close();
189+
}
190+
107191
// Clean up
108192
try {
109193
if (serverProcess.pid && process.kill(serverProcess.pid, 0)) {
@@ -133,5 +217,7 @@ const shouldSkipTest = process.env.CI === 'true' || process.env.CI === '1';
133217
componentHttpSuccess,
134218
`Dev server did not respond with HTTP 200 for component URL. Tried URL: ${componentUrl}`
135219
).to.be.true;
220+
expect(hmrTestPassed, `HMR test failed. Component changes were not hot-swapped. Full stderr: ${stderrOutput}`).to.be
221+
.true;
136222
});
137223
});

test/commands/lightning/dev/helpers/devServerUtils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,19 @@ const currentFile = fileURLToPath(import.meta.url);
2222
const currentDir = path.dirname(currentFile);
2323
const pluginRoot = path.resolve(currentDir, '../../../../..');
2424

25-
export const startLightningDevServer = (projectDir: string, componentName: string): ChildProcess => {
25+
export const startLightningDevServer = (
26+
projectDir: string,
27+
componentName: string,
28+
orgUsername?: string
29+
): ChildProcess => {
2630
const devScriptPath = path.join(pluginRoot, 'bin', 'run.js');
2731

28-
return spawn('node', [devScriptPath, 'lightning', 'dev', 'component', '--name', componentName], {
32+
const args = [devScriptPath, 'lightning', 'dev', 'component', '--name', componentName];
33+
if (orgUsername) {
34+
args.push('--target-org', orgUsername);
35+
}
36+
37+
return spawn('node', args, {
2938
cwd: projectDir,
3039
env: { ...process.env, NODE_ENV: 'production', PORT: '3000', OPEN_BROWSER: process.env.OPEN_BROWSER ?? 'false' },
3140
});

0 commit comments

Comments
 (0)