Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,76 @@ jobs:
uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main
nuts:
needs: linux-unit-tests
uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main
secrets: inherit
name: NUTs (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
fail-fast: false
with:
os: ${{ matrix.os }}
steps:
- name: Configure git longpaths if on Windows
if: runner.os == 'Windows'
run: git config --system core.longpaths true

- uses: actions/checkout@v4

- uses: google/wireit@setup-github-actions-caching/v2
continue-on-error: true

- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: yarn

- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
path: '**/node_modules'
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}

- name: add CLI as global dependency
uses: salesforcecli/github-workflows/.github/actions/retry@main
with:
max_attempts: 3
command: npm install @salesforce/cli@nightly -g

- uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main
if: steps.cache-nodemodules.outputs.cache-hit != 'true'

- name: Install wireit
run: yarn add wireit@^0.14.12

- run: yarn compile

- name: Install Playwright browsers
run: yarn playwright install --with-deps

- name: Check that oclif config exists
id: is-oclif-plugin
run: echo "bool=$(jq 'if .oclif then true else false end' package.json)" >> "$GITHUB_OUTPUT"

- run: yarn oclif manifest
if: steps.is-oclif-plugin.outputs.bool == 'true'

- name: NUTs with 3 attempts
uses: salesforcecli/github-workflows/.github/actions/retry@main
with:
max_attempts: 3
command: yarn test:nuts
retry_on: error
env:
TESTKIT_AUTH_URL: ${{ secrets.TESTKIT_AUTH_URL }}
TESTKIT_HUB_USERNAME: ${{ secrets.TESTKIT_HUB_USERNAME }}
TESTKIT_JWT_CLIENT_ID: ${{ secrets.TESTKIT_JWT_CLIENT_ID }}
TESTKIT_JWT_KEY: ${{ secrets.TESTKIT_JWT_KEY }}
TESTKIT_HUB_INSTANCE: ${{ secrets.TESTKIT_HUB_INSTANCE }}
ONEGP_TESTKIT_AUTH_URL: ${{ secrets.ONEGP_TESTKIT_AUTH_URL }}
SF_CHANGE_CASE_SFDX_AUTH_URL: ${{ secrets.SF_CHANGE_CASE_SFDX_AUTH_URL }}
SF_CHANGE_CASE_TEMPLATE_ID: ${{ secrets.SF_CHANGE_CASE_TEMPLATE_ID }}
SF_CHANGE_CASE_CONFIGURATION_ITEM: ${{ secrets.SF_CHANGE_CASE_CONFIGURATION_ITEM }}
TESTKIT_SETUP_RETRIES: 2
SF_DISABLE_TELEMETRY: true
DEBUG: ${{ vars.DEBUG }}
30 changes: 30 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": "Compile tests"
},
{
"name": "Run Nuts Test",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": [
"--inspect-brk",
"--no-deprecation",
"--no-warnings",
"-r",
"dotenv/config",
"--loader",
"ts-node/esm",
"--loader",
"esmock"
],
"program": "${workspaceFolder}/node_modules/mocha/lib/cli/cli.js",
"args": ["${file}", "--slow", "4500", "--timeout", "600000"],
"cwd": "${workspaceFolder}",
"env": {
"NODE_ENV": "development",
"SFDX_ENV": "development",
"TS_NODE_PROJECT": "test/tsconfig.json"
},
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"internalConsoleOptions": "openOnSessionStart",
"console": "integratedTerminal",
"preLaunchTask": "Compile plugin only"
}
]
}
19 changes: 11 additions & 8 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
"tasks": [
{
"label": "Build CLI Plugin",
"group": {
"kind": "build",
"isDefault": true
},
"group": { "kind": "build", "isDefault": true },
"command": "yarn",
"type": "shell",
"presentation": {
"focus": false,
"panel": "dedicated"
},
"presentation": { "focus": false, "panel": "dedicated" },
"args": ["build"],
"isBackground": false
},
{
"label": "Compile plugin only",
"command": "yarn",
"type": "shell",
"presentation": { "focus": false, "panel": "shared" },
"args": ["compile"],
"isBackground": false,
"problemMatcher": "$tsc"
}
]
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [6.2.9](https://github.com/salesforcecli/plugin-lightning-dev/compare/6.2.8...6.2.9) (2026-02-18)

### Bug Fixes

- **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.2.8](https://github.com/salesforcecli/plugin-lightning-dev/compare/6.2.7...6.2.8) (2026-02-18)

### Bug Fixes
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ EXAMPLES
$ sf lightning dev app --target-org myOrg --device-type ios --device-id "iPhone 15 Pro Max"
```

_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)_
_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)_

## `sf lightning dev component`

Expand Down Expand Up @@ -251,7 +251,7 @@ EXAMPLES
$ sf lightning dev component --name myComponent
```

_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)_
_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)_

## `sf lightning dev site`

