Skip to content

Commit 128a8af

Browse files
committed
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
1 parent d81eb28 commit 128a8af

File tree

12 files changed

+313
-0
lines changed

12 files changed

+313
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 * as dotenv from 'dotenv';
13+
import axios from 'axios';
14+
import { toKebabCase } from './helpers/utils.js';
15+
import { createSfdxProject, createLwcComponent } from './helpers/projectSetup.js';
16+
import { startLightningDevServer } from './helpers/devServerUtils.js';
17+
18+
dotenv.config();
19+
20+
const INSTANCE_URL = process.env.TESTKIT_HUB_INSTANCE;
21+
const TEST_TIMEOUT_MS = 60_000;
22+
const STARTUP_DELAY_MS = 5000;
23+
const DEV_SERVER_PORT = 3000;
24+
25+
// Skip this test in CI environment - run only locally
26+
const shouldSkipTest = process.env.CI === 'true' || process.env.CI === '1';
27+
28+
(shouldSkipTest ? describe.skip : describe)('LWC Local Preview Integration', () => {
29+
let session: TestSession;
30+
let componentName: string;
31+
let projectDir: string;
32+
33+
before(async () => {
34+
componentName = 'helloWorld';
35+
36+
session = await TestSession.create({ devhubAuthStrategy: 'JWT' });
37+
38+
const timestamp = Date.now();
39+
projectDir = path.join(session.dir, `lwc-project-${timestamp}`);
40+
fs.mkdirSync(projectDir, { recursive: true });
41+
42+
await Promise.all([
43+
createSfdxProject(projectDir, INSTANCE_URL ?? ''),
44+
createLwcComponent(projectDir, componentName),
45+
]);
46+
});
47+
48+
after(async () => {
49+
await session?.clean();
50+
});
51+
52+
it('should start lightning dev server and respond to /c-hello-world/ URL', async function () {
53+
this.timeout(TEST_TIMEOUT_MS);
54+
55+
let stderrOutput = '';
56+
let stdoutOutput = '';
57+
let exitedEarly = false;
58+
let exitCode: number | null = null;
59+
60+
const serverProcess = startLightningDevServer(projectDir, componentName);
61+
62+
serverProcess.stderr?.on('data', (data: Buffer) => {
63+
stderrOutput += data.toString();
64+
});
65+
66+
serverProcess.stdout?.on('data', (data: Buffer) => {
67+
stdoutOutput += data.toString();
68+
});
69+
70+
serverProcess.on('exit', (code: number) => {
71+
exitedEarly = true;
72+
exitCode = code;
73+
});
74+
75+
serverProcess.on('error', (error) => {
76+
exitedEarly = true;
77+
stderrOutput += `Process error: ${String(error)}\n`;
78+
});
79+
80+
// Wait for server startup
81+
await new Promise((r) => setTimeout(r, STARTUP_DELAY_MS));
82+
83+
// Test the kebab-case component URL with /c- prefix
84+
const componentKebabName = toKebabCase(componentName);
85+
const componentUrl = `http://localhost:${DEV_SERVER_PORT}/c-${componentKebabName}/`;
86+
let componentHttpSuccess = false;
87+
88+
try {
89+
const componentResponse = await axios.get(componentUrl, { timeout: 2000 });
90+
componentHttpSuccess = componentResponse.status === 200;
91+
} catch (error) {
92+
const err = error as { message?: string };
93+
stderrOutput += `Component URL HTTP request failed: ${err.message ?? 'Unknown error'}\n`;
94+
componentHttpSuccess = false;
95+
}
96+
97+
// Clean up
98+
try {
99+
if (serverProcess.pid && process.kill(serverProcess.pid, 0)) {
100+
process.kill(serverProcess.pid, 'SIGKILL');
101+
}
102+
} catch (error) {
103+
const err = error as NodeJS.ErrnoException;
104+
if (err.code !== 'ESRCH') throw error;
105+
}
106+
107+
// Stderr error check
108+
const criticalPatterns = [
109+
'FATAL',
110+
'Cannot find module',
111+
'ENOENT',
112+
'Unable to find component',
113+
'command lightning:dev:component not found',
114+
];
115+
const hasCriticalError = criticalPatterns.some((pattern) => stderrOutput.includes(pattern));
116+
117+
expect(
118+
exitedEarly,
119+
`Dev server exited early with code ${exitCode}. Full stderr: ${stderrOutput}. Full stdout: ${stdoutOutput}`
120+
).to.be.false;
121+
expect(hasCriticalError, `Critical stderr output detected:\n${stderrOutput}`).to.be.false;
122+
expect(
123+
componentHttpSuccess,
124+
`Dev server did not respond with HTTP 200 for component URL. Tried URL: ${componentUrl}`
125+
).to.be.true;
126+
});
127+
});
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+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>60.0</apiVersion>
4+
<isExposed>true</isExposed>
5+
<targets>
6+
<target>lightning__AppPage</target>
7+
</targets>
8+
</LightningComponentBundle>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"orgName": "My Company",
3+
"edition": "Developer",
4+
"features": ["EnableSetPasswordInApi"],
5+
"settings": {
6+
"lightningExperienceSettings": {
7+
"enableS1DesktopEnabled": true,
8+
"enableLightningPreviewPref": true
9+
},
10+
"mobileSettings": {
11+
"enableS1EncryptedStoragePref2": false
12+
}
13+
}
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.container {
2+
padding: 20px;
3+
text-align: center;
4+
}
5+
6+
h1 {
7+
color: #0176d3;
8+
font-size: 2rem;
9+
margin-bottom: 20px;
10+
}
11+
12+
button {
13+
background-color: #0176d3;
14+
color: white;
15+
border: none;
16+
padding: 10px 20px;
17+
border-radius: 4px;
18+
cursor: pointer;
19+
font-size: 1rem;
20+
}
21+
22+
button:hover {
23+
background-color: #014486;
24+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<div class="container">
3+
<h1>{greeting}</h1>
4+
<button onclick="{handleClick}">Toggle Greeting</button>
5+
</div>
6+
</template>

0 commit comments

Comments
 (0)