diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ec575d0..d5a0f264 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/.vscode/launch.json b/.vscode/launch.json index ba66bf71..6e94830f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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": ["/**"], + "internalConsoleOptions": "openOnSessionStart", + "console": "integratedTerminal", + "preLaunchTask": "Compile plugin only" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 578c4627..06ea1a77 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dabf259..13b46afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 1bc5cbff..bee69800 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` @@ -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)_ diff --git a/package.json b/package.json index d9535641..64f60c40 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", @@ -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" }, @@ -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", diff --git a/src/commands/lightning/dev/component.ts b/src/commands/lightning/dev/component.ts index 370b04cf..dd4511f4 100644 --- a/src/commands/lightning/dev/component.ts +++ b/src/commands/lightning/dev/component.ts @@ -186,6 +186,11 @@ export default class LightningDevComponent extends SfCommand { + let session: TestSession; + let childProcess: ChildProcessByStdio | 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); + }); +}); diff --git a/test/commands/lightning/dev/component-preview/componentError.nut.ts b/test/commands/lightning/dev/component-preview/componentError.nut.ts new file mode 100644 index 00000000..7c48cfc1 --- /dev/null +++ b/test/commands/lightning/dev/component-preview/componentError.nut.ts @@ -0,0 +1,89 @@ +/* + * 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 = 'withError'; +const ERROR_MESSAGE = 'Component generated error'; + +/** Locator for error message text (class from LWR error display / lwr_dev/errorDisplay) */ +const errorMessageEl = (p: Page) => p.locator('.error-message-text'); + +describe('lightning preview component error', () => { + let session: TestSession; + let childProcess: ChildProcessByStdio | undefined; + let browser: Browser; + let page: Page; + + before(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)); + }); + + after(async () => { + if (page) await page.close(); + if (browser) await browser.close(); + killServerProcess(childProcess); + }); + + it('should render the error component and display the error modal', async () => { + const message = errorMessageEl(page); + await message.waitFor({ state: 'visible', timeout: 15_000 }); + expect(await message.textContent()).to.include(ERROR_MESSAGE); + }); + + it('should display the error modal and close it when the dismiss button is clicked', async () => { + const message = errorMessageEl(page); + await message.waitFor({ state: 'visible', timeout: 15_000 }); + + const dismissButton = page.getByRole('button', { name: /dismiss/i }); + await dismissButton.waitFor({ state: 'visible' }); + await dismissButton.click(); + + await message.waitFor({ state: 'hidden', timeout: 10_000 }); + expect(await message.isHidden()).to.be.true; + }); + + it('should copy the error text to the clipboard when copy is clicked', async () => { + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']); + await page.reload({ waitUntil: 'load' }); + + const message = errorMessageEl(page); + await message.waitFor({ state: 'visible', timeout: 15_000 }); + + const copyButton = page.getByRole('button', { name: /copy/i }); + await copyButton.waitFor({ state: 'visible' }); + await copyButton.click(); + + const clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(clipboardText).to.include(ERROR_MESSAGE); + }); +}); diff --git a/test/commands/lightning/dev/component-preview/hmr.nut.ts b/test/commands/lightning/dev/component-preview/hmr.nut.ts new file mode 100644 index 00000000..79e0b8a1 --- /dev/null +++ b/test/commands/lightning/dev/component-preview/hmr.nut.ts @@ -0,0 +1,129 @@ +/* + * 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 fs from 'node:fs'; +import path from 'node:path'; +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, getComponentPath } 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 HMR_GREETING = 'Hello, HMR Test!'; +const BLUE = 'rgb(0, 0, 255)'; +const RED = 'rgb(255, 0, 0)'; + +describe('lightning preview hot module reload', () => { + let session: TestSession; + let childProcess: ChildProcessByStdio | undefined; + let browser: Browser; + let page: Page; + + before(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)); + }); + + after(async () => { + if (page) await page.close(); + if (browser) await browser.close(); + killServerProcess(childProcess); + }); + + it('should re-render component and hot reload .js changes', async () => { + // Assert component rendered with expected content + const greetingLocator = page.getByText(INITIAL_GREETING); + expect(await greetingLocator.textContent()).to.equal(INITIAL_GREETING); + + // Change the component source code and write it to trigger HMR + const componentJsPath = path.join(getComponentPath(session, COMPONENT_NAME), `${COMPONENT_NAME}.js`); + const originalJsContent = await fs.promises.readFile(componentJsPath, 'utf8'); + const modifiedJsContent = originalJsContent.replace( + `greeting = '${INITIAL_GREETING}';`, + `greeting = '${HMR_GREETING}';`, + ); + await fs.promises.writeFile(componentJsPath, modifiedJsContent, 'utf8'); + + // Assert component is re-rendered with updated source code. + const updatedGreetingLocator = page.getByText(HMR_GREETING); + expect(await updatedGreetingLocator.textContent()).to.equal(HMR_GREETING); + + await fs.promises.writeFile(componentJsPath, originalJsContent, 'utf8'); + }); + + it('should re-render component and hot reload .css changes', async () => { + // Assert initial color + // Assert initial HTML + const greeting = page.getByText(INITIAL_GREETING); + expect(await greeting.textContent()).to.equal(INITIAL_GREETING); + const initialGreetingColor = await greeting.evaluate((e) => window.getComputedStyle(e).color); + expect(initialGreetingColor).to.equal(BLUE); + + // Update component .css + const componentCssPath = path.join(getComponentPath(session, COMPONENT_NAME), `${COMPONENT_NAME}.css`); + const originalCssContent = await fs.promises.readFile(componentCssPath, 'utf8'); + const modifiedCssContent = originalCssContent.replace(`color: ${BLUE};`, `color: ${RED};`); + await fs.promises.writeFile(componentCssPath, modifiedCssContent, 'utf8'); + + // Assert updated color + const maxAttempts = 300; + let attempt = 0; + let greetingColor; + while (greetingColor !== RED && attempt < maxAttempts) { + // eslint-disable-next-line no-await-in-loop + await page.waitForTimeout(500); + // eslint-disable-next-line no-await-in-loop + greetingColor = await greeting.evaluate((e) => window.getComputedStyle(e).color); + attempt++; + } + expect(greetingColor).to.equal(RED); + await fs.promises.writeFile(componentCssPath, originalCssContent, 'utf8'); + }); + + it('should re-render component and hot reload .html changes', async () => { + // Assert initial HTML + const greetingLocator = page.getByText(INITIAL_GREETING); + expect(await greetingLocator.textContent()).to.equal(INITIAL_GREETING); + + // Update HTML template + const componentHtmlPath = path.join(getComponentPath(session, COMPONENT_NAME), `${COMPONENT_NAME}.html`); + const originalHtmlContent = await fs.promises.readFile(componentHtmlPath, 'utf8'); + const modifiedHtmlContent = originalHtmlContent.replace( + '
{greeting}
', + `
{greeting}${HMR_GREETING}
`, + ); + await fs.promises.writeFile(componentHtmlPath, modifiedHtmlContent, 'utf8'); + + // Assert updated HTML + const hmrMarkerLocator = page.getByText(HMR_GREETING); + expect(await hmrMarkerLocator.textContent()).to.equal(HMR_GREETING); + + await fs.promises.writeFile(componentHtmlPath, originalHtmlContent, 'utf8'); + }); +}); diff --git a/test/commands/lightning/dev/component-preview/prompts.nut.ts b/test/commands/lightning/dev/component-preview/prompts.nut.ts new file mode 100644 index 00000000..7aacbb12 --- /dev/null +++ b/test/commands/lightning/dev/component-preview/prompts.nut.ts @@ -0,0 +1,120 @@ +/* + * 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 { Connection, Messages, Org } from '@salesforce/core'; +import { expect } from 'chai'; +import { type Browser, type Page } from 'playwright'; +import { MetaUtils } from '../../../../../src/shared/metaUtils.js'; +import { getSession } from '../helpers/sessionUtils.js'; +import { + startLightningDevServer, + waitForPrompt, + waitForProcessExit, + getPreviewURL, +} from '../helpers/devServerUtils.js'; +import { killServerProcess } from '../helpers/processUtils.js'; +import { getPreview } from '../helpers/browserUtils.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const sharedMessages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils'); +const promptMessages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'prompts'); + +const COMPONENT_NAME = 'helloWorld'; + +describe('lightning preview component prompts', () => { + let session: TestSession; + let childProcess: ChildProcessByStdio | undefined; + let connection: Connection; + let browser: Browser; + let page: Page; + + beforeEach(async () => { + session = await getSession(); + const org = await Org.create({ aliasOrUsername: session.hubOrg.username }); + connection = org.getConnection(); + // Unset required org configuration to trigger prompt behavior + await MetaUtils.setLightningPreviewEnabled(connection, false); + await MetaUtils.setMyDomainFirstPartyCookieRequirement(connection, true); + }); + + afterEach(async () => { + if (page) await page.close(); + if (browser) await browser.close(); + killServerProcess(childProcess); + }); + + it('should error out when local dev is not enabled and AUTO_ENABLE_LOCAL_DEV is false', async () => { + childProcess = startLightningDevServer(session.project?.dir, session.hubOrg.username, { + AUTO_ENABLE_LOCAL_DEV: false, + }); + + const { code, stderr } = await waitForProcessExit(childProcess); + expect(code).to.not.equal(0); + expect(stderr).to.include(sharedMessages.getMessage('error.localdev.not.enabled')); + }); + + it('should error out when user answers "n" to enable local dev prompt', async () => { + childProcess = startLightningDevServer(session.project?.dir, session.hubOrg.username); + + // Wait for enable local dev prompt and answer Y to enable local dev + await waitForPrompt(childProcess, promptMessages.getMessage('component.enable-local-dev')); + childProcess.stdin?.write('n\n'); + + const { code, stderr } = await waitForProcessExit(childProcess); + expect(code).to.not.equal(0); + expect(stderr).to.include(sharedMessages.getMessage('error.localdev.not.enabled')); + }); + + it('should enable local dev and disable first party cookies and render page after selecting component when user answers "Y" to enable local dev', async () => { + childProcess = startLightningDevServer(session.project?.dir, session.hubOrg.username); + + // Wait for enable local dev prompt and answer Y to enable local dev + await waitForPrompt(childProcess, promptMessages.getMessage('component.enable-local-dev')); + childProcess.stdin?.write('Y\n'); + + // Select first component + await waitForPrompt(childProcess, promptMessages.getMessage('component.select')); + childProcess.stdin?.write('\n'); + + const previewUrl = await getPreviewURL(childProcess.stdout); + ({ browser, page } = await getPreview(previewUrl, session.hubOrg.accessToken)); + + const greetingLocator = page.getByText('Hello World'); + await greetingLocator.waitFor({ state: 'visible' }); + expect(await greetingLocator.textContent()).to.equal('Hello World'); + }); + + it('should render without a prompt and disable first party cookies when AUTO_ENABLE_LOCAL_DEV=true', async () => { + 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)); + + const greetingLocator = page.getByText('Hello World'); + await greetingLocator.waitFor({ state: 'visible' }); + expect(await greetingLocator.textContent()).to.equal('Hello World'); + + // Command with AUTO_ENABLE_LOCAL_DEV calls ensureFirstPartyCookiesNotRequired, so requirement is disabled + expect(await MetaUtils.isFirstPartyCookieRequired(connection)).to.be.false; + }); +}); diff --git a/test/commands/lightning/dev/component.nut.ts b/test/commands/lightning/dev/component.nut.ts index 49f70b12..38cd13d4 100644 --- a/test/commands/lightning/dev/component.nut.ts +++ b/test/commands/lightning/dev/component.nut.ts @@ -31,7 +31,7 @@ describe('lightning preview component NUTs', () => { it('should display provided name', () => { const name = 'World'; - const command = `lightning preview component --name ${name}`; + const command = `lightning dev component --name ${name}`; const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout; expect(output).to.contain(name); }); diff --git a/test/commands/lightning/dev/componentLocalPreview.nut.ts b/test/commands/lightning/dev/componentLocalPreview.nut.ts deleted file mode 100644 index cd6141a7..00000000 --- a/test/commands/lightning/dev/componentLocalPreview.nut.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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 path from 'node:path'; -import fs from 'node:fs'; -import { expect } from 'chai'; -import { TestSession } from '@salesforce/cli-plugins-testkit'; -import axios from 'axios'; -import * as dotenv from 'dotenv'; -import { toKebabCase } from './helpers/utils.js'; -import { createSfdxProject, createLwcComponent } from './helpers/projectSetup.js'; -import { startLightningDevServer } from './helpers/devServerUtils.js'; - -// Load environment variables from .env file -dotenv.config(); - -const INSTANCE_URL = process.env.TESTKIT_HUB_INSTANCE; -const TEST_TIMEOUT_MS = 60_000; -const STARTUP_DELAY_MS = 5000; -const DEV_SERVER_PORT = 3000; - -// Skip this test in CI environment - run only locally -const shouldSkipTest = process.env.CI === 'true' || process.env.CI === '1'; - -(shouldSkipTest ? describe.skip : describe)('LWC Local Preview Integration', () => { - let session: TestSession; - let componentName: string; - let projectDir: string; - - before(async () => { - componentName = 'helloWorld'; - - session = await TestSession.create({ devhubAuthStrategy: 'JWT' }); - - const timestamp = Date.now(); - projectDir = path.join(session.dir, `lwc-project-${timestamp}`); - fs.mkdirSync(projectDir, { recursive: true }); - - await Promise.all([ - createSfdxProject(projectDir, INSTANCE_URL ?? ''), - createLwcComponent(projectDir, componentName), - ]); - }); - - after(async () => { - await session?.clean(); - }); - - it('should start lightning dev server and respond to /c-hello-world/ URL', async function () { - this.timeout(TEST_TIMEOUT_MS); - - let stderrOutput = ''; - let stdoutOutput = ''; - let exitedEarly = false; - let exitCode: number | null = null; - - const serverProcess = startLightningDevServer(projectDir, componentName); - - serverProcess.stderr?.on('data', (data: Buffer) => { - stderrOutput += data.toString(); - }); - - serverProcess.stdout?.on('data', (data: Buffer) => { - stdoutOutput += data.toString(); - }); - - serverProcess.on('exit', (code: number) => { - exitedEarly = true; - exitCode = code; - }); - - serverProcess.on('error', (error) => { - exitedEarly = true; - stderrOutput += `Process error: ${String(error)}\n`; - }); - - // Wait for server startup - await new Promise((r) => setTimeout(r, STARTUP_DELAY_MS)); - - // Test the kebab-case component URL with /c- prefix - const componentKebabName = toKebabCase(componentName); - const componentUrl = `http://localhost:${DEV_SERVER_PORT}/c-${componentKebabName}/`; - let componentHttpSuccess = false; - - try { - const componentResponse = await axios.get(componentUrl, { timeout: 2000 }); - componentHttpSuccess = componentResponse.status === 200; - } catch (error) { - const err = error as { message?: string }; - stderrOutput += `Component URL HTTP request failed: ${err.message ?? 'Unknown error'}\n`; - componentHttpSuccess = false; - } - - // Clean up - try { - if (serverProcess.pid && process.kill(serverProcess.pid, 0)) { - process.kill(serverProcess.pid, 'SIGKILL'); - } - } catch (error) { - const err = error as NodeJS.ErrnoException; - if (err.code !== 'ESRCH') throw error; - } - - // Stderr error check - const criticalPatterns = [ - 'FATAL', - 'Cannot find module', - 'ENOENT', - 'Unable to find component', - 'command lightning:dev:component not found', - ]; - const hasCriticalError = criticalPatterns.some((pattern) => stderrOutput.includes(pattern)); - - expect( - exitedEarly, - `Dev server exited early with code ${exitCode}. Full stderr: ${stderrOutput}. Full stdout: ${stdoutOutput}` - ).to.be.false; - expect(hasCriticalError, `Critical stderr output detected:\n${stderrOutput}`).to.be.false; - expect( - componentHttpSuccess, - `Dev server did not respond with HTTP 200 for component URL. Tried URL: ${componentUrl}` - ).to.be.true; - }); -}); diff --git a/test/commands/lightning/dev/helpers/browserUtils.ts b/test/commands/lightning/dev/helpers/browserUtils.ts new file mode 100644 index 00000000..d6cfab7d --- /dev/null +++ b/test/commands/lightning/dev/helpers/browserUtils.ts @@ -0,0 +1,42 @@ +/* + * 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 { chromium, type Browser, type Page } from 'playwright'; + +export async function getPreview( + previewUrl: string, + accessToken: string | undefined, +): Promise<{ browser: Browser; page: Page }> { + const previewOrigin = new URL(previewUrl).origin; + let browser: Browser | null = null; + let page: Page | null = null; + const headed = process.env.HEADED === 'true' || process.env.HEADED === '1'; + browser = await chromium.launch({ headless: !headed }); + page = await browser.newPage(); + if (accessToken) { + await page.context().addCookies([ + { + name: 'sid', + value: accessToken, + domain: new URL(previewOrigin).hostname, + path: '/', + expires: Math.floor(Date.now() / 1000) + 86_400, + }, + ]); + } + await page.goto(previewUrl, { waitUntil: 'load' }); + return new Promise((r) => r({ browser, page })); +} diff --git a/test/commands/lightning/dev/helpers/devServerUtils.ts b/test/commands/lightning/dev/helpers/devServerUtils.ts index d4e1f1fd..b3caf498 100644 --- a/test/commands/lightning/dev/helpers/devServerUtils.ts +++ b/test/commands/lightning/dev/helpers/devServerUtils.ts @@ -13,20 +13,121 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { spawn, ChildProcessByStdio, ChildProcessWithoutNullStreams } from 'node:child_process'; +import { Readable } from 'node:stream'; +import type { Writable } from 'node:stream'; import path from 'node:path'; -import { spawn, ChildProcess } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; +import { PLUGIN_ROOT_PATH } from './utils.js'; + +const PREVIEW_URL_REGEX = /(https:\/\/[^\s]*\/lwr\/application\/[^\s]+)/; +const MAX_WAIT_MS = 30_000; -const currentFile = fileURLToPath(import.meta.url); -const currentDir = path.dirname(currentFile); -const pluginRoot = path.resolve(currentDir, '../../../../..'); +function parsePreviewUrl(output: string): string | null { + const match = output.match(PREVIEW_URL_REGEX); + return match?.[1]?.trim() ?? null; +} + +/** + * Wait for the preview URL to appear on the given stdout stream (e.g. childProcess.stdout). + * The preview URL is only ever printed to stdout. + * + * @param stdout - Readable stream (e.g. from a spawned process) + */ +export function getPreviewURL(stdout: Readable): Promise { + return new Promise((resolve, reject) => { + let output = ''; + const timerId = setTimeout( + () => reject(new Error(`Preview URL not returned within ${MAX_WAIT_MS / 1000} seconds`)), + MAX_WAIT_MS, + ); + stdout?.on('data', (chunk: Buffer) => { + output += chunk.toString(); + const previewUrl = parsePreviewUrl(output); + if (previewUrl) { + clearTimeout(timerId); + resolve(previewUrl); + } + }); + }); +} -export const startLightningDevServer = (projectDir: string, componentName: string): ChildProcess => { - const devScriptPath = path.join(pluginRoot, 'bin', 'run.js'); +/** + * Wait until the given prompt string appears in the process output (stdout or stderr). + * + * @param child - Spawned process with piped stdio + * @param prompt - Substring that indicates the prompt is shown + * @returns The combined output collected so far, so callers can assert on it + */ +export function waitForPrompt( + child: ChildProcessByStdio, + prompt: string, +): Promise { + return new Promise((resolve, reject) => { + let output = ''; + const timerId = setTimeout(() => { + const err = new Error( + `Prompt "${prompt.slice(0, 40)}..." not shown within ${MAX_WAIT_MS / 1000} seconds.`, + ) as Error & { output?: string }; + err.output = output; + reject(err); + }, MAX_WAIT_MS); + const check = (chunk: Buffer | string): void => { + output += chunk.toString(); + if (output.includes(prompt)) { + clearTimeout(timerId); + resolve(output); + } + }; + child.stdout?.on('data', check); + child.stderr?.on('data', check); + }); +} + +/** + * Collect stdout and stderr until the process exits. Resolves with exit code, signal, and combined output. + */ +export function waitForProcessExit( + child: ChildProcessByStdio, +): Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }> { + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', (chunk) => (stdout += chunk)); + child.stderr?.on('data', (chunk) => (stderr += chunk)); + + return new Promise((resolve, reject) => { + const timerId = setTimeout(() => { + const err = new Error(`Process not exited within ${MAX_WAIT_MS / 1000} seconds.`) as Error & { output?: string }; + reject(err); + }, MAX_WAIT_MS); + + child.on('close', (code, signal) => { + clearTimeout(timerId); + resolve({ code, signal, stdout, stderr }); + }); + }); +} - return spawn('node', [devScriptPath, 'lightning', 'dev', 'component', '--name', componentName], { +export function startLightningDevServer( + projectDir: string, + username: string = '', + env = {}, + componentName?: string, +): ChildProcessWithoutNullStreams { + const runJs = path.join(PLUGIN_ROOT_PATH, 'bin', 'run.js'); + const spawnEnv = { + ...process.env, + ...env, + OPEN_BROWSER: 'false', + LIGHTNING_DEV_PRINT_PREVIEW_URL: 'true', + }; + const args = [runJs, 'lightning', 'dev', 'component', '-o', username]; + if (componentName) { + args.push('--name', componentName); + } + return spawn('node', args, { cwd: projectDir, - env: { ...process.env, NODE_ENV: 'production', PORT: '3000', OPEN_BROWSER: process.env.OPEN_BROWSER ?? 'false' }, + env: spawnEnv, + stdio: ['pipe', 'pipe', 'pipe'], }); -}; +} diff --git a/test/commands/lightning/dev/helpers/processUtils.ts b/test/commands/lightning/dev/helpers/processUtils.ts new file mode 100644 index 00000000..a5fddb39 --- /dev/null +++ b/test/commands/lightning/dev/helpers/processUtils.ts @@ -0,0 +1,33 @@ +/* + * 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 ChildProcess } from 'node:child_process'; + +/** + * Clean up a server process and all its child processes. Uses tree-kill so + * descendant node processes (e.g. LWR workers) are terminated. + */ +export function killServerProcess(serverProcess: ChildProcess | undefined): void { + // Clean up + try { + if (serverProcess?.pid && process.kill(serverProcess.pid, 0)) { + process.kill(serverProcess.pid, 'SIGTERM'); + } + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err.code !== 'ESRCH') throw error; + } +} diff --git a/test/commands/lightning/dev/helpers/sessionUtils.ts b/test/commands/lightning/dev/helpers/sessionUtils.ts new file mode 100644 index 00000000..91d758a4 --- /dev/null +++ b/test/commands/lightning/dev/helpers/sessionUtils.ts @@ -0,0 +1,34 @@ +/* + * 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 path from 'node:path'; +import { TestSession } from '@salesforce/cli-plugins-testkit'; +import { PROJECT_PATH } from './utils.js'; + +let cachedSession: TestSession; + +export async function getSession(): Promise { + if (!cachedSession) { + cachedSession = await TestSession.create({ + devhubAuthStrategy: 'AUTO', + project: { sourceDir: PROJECT_PATH }, + }); + } + return new Promise((r) => r(cachedSession)); +} + +export function getComponentPath(session: TestSession, componentName: string) { + return path.join(session.project?.dir, 'force-app', 'main', 'default', 'lwc', componentName); +} diff --git a/test/commands/lightning/dev/helpers/utils.ts b/test/commands/lightning/dev/helpers/utils.ts index 96926f2c..91af467f 100644 --- a/test/commands/lightning/dev/helpers/utils.ts +++ b/test/commands/lightning/dev/helpers/utils.ts @@ -13,6 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const CURRENT_FILE_PATH = fileURLToPath(import.meta.url); +const CURRENT_DIR_PATH = path.dirname(CURRENT_FILE_PATH); + +export const PLUGIN_ROOT_PATH = path.resolve(CURRENT_DIR_PATH, '../../../../..'); +export const PROJECT_PATH = path.resolve(PLUGIN_ROOT_PATH, 'test/projects/component-preview-project'); + export function toKebabCase(str: string): string { return str .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // insert dash between camelCase boundaries diff --git a/test/projects/component-preview-project/config/project-scratch-def.json b/test/projects/component-preview-project/config/project-scratch-def.json new file mode 100644 index 00000000..620c1301 --- /dev/null +++ b/test/projects/component-preview-project/config/project-scratch-def.json @@ -0,0 +1,14 @@ +{ + "orgName": "My Company", + "edition": "Developer", + "features": ["EnableSetPasswordInApi"], + "settings": { + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true, + "enableLightningPreviewPref": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + } + } +} diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.css b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.css new file mode 100644 index 00000000..2f8d25f9 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.css @@ -0,0 +1,3 @@ +.greeting { + color: rgb(0, 0, 255); +} diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.html b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.html new file mode 100644 index 00000000..a0d2405d --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.html @@ -0,0 +1,3 @@ + diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.js b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.js new file mode 100644 index 00000000..e396e687 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.js @@ -0,0 +1,4 @@ +import { LightningElement } from 'lwc'; +export default class helloWorld extends LightningElement { + greeting = 'Hello World'; +} diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.js-meta.xml b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.js-meta.xml new file mode 100644 index 00000000..023fc7f7 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/helloWorld/helloWorld.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/static/static.html b/test/projects/component-preview-project/force-app/main/default/lwc/static/static.html new file mode 100644 index 00000000..eeacb410 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/static/static.html @@ -0,0 +1,3 @@ + diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/static/static.js b/test/projects/component-preview-project/force-app/main/default/lwc/static/static.js new file mode 100644 index 00000000..0e475954 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/static/static.js @@ -0,0 +1,2 @@ +import { LightningElement } from 'lwc'; +export default class Static extends LightningElement {} diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/static/static.js-meta.xml b/test/projects/component-preview-project/force-app/main/default/lwc/static/static.js-meta.xml new file mode 100644 index 00000000..023fc7f7 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/static/static.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.html b/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.html new file mode 100644 index 00000000..12424d33 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.html @@ -0,0 +1,3 @@ + diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.js b/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.js new file mode 100644 index 00000000..95111a11 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.js @@ -0,0 +1,6 @@ +import { LightningElement } from 'lwc'; +export default class helloWorld extends LightningElement { + renderedCallback() { + throw new Error('Component generated error'); + } +} diff --git a/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.js-meta.xml b/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.js-meta.xml new file mode 100644 index 00000000..023fc7f7 --- /dev/null +++ b/test/projects/component-preview-project/force-app/main/default/lwc/withError/withError.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/test/projects/component-preview-project/sfdx-project.json b/test/projects/component-preview-project/sfdx-project.json new file mode 100644 index 00000000..42ed704d --- /dev/null +++ b/test/projects/component-preview-project/sfdx-project.json @@ -0,0 +1,6 @@ +{ + "packageDirectories": [{ "path": "force-app", "default": true }], + "name": "component-preview-project", + "namespace": "", + "sourceApiVersion": "60.0" +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 8e058f14..45423a2e 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -5,6 +5,7 @@ "skipLibCheck": true, "resolveJsonModule": true, "module": "nodenext", - "moduleResolution": "nodenext" + "moduleResolution": "nodenext", + "lib": ["dom", "es2017"] } } diff --git a/yarn.lock b/yarn.lock index d5e10f97..2c32509d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3607,6 +3607,13 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== +"@playwright/test@^1.49.0": + version "1.58.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.58.2.tgz#b0ad585d2e950d690ef52424967a42f40c6d2cbd" + integrity sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA== + dependencies: + playwright "1.58.2" + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -7435,6 +7442,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -7605,10 +7617,10 @@ glob@^11.0.3: package-json-from-dist "^1.0.0" path-scurry "^2.0.0" -glob@^13.0.4: - version "13.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.4.tgz#8479df26cb72f9a878179704f6ef3fb7db7cba09" - integrity sha512-KACie1EOs9BIOMtenFaxwmYODWA3/fTfGSUnLhMJpXRntu1g+uL/Xvub5f8SCTppvo9q62Qy4LeOoUiaL54G5A== +glob@^13.0.5: + version "13.0.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.5.tgz#a48f760c6312b1a19d2950fcb577384221c4ec00" + integrity sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw== dependencies: minimatch "^10.2.1" minipass "^7.1.2" @@ -10394,6 +10406,20 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" +playwright-core@1.58.2: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.58.2.tgz#ac5f5b4b10d29bcf934415f0b8d133b34b0dcb13" + integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg== + +playwright@1.58.2, playwright@^1.49.0: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.58.2.tgz#afe547164539b0bcfcb79957394a7a3fa8683cfd" + integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A== + dependencies: + playwright-core "1.58.2" + optionalDependencies: + fsevents "2.3.2" + pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"