diff --git a/test/nuts/digitalExperienceBundleWithWebapps/README.md b/test/nuts/digitalExperienceBundleWithWebapps/README.md new file mode 100644 index 00000000..3a6ca4c7 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/README.md @@ -0,0 +1,68 @@ +# WebApp DigitalExperienceBundle NUT Tests + +This directory contains NUT (Non-Unit Tests) for testing web_app DigitalExperienceBundle deployments with path-based fullNames. + +## Test Files + +### 1. `webapp.nut.ts` + +Basic deployment and retrieval tests for webapp bundles. + +**What it tests**: + +- Deploy webapp bundle without "not found in local project" warnings +- Path-based fullNames for web_app files (e.g., `web_app/WebApp/src/App.js`) +- Retrieve webapp bundle with path-based fullNames +- .forceignore pattern matching for webapp files + +## Test Project Structure + +The test project includes: + +``` +force-app/main/default/digitalExperiences/web_app/WebApp/ +├── webapp.json +├── public/ +│ ├── index.html +│ └── images/ +│ ├── icon.png +│ ├── photo.jpg +│ └── logo.svg +└── src/ + ├── App.css + ├── App.js + ├── index.css + └── index.js +``` + +## Running Tests + +Run all webapp NUT tests: + +```bash +yarn test:nuts:deb-webapp +``` + +Run individual tests: + +```bash +yarn mocha test/nuts/digitalExperienceBundleWithWebapps/webapp.nut.ts +``` + +## Technical Details + +### FullName Format + +Web_app files use path-based fullNames: + +``` +web_app/WebApp/src/App.js +└──────┘└─────┘└────────┘ +baseType bundle relative path +``` + +## Related Files + +- **Implementation**: `/src/resolve/adapters/digitalExperienceSourceAdapter.ts` +- **Deploy Logic**: `/src/client/deployMessages.ts` +- **Path Name Util**: `/src/client/utils.ts` (`computeWebAppPathName`) diff --git a/test/nuts/digitalExperienceBundleWithWebapps/constants.ts b/test/nuts/digitalExperienceBundleWithWebapps/constants.ts new file mode 100644 index 00000000..8f5a5d72 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/constants.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2025, 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 { join } from 'node:path'; +import { TestSessionOptions } from '@salesforce/cli-plugins-testkit/lib/testSession.js'; +import { registry } from '@salesforce/source-deploy-retrieve'; +import { assert } from 'chai'; + +export const SOURCE_BASE_RELATIVE_PATH = join('force-app', 'main', 'default'); +export const DEB_WEBAPP_NUTS_PATH = join(process.cwd(), 'test', 'nuts', 'digitalExperienceBundleWithWebapps'); + +export const TYPES = { + DEB: registry.types.digitalexperiencebundle, +} as const; + +export const DIR_NAMES = { + PROJECT: 'project', + DIGITAL_EXPERIENCES: TYPES.DEB.directoryName, + WEB_APP: 'web_app', + WEBAPP_NAME: 'WebApp', +} as const; + +assert(DIR_NAMES.DIGITAL_EXPERIENCES); + +export const WEBAPPS_RELATIVE_PATH = join(SOURCE_BASE_RELATIVE_PATH, DIR_NAMES.DIGITAL_EXPERIENCES, DIR_NAMES.WEB_APP); +export const WEBAPP_RELATIVE_PATH = join(WEBAPPS_RELATIVE_PATH, DIR_NAMES.WEBAPP_NAME); + +export const FULL_NAMES = { + WEBAPP: `${DIR_NAMES.WEB_APP}/${DIR_NAMES.WEBAPP_NAME}`, +} as const; + +export const METADATA = { + WEBAPP: `${TYPES.DEB.name}:${FULL_NAMES.WEBAPP}`, + ALL_DEBS: TYPES.DEB.name, +}; + +export const TEST_SESSION_OPTIONS: TestSessionOptions = { + project: { + sourceDir: join(DEB_WEBAPP_NUTS_PATH, DIR_NAMES.PROJECT), + }, + devhubAuthStrategy: 'AUTO', + scratchOrgs: [ + { + setDefault: true, + config: join('config', 'project-scratch-def.json'), + }, + ], +}; diff --git a/test/nuts/digitalExperienceBundleWithWebapps/helper.ts b/test/nuts/digitalExperienceBundleWithWebapps/helper.ts new file mode 100644 index 00000000..b879b24e --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/helper.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025, 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 { join } from 'node:path'; +import * as fs from 'node:fs'; +import { expect } from 'chai'; +import { WEBAPP_RELATIVE_PATH } from './constants.js'; + +/** + * Delete local webapp source + */ +export async function deleteLocalSource(sourceRelativePath: string, projectDir: string): Promise { + const fullPath = join(projectDir, sourceRelativePath); + if (fs.existsSync(fullPath)) { + await fs.promises.rm(fullPath, { recursive: true }); + await fs.promises.mkdir(fullPath, { recursive: true }); + } +} + +/** + * Convert metadata string to array format for CLI commands + */ +export const metadataToArray = (metadata: string): string => `--metadata ${metadata.split(',').join(' --metadata ')}`; + +/** + * Verify webapp files exist locally after retrieve + */ +export function assertWebAppFilesExist(projectDir: string): void { + const webappPath = join(projectDir, WEBAPP_RELATIVE_PATH); + expect(fs.existsSync(webappPath), `WebApp directory should exist at ${webappPath}`).to.be.true; + expect(fs.existsSync(join(webappPath, 'webapp.json')), 'webapp.json should exist').to.be.true; + expect(fs.existsSync(join(webappPath, 'src', 'App.js')), 'src/App.js should exist').to.be.true; +} diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/README.md b/test/nuts/digitalExperienceBundleWithWebapps/project/README.md new file mode 100644 index 00000000..703cc2ca --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/README.md @@ -0,0 +1,21 @@ +# Salesforce DX Project: Next Steps + +Now that you've created a Salesforce DX project, what's next? Here are some documentation resources to get you started. + +## How Do You Plan to Deploy Your Changes? + +Do you want to deploy a set of changes, or create a self-contained application? Choose +a [development model](https://developer.salesforce.com/tools/vscode/en/user-guide/development-models). + +## Configure Your Salesforce DX Project + +The `sfdx-project.json` file contains useful configuration information for your project. +See [Salesforce DX Project Configuration](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) +in the _Salesforce DX Developer Guide_ for details about this file. + +## Read All About It + +- [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/) +- [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm) +- [Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm) +- [Salesforce CLI Command Reference](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm) diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/config/project-scratch-def.json b/test/nuts/digitalExperienceBundleWithWebapps/project/config/project-scratch-def.json new file mode 100644 index 00000000..f45dec89 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/config/project-scratch-def.json @@ -0,0 +1,19 @@ +{ + "orgName": "DEB Webapp NUT Org", + "edition": "Developer", + "features": ["EnableSetPasswordInApi", "Communities"], + "settings": { + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + }, + "experienceBundleSettings": { + "enableExperienceBundleMetadata": true + }, + "communitiesSettings": { + "enableNetworksEnabled": true + } + } +} diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/icon.png b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/icon.png new file mode 100644 index 00000000..08cd6f2b Binary files /dev/null and b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/icon.png differ diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/logo.svg b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/logo.svg new file mode 100644 index 00000000..b551f8a2 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/photo.jpg b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/photo.jpg new file mode 100644 index 00000000..3283b94c Binary files /dev/null and b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/images/photo.jpg differ diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html new file mode 100644 index 00000000..231dd3c8 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html @@ -0,0 +1,14 @@ + + + + + + + + WebApp2 + + + +
+ + diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css new file mode 100644 index 00000000..a3a8a2d3 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css @@ -0,0 +1,35 @@ +.App { + text-align: center; +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.card { + padding: 2rem; + background-color: #1a1d23; + border-radius: 8px; + margin-top: 2rem; + max-width: 600px; +} + +.card h2 { + margin-top: 0; + color: #61dafb; +} + +code { + background-color: #1a1d23; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + color: #61dafb; +} diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js new file mode 100644 index 00000000..f39510ee --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js @@ -0,0 +1,21 @@ +import React from 'react'; +import './App.css'; + +function App() { + return ( +
+
+

Welcome to WebApp2

+

This is a dummy React application.

+
+

Getting Started

+

+ Edit src/App.js and save to reload. +

+
+
+
+ ); +} + +export default App; diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css new file mode 100644 index 00000000..7323ae85 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; +} diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js new file mode 100644 index 00000000..2cb1087e --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/webapp.json b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/webapp.json new file mode 100644 index 00000000..55a6ec12 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/force-app/main/default/digitalExperiences/web_app/WebApp/webapp.json @@ -0,0 +1,8 @@ +{ + "outputDir": "dist", + "apiVersion": "67.0", + "i18n": { + "defaultLocale": "en-US", + "locales": ["en-US"] + } +} diff --git a/test/nuts/digitalExperienceBundleWithWebapps/project/sfdx-project.json b/test/nuts/digitalExperienceBundleWithWebapps/project/sfdx-project.json new file mode 100644 index 00000000..0b876114 --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/project/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "digitalExperienceBundleWithWebapps", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "67.0" +} diff --git a/test/nuts/digitalExperienceBundleWithWebapps/webapp.nut.ts b/test/nuts/digitalExperienceBundleWithWebapps/webapp.nut.ts new file mode 100644 index 00000000..7631262d --- /dev/null +++ b/test/nuts/digitalExperienceBundleWithWebapps/webapp.nut.ts @@ -0,0 +1,218 @@ +/* + * Copyright 2025, 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 * as fs from 'node:fs'; +import { join } from 'node:path'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { assert, expect } from 'chai'; +import { DeployResultJson, RetrieveResultJson } from '../../../src/utils/types.js'; +import { METADATA, TEST_SESSION_OPTIONS, WEBAPPS_RELATIVE_PATH } from './constants.js'; +import { assertWebAppFilesExist, deleteLocalSource, metadataToArray } from './helper.js'; + +describe('web_app DigitalExperienceBundle', () => { + let session: TestSession; + + before(async function () { + this.timeout(600000); // 10 minutes for scratch org creation + session = await TestSession.create(TEST_SESSION_OPTIONS); + }); + + after(async function () { + this.timeout(60000); // 1 minute for cleanup + await session?.clean(); + }); + + describe('deploy', () => { + it('should deploy webapp bundle without "not found in local project" warnings', function () { + this.timeout(120000); // 2 minutes for deploy + const result = execCmd(`project deploy start ${metadataToArray(METADATA.WEBAPP)} --json`, { + ensureExitCode: 0, + }); + + const deployResult = result.jsonOutput?.result; + const warnings = result.jsonOutput?.warnings ?? []; + + assert(deployResult, 'Deploy result should exist'); + expect(deployResult.status).to.equal('Succeeded', 'Deployment should succeed'); + + // Verify no "not found in local project" warnings + const notFoundWarnings = warnings.filter((w: string) => + w.includes('returned from org, but not found in the local project') + ); + expect( + notFoundWarnings, + `Should have no "not found in local project" warnings. Found: ${notFoundWarnings.join(', ')}` + ).to.have.lengthOf(0); + + // Get deployed DigitalExperience files (path-based fullNames like web_app/WebApp/src/App.js) + const deFiles = deployResult.files.filter( + (file) => file.type === 'DigitalExperience' && file.fullName.startsWith('web_app/') + ); + + expect(deFiles.length).to.be.greaterThan(0, 'Should have deployed DigitalExperience files'); + + // All files should have valid file paths (means they matched local files) + deFiles.forEach((file) => { + expect(file.filePath, `File ${file.fullName} should have a filePath`).to.exist; + expect(file.state).to.match(/Changed|Created/); + }); + }); + + it('should use path-based fullNames for web_app files', function () { + this.timeout(120000); // 2 minutes for deploy + const deployResult = execCmd( + `project deploy start ${metadataToArray(METADATA.WEBAPP)} --json`, + { + ensureExitCode: 0, + } + ).jsonOutput?.result; + + assert(deployResult?.files, 'Deploy result should contain files'); + + const deFiles = deployResult.files.filter( + (file) => file.type === 'DigitalExperience' && file.fullName.startsWith('web_app/') + ); + + // All files should follow path-based pattern: web_app/WebApp/path/to/file + const fullNamePattern = /^web_app\/\w+\/.+$/; + deFiles.forEach((file) => { + expect(file.fullName).to.match(fullNamePattern, `Invalid fullName format: ${file.fullName}`); + }); + + // Verify specific files have path-based fullNames + const expectedFullNames = [ + 'web_app/WebApp/webapp.json', + 'web_app/WebApp/src/App.js', + 'web_app/WebApp/src/App.css', + 'web_app/WebApp/public/index.html', + ]; + + expectedFullNames.forEach((expectedFullName) => { + const matchingFile = deFiles.find((file) => file.fullName === expectedFullName); + expect(matchingFile, `Should find file with fullName: ${expectedFullName}`).to.exist; + }); + }); + }); + + describe('retrieve', () => { + beforeEach(async function () { + this.timeout(30000); // 30 seconds for file deletion + await deleteLocalSource(WEBAPPS_RELATIVE_PATH, session.project.dir); + }); + + it('should retrieve webapp bundle with path-based fullNames', async function () { + this.timeout(120000); // 2 minutes for retrieve + const result = execCmd(`project retrieve start ${metadataToArray(METADATA.WEBAPP)} --json`, { + ensureExitCode: 0, + }); + + const retrieveResult = result.jsonOutput?.result; + assert(retrieveResult?.files, 'Retrieve result should contain files'); + + // Verify files were retrieved + expect(retrieveResult.files.length).to.be.greaterThan(0, 'Should have retrieved files'); + + // Verify local files exist + assertWebAppFilesExist(session.project.dir); + + // Verify DigitalExperience files have path-based fullNames + const deFiles = retrieveResult.files.filter( + (file) => file.type === 'DigitalExperience' && file.fullName.startsWith('web_app/') + ); + + if (deFiles.length > 0) { + // All files should follow path-based pattern: web_app/WebApp/path/to/file + const fullNamePattern = /^web_app\/\w+\/.+$/; + deFiles.forEach((file) => { + expect(file.fullName).to.match(fullNamePattern, `Invalid fullName format: ${file.fullName}`); + }); + } + }); + }); + + describe('forceignore', () => { + let originalForceignore: string; + const forceignorePath = (): string => join(session.project.dir, '.forceignore'); + + beforeEach(() => { + // Save original .forceignore content + originalForceignore = fs.existsSync(forceignorePath()) ? fs.readFileSync(forceignorePath(), 'utf8') : ''; + }); + + afterEach(() => { + // Restore original .forceignore + fs.writeFileSync(forceignorePath(), originalForceignore); + }); + + it('should not deploy files that match .forceignore patterns', function () { + this.timeout(120000); // 2 minutes for deploy + // Add App.css to forceignore + const ignorePattern = '**/web_app/**/App.css'; + fs.appendFileSync(forceignorePath(), `\n${ignorePattern}\n`); + + const deployResult = execCmd( + `project deploy start ${metadataToArray(METADATA.WEBAPP)} --json`, + { + ensureExitCode: 0, + } + ).jsonOutput?.result; + + assert(deployResult?.files, 'Deploy result should contain files'); + + // App.css should NOT be in deployed files + const appCssFile = deployResult.files.find((f) => f.filePath?.endsWith('App.css')); + expect(appCssFile, 'App.css should not be deployed when in .forceignore').to.be.undefined; + + // Other files should still be deployed + const otherFiles = deployResult.files.filter( + (f) => f.type === 'DigitalExperience' && f.fullName.startsWith('web_app/') && !f.filePath?.endsWith('App.css') + ); + expect(otherFiles.length).to.be.greaterThan(0, 'Other files should still be deployed'); + }); + + it('should not retrieve files that match .forceignore patterns', async function () { + this.timeout(180000); // 3 minutes for deploy + retrieve + // First deploy everything + execCmd(`project deploy start ${metadataToArray(METADATA.WEBAPP)} --json`, { + ensureExitCode: 0, + }); + + // Add App.js to forceignore before retrieve + const ignorePattern = '**/web_app/**/App.js'; + fs.appendFileSync(forceignorePath(), `\n${ignorePattern}\n`); + + // Delete local source to force fresh retrieve + await deleteLocalSource(WEBAPPS_RELATIVE_PATH, session.project.dir); + + // Retrieve + const retrieveResult = execCmd( + `project retrieve start ${metadataToArray(METADATA.WEBAPP)} --json`, + { + ensureExitCode: 0, + } + ).jsonOutput?.result; + + assert(retrieveResult?.files, 'Retrieve result should contain files'); + + // App.js should NOT be in retrieved files + const appJsFile = retrieveResult.files.find((f) => f.filePath?.endsWith('App.js')); + expect(appJsFile, 'App.js should not be retrieved when in .forceignore').to.be.undefined; + + // Other files should still be retrieved + const otherFiles = retrieveResult.files.filter((f) => f.type === 'DigitalExperience'); + expect(otherFiles.length).to.be.greaterThan(0, 'Other files should still be retrieved'); + }); + }); +});