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 }}) / yarn test:nuts
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't use template as the playwright browser install step is required

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 5 attempts
uses: salesforcecli/github-workflows/.github/actions/retry@main
with:
max_attempts: 5
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"
}
]
}
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,49 @@ yarn && yarn build
yarn update-snapshots
```

## e2e Org configuration (for NUTs)

If a new org is required for NUTs tests, these are the steps to create and configure one.

1. Create a new STM org in [Org Farm](https://orgfarm.salesforce.com/farms)
2. [Create a Connected App](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_connected_app.htm)
3. Enable JWT authentication and [test it](https://developer.salesforce.com/docs/atlas.en-us.260.0.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_org_commands_unified.htm#cli_reference_org_login_jwt_unified)
4. Use the credentials as values for the respective NUTS environment variables.

## Running NUTs (integration tests) locally

NUTs (integration tests) run the plugin against a real org and, for component-preview tests, a real browser (Playwright). To run them locally:

1. **Environment variables**
Copy `.env.template` to `.env` and set the values for your Dev Hub org and test setup:
- `TESTKIT_JWT_KEY`, `TESTKIT_JWT_CLIENT_ID` – JWT auth for the test org
- `TESTKIT_HUB_USERNAME`, `TESTKIT_HUB_INSTANCE` – Dev Hub org
- `TESTKIT_EXECUTABLE_PATH` – path to the `sf` CLI (default in template is `./node_modules/.bin/sf`)

2. **Run all NUTs** (loads variables from `.env` via `dotenv/config`):

```bash
yarn test:nuts:local
```

3. **Run a single NUT file** (e.g. one test file):

```bash
yarn test:nut:local path/to/file.nut.ts
```

4. **Run with a visible browser** (headed mode) for debugging:

```bash
HEADED=true yarn test:nuts:local
```

or, for a single file:

```bash
HEADED=true yarn test:nut:local path/to/file.nut.ts
```

## Commands

<!-- commands -->
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@types/node-fetch": "^2.6.13",
"@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@playwright/test": "^1.49.0",
"playwright": "^1.49.0",
"dotenv": "^16.5.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.2",
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;
}
}
39 changes: 0 additions & 39 deletions test/commands/lightning/dev/app.nut.ts

This file was deleted.

109 changes: 109 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,109 @@
/*
* 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, killServerProcess } from '../helpers/devServerUtils.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