Skip to content

Commit 34ed97f

Browse files
Shinonilesya7
andauthored
feat: off-core integration test on your local @W-18852148@ (#435)
* test: add CI test comment to verify main branch CI functionality - Small comment added to README to trigger CI - Testing if @lwrjs/api issue exists in main branch CI - This will help isolate whether the TypeScript error is pre-existing or introduced by our changes * test: add package.json changes from nut-test branch - Downgraded @oclif/plugin-command-snapshot from ^5.3.3 to ^5.3.2 - Added @salesforce/cli ^2.93.7 and dotenv ^16.5.0 dependencies - Added test:nut:local script - Updated volta Node.js version from 20.11.0 to 20.19.3 - Added yarn resolutions for @salesforce/core ^8.15.0 Testing if these package.json changes cause the @lwrjs/api TypeScript error in CI. * test: add TypeScript config changes from nut-test - Updated tsconfig.json to include ./src/**/*.d.ts files - Updated test/tsconfig.json to include ./**/*.nut.ts files - Testing if these TypeScript configuration changes cause @lwrjs/api module resolution issues in CI * test: add TypeScript declaration file from nut-test - Added src/lwrjs-api.d.ts with type declarations for @lwrjs/api - Declares SitesLocalDevOptions type and expDev/setupDev functions - Testing if this TypeScript declaration file causes @lwrjs/api module resolution issues in CI - This is the most likely culprit for the TS7016 error * fix: revert yarn.lock to main branch version - Reverted yarn.lock to remove problematic dependencies from earlier package.json changes - This should resolve the mocha/cliui dependency conflict in CI - Now we can properly test if the TypeScript declaration file causes @lwrjs/api issues * test: add NUT test files from nut-test branch - Added componentLocalPreview.nut.ts and supporting test infrastructure - Includes test helpers, test data, and test projects - Contains fixed dotenv import pattern (import * as dotenv) - Testing if NUT test files cause @lwrjs/api module resolution issues in CI * chore: add dotenv dependency for NUT test - Added dotenv ^16.5.0 to devDependencies - Required for componentLocalPreview.nut.ts test - Only adding dotenv, not other problematic dependencies * fix: remove dotenv dependency from NUT test to avoid dependency conflicts - Removed dotenv import and config() call from componentLocalPreview.nut.ts - Using process.env directly instead of dotenv.config() - This avoids the mocha/cliui dependency conflict that was preventing CI tests - Now we can test if NUT test files cause @lwrjs/api issues without package.json changes * refactor: remove TypeScript declaration file and clean up tsconfig - Removed src/lwrjs-api.d.ts declaration file - Cleaned up tsconfig.json to remove .d.ts includes - Cleaned up test/tsconfig.json to remove .nut.ts includes - Testing if dependency fixes alone resolve @lwrjs/api issue without TypeScript declarations * fix: resolve dependency conflicts and restore dotenv functionality - Fix mocha/cliui dependency conflict by adding [email protected] resolution - Restore dotenv functionality for .nut.ts test files - Update test/tsconfig.json to properly include .nut.ts files - All 56 tests passing with dependencies resolved * docs: add integration testing documentation to README * chore: add .env.template and update .gitignore for testing infrastructure * chore: remove redundant helloWorld component from force-app test project * docs: clarify SF CLI requirement for local integration testing - Added prerequisite that SF CLI must be installed before running integration tests locally - Clarifies that sf command must be available during local test execution - Provides installation link and verification command --------- Co-authored-by: lturanscaia <[email protected]>
1 parent 63e7f30 commit 34ed97f

File tree

14 files changed

+394
-49
lines changed

14 files changed

+394
-49
lines changed

.env.template

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# JWT private key
2+
TESTKIT_JWT_KEY="-----BEGIN RSA PRIVATE KEY-----
3+
FAKEPRIVATEKEY1234567890abcdefg==
4+
-----END RSA PRIVATE KEY-----"
5+
6+
# Connected App Client ID for JWT auth
7+
TESTKIT_JWT_CLIENT_ID=FAKE_CLIENT_ID_123456
8+
9+
# Username of your Dev Hub org
10+
11+
12+
# Path to the Salesforce CLI executable
13+
TESTKIT_EXECUTABLE_PATH=./node_modules/.bin/sf
14+
15+
# Instance URL of your Dev Hub org
16+
TESTKIT_HUB_INSTANCE=https://fake-devhub-instance.salesforce.com
17+
18+
# Prevent the dev server from opening the browser during tests
19+
OPEN_BROWSER=false

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,9 @@ bld
5656

5757
# sf cli
5858
.sf
59-
.sfdx
59+
.sfdx
60+
61+
stdout*
62+
stderr*
63+
64+
.env

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,94 @@ EXAMPLES
306306

307307
_See code: [src/commands/lightning/dev/site.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.3.3/src/commands/lightning/dev/site.ts)_
308308

309+
## Integration Testing
310+
311+
This plugin includes integration (NUT) tests for verifying the Lightning Dev Server functionality using SFDX projects and components. Test data like SFDX projects are created at runtime by the testkit.
312+
313+
### Prerequisites
314+
315+
**Salesforce CLI Installation**
316+
The Salesforce CLI must be installed and accessible in your system PATH before running integration tests locally. The plugin expects the `sf` command to be available during local test execution.
317+
318+
- Install the Salesforce CLI: https://developer.salesforce.com/tools/sfdxcli
319+
- Verify installation: `sf --version`
320+
321+
**Connected App Setup**
322+
Follow the [Connected App Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_connected_app.htm) to:
323+
324+
- Create a connected app
325+
- Enable JWT OAuth
326+
- Configure callback URL and OAuth scopes
327+
328+
**JWT Credentials**
329+
Follow the [Private Key and Certificate Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_key_and_cert.htm) to:
330+
331+
- Generate a private key and certificate
332+
- Upload certificate to the connected app
333+
334+
Set the environment variables (copy from `.env.template` to `.env`):
335+
336+
- `TESTKIT_JWT_CLIENT_ID` - Your connected app client ID
337+
- `TESTKIT_JWT_KEY` - Your private key contents
338+
- `TESTKIT_HUB_INSTANCE` - Salesforce instance URL
339+
- `TESTKIT_ORG_USERNAME` - Your org username for testing
340+
- `OPEN_BROWSER` - Control browser opening (true/false)
341+
342+
### Running Tests
343+
344+
Run all or specific integration tests (NUTs) via:
345+
346+
```bash
347+
# Run all integration tests
348+
yarn test:nuts
349+
350+
# Run component local preview test (local development only)
351+
yarn test:nut:local
352+
353+
# Run by category
354+
yarn test:nuts "test/commands/lightning/dev/component*.nut.ts"
355+
356+
# Run with environment variables
357+
OPEN_BROWSER=false NODE_ENV=production yarn test:nuts
358+
```
359+
360+
### Local-Only Tests
361+
362+
Some NUT tests are designed to run only in local development environments and are automatically skipped in CI pipelines. These tests typically:
363+
364+
- Require specific local setup or resources
365+
- Are too slow for CI environments
366+
- Need manual verification or debugging
367+
- Test features that aren't suitable for automated testing
368+
369+
**Component Local Preview Test**
370+
The `componentLocalPreview.nut.ts` test verifies that the Lightning Dev Server starts correctly and responds to component URLs. This test:
371+
372+
- Runs only locally (skipped when `CI=true`)
373+
- Tests server startup and HTTP response
374+
- Verifies component URL routing (`/c-hello-world/`)
375+
- Can be run with: `yarn test:nut:local`
376+
377+
### Test Data Structure
378+
379+
The testkit creates SFDX projects and test data at runtime:
380+
381+
```
382+
test/
383+
├── testdata/
384+
│ ├── lwc/
385+
│ │ └── helloWorld/
386+
│ └── project-definition.json
387+
```
388+
389+
### Automation with TestKit
390+
391+
Tests use [@salesforce/cli-plugins-testkit](https://github.com/salesforcecli/cli-plugins-testkit) for:
392+
393+
- Temporary project creation at runtime
394+
- JWT-based org login
395+
- Cleanup after each run
396+
397+
No manual authentication is needed. Set the required environment variables once and run tests headlessly.
398+
309399
<!-- commandsstop -->

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@types/node-fetch": "^2.6.12",
3232
"@types/xml2js": "^0.4.14",
3333
"@typescript-eslint/eslint-plugin": "^6.21.0",
34+
"dotenv": "^16.5.0",
3435
"eslint": "^8.57.0",
3536
"eslint-config-prettier": "^9.1.0",
3637
"eslint-plugin-header": "^3.1.1",
@@ -111,6 +112,9 @@
111112
"publishConfig": {
112113
"access": "public"
113114
},
115+
"resolutions": {
116+
"cliui": "7.0.4"
117+
},
114118
"wireit": {
115119
"build": {
116120
"dependencies": [
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import path from 'node:path';
9+
import fs from 'node:fs';
10+
import { expect } from 'chai';
11+
import { TestSession } from '@salesforce/cli-plugins-testkit';
12+
import axios from 'axios';
13+
import * as dotenv from 'dotenv';
14+
import { toKebabCase } from './helpers/utils.js';
15+
import { createSfdxProject, createLwcComponent } from './helpers/projectSetup.js';
16+
import { startLightningDevServer } from './helpers/devServerUtils.js';
17+
18+
// Load environment variables from .env file
19+
dotenv.config();
20+
21+
const INSTANCE_URL = process.env.TESTKIT_HUB_INSTANCE;
22+
const TEST_TIMEOUT_MS = 60_000;
23+
const STARTUP_DELAY_MS = 5000;
24+
const DEV_SERVER_PORT = 3000;
25+
26+
// Skip this test in CI environment - run only locally
27+
const shouldSkipTest = process.env.CI === 'true' || process.env.CI === '1';
28+
29+
(shouldSkipTest ? describe.skip : describe)('LWC Local Preview Integration', () => {
30+
let session: TestSession;
31+
let componentName: string;
32+
let projectDir: string;
33+
34+
before(async () => {
35+
componentName = 'helloWorld';
36+
37+
session = await TestSession.create({ devhubAuthStrategy: 'JWT' });
38+
39+
const timestamp = Date.now();
40+
projectDir = path.join(session.dir, `lwc-project-${timestamp}`);
41+
fs.mkdirSync(projectDir, { recursive: true });
42+
43+
await Promise.all([
44+
createSfdxProject(projectDir, INSTANCE_URL ?? ''),
45+
createLwcComponent(projectDir, componentName),
46+
]);
47+
});
48+
49+
after(async () => {
50+
await session?.clean();
51+
});
52+
53+
it('should start lightning dev server and respond to /c-hello-world/ URL', async function () {
54+
this.timeout(TEST_TIMEOUT_MS);
55+
56+
let stderrOutput = '';
57+
let stdoutOutput = '';
58+
let exitedEarly = false;
59+
let exitCode: number | null = null;
60+
61+
const serverProcess = startLightningDevServer(projectDir, componentName);
62+
63+
serverProcess.stderr?.on('data', (data: Buffer) => {
64+
stderrOutput += data.toString();
65+
});
66+
67+
serverProcess.stdout?.on('data', (data: Buffer) => {
68+
stdoutOutput += data.toString();
69+
});
70+
71+
serverProcess.on('exit', (code: number) => {
72+
exitedEarly = true;
73+
exitCode = code;
74+
});
75+
76+
serverProcess.on('error', (error) => {
77+
exitedEarly = true;
78+
stderrOutput += `Process error: ${String(error)}\n`;
79+
});
80+
81+
// Wait for server startup
82+
await new Promise((r) => setTimeout(r, STARTUP_DELAY_MS));
83+
84+
// Test the kebab-case component URL with /c- prefix
85+
const componentKebabName = toKebabCase(componentName);
86+
const componentUrl = `http://localhost:${DEV_SERVER_PORT}/c-${componentKebabName}/`;
87+
let componentHttpSuccess = false;
88+
89+
try {
90+
const componentResponse = await axios.get(componentUrl, { timeout: 2000 });
91+
componentHttpSuccess = componentResponse.status === 200;
92+
} catch (error) {
93+
const err = error as { message?: string };
94+
stderrOutput += `Component URL HTTP request failed: ${err.message ?? 'Unknown error'}\n`;
95+
componentHttpSuccess = false;
96+
}
97+
98+
// Clean up
99+
try {
100+
if (serverProcess.pid && process.kill(serverProcess.pid, 0)) {
101+
process.kill(serverProcess.pid, 'SIGKILL');
102+
}
103+
} catch (error) {
104+
const err = error as NodeJS.ErrnoException;
105+
if (err.code !== 'ESRCH') throw error;
106+
}
107+
108+
// Stderr error check
109+
const criticalPatterns = [
110+
'FATAL',
111+
'Cannot find module',
112+
'ENOENT',
113+
'Unable to find component',
114+
'command lightning:dev:component not found',
115+
];
116+
const hasCriticalError = criticalPatterns.some((pattern) => stderrOutput.includes(pattern));
117+
118+
expect(
119+
exitedEarly,
120+
`Dev server exited early with code ${exitCode}. Full stderr: ${stderrOutput}. Full stdout: ${stdoutOutput}`
121+
).to.be.false;
122+
expect(hasCriticalError, `Critical stderr output detected:\n${stderrOutput}`).to.be.false;
123+
expect(
124+
componentHttpSuccess,
125+
`Dev server did not respond with HTTP 200 for component URL. Tried URL: ${componentUrl}`
126+
).to.be.true;
127+
});
128+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import path from 'node:path';
9+
import { spawn, ChildProcess } from 'node:child_process';
10+
import { fileURLToPath } from 'node:url';
11+
12+
const currentFile = fileURLToPath(import.meta.url);
13+
const currentDir = path.dirname(currentFile);
14+
const pluginRoot = path.resolve(currentDir, '../../../../..');
15+
16+
export const startLightningDevServer = (projectDir: string, componentName: string): ChildProcess => {
17+
const devScriptPath = path.join(pluginRoot, 'bin', 'run.js');
18+
19+
return spawn('node', [devScriptPath, 'lightning', 'dev', 'component', '--name', componentName], {
20+
cwd: projectDir,
21+
env: { ...process.env, NODE_ENV: 'production', PORT: '3000', OPEN_BROWSER: process.env.OPEN_BROWSER ?? 'false' },
22+
});
23+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import path from 'node:path';
9+
import fs from 'node:fs';
10+
import { fileURLToPath } from 'node:url';
11+
12+
const currentFile = fileURLToPath(import.meta.url);
13+
const currentDir = path.dirname(currentFile);
14+
15+
const TEMPLATE_DIR = path.resolve(currentDir, '../testdata/lwc/helloWorld');
16+
const SCRATCH_DEF_PATH = path.resolve(currentDir, '../testdata/project-definition.json');
17+
18+
let templateCache: { js: string; html: string; meta: string } | null = null;
19+
20+
const loadTemplateContent = async (): Promise<{ js: string; html: string; meta: string }> => {
21+
if (!templateCache) {
22+
const [js, html, meta] = await Promise.all([
23+
fs.promises.readFile(path.join(TEMPLATE_DIR, 'helloWorld.js'), 'utf8'),
24+
fs.promises.readFile(path.join(TEMPLATE_DIR, 'helloWorld.html'), 'utf8'),
25+
fs.promises.readFile(path.join(TEMPLATE_DIR, 'helloWorld.js-meta.xml'), 'utf8'),
26+
]);
27+
templateCache = { js, html, meta };
28+
}
29+
return templateCache;
30+
};
31+
32+
export const createSfdxProject = async (projectDir: string, customInstanceUrl: string): Promise<void> => {
33+
const sfdxProject = {
34+
packageDirectories: [{ path: 'force-app', default: true }],
35+
name: 'temp-project',
36+
namespace: '',
37+
instanceUrl: customInstanceUrl,
38+
sourceApiVersion: '60.0',
39+
};
40+
41+
// Parallel operations: create directories and read scratch def
42+
const [, scratchDefContent] = await Promise.all([
43+
Promise.all([
44+
fs.promises.mkdir(path.join(projectDir, 'force-app', 'main', 'default', 'lwc'), { recursive: true }),
45+
fs.promises.mkdir(path.join(projectDir, 'config'), { recursive: true }),
46+
]),
47+
fs.promises.readFile(SCRATCH_DEF_PATH, 'utf8'),
48+
]);
49+
50+
await Promise.all([
51+
fs.promises.writeFile(path.join(projectDir, 'sfdx-project.json'), JSON.stringify(sfdxProject, null, 2)),
52+
fs.promises.writeFile(path.join(projectDir, 'config', 'project-scratch-def.json'), scratchDefContent),
53+
]);
54+
};
55+
56+
export const createLwcComponent = async (projectDir: string, name: string): Promise<void> => {
57+
const lwcPath = path.join(projectDir, 'force-app', 'main', 'default', 'lwc', name);
58+
59+
const [, templates] = await Promise.all([fs.promises.mkdir(lwcPath, { recursive: true }), loadTemplateContent()]);
60+
61+
await Promise.all([
62+
fs.promises.writeFile(path.join(lwcPath, `${name}.js`), templates.js.replace(/helloWorld/g, name)),
63+
fs.promises.writeFile(path.join(lwcPath, `${name}.html`), templates.html),
64+
fs.promises.writeFile(path.join(lwcPath, `${name}.js-meta.xml`), templates.meta),
65+
]);
66+
};
67+
68+
export const clearTemplateCache = (): void => {
69+
templateCache = null;
70+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
export function toKebabCase(str: string): string {
8+
return str
9+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2') // insert dash between camelCase boundaries
10+
.toLowerCase();
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template>
2+
<h1>{greeting}</h1>
3+
<button onclick="{handleClick}">Toggle</button>
4+
</template>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { LightningElement } from 'lwc';
2+
export default class helloWorld extends LightningElement {
3+
greeting = 'Hello, World!';
4+
handleClick() {
5+
this.greeting = this.greeting === 'Hello, World!' ? 'Hi again!' : 'Hello, World!';
6+
}
7+
}

0 commit comments

Comments
 (0)