Skip to content

Commit b777756

Browse files
feat: e2e dev preview tests
1 parent d1b6aa6 commit b777756

31 files changed

Lines changed: 830 additions & 168 deletions

.vscode/launch.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,36 @@
4040
"smartStep": true,
4141
"internalConsoleOptions": "openOnSessionStart",
4242
"preLaunchTask": "Compile tests"
43+
},
44+
{
45+
"name": "Run Nuts Test",
46+
"type": "node",
47+
"request": "launch",
48+
"runtimeExecutable": "node",
49+
"runtimeArgs": [
50+
"--inspect-brk",
51+
"--no-deprecation",
52+
"--no-warnings",
53+
"-r",
54+
"dotenv/config",
55+
"--loader",
56+
"ts-node/esm",
57+
"--loader",
58+
"esmock"
59+
],
60+
"program": "${workspaceFolder}/node_modules/mocha/lib/cli/cli.js",
61+
"args": ["${file}", "--slow", "4500", "--timeout", "600000"],
62+
"cwd": "${workspaceFolder}",
63+
"env": {
64+
"NODE_ENV": "development",
65+
"SFDX_ENV": "development",
66+
"TS_NODE_PROJECT": "test/tsconfig.json"
67+
},
68+
"sourceMaps": true,
69+
"skipFiles": ["<node_internals>/**"],
70+
"internalConsoleOptions": "openOnSessionStart",
71+
"console": "integratedTerminal",
72+
"preLaunchTask": "Compile plugin only"
4373
}
4474
]
4575
}

