diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 180bddb98..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2.1 - -jobs: - build: - docker: - - image: circleci/node:12 - steps: - - checkout - - run: scripts/deploy_to_chrome_web_store.sh aodjmnfhjibkcdimpodiifdjnnncaafh PROD # build chrome version and push to chrome store, but only if version number > chrome store version -# - run: scripts/deploy_to_chrome_web_store.sh pbnihbeifpolhmekheiedlelddoagook BETA diff --git a/.forceignore b/.forceignore new file mode 100644 index 000000000..9cfa4ac3a --- /dev/null +++ b/.forceignore @@ -0,0 +1,2 @@ +**/jsconfig.json +**/.eslintrc.json \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/2-bug.yml b/.github/ISSUE_TEMPLATE/2-bug.yml index a526ee41c..2ed58706d 100644 --- a/.github/ISSUE_TEMPLATE/2-bug.yml +++ b/.github/ISSUE_TEMPLATE/2-bug.yml @@ -41,6 +41,7 @@ body: label: Version description: Which version are you using? options: + - "2.0" - "1.27" - "1.26" - "beta" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 96eba5841..b153ad495 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,13 @@ -## Describe your changes +# Describe your changes ## Issue ticket number and link ## Checklist before requesting a review -- [ ] I have read and understand the [Contributions section](https://github.com/tprouvot/Salesforce-Inspector-reloaded#contributions) -- [ ] Target branch is releaseCandidate and not master + +- [ ] I have **read and understand** the [Contributions section](https://github.com/tprouvot/Salesforce-Inspector-reloaded#contributions) +- [ ] My PR relates to an existing issue or feature request and **I discussed it with maintainer** +- [ ] I used SLDS style and limit the usage of custom CSS - [ ] I have performed a self-review of my code - [ ] I ran the [unit tests](https://github.com/tprouvot/Salesforce-Inspector-reloaded#unit-tests) and my PR does not break any tests - [ ] I documented the changes I've made on the [CHANGES.md](https://github.com/tprouvot/Salesforce-Inspector-reloaded/blob/master/CHANGES.md) and followed actual conventions -- [ ] I added a new section on [how-to.md](https://github.com/tprouvot/Salesforce-Inspector-reloaded/blob/master/docs/how-to.md) (optional) - +- [ ] I added a new section on [how-to.md](https://github.com/tprouvot/Salesforce-Inspector-reloaded/blob/master/docs/how-to.md) (optional) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..b36b7d447 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,193 @@ +name: Playwright Tests + +on: + workflow_dispatch: + pull_request: + branches: [ releaseCandidate, beta, main ] + push: + branches: [ releaseCandidate, beta, main ] + +jobs: + # Fork PRs: run tests with mocks (no access to secrets) + test-mocked: + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Restore node_modules cache + id: cache-npm + uses: actions/cache@v5 + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install chromium + + - name: Run Playwright tests (mocked) + run: | + echo "::notice::🧪 Tests running in MOCKED mode (API calls intercepted, no real Salesforce org)" + npm run test:e2e:mock + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-mocked + path: playwright-report/ + retention-days: 30 + + # Repo PRs + manual dispatch + post-merge push: run tests against real org + test-real-org: + if: >- + github.event_name == 'workflow_dispatch' + || github.event_name == 'push' + || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true) + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Restore node_modules cache + id: cache-npm + uses: actions/cache@v5 + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Setup Salesforce CLI and authenticate + if: ${{ vars.SF_TEST_MOCKENABLED == 'false' }} + env: + SF_CLI_URL: ${{ vars.SF_CLI_URL }} + SF_TEST_APIVERSION: ${{ vars.SF_TEST_APIVERSION }} + SF_TEST_MOCKENABLED: ${{ vars.SF_TEST_MOCKENABLED }} + SF_TEST_ACCOUNTID: ${{ vars.SF_TEST_ACCOUNTID }} + SF_TEST_FLOWID: ${{ vars.SF_TEST_FLOWID }} + SF_TEST_FLOWDEFID: ${{ vars.SF_TEST_FLOWDEFID }} + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + # Install Salesforce CLI + echo "Installing Salesforce CLI from $SF_CLI_URL" + wget $SF_CLI_URL -O sf-cli.tar.xz + mkdir -p sf-cli + tar xJf sf-cli.tar.xz -C sf-cli --strip-components=1 + echo "$PWD/sf-cli/bin" >> $GITHUB_PATH + export PATH="$PWD/sf-cli/bin:$PATH" + sf --version + + # Authenticate and get access token + echo "Authenticating to Salesforce org using SF_AUTH_URL..." + echo $SF_AUTH_URL > ./SFDX_AUTH_URL.txt + sf org login sfdx-url -f ./SFDX_AUTH_URL.txt -a cicdorg -d + + # Display the authenticated org and extract the instance url + echo "Extracting authentication information..." + ORG_INFO=$(sf org display --json -o cicdorg 2>/dev/null) + SF_EXIT_CODE=$? + + if [ $SF_EXIT_CODE -ne 0 ]; then + echo "Error: sf org display failed with exit code $SF_EXIT_CODE" + exit 1 + fi + + # Check if we got valid JSON + if [ -z "$ORG_INFO" ]; then + echo "Error: sf org display returned empty output" + exit 1 + fi + + INSTANCE_URL=$(echo "$ORG_INFO" | jq -r '.result.instanceUrl // empty' 2>/dev/null) + + # The access token is redacted from "sf org display" since the CLI + # security update (~May 2026). Retrieve it from the dedicated command + # (--json/--no-prompt skip the interactive security confirmation). + TOKEN_INFO=$(sf org auth show-access-token --json -o cicdorg --no-prompt 2>/dev/null) + ACCESS_TOKEN=$(echo "$TOKEN_INFO" | jq -r '.result.accessToken // empty' 2>/dev/null) + + if [ -z "$ACCESS_TOKEN" ] || [ "${ACCESS_TOKEN#\[REDACTED\]}" != "$ACCESS_TOKEN" ]; then + echo "Error: Could not extract access token from 'sf org auth show-access-token'" + exit 1 + fi + + # Mask the token so it never appears in logs + echo "::add-mask::$ACCESS_TOKEN" + + # Extract hostname from instance URL + if [ -n "$INSTANCE_URL" ]; then + SF_HOST=$(echo "$INSTANCE_URL" | sed 's|https\?://||' | sed 's|/.*||') + else + echo "Error: Could not determine Salesforce instance URL" + exit 1 + fi + + if [ -z "$SF_HOST" ]; then + echo "Error: Could not determine Salesforce hostname" + exit 1 + fi + + # Generate test-constants.local.js with real credentials + echo "Generating tests/e2e/test-constants.local.js..." + printf '%s\n' \ + 'export const TEST_CONSTANTS = {' \ + ' mockHost: "'"$SF_HOST"'",' \ + ' mockToken: "'"$ACCESS_TOKEN"'",' \ + ' apiVersion: "'"$SF_TEST_APIVERSION"'",' \ + ' accountRecordId: "'"$SF_TEST_ACCOUNTID"'",' \ + ' accountRecordName: "Test Account 1",' \ + ' testUserSearchTerm: "Integration User",' \ + ' flowId: "'"$SF_TEST_FLOWID"'",' \ + ' flowDefId: "'"$SF_TEST_FLOWDEFID"'",' \ + ' mockEnabled: '"$SF_TEST_MOCKENABLED" \ + '};' > tests/e2e/test-constants.local.js + + - name: Ensure test constants exist (fallback when auth step skipped) + if: vars.SF_TEST_MOCKENABLED != 'false' + run: cp tests/e2e/test-constants.template.js tests/e2e/test-constants.local.js + + - name: Display test mode + run: | + if [ "${{ vars.SF_TEST_MOCKENABLED }}" = "false" ]; then + echo "::notice::🔗 Tests running against REAL Salesforce org (SF_TEST_MOCKENABLED=false)" + else + echo "::notice::🧪 Tests running in MOCKED mode (SF_TEST_MOCKENABLED not set to false, API calls intercepted)" + fi + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install chromium + + - name: Run Playwright tests + run: npm run test:e2e + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index b12381a4a..e26cbebef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,13 @@ venv/ .sf/ .vscode/ .DS_Store -.cursorrules \ No newline at end of file +.cursorrules + +# Playwright +playwright-report/ +test-results/ +blob-report/ +tests/coverage/ + +# Local test constants (contains org credentials) +tests/e2e/test-constants.local.js diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2461a5142..000000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - "node" -script: - - npm run eslint diff --git a/CHANGES.md b/CHANGES.md index 0743c6ce0..870e9b840 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,44 @@ # Release Notes +## Version 2.1 + +- `Event Monitor` Fix "no customChannel found" when the org has more than one custom channel [issue #1323](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1323) +- `Popup` Fix missing record details in the Winter '27 release and resolve missing Org details when no maintenance is scheduled [issue #1316](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1316) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Data Import` Allow editing Batch Size and Threads during an active import [issue #1036](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1036) +- `Popup` Fix "Show Field API Names" button not rendering correctly in certain environments [issue #1246](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1246) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Popup` Restore LoginAs buttons for frozen users [discussion #1270](https://github.com/tprouvot/Salesforce-Inspector-reloaded/discussions/1270) +- `Metadata` Add 'Run Relevant Tests' option to metadata deploy Test Level picklist [feature 1286](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1286) +- `Popup` Fix language flag icons for Catalan and Basque on the Users tab [issue #1196](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1196) +- `Custom Shortcuts` Add "Global" toggle to share a shortcut across all orgs instead of keeping it specific to the current org [feature 191](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/191) +- `Typo Fix` Fix Dependencies Explorer Typo [issue #1276](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1276) (contribution by [Divyanshu Bist](https://github.com/DivyanshuBist) +- `Popup` Add search prefixes to speed up Shortcut tab queries: `/` for setup links only, `!` for metadata, and typed filters (`!flow`, `!profile`, `!class`, `!perm`) [feature 1265](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1265) +- `Data Export` Fixed Field Suggestions label visibility [issue #1255](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1255) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Popup` Fixed search icon spacing on the Objects, Shortcuts, and Users tabs [#1227](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/1227) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Data Export` Disabled the Copy/Download buttons when the query output has no results [#1258](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/1258) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Shortcuts` Fixed broken links for some Setup shortcuts [#1223](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/1223) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Metadata` Fix "Retrieve Metadata" button [issue #1256](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1256) and "Save status info" button [issue #1257](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1257) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Show All Data` Add **Refresh** button to re-fetch the latest field values and persist the filter value in the URL [feature 1201](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1201) +- `Popup` Fix object not refreshing when switching between object list views [issue #1254](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1254) (contribution by [Prem Kumar](https://github.com/prem-k-r)) +- `Metadata Retrieve` Fix sort preference (`Sort metadata by`) not persisting due to misspelled localStorage key +- `Popup` Add filter icon and menu on User tab search input [discussion #1147](https://github.com/tprouvot/Salesforce-Inspector-reloaded/discussions/1147) +- `Event Monitor` Allow users to generate, publish and save Platform Events based on their definition +- `Cache` Resolve "Unknown object" error for standard objects in Data Import and Field Explorer #1231 (contribution by [Prem Kumar](https://github.com/prem-k-r)) + ## Version 2.0 +- `Data Export` Fix unrecognized Salesforce Ids [issue #984](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/984) +- `Data Import` Expose metadata updates through Tooling API (ie Bulk Deactivate Flows) [feature 1125](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1125) +- Fix Lightning Navigation from Analytics / Tableau [issue #1121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1121) +- `Popup` review cache management in "Preload SObjects before popup opens". If enabled, refresh of SObject definition is done every "SObjects List Cache" hours, else done in background when the popup is expanded +- `Rest Explorer` fix a performance issue (edit entrypoint rerender completely the output even if no changes) (contribution by [Nicolas Greard](https://github.com/ngreardSF)) +- `Popup` fix recent items scroll not working anymore in v2 (contribution by [Nicolas Greard](https://github.com/ngreardSF)) +- Fix popup/detail record name field [issue #274](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/274) (contribution by [Nicolas Greard](https://github.com/ngreardSF)) +- `Data Export` Fix Column order not respected when performing subqueries [#598](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/598) +- `Accessibility` Improve screen reader support: fix AlertBanner silent announcements, convert inspector button and tooltip trigger to semantic elements, add popup iframe title issues [#1107](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1107) & [#1108](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1108) (contribution by [akj](https://github.com/akj)) +- `Dependencies Explorer` (contribution by [Georgi Dobrishinov](https://github.com/dobrishinov)) +- `Flow Scanner` Extract Flow Scanner rules logic into shared module (fixes the Flow Scanner import flash on the Options page) contribution by [Camille Guillory](https://github.com/CamilleGuillory)) +- Introduce SObjectList cache to prevent request to Salesforce requests to be sent each time the popup opens [issue #437](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/437) +- `Popup` Support record ID detection in Lightning Setup URLs with address parameter [feature 1099](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1099) - `Data Export` Fix Incorrect SOSL Query stamping when selected from Saved Queries [issue #1075](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1075) (contribution by [Divyanshu Bist](https://github.com/DivyanshuBist) - `Data Import` Fix data import issue with quoted words [issue #748](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/748) (contribution by [Maral Zakarian](https://github.com/MaralZak) - `Data Export` Refactor "Hide Object Columns" toggle to update UI instantly without re-querying Salesforce (contribution by [Camille Guillory](https://github.com/CamilleGuillory)) diff --git a/README.md b/README.md index 326516e21..f891dbbd5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Chrome and Firefox extension to add a metadata layout on top of the standard Salesforce UI to improve the productivity and joy of Salesforce configuration, development, and integration work. -We all know and love Salesforce Inspector: As the great Søren Krabbe did not have the time to maintain it anymore, I decided to take over so trailblazer community can keep asking for new features ! +We all know and love Salesforce Inspector: As the great Søren Krabbe did not maintain it anymore, I decided to take over so the trailblazer community can keep asking for new features! - [New features compared to the original SF Inspector](#new-features-compared-to-original-sf-inspector) - [Security and Privacy](#security-and-privacy) @@ -54,19 +54,62 @@ We all know and love Salesforce Inspector: As the great Søren Krabbe did not ha ## New features compared to original SF Inspector -- REST Explore: use Salesforce APIs without external tool -- Favicon customization for each org +### New Pages & Features + +- **[REST Explorer](https://tprouvot.github.io/Salesforce-Inspector-reloaded/rest-explorer/)**: Interact with Salesforce REST APIs directly from the extension with request templates, query history, and auto-completion +- **[Dependencies Explorer](https://tprouvot.github.io/Salesforce-Inspector-reloaded/dependencies-explorer/)**: Analyze Salesforce metadata dependencies to understand what depends on your metadata and what your metadata depends on +- **[Field Creator](https://tprouvot.github.io/Salesforce-Inspector-reloaded/field-creator/)**: Create fields for standard objects, custom objects, platform events, and custom metadata types with bulk import support +- **[Flow Scanner](https://tprouvot.github.io/Salesforce-Inspector-reloaded/flow-scanner/)**: Analyze Salesforce Flows for best practices, errors, and potential issues with comprehensive rule checking +- **[Debug Logs Viewer](https://tprouvot.github.io/Salesforce-Inspector-reloaded/logs-viewer/)**: View, filter, analyze, and manage Salesforce debug logs with Agentforce-powered analysis and grep-like filtering +- **[Event Monitor](https://tprouvot.github.io/Salesforce-Inspector-reloaded/event-monitor/)**: Subscribe to and display Salesforce Platform Events in real-time, including Change Events and custom channels +- **[Metadata Retrieve](https://tprouvot.github.io/Salesforce-Inspector-reloaded/download-metadata/)**: Retrieve and deploy metadata from Salesforce with package.xml generation and deployment options +- **[API Statistics](https://tprouvot.github.io/Salesforce-Inspector-reloaded/api-statistics/)**: Track and monitor all REST and SOAP API calls with performance metrics and error tracking +- **[Options](https://tprouvot.github.io/Salesforce-Inspector-reloaded/how-to/)**: Configure extension settings including favicon color picker, export/import configuration, custom shortcuts, default popup tab, and hide buttons options + +### Enhanced Features + +- **Show All Data** (Inspect page): + - Agentforce Helper for formula fields to analyze and generate improvements + - Analyze field usage by showing percentage of records that have a value for each field + - Save fields selection to persist across sessions + - Keyboard shortcut to save edited record values + - Back to record button for easy navigation + +- **Popup**: + - New **Org tab** to display org and instance information + - New **Shortcuts tab** to accelerate setup navigation with search functionality + - **Reset Password** button in User tab + - **Unfreeze User** button in User tab + - **Copy Id** icon in User tab + +- **Data Export** - [Documentation](https://tprouvot.github.io/Salesforce-Inspector-reloaded/data-export/): + - Multiple query tabs support with drag & drop reordering and editable tab names + - Agentforce SOQL generation from natural language descriptions + - Auto-populate SOQL editor from Salesforce List View context + - Query performance metrics with batch statistics + - Filter result by column for easier data analysis + +- **Data Import** - [Documentation](https://tprouvot.github.io/Salesforce-Inspector-reloaded/data-import/): + - Configure SOAP headers for assignment rules, duplicate rules, and owner change options + - Automatically detect SObject based on Id field + - Grey out columns that were not imported + - Guess file format on paste (CSV, JSON, Excel) + - Undelete records support + +- **Org Limits**: + - Refresh button to update limits in real-time + - Persist filter in URL for easy bookmarking + - Restyled Org Limits UI with improved readability + - Fix gauge display and text when limits are exceeded + +### Other Improvements + +- Favicon and banner customization for each org - Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) - Add new "Shortcuts" tab to accelerate setup navigation [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) -- Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) +- Add shortcuts links to (list of record types, current SObject RecordType and object details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) - Control access to Salesforce Inspector reloaded with profiles / permissions (Implement OAuth2 flow to generate access token for connected App) [how to](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) - Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 (extensions using manifest v2 will be removed from the store) -- New UI for Export / Import -- Enhanced Event Monitor: - - Support for Change Events to monitor data changes in Salesforce - - Manual channel configuration with custom channel path input - - Improved channel type selection with dedicated dropdown - - Streamlined UI with all inputs in a single row for better usability ## Security and Privacy @@ -111,7 +154,7 @@ Thank you for shaping our extension's future! Your feedback makes it better. ### Local Installation 1. Download or clone the repo. -2. Checkout the releaseCandidate branch. +2. Checkout the `releaseCandidate` branch. 3. Open `chrome://extensions/`. 4. Enable `Developer mode`. 5. Click `Load unpacked`. @@ -175,22 +218,15 @@ This branch will be merged into beta and then master when the new version is pub ### Firefox -1. Rename the manifest-firefox.json file to manifest.json (and manifest.json to manifest-chrome.json). +1. Generate the Firefox add-on with `npm run firefox-release-build`. This copies `addon/manifest-firefox.json` to `target/firefox/dist/manifest.json` (see `scripts/release-build.js`). 2. In Firefox, open `about:debugging`. 3. Select `This Firefox` at the top left. 4. Click `Load Temporary Add-on…`. -5. Select the file `addon/manifest.json`. +5. Select the file `target/firefox/dist/manifest.json`. ### Unit tests -1. Set up an org (e.g. a Developer Edition) and apply the following customizations: - 1. Everything described in metadata in `test/`. Push to org with `sf deploy metadata -d test/ -o [your-test-org-alias]`. - 2. Make sure your user language is set to English. - 3. Ensure the org has no _namespace prefix_ (Setup → Package Manager). - 4. Assign Permission Set `SfInspector` to your user. -2. Navigate to one of the extension pages and replace the file name with `test-framework.html`, for example `chrome-extension://example/test-framework.html?host=example.my.salesforce.com`. -3. Wait until "Salesforce Inspector unit test finished successfully" is shown. -4. If the test fails, open your browser's developer tools console to see error messages. +See [How to run tests](tests/HOW_TO_RUN_TESTS.md). ### Linting @@ -218,7 +254,7 @@ By Thomas Prouvot and forked from [Søren Krabbe and Jesper Kristensen](https:// This extension uses the following third-party libraries: -- [Lightning Flow Scanner Core](https://github.com/Flow-Scanner/lightning-flow-scanner-core) - A lightweight engine for Flow metadata analysis in Node.js and browser environments (MIT License) +- [Lightning Flow Scanner Core](https://github.com/Flow-Scanner/lightning-flow-scanner) - A lightweight engine for Flow metadata analysis in Node.js and browser environments (MIT License) - [PrismJS](https://prismjs.com/) - Lightweight, extensible syntax highlighter (MIT License) For full license details, see [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 4bb17d278..1af160048 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -6,7 +6,7 @@ This project includes code from the following open source projects: - **License**: MIT License - **Copyright**: Flow-Scanner Contributors -- **Source**: https://github.com/Flow-Scanner/lightning-flow-scanner-core +- **Source**: https://github.com/Flow-Scanner/lightning-flow-scanner - **Files**: `addon/lib/flow-scanner-core.js` MIT License diff --git a/addon/api-statistics-page.js b/addon/api-statistics-page.js index dfc2fddd0..38a580e03 100644 --- a/addon/api-statistics-page.js +++ b/addon/api-statistics-page.js @@ -1,8 +1,9 @@ /* global React ReactDOM */ import {sfConn} from "./inspector.js"; import {PageHeader} from "./components/PageHeader.js"; -import {UserInfoModel, copyToClipboard} from "./utils.js"; +import {UserInfoModel, copyToClipboard, formatDuration} from "./utils.js"; import {apiStatistics, ApiStatistics} from "./api-statistics.js"; +import ConfirmModal from "./components/ConfirmModal.js"; /* global initButton */ class Model { @@ -15,6 +16,8 @@ class Model { this.title = "API Debug Stats"; this.stats = null; this.debugModeEnabled = ApiStatistics.isDebugModeEnabled(); + this.showErrorModal = false; + this.errorMessages = []; // Initialize user info model this.userInfoModel = new UserInfoModel((promise) => { @@ -54,14 +57,25 @@ class Model { copyAsJson() { copyToClipboard(JSON.stringify(this.stats, null, 2)); } + + showErrors() { + this.errorMessages = apiStatistics.getLastErrors(); + this.showErrorModal = true; + this.didUpdate(); + } + + hideErrors() { + this.showErrorModal = false; + this.didUpdate(); + } } let h = React.createElement; class StatsCard extends React.Component { render() { - const {title, value, subtitle, className = ""} = this.props; - return h("div", {className: `slds-card slds-m-around_small ${className}`}, + const {title, value, subtitle, className = "", titleAttr} = this.props; + return h("div", {className: `slds-card slds-m-around_small ${className}`, title: titleAttr}, h("div", {className: "slds-card__body slds-card__body_inner"}, h("h3", {className: "slds-text-heading_small slds-m-bottom_x-small"}, title), h("div", {className: "slds-text-heading_large slds-text-color_success"}, value), @@ -73,7 +87,7 @@ class StatsCard extends React.Component { class EndpointTable extends React.Component { render() { - const {endpoints, title} = this.props; + const {endpoints, title, onErrorClick} = this.props; if (!endpoints || endpoints.length === 0) { return null; } @@ -97,7 +111,11 @@ class EndpointTable extends React.Component { h("tr", {key: index}, h("td", {}, endpoint.endpoint), h("td", {className: "slds-text-align_right"}, endpoint.count.toLocaleString()), - h("td", {className: "slds-text-align_right"}, endpoint.errors > 0 ? h("span", {className: "slds-text-color_error"}, endpoint.errors) : "0"), + h("td", {className: "slds-text-align_right"}, endpoint.errors > 0 ? h("span", { + className: "slds-text-color_error sfir-error-link", + onClick: onErrorClick, + title: "Click to view error details" + }, endpoint.errors) : "0"), h("td", {className: "slds-text-align_right"}, endpoint.averageDuration.toLocaleString()) ) ) @@ -110,7 +128,7 @@ class EndpointTable extends React.Component { class MethodTable extends React.Component { render() { - const {methods, title} = this.props; + const {methods, title, onErrorClick} = this.props; if (!methods || methods.length === 0) { return null; } @@ -134,7 +152,11 @@ class MethodTable extends React.Component { h("tr", {key: index}, h("td", {}, method.method), h("td", {className: "slds-text-align_right"}, method.count.toLocaleString()), - h("td", {className: "slds-text-align_right"}, method.errors > 0 ? h("span", {className: "slds-text-color_error"}, method.errors) : "0"), + h("td", {className: "slds-text-align_right"}, method.errors > 0 ? h("span", { + className: "slds-text-color_error sfir-error-link", + onClick: onErrorClick, + title: "Click to view error details" + }, method.errors) : "0"), h("td", {className: "slds-text-align_right"}, method.averageDuration.toLocaleString()) ) ) @@ -151,6 +173,8 @@ class App extends React.Component { this.model = this.props.vm; this.onResetStats = this.onResetStats.bind(this); this.onCopyAsJson = this.onCopyAsJson.bind(this); + this.onErrorClick = this.onErrorClick.bind(this); + this.onCloseErrorModal = this.onCloseErrorModal.bind(this); } onResetStats() { @@ -163,6 +187,14 @@ class App extends React.Component { this.model.copyAsJson(); } + onErrorClick() { + this.model.showErrors(); + } + + onCloseErrorModal() { + this.model.hideErrors(); + } + render() { const {stats, debugModeEnabled} = this.model; @@ -244,9 +276,10 @@ class App extends React.Component { .map(([method, data]) => ({method, count: data.count})) .sort((a, b) => b.count - a.count); - const sessionDurationText = stats.sessionDurationMinutes > 0 - ? `${stats.sessionDurationMinutes} minute${stats.sessionDurationMinutes > 1 ? "s" : ""}` - : "Less than a minute"; + const sessionDurationFull = formatDuration(stats.sessionDurationMinutes); + // Truncate to first 2 units (each unit is 2 words: number + unit name) + const sessionDurationParts = sessionDurationFull.split(" "); + const sessionDurationText = sessionDurationParts.slice(0, 4).join(" "); return h("div", {}, h(PageHeader, { @@ -294,7 +327,8 @@ class App extends React.Component { h(StatsCard, { title: "Session Duration", value: sessionDurationText, - subtitle: `Started: ${new Date(stats.startTime).toLocaleString()}` + subtitle: `Started: ${new Date(stats.startTime).toLocaleString()}`, + titleAttr: sessionDurationFull }), h(StatsCard, { title: "Average Response Time", @@ -329,8 +363,60 @@ class App extends React.Component { ) ) ), - h(EndpointTable, {endpoints: filteredEndpoints, title: "Top REST Endpoints"}), - h(MethodTable, {methods: filteredSoapMethods, title: "Top SOAP Methods"}) + h(EndpointTable, { + endpoints: filteredEndpoints, + title: "Top REST Endpoints", + onErrorClick: this.onErrorClick + }), + h(MethodTable, { + methods: filteredSoapMethods, + title: "Top SOAP Methods", + onErrorClick: this.onErrorClick + }), + h(ConfirmModal, { + isOpen: this.model.showErrorModal, + title: "Last 10 API Errors", + onCancel: this.onCloseErrorModal, + confirmLabel: "Close", + confirmVariant: "brand", + cancelLabel: null, + onConfirm: this.onCloseErrorModal, + containerClassName: "slds-modal_large" + }, + this.model.errorMessages.length > 0 ? h("div", {}, + h("div", {className: "slds-scrollable_y sfir-error-modal-content"}, + this.model.errorMessages.map((error, index) => + h("div", { + key: index, + className: "slds-box slds-m-bottom_small" + }, + h("div", {className: "slds-grid slds-grid_vertical-align-center slds-m-bottom_x-small"}, + h("div", {className: "slds-col slds-text-heading_small"}, + h("span", {className: "slds-badge slds-badge_inverse"}, error.mode.toUpperCase()) + ), + h("div", {className: "slds-col slds-text-body_small slds-text-color_weak"}, + new Date(error.timestamp).toLocaleString() + ) + ), + error.url && h("div", {className: "slds-text-body_small slds-m-bottom_x-small"}, + h("strong", {}, "URL: "), + error.url + ), + error.method && h("div", {className: "slds-text-body_small slds-m-bottom_x-small"}, + h("strong", {}, "Method: "), + error.method + ), + h("div", {className: "slds-text-body_regular"}, + h("strong", {}, "Error: "), + h("span", {className: "slds-text-color_error"}, error.message) + ) + ) + ) + ) + ) : h("div", {className: "slds-text-align_center slds-p-vertical_large"}, + h("p", {}, "No errors recorded.") + ) + ) ) ); } diff --git a/addon/api-statistics.js b/addon/api-statistics.js index a72da1d39..59ee295b5 100644 --- a/addon/api-statistics.js +++ b/addon/api-statistics.js @@ -20,7 +20,8 @@ export class ApiStatistics { errors: 0, totalDuration: 0 }, - startTime: Date.now() + startTime: Date.now(), + errorMessages: [] }; this._statsLoaded = false; } @@ -63,7 +64,8 @@ export class ApiStatistics { this.stats = { ...this.stats, ...parsed, - startTime: parsed.startTime || Date.now() + startTime: parsed.startTime || Date.now(), + errorMessages: parsed.errorMessages || [] }; } catch (e) { console.error("Error loading API debug statistics:", e); @@ -94,8 +96,9 @@ export class ApiStatistics { * @param {string} method - Method name * @param {number} duration - Duration in milliseconds * @param {boolean} isError - Whether the call resulted in an error + * @param {string} errorMessage - Error message if isError is true */ - trackApiCall(mode, url, method, duration, isError = false){ + trackApiCall(mode, url, method, duration, isError = false, errorMessage = null){ if (!ApiStatistics.isDebugModeEnabled()) { return; } @@ -125,7 +128,8 @@ export class ApiStatistics { errors: parsed.soap?.errors || 0, totalDuration: parsed.soap?.totalDuration || 0 }, - startTime: parsed.startTime || Date.now() + startTime: parsed.startTime || Date.now(), + errorMessages: parsed.errorMessages || [] }; } catch (e) { console.error("Error loading API debug statistics:", e); @@ -135,12 +139,29 @@ export class ApiStatistics { stats = this._getDefaultStats(); } - this.handleStatsUpdates(mode, stats[mode], url, method, duration, isError); + this.handleStatsUpdates(mode, stats[mode], url, method, duration, isError, errorMessage); + // Store error message if this is an error + if (isError && errorMessage) { + if (!stats.errorMessages) { + stats.errorMessages = []; + } + const errorEntry = { + timestamp: Date.now(), + mode, + url, + method, + message: errorMessage + }; + stats.errorMessages.push(errorEntry); + // Keep only the last 10 errors + if (stats.errorMessages.length > 10) { + stats.errorMessages = stats.errorMessages.slice(-10); + } + } this.saveStats(stats); } handleStatsUpdates(mode, statsType, url, method, duration, isError){ - const timestamp = Date.now(); statsType.total++; statsType.totalDuration += duration; @@ -208,7 +229,8 @@ export class ApiStatistics { errors: 0, totalDuration: 0 }, - startTime: Date.now() + startTime: Date.now(), + errorMessages: [] }; } @@ -236,7 +258,7 @@ export class ApiStatistics { simplified = simplified.replace(/[a-zA-Z0-9]{18}/g, "{id}"); return simplified; - } catch (e) { + } catch { return url; } } @@ -300,6 +322,18 @@ export class ApiStatistics { localStorage.setItem(Constants.API_DEBUG_STATISTICS, JSON.stringify(stats)); } + /** + * Get the last 10 error messages + * @returns {Array} Array of error objects with timestamp, mode, url, method, and message + */ + getLastErrors() { + if (!this._statsLoaded) { + this.loadStats(); + } + this.getStatsFromLocalStorage(); + return (this.stats.errorMessages || []).slice(-10); + } + /** * Reset statistics */ @@ -318,7 +352,8 @@ export class ApiStatistics { errors: 0, totalDuration: 0 }, - startTime: Date.now() + startTime: Date.now(), + errorMessages: [] }; this.saveStats(); } diff --git a/addon/background.js b/addon/background.js index 4cf2e27e0..8052c8089 100644 --- a/addon/background.js +++ b/addon/background.js @@ -1,102 +1,121 @@ - -let sfHost; - -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - // Perform cookie operations in the background page, because not all foreground pages have access to the cookie API. - // Firefox does not support incognito split mode, so we use sender.tab.cookieStoreId to select the right cookie store. - // Chrome does not support sender.tab.cookieStoreId, which means it is undefined, and we end up using the default cookie store according to incognito split mode. - if (request.message == "getSfHost") { - const currentDomain = new URL(request.url).hostname; - // When on a *.visual.force.com page, the session in the cookie does not have API access, - // so we read the corresponding session from *.salesforce.com page. - // The first part of the session cookie is the OrgID, - // which we use as key to support being logged in to multiple orgs at once. - // http://salesforce.stackexchange.com/questions/23277/different-session-ids-in-different-contexts - // There is no straight forward way to unambiguously understand if the user authenticated against salesforce.com or cloudforce.com - // (and thereby the domain of the relevant cookie) cookie domains are therefore tried in sequence. - chrome.cookies.get({url: request.url, name: "sid", storeId: sender.tab.cookieStoreId}, cookie => { - if (!cookie || currentDomain.endsWith(".mcas.ms")) { //Domain used by Microsoft Defender for Cloud Apps, where sid exists but cannot be read - sendResponse(currentDomain); - return; - } - const [orgId] = cookie.value.split("!"); - const orderedDomains = ["salesforce.com", "cloudforce.com", "salesforce.mil", "cloudforce.mil", "sfcrmproducts.cn", "force.com"]; - - orderedDomains.forEach(currentDomain => { - chrome.cookies.getAll({name: "sid", domain: currentDomain, secure: true, storeId: sender.tab.cookieStoreId}, cookies => { - - let sessionCookie = cookies.find(c => c.value.startsWith(orgId + "!") && c.domain != "help.salesforce.com"); - if (sessionCookie) { - sendResponse(sessionCookie.domain); - } - }); - }); - }); - return true; // Tell Chrome that we want to call sendResponse asynchronously. - } - if (request.message == "getSession") { - sfHost = request.sfHost; - chrome.cookies.get({url: "https://" + request.sfHost, name: "sid", storeId: sender.tab.cookieStoreId}, sessionCookie => { - if (!sessionCookie) { - sendResponse(null); - return; - } - let session = {key: sessionCookie.value, hostname: sessionCookie.domain}; - sendResponse(session); - }); - return true; // Tell Chrome that we want to call sendResponse asynchronously. - } else if (request.message == "createWindow") { - const brow = typeof browser === "undefined" ? chrome : browser; - brow.windows.create({ - url: request.url, - incognito: request.incognito ?? false - }); - } else if (request.message == "reloadPage") { - chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { - chrome.tabs.reload(tabs[0].id); - }); - } - return false; -}); -chrome.action.onClicked.addListener(() => { - chrome.runtime.sendMessage({ - msg: "shortcut_pressed", sfHost, command: "open-popup" - }); -}); -chrome.commands?.onCommand.addListener((command) => { - if (command.startsWith("link-")){ - let link; - switch (command){ - case "link-setup": - link = "/lightning/setup/SetupOneHome/home"; - break; - case "link-home": - link = "/"; - break; - case "link-dev": - link = "/_ui/common/apex/debug/ApexCSIPage"; - break; - } - chrome.tabs.create({ - url: `https:///${sfHost}${link}` - }); - - } else if (command.startsWith("open-")){ - chrome.runtime.sendMessage({ - msg: "shortcut_pressed", command, sfHost - }); - } else { - chrome.tabs.create({ - url: `chrome-extension://${chrome.i18n.getMessage("@@extension_id")}/${command}.html?host=${sfHost}` - }); - } -}); - -chrome.runtime.onInstalled.addListener(({reason}) => { - if (reason === "install") { - chrome.tabs.create({ - url: "https://tprouvot.github.io/Salesforce-Inspector-reloaded/welcome/" - }); - } -}); -chrome.runtime.setUninstallURL("https://forms.gle/y7LbTNsFqEqSrtyc6"); \ No newline at end of file + +let sfHost; + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + // Perform cookie operations in the background page, because not all foreground pages have access to the cookie API. + // Firefox does not support incognito split mode, so we use sender.tab.cookieStoreId to select the right cookie store. + // Chrome does not support sender.tab.cookieStoreId, which means it is undefined, and we end up using the default cookie store according to incognito split mode. + if (request.message == "getSfHost") { + const currentDomain = new URL(request.url).hostname; + // When on a *.visual.force.com page, the session in the cookie does not have API access, + // so we read the corresponding session from *.salesforce.com page. + // The first part of the session cookie is the OrgID, + // which we use as key to support being logged in to multiple orgs at once. + // http://salesforce.stackexchange.com/questions/23277/different-session-ids-in-different-contexts + // There is no straight forward way to unambiguously understand if the user authenticated against salesforce.com or cloudforce.com + // (and thereby the domain of the relevant cookie) cookie domains are therefore tried in sequence. + chrome.cookies.get({url: request.url, name: "sid", storeId: sender.tab.cookieStoreId}, cookie => { + if (!cookie || currentDomain.endsWith(".mcas.ms")) { //Domain used by Microsoft Defender for Cloud Apps, where sid exists but cannot be read + sendResponse(currentDomain); + return; + } + const [orgId] = cookie.value.split("!"); + const orderedDomains = ["salesforce.com", "cloudforce.com", "salesforce.mil", "cloudforce.mil", "sfcrmproducts.cn", "force.com"]; + + orderedDomains.forEach(currentDomain => { + chrome.cookies.getAll({name: "sid", domain: currentDomain, secure: true, storeId: sender.tab.cookieStoreId}, cookies => { + + let sessionCookie = cookies.find(c => c.value.startsWith(orgId + "!") && c.domain != "help.salesforce.com"); + if (sessionCookie) { + sendResponse(sessionCookie.domain); + } + }); + }); + }); + return true; // Tell Chrome that we want to call sendResponse asynchronously. + } + if (request.message == "getSession") { + sfHost = request.sfHost; + chrome.cookies.get({url: "https://" + request.sfHost, name: "sid", storeId: sender.tab.cookieStoreId}, sessionCookie => { + if (!sessionCookie) { + sendResponse(null); + return; + } + let session = {key: sessionCookie.value, hostname: sessionCookie.domain}; + sendResponse(session); + }); + return true; // Tell Chrome that we want to call sendResponse asynchronously. + } else if (request.message == "createWindow") { + const brow = typeof browser === "undefined" ? chrome : browser; + brow.windows.create({ + url: request.url, + incognito: request.incognito ?? false + }); + } else if (request.message == "reloadPage") { + chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { + chrome.tabs.reload(tabs[0].id); + }); + } + return false; +}); +chrome.action.onClicked.addListener(() => { + chrome.runtime.sendMessage({ + msg: "shortcut_pressed", sfHost, command: "open-popup" + }); +}); +chrome.commands?.onCommand.addListener((command) => { + if (command.startsWith("link-")){ + let link; + switch (command){ + case "link-setup": + link = "/lightning/setup/SetupOneHome/home"; + break; + case "link-home": + link = "/"; + break; + case "link-dev": + link = "/_ui/common/apex/debug/ApexCSIPage"; + break; + } + chrome.tabs.create({ + url: `https://${sfHost}${link}` + }); + + } else if (command.startsWith("open-")){ + chrome.runtime.sendMessage({ + msg: "shortcut_pressed", command, sfHost + }); + } else { + chrome.tabs.create({ + url: `chrome-extension://${chrome.i18n.getMessage("@@extension_id")}/${command}.html?host=${sfHost}` + }); + } +}); + +chrome.runtime.onInstalled.addListener(async (details) => { + if (details.reason === "install") { + chrome.tabs.create({ + url: "https://tprouvot.github.io/Salesforce-Inspector-reloaded/welcome/" + }); + } else if (details.reason === "update" && details.previousVersion?.startsWith("2.0")) { + //TODO delete clearSobjectsListCache after 2.0.1 release, only for upgrade from 2.0.0 to 2.0.1 + await clearSobjectsListCache(); + } +}); + +async function clearSobjectsListCache() { + try { + const storage = (typeof chrome !== "undefined" && chrome.storage) ? chrome.storage : browser.storage; + if (!storage?.local) return; + const allData = await storage.local.get(null); + const keysToRemove = Object.keys(allData || {}).filter(key => + key === "cache_sobjectsList" + ); + if (keysToRemove.length > 0) { + await storage.local.remove(keysToRemove); + } + } catch (e) { + console.error("Error clearing sobjectsList cache on update:", e); + } +} +chrome.runtime.setUninstallURL("https://forms.gle/y7LbTNsFqEqSrtyc6"); diff --git a/addon/button.css b/addon/button.css index 432db6654..03e66e44e 100644 --- a/addon/button.css +++ b/addon/button.css @@ -65,6 +65,14 @@ #insext.insext-active .insext-popup { display: block; } + #insext button.insext-btn { + appearance: none; + -webkit-appearance: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + } #insext .insext-btn { box-sizing: border-box; background: rgba(255, 255, 255, 0.9); diff --git a/addon/button.js b/addon/button.js index e9ba78c75..5a00b7eef 100644 --- a/addon/button.js +++ b/addon/button.js @@ -19,105 +19,16 @@ if (document.querySelector("body.sfdcBody, body.ApexCSIPage, #auraLoadingBox, #s function initButton(sfHost, inInspector) { let rootEl = document.createElement("div"); rootEl.id = "insext"; - let btn = document.createElement("div"); + let btn = document.createElement("button"); let iFrameLocalStorage = {}; + btn.type = "button"; btn.className = "insext-btn"; - btn.tabIndex = 0; btn.accessKey = "i"; btn.title = "Show Salesforce details (Alt+I / Shift+Alt+I)"; rootEl.appendChild(btn); loadPopup(sfHost); document.body.appendChild(rootEl); - function addFlowScrollability(popupEl) { - const currentUrl = window.location.href; - // Check the current URL for the string "builder_platform_interaction" - if (currentUrl.includes("builder_platform_interaction")) { - const checkboxState = iFrameLocalStorage.scrollOnFlowBuilder; - - // Create toggle container with styling to match navbar - const toggleContainer = document.createElement("div"); - toggleContainer.className = "slds-form-element__control"; - toggleContainer.style.cssText = "margin-left: auto; display: flex; align-items: center; gap: 0.5rem;"; - - // Create descriptive label text - const labelText = document.createElement("span"); - labelText.textContent = "Enable flow scrollability"; - labelText.style.color = "white"; - - // Create label wrapper - const overflowLabel = document.createElement("label"); - overflowLabel.className = "slds-checkbox_toggle slds-grid"; - - // Create checkbox input - const overflowCheckbox = document.createElement("input"); - overflowCheckbox.type = "checkbox"; - overflowCheckbox.id = "overflow-checkbox"; - overflowCheckbox.className = "slds-input"; - overflowCheckbox.checked = (checkboxState != null) ? checkboxState : true; - - // Create faux container - const fauxContainer = document.createElement("span"); - fauxContainer.className = "slds-checkbox_faux_container center-label"; - fauxContainer.setAttribute("aria-live", "assertive"); - - // Create faux checkbox - const fauxCheckbox = document.createElement("span"); - fauxCheckbox.className = "slds-checkbox_faux"; - - // Assemble the structure - fauxContainer.appendChild(fauxCheckbox); - overflowLabel.appendChild(overflowCheckbox); - overflowLabel.appendChild(fauxContainer); - toggleContainer.appendChild(labelText); - toggleContainer.appendChild(overflowLabel); - - // Get a reference to the element - const head = document.head; - // Create a new