Expand Down Expand Up @@ -308,6 +308,6 @@ EXAMPLES
$ sf lightning dev site --name "Partner Central" --target-org myOrg --get-latest
```

_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)_
_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)_

<!-- commandsstop -->
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/plugin-lightning-dev",
"description": "Lightning development tools for LEX, Mobile, and Experience Sites",
"version": "6.2.8",
"version": "6.2.9",
"author": "Salesforce",
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
Expand All @@ -17,7 +17,7 @@
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.14",
"@salesforce/sf-plugins-core": "^11.2.4",
"axios": "^1.13.5",
"glob": "^13.0.4",
"glob": "^13.0.5",
"lwc": "~8.28.2",
"node-fetch": "^3.3.2",
"open": "^10.2.0",
Expand All @@ -40,6 +40,8 @@
"eslint-plugin-unicorn": "^50.0.1",
"esmock": "^2.7.3",
"oclif": "^4.22.77",
"playwright": "^1.49.0",
"@playwright/test": "^1.49.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
},
Expand Down Expand Up @@ -103,7 +105,9 @@
"prepack": "sf-prepack",
"prepare": "sf-install",
"test": "wireit",
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel",
"test:nuts": "mocha \"**/*.nut.ts\" --slow 30000 --timeout 600000 --parallel=false",
"test:nuts:local": "node -r dotenv/config ./node_modules/.bin/nyc mocha \"**/*.nut.ts\" --slow 30000 --timeout 600000 --parallel=false",
"test:nut:local": "node -r dotenv/config ./node_modules/.bin/nyc mocha --slow 30000 --timeout 600000",
"test:only": "wireit",
"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",
"update-snapshots": "node --loader ts-node/esm --no-warnings=ExperimentalWarning \"./bin/dev.js\" snapshot:generate",
Expand Down
5 changes: 5 additions & 0 deletions src/commands/lightning/dev/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ export default class LightningDevComponent extends SfCommand<ComponentPreviewRes
await this.config.runCommand('org:open', launchArguments);
}

// Emit preview URL for tests (e.g. NUTs that drive Playwright against the preview page)
if (process.env.LIGHTNING_DEV_PRINT_PREVIEW_URL === 'true') {
this.log(previewUrl);
}

return result;
}
}
110 changes: 110 additions & 0 deletions test/commands/lightning/dev/component-preview/browserMenu.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { ChildProcessByStdio } from 'node:child_process';
import type { Readable, Writable } from 'node:stream';
import { TestSession } from '@salesforce/cli-plugins-testkit';
import { expect } from 'chai';
import { type Browser, type Page } from 'playwright';
import { getSession } from '../helpers/sessionUtils.js';
import { startLightningDevServer, getPreviewURL } from '../helpers/devServerUtils.js';
import { killServerProcess } from '../helpers/processUtils.js';
import { getPreview } from '../helpers/browserUtils.js';

const COMPONENT_NAME = 'helloWorld';
const INITIAL_GREETING = 'Hello World';
const STATIC_CONTENT = 'Static Content';

describe('lightning preview menu', () => {
let session: TestSession;
let childProcess: ChildProcessByStdio<Writable, Readable, Readable> | undefined;
let browser: Browser;
let page: Page;

beforeEach(async () => {
session = await getSession();
childProcess = startLightningDevServer(
session.project?.dir ?? '',
session.hubOrg.username,
{ AUTO_ENABLE_LOCAL_DEV: 'true' },
COMPONENT_NAME,
);
const previewUrl = await getPreviewURL(childProcess.stdout);
({ browser, page } = await getPreview(previewUrl, session.hubOrg.accessToken));
});

afterEach(async () => {
if (page) await page.close();
if (browser) await browser.close();
killServerProcess(childProcess);
});

it('should render select link and hamburger menu with helloWorld available and clickable', async () => {
const greetingLocator = page.getByText(INITIAL_GREETING);
await greetingLocator.waitFor({ state: 'visible' });

// When a component is already selected (e.g. --name helloWorld), the canvas shows the component,
// not the "Select a component..." link. Open the hamburger to verify the panel and helloWorld.
const menuToggle = page.getByRole('link', { name: 'Toggle menu' });
await menuToggle.waitFor({ state: 'visible' });
await menuToggle.scrollIntoViewIfNeeded();
await menuToggle.click({ force: true });

// Hamburger opens lwr_dev-component-panel (slide-in panel)
const componentPanel = page.locator('lwr_dev-component-panel >> .lwr-dev-component-panel__panel--visible');
await componentPanel.waitFor({ state: 'visible' });

const staticItem = page.locator(
'lwr_dev-component-panel >> .lwr-dev-component-panel__item[data-specifier="c/static"]',
);
await staticItem.waitFor({ state: 'visible' });
await staticItem.click();

// Wait for the app to load the selected component (URL updates with specifier)
await page.waitForURL(/specifier=c%2Fstatic|c\/static/, { timeout: 15_000 });

const staticContentLocator = page.getByText(STATIC_CONTENT);
await staticContentLocator.waitFor({ state: 'visible', timeout: 15_000 });
expect(await staticContentLocator.textContent()).to.include(STATIC_CONTENT);
});

it('should render component in performance mode when performance mode button is clicked', async () => {
const greetingLocator = page.getByText(INITIAL_GREETING);
await greetingLocator.waitFor({ state: 'visible' });

const performanceLink = page.locator(
'lwr_dev-preview-application >> lwr_dev-preview-header >> .lwr-dev-preview-header__performance-mode-link',
);
await performanceLink.waitFor({ state: 'visible' });
await performanceLink.click();

await page.waitForURL(/mode=performance/);
expect(page.url()).to.include('mode=performance');

const header = page.locator(
'lwr_dev-preview-application >> lwr_dev-preview-header >> .lwr-dev-preview-header__header',
);
expect(await header.first().isHidden()).to.be.true;

const performanceLinkAfter = page.locator(
'lwr_dev-preview-application >> lwr_dev-preview-header >> .lwr-dev-preview-header__performance-mode-link',
);
expect(await performanceLinkAfter.first().isHidden()).to.be.true;

await greetingLocator.waitFor({ state: 'visible' });
expect(await greetingLocator.textContent()).to.equal(INITIAL_GREETING);
});
});
Loading
Loading