.vscode/tasks.json

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@
44
"tasks": [
55
{
66
"label": "Build CLI Plugin",
7-
"group": {
8-
"kind": "build",
9-
"isDefault": true
10-
},
7+
"group": { "kind": "build", "isDefault": true },
118
"command": "yarn",
129
"type": "shell",
13-
"presentation": {
14-
"focus": false,
15-
"panel": "dedicated"
16-
},
10+
"presentation": { "focus": false, "panel": "dedicated" },
1711
"args": ["build"],
1812
"isBackground": false
13+
},
14+
{
15+
"label": "Compile plugin only",
16+
"command": "yarn",
17+
"type": "shell",
18+
"presentation": { "focus": false, "panel": "shared" },
19+
"args": ["compile"],
20+
"isBackground": false,
21+
"problemMatcher": "$tsc"
1922
}
2023
]
2124
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [6.2.9](https://github.com/salesforcecli/plugin-lightning-dev/compare/6.2.8...6.2.9) (2026-02-18)
2+
3+
### Bug Fixes
4+
5+
- **deps:** bump glob from 13.0.4 to 13.0.5 ([#625](https://github.com/salesforcecli/plugin-lightning-dev/issues/625)) ([7a3c708](https://github.com/salesforcecli/plugin-lightning-dev/commit/7a3c7088ce925c6d4b68ee1e9a1d9e440bf6ee81))
6+
17
## [6.2.8](https://github.com/salesforcecli/plugin-lightning-dev/compare/6.2.7...6.2.8) (2026-02-18)
28

39
### Bug Fixes

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ EXAMPLES
203203
$ sf lightning dev app --target-org myOrg --device-type ios --device-id "iPhone 15 Pro Max"
204204
```
205205

206-
_See code: [src/commands/lightning/dev/app.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/6.2.8/src/commands/lightning/dev/app.ts)_
206+
_See code: [src/commands/lightning/dev/app.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/6.2.9/src/commands/lightning/dev/app.ts)_
207207

208208
## `sf lightning dev component`
209209

@@ -251,7 +251,7 @@ EXAMPLES
251251
$ sf lightning dev component --name myComponent
252252
```
253253

254-
_See code: [src/commands/lightning/dev/component.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/6.2.8/src/commands/lightning/dev/component.ts)_
254+
_See code: [src/commands/lightning/dev/component.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/6.2.9/src/commands/lightning/dev/component.ts)_
255255

256256
## `sf lightning dev site`
257257

@@ -308,6 +308,6 @@ EXAMPLES
308308
$ sf lightning dev site --name "Partner Central" --target-org myOrg --get-latest
309309
```
310310

311-
_See code: [src/commands/lightning/dev/site.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/6.2.8/src/commands/lightning/dev/site.ts)_
311+
_See code: [src/commands/lightning/dev/site.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/6.2.9/src/commands/lightning/dev/site.ts)_
312312

313313
<!-- commandsstop -->

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/plugin-lightning-dev",
33
"description": "Lightning development tools for LEX, Mobile, and Experience Sites",
4-
"version": "6.2.8",
4+
"version": "6.2.9",
55
"author": "Salesforce",
66
"bugs": "https://github.com/forcedotcom/cli/issues",
77
"dependencies": {
@@ -17,7 +17,7 @@
1717
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.14",
1818
"@salesforce/sf-plugins-core": "^11.2.4",
1919
"axios": "^1.13.5",
20-
"glob": "^13.0.4",
20+
"glob": "^13.0.5",
2121
"lwc": "~8.28.2",
2222
"node-fetch": "^3.3.2",
2323
"open": "^10.2.0",
@@ -40,6 +40,7 @@
4040
"eslint-plugin-unicorn": "^50.0.1",
4141
"esmock": "^2.7.3",
4242
"oclif": "^4.22.77",
43+
"playwright": "^1.49.0",
4344
"ts-node": "^10.9.2",
4445
"typescript": "^5.5.4"
4546
},
@@ -103,7 +104,9 @@
103104
"prepack": "sf-prepack",
104105
"prepare": "sf-install",
105106
"test": "wireit",
106-
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel",
107+
"test:nuts": "mocha \"**/*.nut.ts\" --slow 30000 --timeout 600000 --parallel=false",
108+
"test:nuts:local": "node -r dotenv/config ./node_modules/.bin/nyc mocha \"**/*.nut.ts\" --slow 30000 --timeout 600000 --parallel=false",
109+
"test:nut:local": "node -r dotenv/config ./node_modules/.bin/nyc mocha --slow 30000 --timeout 600000",
107110
"test:only": "wireit",
108111
"unlink-lwr": "yarn unlink @lwrjs/api @lwrjs/app-service @lwrjs/asset-registry @lwrjs/asset-transformer @lwrjs/auth-middleware @lwrjs/base-view-provider @lwrjs/base-view-transformer @lwrjs/client-modules @lwrjs/config @lwrjs/core @lwrjs/dev-proxy-server @lwrjs/diagnostics @lwrjs/esbuild @lwrjs/everywhere @lwrjs/fs-asset-provider @lwrjs/fs-watch @lwrjs/html-view-provider @lwrjs/instrumentation @lwrjs/label-module-provider @lwrjs/lambda @lwrjs/legacy-npm-module-provider @lwrjs/loader @lwrjs/lwc-module-provider @lwrjs/lwc-ssr @lwrjs/markdown-view-provider @lwrjs/module-bundler @lwrjs/module-registry @lwrjs/npm-module-provider @lwrjs/nunjucks-view-provider @lwrjs/o11y @lwrjs/resource-registry @lwrjs/router @lwrjs/security @lwrjs/server @lwrjs/shared-utils @lwrjs/static @lwrjs/tools @lwrjs/types @lwrjs/view-registry lwr",
109112
"update-snapshots": "node --loader ts-node/esm --no-warnings=ExperimentalWarning \"./bin/dev.js\" snapshot:generate",

src/commands/lightning/dev/component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ export default class LightningDevComponent extends SfCommand<ComponentPreviewRes
186186
await this.config.runCommand('org:open', launchArguments);
187187
}
188188

189+
// Emit preview URL for tests (e.g. NUTs that drive Playwright against the preview page)
190+
if (process.env.LIGHTNING_DEV_PRINT_PREVIEW_URL === 'true') {
191+
this.log(previewUrl);
192+
}
193+
189194
return result;
190195
}
191196
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ChildProcessByStdio } from 'node:child_process';
18+
import type { Readable, Writable } from 'node:stream';
19+
import { TestSession } from '@salesforce/cli-plugins-testkit';
20+
import { expect } from 'chai';
21+
import { type Browser, type Page } from 'playwright';
22+
import { getSession } from '../helpers/sessionUtils.js';
23+
import { startLightningDevServer, getPreviewURL } from '../helpers/devServerUtils.js';
24+
import { killServerProcess } from '../helpers/processUtils.js';
25+
import { getPreview } from '../helpers/browserUtils.js';
26+
27+
const COMPONENT_NAME = 'helloWorld';
28+
const INITIAL_GREETING = 'Hello World';
29+
const STATIC_CONTENT = 'Static Content';
30+
31+
describe('lightning preview menu', () => {
32+
let session: TestSession;
33+
let childProcess: ChildProcessByStdio<Writable, Readable, Readable> | undefined;
34+
let browser: Browser;
35+
let page: Page;
36+
37+
beforeEach(async () => {
38+
session = await getSession();
39+
childProcess = startLightningDevServer(
40+
session.project?.dir ?? '',
41+
session.hubOrg.username,
42+
{ AUTO_ENABLE_LOCAL_DEV: 'true' },
43+
COMPONENT_NAME,
44+
);
45+
const previewUrl = await getPreviewURL(childProcess.stdout);
46+
({ browser, page } = await getPreview(previewUrl, session.hubOrg.accessToken));
47+
});
48+
49+
afterEach(async () => {
50+
if (page) await page.close();
51+
if (browser) await browser.close();
52+
killServerProcess(childProcess);
53+
});
54+
55+
it('should render select link and hamburger menu with helloWorld available and clickable', async () => {
56+
const greetingLocator = page.getByText(INITIAL_GREETING);
57+
await greetingLocator.waitFor({ state: 'visible' });
58+
59+
// When a component is already selected (e.g. --name helloWorld), the canvas shows the component,
60+
// not the "Select a component..." link. Open the hamburger to verify the panel and helloWorld.
61+
const menuToggle = page.getByRole('link', { name: 'Toggle menu' });
62+
await menuToggle.waitFor({ state: 'visible' });
63+
await menuToggle.scrollIntoViewIfNeeded();
64+
await menuToggle.click({ force: true });
65+
66+
// Hamburger opens lwr_dev-component-panel (slide-in panel)
67+
const componentPanel = page.locator('lwr_dev-component-panel >> .lwr-dev-component-panel__panel--visible');
68+
await componentPanel.waitFor({ state: 'visible' });
69+
70+
const staticItem = page.locator(
71+
'lwr_dev-component-panel >> .lwr-dev-component-panel__item[data-specifier="c/static"]',
72+
);
73+
await staticItem.waitFor({ state: 'visible' });
74+
await staticItem.click();
75+
76+
// Wait for the app to load the selected component (URL updates with specifier)
77+
await page.waitForURL(/specifier=c%2Fstatic|c\/static/, { timeout: 15_000 });
78+
79+
const staticContentLocator = page.getByText(STATIC_CONTENT);
80+
await staticContentLocator.waitFor({ state: 'visible', timeout: 15_000 });
81+
expect(await staticContentLocator.textContent()).to.include(STATIC_CONTENT);
82+
});
83+
84+
it('should render component in performance mode when performance mode button is clicked', async () => {
85+
const greetingLocator = page.getByText(INITIAL_GREETING);
86+
await greetingLocator.waitFor({ state: 'visible' });
87+
88+
const performanceLink = page.locator(
89+
'lwr_dev-preview-application >> lwr_dev-preview-header >> .lwr-dev-preview-header__performance-mode-link',
90+
);
91+
await performanceLink.waitFor({ state: 'visible' });
92+
await performanceLink.click();
93+
94+
await page.waitForURL(/mode=performance/);
95+
expect(page.url()).to.include('mode=performance');
96+
97+
const header = page.locator(
98+
'lwr_dev-preview-application >> lwr_dev-preview-header >> .lwr-dev-preview-header__header',
99+
);
100+
expect(await header.first().isHidden()).to.be.true;
101+
102+
const performanceLinkAfter = page.locator(
103+
'lwr_dev-preview-application >> lwr_dev-preview-header >> .lwr-dev-preview-header__performance-mode-link',
104+
);
105+
expect(await performanceLinkAfter.first().isHidden()).to.be.true;
106+
107+
await greetingLocator.waitFor({ state: 'visible' });
108+
expect(await greetingLocator.textContent()).to.equal(INITIAL_GREETING);
109+
});
110+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ChildProcessByStdio } from 'node:child_process';
18+
import type { Readable, Writable } from 'node:stream';
19+
import { TestSession } from '@salesforce/cli-plugins-testkit';
20+
import { expect } from 'chai';
21+
import { type Browser, type Page } from 'playwright';
22+
import { getSession } from '../helpers/sessionUtils.js';
23+
import { startLightningDevServer, getPreviewURL } from '../helpers/devServerUtils.js';
24+
import { killServerProcess } from '../helpers/processUtils.js';
25+
import { getPreview } from '../helpers/browserUtils.js';
26+
27+
const COMPONENT_NAME = 'withError';
28+
const ERROR_MESSAGE = 'Component generated error';
29+
30+
/** Locator for error message text (class from LWR error display / lwr_dev/errorDisplay) */
31+
const errorMessageEl = (p: Page) => p.locator('.error-message-text');
32+
33+
describe('lightning preview component error', () => {
34+
let session: TestSession;
35+
let childProcess: ChildProcessByStdio<Writable, Readable, Readable> | undefined;
36+
let browser: Browser;
37+
let page: Page;
38+
39+
before(async () => {
40+
session = await getSession();
41+
childProcess = startLightningDevServer(
42+
session.project?.dir ?? '',
43+
session.hubOrg.username,
44+
{ AUTO_ENABLE_LOCAL_DEV: 'true' },
45+
COMPONENT_NAME,
46+
);
47+
const previewUrl = await getPreviewURL(childProcess.stdout);
48+
({ browser, page } = await getPreview(previewUrl, session.hubOrg.accessToken));
49+
});
50+
51+
after(async () => {
52+
if (page) await page.close();
53+
if (browser) await browser.close();
54+
killServerProcess(childProcess);
55+
});
56+
57+
it('should render the error component and display the error modal', async () => {
58+
const message = errorMessageEl(page);
59+
await message.waitFor({ state: 'visible', timeout: 15_000 });
60+
expect(await message.textContent()).to.include(ERROR_MESSAGE);
61+
});
62+
63+
it('should display the error modal and close it when the dismiss button is clicked', async () => {
64+
const message = errorMessageEl(page);
65+
await message.waitFor({ state: 'visible', timeout: 15_000 });
66+
67+
const dismissButton = page.getByRole('button', { name: /dismiss/i });
68+
await dismissButton.waitFor({ state: 'visible' });
69+
await dismissButton.click();
70+
71+
await message.waitFor({ state: 'hidden', timeout: 10_000 });
72+
expect(await message.isHidden()).to.be.true;
73+
});
74+
75+
it('should copy the error text to the clipboard when copy is clicked', async () => {
76+
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
77+
await page.reload({ waitUntil: 'load' });
78+
79+
const message = errorMessageEl(page);
80+
await message.waitFor({ state: 'visible', timeout: 15_000 });
81+
82+
const copyButton = page.getByRole('button', { name: /copy/i });
83+
await copyButton.waitFor({ state: 'visible' });
84+
await copyButton.click();
85+
86+
const clipboardText = await page.evaluate('navigator.clipboard.readText()');
87+
expect(clipboardText).to.include(ERROR_MESSAGE);
88+
});
89+
});

0 commit comments

Comments
 (0)