diff --git a/.github/actions/cdn-checks/action.yml b/.github/actions/cdn-checks/action.yml index 90950a698d8..50634287e80 100644 --- a/.github/actions/cdn-checks/action.yml +++ b/.github/actions/cdn-checks/action.yml @@ -3,7 +3,7 @@ description: "Runs CDN checks" runs: using: composite steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup with: cache-prefix: "cdn" diff --git a/.github/actions/e2e-quantic-playwright/action.yml b/.github/actions/e2e-quantic-playwright/action.yml index 12dead3c4d8..da4dcc4cdbe 100644 --- a/.github/actions/e2e-quantic-playwright/action.yml +++ b/.github/actions/e2e-quantic-playwright/action.yml @@ -10,7 +10,7 @@ inputs: runs: using: composite steps: - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 with: path: packages/quantic/.env key: quantic-playwright-env-${{ github.sha }} diff --git a/.github/actions/e2e-quantic-setup/action.yml b/.github/actions/e2e-quantic-setup/action.yml index e9ca9009384..a88773f23f0 100644 --- a/.github/actions/e2e-quantic-setup/action.yml +++ b/.github/actions/e2e-quantic-setup/action.yml @@ -23,7 +23,7 @@ runs: SFDX_AUTH_JWT_KEY_FILE: server.key SFDX_AUTH_JWT_USERNAME: rdaccess@coveo.com SFDX_AUTH_JWT_INSTANCE_URL: https://login.salesforce.com - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 with: path: packages/quantic/.env key: quantic-playwright-env-${{ github.sha }} diff --git a/.github/actions/post-scratch-org-links-on-pr/action.yml b/.github/actions/post-scratch-org-links-on-pr/action.yml index 2185eb21358..170e99a0132 100644 --- a/.github/actions/post-scratch-org-links-on-pr/action.yml +++ b/.github/actions/post-scratch-org-links-on-pr/action.yml @@ -4,7 +4,7 @@ description: Publish Scratch Org Links as comments in a given PR runs: using: composite steps: - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 with: path: packages/quantic/.env key: quantic-playwright-env-${{ github.sha }} @@ -19,7 +19,7 @@ runs: echo "lws_disabled_url=$LWS_DISABLED_URL" >> $GITHUB_OUTPUT - name: Post or update PR comment with Scratch Org links - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6 with: script: | const lwsEnabledUrl = '${{ steps.read-env.outputs.lws_enabled_url }}'; diff --git a/.github/actions/publish-pr-review-site/action.yml b/.github/actions/publish-pr-review-site/action.yml index ed56de5315d..6b620f2be0b 100644 --- a/.github/actions/publish-pr-review-site/action.yml +++ b/.github/actions/publish-pr-review-site/action.yml @@ -11,7 +11,7 @@ inputs: runs: using: composite steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: repository: coveo/ui-kit-prs path: prs diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index daa1365f585..cd0a19c07a9 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -24,13 +24,13 @@ runs: - name: Install npm run: npm i -g npm@11.6.0 shell: bash - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 with: path: ~/.cache/Cypress key: ${{ runner.os }}-${{ inputs.cache-prefix }}-cy-${{ github.sha }} restore-keys: | ${{ runner.os }}-${{ inputs.cache-prefix }}-cy- - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 with: path: .turbo key: ${{ runner.os }}-${{ inputs.cache-prefix }}-turbo-${{ github.sha }} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a3507321b8e..70a475abc2c 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -23,7 +23,7 @@ jobs: environment: 'Manual Release' steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -39,17 +39,17 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - uses: ./.github/actions/setup - name: Generate a token id: generate-token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 with: app-id: ${{ secrets.RELEASER_APP_ID }} private-key: ${{ secrets.RELEASER_PRIVATE_KEY }} @@ -79,11 +79,11 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: 'release/v3' - uses: ./.github/actions/setup @@ -100,11 +100,11 @@ jobs: discussions: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: 'release/v3' - uses: ./.github/actions/setup @@ -127,12 +127,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: 'release/v3' @@ -168,12 +168,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: 'release/v3' @@ -210,11 +210,11 @@ jobs: environment: 'Docs Production' steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: 'release/v3' - uses: ./.github/actions/setup diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c68a6ceded..d17083505bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,19 +14,19 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Generate a token if: ${{ always() && github.event_name == 'pull_request'}} id: generate-token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} owner: coveo repositories: "ui-kit" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - run: git branch main origin/main @@ -49,11 +49,11 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: ${{ github.head_ref }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -75,11 +75,11 @@ jobs: environment: PR Artifacts steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: ${{ github.head_ref }} - uses: ./.github/actions/setup @@ -94,7 +94,7 @@ jobs: - name: Generate a token if: ${{ steps.check-changes.outputs.has_changes == 'true' && github.event_name == 'pull_request' }} id: generate-token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -113,11 +113,11 @@ jobs: DEPLOYMENT_ENVIRONMENT: CDN steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup with: cache-prefix: "cdn" @@ -149,11 +149,11 @@ jobs: projects: ${{ steps.set.outputs.projects }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 filter: blob:none @@ -175,10 +175,10 @@ jobs: needs: build steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - name: Run knip run: npm run knip @@ -191,12 +191,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Setup for CDN checks - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup with: cache-prefix: "cdn" @@ -210,11 +210,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run lint:check unit-test: @@ -224,11 +224,11 @@ jobs: timeout-minutes: 15 steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 filter: blob:none @@ -243,11 +243,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run package-compatibility typedoc: @@ -257,11 +257,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - name: "Build headless typedoc" run: npm run build:typedoc @@ -276,11 +276,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-csp prepare-playwright-atomic: @@ -296,11 +296,11 @@ jobs: shardTotal: ${{ steps.determine-tests.outputs.shardTotal }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - run: git branch main origin/main @@ -333,11 +333,11 @@ jobs: shardTotal: ${{ fromJson(needs.prepare-playwright-atomic.outputs.shardTotal) }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run build - uses: ./.github/actions/playwright-atomic @@ -354,11 +354,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run build - name: Merge Playwright reports @@ -371,7 +371,7 @@ jobs: - name: Generate a token if: ${{ always() && github.event_name == 'pull_request'}} id: generate-token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -389,11 +389,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run build - uses: ./.github/actions/playwright-atomic-theming @@ -440,11 +440,11 @@ jobs: ] steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic with: @@ -458,11 +458,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-screenshots e2e-atomic-search-commerce-react-test: @@ -472,11 +472,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-search-commerce-react e2e-atomic-search-nextjs-pages-router-test: @@ -486,11 +486,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-search-nextjs-pages-router e2e-atomic-search-commerce-angular-test: @@ -500,11 +500,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-search-commerce-angular e2e-atomic-search-vuejs-test: @@ -514,11 +514,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-search-vuejs e2e-atomic-search-stencil-test: @@ -528,11 +528,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-search-stencil playwright-atomic-hosted-page-test: @@ -542,11 +542,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run build - uses: ./.github/actions/playwright-atomic-hosted-pages @@ -557,11 +557,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - run: npm run build - uses: ./.github/actions/playwright-headless-ssr-commerce-nextjs @@ -572,11 +572,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-insight-panel e2e-headless-ssr-search-nextjs-app-router-test: @@ -586,11 +586,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-headless-ssr-search-nextjs-app-router e2e-headless-ssr-search-nextjs-pages-router-test: @@ -600,11 +600,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-headless-ssr-search-nextjs-pages-router e2e-quantic: @@ -629,11 +629,11 @@ jobs: environment: "Prerelease" steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - run: git branch main origin/main @@ -663,10 +663,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - run: git branch main origin/main @@ -711,7 +711,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -749,7 +749,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -771,7 +771,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -798,11 +798,11 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - run: git branch main origin/main @@ -831,10 +831,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - run: git branch main origin/main diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 8e076c97138..4e1d5df90e5 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -26,11 +26,11 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - uses: ./.github/actions/setup diff --git a/.github/workflows/create-quantic-package.yml b/.github/workflows/create-quantic-package.yml index cfac53efc62..30acc231f98 100644 --- a/.github/workflows/create-quantic-package.yml +++ b/.github/workflows/create-quantic-package.yml @@ -17,25 +17,25 @@ jobs: timeout-minutes: 30 steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Check Out Repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Create cache file run: | mkdir check-SHA echo ${{ github.sha }} > github-sha.txt - name: Check SHA id: check_sha - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 with: path: check-SHA key: check-SHA-${{ github.sha }} - name: Cancel current workflow run if no changes made if: steps.check_sha.outputs.cache-hit == 'true' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | github.rest.actions.cancelWorkflowRun({ diff --git a/.github/workflows/delete-pr-artifact-on-merge.yml b/.github/workflows/delete-pr-artifact-on-merge.yml index 5da4ca50413..080a4690f8a 100644 --- a/.github/workflows/delete-pr-artifact-on-merge.yml +++ b/.github/workflows/delete-pr-artifact-on-merge.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/publish-pr-review-site with: token: ${{ secrets.GH_PUBLISH_TOKEN }} diff --git a/.github/workflows/e2e-quantic.yml b/.github/workflows/e2e-quantic.yml index fa6eb67a823..51e7ba74d6c 100644 --- a/.github/workflows/e2e-quantic.yml +++ b/.github/workflows/e2e-quantic.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/setup-sfdx - uses: ./.github/actions/e2e-quantic-setup @@ -30,11 +30,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/post-scratch-org-links-on-pr e2e-quantic-playwright-test: name: 'Run Playwright e2e tests on Quantic' @@ -47,11 +47,11 @@ jobs: shardTotal: [4] steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-quantic-playwright with: @@ -67,11 +67,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - name: Merge Playwright reports uses: ./.github/actions/merge-playwright-reports @@ -86,11 +86,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/setup-sfdx - run: | diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 7f498acc844..1c359bc1778 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit diff --git a/.github/workflows/package-lock-root-fail.yml b/.github/workflows/package-lock-root-fail.yml index 68a44ea2f3c..500a41b8185 100644 --- a/.github/workflows/package-lock-root-fail.yml +++ b/.github/workflows/package-lock-root-fail.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit diff --git a/.github/workflows/package-lock-root-success.yml b/.github/workflows/package-lock-root-success.yml index 1d86e263945..002d07485c6 100644 --- a/.github/workflows/package-lock-root-success.yml +++ b/.github/workflows/package-lock-root-success.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit diff --git a/.github/workflows/setup-quantic-examples-community.yml b/.github/workflows/setup-quantic-examples-community.yml index 2e9c48d8242..9842189bc2b 100644 --- a/.github/workflows/setup-quantic-examples-community.yml +++ b/.github/workflows/setup-quantic-examples-community.yml @@ -20,12 +20,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Project setup uses: ./.github/actions/setup diff --git a/.github/workflows/update-pr-template-lit-migration.yml b/.github/workflows/update-pr-template-lit-migration.yml index 9b322e92ef5..9ed946326a8 100644 --- a/.github/workflows/update-pr-template-lit-migration.yml +++ b/.github/workflows/update-pr-template-lit-migration.yml @@ -9,12 +9,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout Repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Get Label id: get_label diff --git a/.nvmrc b/.nvmrc index 818ab238a53..3a6161c2af2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.17.1 \ No newline at end of file +22.19.0 \ No newline at end of file diff --git a/package.json b/package.json index f5f28c0b1d2..75c39af5fd7 100644 --- a/package.json +++ b/package.json @@ -36,23 +36,23 @@ "@cspell/dict-fr-fr": "2.3.2", "@tsconfig/node20": "20.1.6", "@types/jest": "29.5.14", - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@vitest/browser": "3.2.4", - "concurrently": "9.2.0", - "cspell": "9.2.0", - "esbuild": "0.25.8", + "concurrently": "9.2.1", + "cspell": "9.2.1", + "esbuild": "0.25.9", "esbuild-plugin-alias": "0.2.1", "esbuild-plugin-umd-wrapper": "3.0.0", "husky": "9.1.7", - "knip": "5.62.0", - "lint-staged": "16.1.2", + "knip": "5.63.1", + "lint-staged": "16.1.6", "patch-package": "8.0.0", - "playwright": "1.54.1", + "playwright": "1.55.0", "publint": "0.3.12", "rimraf": "6.0.1", "semver": "7.7.2", - "turbo": "2.5.5", - "typescript": "5.8.3", + "turbo": "2.5.6", + "typescript": "5.9.2", "vitest": "3.2.4" }, "overrides": { diff --git a/packages/atomic-angular/package.json b/packages/atomic-angular/package.json index f2eeffb7cf3..5646c40f34b 100644 --- a/packages/atomic-angular/package.json +++ b/packages/atomic-angular/package.json @@ -23,10 +23,10 @@ "@angular-devkit/build-angular": "19.2.3", "@angular/cli": "19.2.3", "@angular/compiler-cli": "19.2.2", - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@coveo/headless": "3.30.2", "ng-packagr": "19.2.0", - "typescript": "5.8.3", + "typescript": "5.9.2", "ncp": "2.0.0", "tslib": "2.8.1" }, diff --git a/packages/atomic-hosted-page/package.json b/packages/atomic-hosted-page/package.json index 18dfd529134..03ee308d270 100644 --- a/packages/atomic-hosted-page/package.json +++ b/packages/atomic-hosted-page/package.json @@ -35,10 +35,10 @@ "lit": "3.3.1" }, "devDependencies": { - "@playwright/test": "1.54.1", - "@types/node": "22.16.5", + "@playwright/test": "1.55.0", + "@types/node": "22.18.2", "rimraf": "3.0.2", - "vite": "7.0.6" + "vite": "7.1.5" }, "engines": { "node": "^20.9.0 || ^22.11.0" diff --git a/packages/atomic-react/package.json b/packages/atomic-react/package.json index d8d7adc782f..cc2594c1f61 100644 --- a/packages/atomic-react/package.json +++ b/packages/atomic-react/package.json @@ -39,13 +39,13 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "16.0.1", "@rollup/plugin-typescript": "12.1.4", - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "fix-esm-import-path": "1.10.1", "react": "19.1.1", "react-dom": "19.1.1", - "rollup": "4.46.2", + "rollup": "4.50.1", "rollup-plugin-polyfill-node": "^0.13.0", "ncp": "2.0.0" }, diff --git a/packages/atomic/package.json b/packages/atomic/package.json index e4721a734d2..8d3c8c84f73 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -85,14 +85,14 @@ "@lit/context": "1.1.6", "@open-wc/lit-helpers": "0.7.0", "@popperjs/core": "2.11.8", - "@salesforce-ux/design-system": "2.27.2", + "@salesforce-ux/design-system": "2.28.0", "@stencil/core": "4.20.0", "cssnano": "7.1.1", - "dayjs": "1.11.13", + "dayjs": "1.11.18", "dompurify": "3.2.6", "escape-html": "1.0.3", "focus-visible": "5.2.1", - "i18next": "25.3.2", + "i18next": "25.5.2", "i18next-http-backend": "3.0.2", "lit": "3.3.1", "marked": "12.0.2", @@ -101,8 +101,8 @@ }, "devDependencies": { "@axe-core/playwright": "4.10.2", - "@custom-elements-manifest/analyzer": "0.10.4", - "@playwright/test": "1.54.1", + "@custom-elements-manifest/analyzer": "0.10.5", + "@playwright/test": "1.55.0", "@rollup/plugin-commonjs": "28.0.6", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "16.0.1", @@ -111,24 +111,24 @@ "@stencil/angular-output-target": "0.8.4", "@stencil/react-output-target": "0.5.3", "@storybook/addon-a11y": "9.1.2", - "@storybook/addon-docs": "9.1.2", + "@storybook/addon-docs": "9.1.5", "@storybook/icons": "1.4.0", "@storybook/web-components-vite": "9.1.2", "@tailwindcss/postcss": "4.1.12", "@tailwindcss/vite": "4.1.12", - "@testing-library/jest-dom": "6.6.3", + "@testing-library/jest-dom": "6.8.0", "@types/core-js": "2.5.8", "@types/escape-html": "1.0.4", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/postcss-import": "14.0.3", "@types/puppeteer": "7.0.4", "@vitest/browser": "3.2.4", "@wc-toolkit/storybook-helpers": "9.0.1", "@whitespace/storybook-addon-html": "6.1.1", "axe-core": "4.10.3", - "chalk": "5.4.1", + "chalk": "5.6.2", "cypress": "13.7.3", "cypress-axe": "1.6.0", "cypress-repeat": "2.3.9", @@ -142,23 +142,23 @@ "local-web-server": "5.4.0", "natural-orderby": "5.0.0", "ora": "8.2.0", - "playwright": "1.54.1", + "playwright": "1.55.0", "postcss": "8.5.6", "postcss-load-config": "6.0.1", "postcss-nested": "7.0.2", "prettier": "3.6.2", - "puppeteer": "24.15.0", + "puppeteer": "24.20.0", "react": "19.1.1", - "rollup": "4.46.2", + "rollup": "4.50.1", "rollup-plugin-copy": "3.5.0", "rollup-plugin-html": "0.2.1", - "shadow-dom-testing-library": "1.12.0", + "shadow-dom-testing-library": "1.13.1", "storybook": "9.1.2", "tailwindcss": "4.1.12", "ts-dedent": "2.2.0", "ts-lit-plugin": "2.0.2", - "typescript": "5.8.3", - "vite": "7.0.6", + "typescript": "5.9.2", + "vite": "7.1.5", "vitest": "3.2.4", "wait-on": "8.0.4" }, diff --git a/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.spec.ts b/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.spec.ts index 0adbc77c5a6..53d75f400e6 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.spec.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.spec.ts @@ -21,7 +21,7 @@ import {bindings} from '@/src/decorators/bindings'; import type {InitializableComponent} from '@/src/decorators/types'; import {markParentAsReady} from '@/src/utils/init-queue.js'; import {SafeStorage, StorageItems} from '@/src/utils/local-storage-utils'; -import {DEFAULT_MOBILE_BREAKPOINT} from '@/src/utils/replace-breakpoint'; +import {DEFAULT_MOBILE_BREAKPOINT} from '@/src/utils/replace-breakpoint-utils'; import {fixture} from '@/vitest-utils/testing-helpers/fixture'; import {buildFakeContext} from '@/vitest-utils/testing-helpers/fixtures/headless/commerce/context-controller'; import {buildFakeCommerceEngine} from '@/vitest-utils/testing-helpers/fixtures/headless/commerce/engine'; diff --git a/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts b/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts index 4d726cffd37..583f60ee01f 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts @@ -3,7 +3,7 @@ import { type CommerceEngine, Selectors, } from '@coveo/headless/commerce'; -import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint'; +import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint-utils'; import { type BaseStore, createBaseStore, diff --git a/packages/atomic/src/components/commerce/atomic-commerce-layout/atomic-commerce-layout.ts b/packages/atomic/src/components/commerce/atomic-commerce-layout/atomic-commerce-layout.ts index 25aced56ea3..13860e87d05 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-layout/atomic-commerce-layout.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-layout/atomic-commerce-layout.ts @@ -3,7 +3,7 @@ import {customElement, property, state} from 'lit/decorators.js'; import {ChildrenUpdateCompleteMixin} from '@/src/mixins/children-update-complete-mixin'; import {LightDomMixin} from '@/src/mixins/light-dom'; import {randomID} from '@/src/utils/utils'; -import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint'; +import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint-utils'; import styles from './atomic-commerce-layout.tw.css'; import {buildCommerceLayout} from './commerce-layout'; diff --git a/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.spec.ts b/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.spec.ts index 8a14774d9b9..14dcbf50b0a 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.spec.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.spec.ts @@ -1,6 +1,6 @@ import {AriaLiveRegionController} from '@/src/utils/accessibility-utils'; import {isMacOS} from '@/src/utils/device-utils'; -import * as replaceBreakpoint from '@/src/utils/replace-breakpoint'; +import * as replaceBreakpoint from '@/src/utils/replace-breakpoint-utils'; import {renderInAtomicCommerceInterface} from '@/vitest-utils/testing-helpers/fixtures/atomic/commerce/atomic-commerce-interface-fixture'; import '@/vitest-utils/testing-helpers/fixtures/atomic/commerce/fake-atomic-commerce-search-box-suggestions-fixture'; import { @@ -29,7 +29,7 @@ vi.mock(import('../../../utils/utils'), async (importOriginal) => { randomID: vi.fn((prefix?: string, _length?: number) => `${prefix}123`), }; }); -vi.mock('@/src/utils/replace-breakpoint', {spy: true}); +vi.mock('@/src/utils/replace-breakpoint-utils', {spy: true}); const commonSearchBoxOptions = { id: 'atomic-commerce-search-box-123', diff --git a/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.ts b/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.ts index 6686b983650..e82539d1459 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.ts @@ -27,7 +27,7 @@ import { type StandaloneSearchBoxData, StorageItems, } from '@/src/utils/local-storage-utils'; -import {updateBreakpoints} from '@/src/utils/replace-breakpoint'; +import {updateBreakpoints} from '@/src/utils/replace-breakpoint-utils'; import { isFocusingOut, once, diff --git a/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx b/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx index e102ae1f938..150223b2664 100644 --- a/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx +++ b/packages/atomic/src/components/common/atomic-modal/atomic-modal.tsx @@ -16,7 +16,7 @@ import { InitializableComponent, InitializeBindings, } from '../../../utils/initialization-utils'; -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import {once, randomID} from '../../../utils/utils'; import {AnyBindings} from '../interface/bindings'; diff --git a/packages/atomic/src/components/common/button.spec.ts b/packages/atomic/src/components/common/button.spec.ts index 120859fb179..71ba7c2a5e6 100644 --- a/packages/atomic/src/components/common/button.spec.ts +++ b/packages/atomic/src/components/common/button.spec.ts @@ -1,10 +1,10 @@ import {html, nothing, render} from 'lit'; import {fireEvent, within} from 'storybook/test'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; -import {createRipple} from '@/src/utils/ripple'; +import {createRipple} from '@/src/utils/ripple-utils'; import {type ButtonProps, renderButton as button} from './button'; -vi.mock('../../utils/ripple'); +vi.mock('../../utils/ripple-utils'); describe('button', () => { let container: HTMLElement; diff --git a/packages/atomic/src/components/common/button.ts b/packages/atomic/src/components/common/button.ts index fe9bc5c96de..f23a1a4edf2 100644 --- a/packages/atomic/src/components/common/button.ts +++ b/packages/atomic/src/components/common/button.ts @@ -3,7 +3,7 @@ import {ifDefined} from 'lit/directives/if-defined.js'; import {type RefOrCallback, ref} from 'lit/directives/ref.js'; import {when} from 'lit/directives/when.js'; import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils'; -import {createRipple} from '../../utils/ripple'; +import {createRipple} from '../../utils/ripple-utils'; import { type ButtonStyle, getClassNameForButtonStyle, diff --git a/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.spec.ts b/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.spec.ts index a33ffb4553b..fb79a240710 100644 --- a/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.spec.ts +++ b/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.spec.ts @@ -2,7 +2,7 @@ import {page} from '@vitest/browser/context'; import {html} from 'lit'; import {createRef} from 'lit/directives/ref.js'; import {beforeAll, describe, expect, it, vi} from 'vitest'; -import {createRipple} from '@/src/utils/ripple'; +import {createRipple} from '@/src/utils/ripple-utils'; import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture'; import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils'; import {renderCheckbox} from '../../checkbox'; @@ -17,7 +17,7 @@ import { vi.mock('../../triStateCheckbox', {spy: true}); vi.mock('../../checkbox', {spy: true}); vi.mock('../facet-value-exclude/facet-value-exclude', {spy: true}); -vi.mock('@/src/utils/ripple', {spy: true}); +vi.mock('@/src/utils/ripple-utils', {spy: true}); describe('renderFacetValueCheckbox', () => { let i18n: Awaited>; diff --git a/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.ts b/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.ts index 70bdaa6bc14..ea5ab68a4aa 100644 --- a/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.ts +++ b/packages/atomic/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.ts @@ -2,7 +2,7 @@ import {html} from 'lit'; import {keyed} from 'lit/directives/keyed.js'; import {ref} from 'lit/directives/ref.js'; import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils'; -import {createRipple} from '../../../../utils/ripple'; +import {createRipple} from '../../../../utils/ripple-utils'; import {randomID} from '../../../../utils/utils'; import {renderCheckbox as checkbox} from '../../checkbox'; import {renderTriStateCheckbox} from '../../triStateCheckbox'; diff --git a/packages/atomic/src/components/common/facets/facet-value-checkbox/stencil-facet-value-checkbox.tsx b/packages/atomic/src/components/common/facets/facet-value-checkbox/stencil-facet-value-checkbox.tsx index 0645787411e..9a092339a3c 100644 --- a/packages/atomic/src/components/common/facets/facet-value-checkbox/stencil-facet-value-checkbox.tsx +++ b/packages/atomic/src/components/common/facets/facet-value-checkbox/stencil-facet-value-checkbox.tsx @@ -1,5 +1,5 @@ import {FunctionalComponent, h} from '@stencil/core'; -import {createRipple} from '../../../../utils/ripple'; +import {createRipple} from '../../../../utils/ripple-utils'; import {randomID} from '../../../../utils/utils'; import {StencilCheckbox} from '../../stencil-checkbox'; import {TriStateCheckbox} from '../../stencil-triStateCheckbox'; diff --git a/packages/atomic/src/components/common/generated-answer/atomic-generated-answer-feedback/atomic-generated-answer-feedback-modal.tsx b/packages/atomic/src/components/common/generated-answer/atomic-generated-answer-feedback/atomic-generated-answer-feedback-modal.tsx index dae4c2a73bc..283ad2f59c8 100644 --- a/packages/atomic/src/components/common/generated-answer/atomic-generated-answer-feedback/atomic-generated-answer-feedback-modal.tsx +++ b/packages/atomic/src/components/common/generated-answer/atomic-generated-answer-feedback/atomic-generated-answer-feedback-modal.tsx @@ -19,7 +19,7 @@ import { InitializableComponent, InitializeBindings, } from '../../../../utils/initialization-utils'; -import {updateBreakpoints} from '../../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../../utils/replace-breakpoint-utils'; import {once, randomID} from '../../../../utils/utils'; import {ATOMIC_MODAL_EXPORT_PARTS} from '../../atomic-modal/export-parts'; import {IconButton} from '../../stencil-iconButton'; diff --git a/packages/atomic/src/components/common/icon-button.spec.ts b/packages/atomic/src/components/common/icon-button.spec.ts index 8011300caf8..c8cecbef0e2 100644 --- a/packages/atomic/src/components/common/icon-button.spec.ts +++ b/packages/atomic/src/components/common/icon-button.spec.ts @@ -3,7 +3,7 @@ import {describe, expect, it, vi} from 'vitest'; import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture'; import {type IconButtonProps, renderIconButton} from './icon-button'; -vi.mock('../../utils/ripple', () => ({ +vi.mock('../../utils/ripple-utils', () => ({ createRipple: vi.fn(), })); diff --git a/packages/atomic/src/components/common/item-list/item-list-common.spec.ts b/packages/atomic/src/components/common/item-list/item-list-common.spec.ts index b63b3ddb42e..fddef172aeb 100644 --- a/packages/atomic/src/components/common/item-list/item-list-common.spec.ts +++ b/packages/atomic/src/components/common/item-list/item-list-common.spec.ts @@ -4,12 +4,12 @@ import { type FocusTargetController, getFirstFocusableDescendant, } from '@/src/utils/accessibility-utils'; -import {updateBreakpoints} from '@/src/utils/replace-breakpoint'; +import {updateBreakpoints} from '@/src/utils/replace-breakpoint-utils'; import {defer} from '@/src/utils/utils'; import {ItemListCommon, type ItemListCommonProps} from './item-list-common'; vi.mock('@/src/utils/accessibility-utils', {spy: true}); -vi.mock('@/src/utils/replace-breakpoint', {spy: true}); +vi.mock('@/src/utils/replace-breakpoint-utils', {spy: true}); vi.mock('@/src/utils/utils', async () => { const actual = await vi.importActual('@/src/utils/utils'); return { diff --git a/packages/atomic/src/components/common/item-list/item-list-common.ts b/packages/atomic/src/components/common/item-list/item-list-common.ts index 6b4a5b3ffc1..a37ef581c60 100644 --- a/packages/atomic/src/components/common/item-list/item-list-common.ts +++ b/packages/atomic/src/components/common/item-list/item-list-common.ts @@ -3,7 +3,7 @@ import { getFirstFocusableDescendant, } from '@/src/utils/accessibility-utils'; import {defer, once} from '@/src/utils/utils'; -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import type {CommerceStore} from '../../commerce/atomic-commerce-interface/store'; import type {CommerceRecommendationStore} from '../../commerce/atomic-commerce-recommendation-interface/store'; import type {InsightStore} from '../../insight/atomic-insight-interface/store'; diff --git a/packages/atomic/src/components/common/item-list/stencil-item-list-common.tsx b/packages/atomic/src/components/common/item-list/stencil-item-list-common.tsx index cb1b9469efb..ca8fd5b8d15 100644 --- a/packages/atomic/src/components/common/item-list/stencil-item-list-common.tsx +++ b/packages/atomic/src/components/common/item-list/stencil-item-list-common.tsx @@ -1,4 +1,4 @@ -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import { FocusTargetController, getFirstFocusableDescendant, diff --git a/packages/atomic/src/components/common/radio-button.spec.ts b/packages/atomic/src/components/common/radio-button.spec.ts index a05a3aef963..47675a6891a 100644 --- a/packages/atomic/src/components/common/radio-button.spec.ts +++ b/packages/atomic/src/components/common/radio-button.spec.ts @@ -1,10 +1,10 @@ import {html, render} from 'lit'; import {fireEvent, within} from 'storybook/test'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; -import {createRipple} from '@/src/utils/ripple'; +import {createRipple} from '@/src/utils/ripple-utils'; import {type RadioButtonProps, renderRadioButton} from './radio-button'; -vi.mock('../../utils/ripple'); +vi.mock('../../utils/ripple-utils'); describe('radioButton', () => { let container: HTMLElement; diff --git a/packages/atomic/src/components/common/radio-button.ts b/packages/atomic/src/components/common/radio-button.ts index 95d8df76a8a..217fa8eae57 100644 --- a/packages/atomic/src/components/common/radio-button.ts +++ b/packages/atomic/src/components/common/radio-button.ts @@ -3,7 +3,7 @@ import {ifDefined} from 'lit/directives/if-defined.js'; import {type RefOrCallback, ref} from 'lit/directives/ref.js'; import {multiClassMap} from '@/src/directives/multi-class-map'; import type {FunctionalComponent} from '@/src/utils/functional-component-utils'; -import {createRipple} from '../../utils/ripple'; +import {createRipple} from '../../utils/ripple-utils'; import { type ButtonStyle, getClassNameForButtonStyle, diff --git a/packages/atomic/src/components/common/stencil-button.tsx b/packages/atomic/src/components/common/stencil-button.tsx index e6c927dda7c..33a20e379fd 100644 --- a/packages/atomic/src/components/common/stencil-button.tsx +++ b/packages/atomic/src/components/common/stencil-button.tsx @@ -1,5 +1,5 @@ import {FunctionalComponent, h} from '@stencil/core'; -import {createRipple} from '../../utils/ripple'; +import {createRipple} from '../../utils/ripple-utils'; import { ButtonStyle, getRippleColorForButtonStyle, diff --git a/packages/atomic/src/components/common/stencil-radio-button.tsx b/packages/atomic/src/components/common/stencil-radio-button.tsx index 360ae40477e..cc6c64f810a 100644 --- a/packages/atomic/src/components/common/stencil-radio-button.tsx +++ b/packages/atomic/src/components/common/stencil-radio-button.tsx @@ -1,6 +1,6 @@ import {FunctionalComponent, h} from '@stencil/core'; import {JSXBase} from '@stencil/core/internal'; -import {createRipple} from '../../utils/ripple'; +import {createRipple} from '../../utils/ripple-utils'; import {RadioButtonProps} from './radio-button'; import { getClassNameForButtonStyle, diff --git a/packages/atomic/src/components/common/suggestions/suggestions-common.spec.ts b/packages/atomic/src/components/common/suggestions/suggestions-common.spec.ts new file mode 100644 index 00000000000..b2531ccf59f --- /dev/null +++ b/packages/atomic/src/components/common/suggestions/suggestions-common.spec.ts @@ -0,0 +1,165 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {closest} from '../../../utils/dom-utils'; +import { + dispatchSearchBoxSuggestionsEvent, + elementHasNoQuery, + elementHasQuery, + type SearchBoxSuggestionElement, + type SearchBoxSuggestionsEvent, +} from './suggestions-common'; + +vi.mock('../../../utils/event-utils', {spy: true}); +vi.mock('../../../utils/dom-utils', {spy: true}); + +describe('suggestions-common', () => { + const createTestElement = (query?: string): SearchBoxSuggestionElement => ({ + key: 'test-key', + content: document.createElement('div'), + ...(query !== undefined && {query}), + }); + + describe('#elementHasNoQuery', () => { + it('should return true when element has undefined query', () => { + const element = createTestElement(undefined); + expect(elementHasNoQuery(element)).toBe(true); + }); + + it('should return true when element has no query property', () => { + const element = createTestElement(); + expect(elementHasNoQuery(element)).toBe(true); + }); + + it('should return true when element has empty string query', () => { + const element = createTestElement(''); + expect(elementHasNoQuery(element)).toBe(true); + }); + + it('should return false when element has non-empty query', () => { + const element = createTestElement('search query'); + expect(elementHasNoQuery(element)).toBe(false); + }); + + it('should return true when element has whitespace-only query', () => { + const element = createTestElement(' '); + expect(elementHasNoQuery(element)).toBe(true); + }); + + it('should return true when element has tab and newline characters', () => { + const element = createTestElement('\t\n \r'); + expect(elementHasNoQuery(element)).toBe(true); + }); + }); + + describe('#elementHasQuery', () => { + it('should return false when element has undefined query', () => { + const element = createTestElement(undefined); + expect(elementHasQuery(element)).toBe(false); + }); + + it('should return false when element has no query property', () => { + const element = createTestElement(); + expect(elementHasQuery(element)).toBe(false); + }); + + it('should return false when element has empty string query', () => { + const element = createTestElement(''); + expect(elementHasQuery(element)).toBe(false); + }); + + it('should return true when element has non-empty query', () => { + const element = createTestElement('search query'); + expect(elementHasQuery(element)).toBe(true); + }); + + it('should return false when element has whitespace-only query', () => { + const element = createTestElement(' '); + expect(elementHasQuery(element)).toBe(false); + }); + + it('should return false when element has tab and newline characters', () => { + const element = createTestElement('\t\n \r'); + expect(elementHasQuery(element)).toBe(false); + }); + + it('should return true when element has query with leading/trailing whitespace', () => { + const element = createTestElement(' search query '); + expect(elementHasQuery(element)).toBe(true); + }); + + it('should handle numeric zero as valid query', () => { + const element = createTestElement('0'); + expect(elementHasQuery(element)).toBe(true); + }); + }); + + describe('#dispatchSearchBoxSuggestionsEvent', () => { + let mockElement: HTMLElement; + let mockEvent: SearchBoxSuggestionsEvent; + + beforeEach(() => { + document.body.innerHTML = ''; + mockElement = document.createElement('div'); + mockEvent = vi.fn(); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should call closest with correct selector', () => { + vi.mocked(closest).mockReturnValue( + document.createElement('atomic-search-box') + ); + + dispatchSearchBoxSuggestionsEvent(mockEvent, mockElement); + + expect(closest).toHaveBeenCalledWith( + mockElement, + 'atomic-search-box, atomic-insight-search-box, atomic-commerce-search-box' + ); + }); + + it('should work with any valid search box type', () => { + const searchBoxTypes = [ + 'atomic-search-box', + 'atomic-insight-search-box', + 'atomic-commerce-search-box', + ]; + + searchBoxTypes.forEach((type) => { + const mockParent = document.createElement(type); + vi.mocked(closest).mockReturnValue(mockParent); + + expect(() => { + dispatchSearchBoxSuggestionsEvent(mockEvent, mockElement); + }).not.toThrow(); + }); + }); + + it('should throw error when element is not child of allowed search box elements', () => { + vi.mocked(closest).mockReturnValue(null); + + expect(() => { + dispatchSearchBoxSuggestionsEvent(mockEvent, mockElement); + }).toThrow( + 'The "div" component was not handled, as it is not a child of the following elements: atomic-search-box, atomic-insight-search-box, atomic-commerce-search-box' + ); + }); + + it('should normalize nodeName to lowercase in error message', () => { + const upperCaseElement = document.createElement('div'); + Object.defineProperty(upperCaseElement, 'nodeName', { + value: 'CUSTOM-ELEMENT', + writable: false, + configurable: true, + }); + vi.mocked(closest).mockReturnValue(null); + + expect(() => { + dispatchSearchBoxSuggestionsEvent(mockEvent, upperCaseElement); + }).toThrow( + 'The "custom-element" component was not handled, as it is not a child of the following elements: atomic-search-box, atomic-insight-search-box, atomic-commerce-search-box' + ); + }); + }); +}); diff --git a/packages/atomic/src/components/common/suggestions/suggestions-common.ts b/packages/atomic/src/components/common/suggestions/suggestions-common.ts index 836a3cf4b75..01488f58b78 100644 --- a/packages/atomic/src/components/common/suggestions/suggestions-common.ts +++ b/packages/atomic/src/components/common/suggestions/suggestions-common.ts @@ -196,9 +196,9 @@ export const dispatchSearchBoxSuggestionsEvent = < }; export function elementHasNoQuery(el: SearchBoxSuggestionElement) { - return !el.query; + return !el.query || el.query.trim() === ''; } export function elementHasQuery(el: SearchBoxSuggestionElement) { - return !!el.query; + return !!el.query && el.query.trim() !== ''; } diff --git a/packages/atomic/src/components/insight/atomic-insight-interface/store.ts b/packages/atomic/src/components/insight/atomic-insight-interface/store.ts index 287fac6130d..5d576c557ff 100644 --- a/packages/atomic/src/components/insight/atomic-insight-interface/store.ts +++ b/packages/atomic/src/components/insight/atomic-insight-interface/store.ts @@ -3,7 +3,7 @@ import type { InsightEngine, NumericFacetValue, } from '@coveo/headless/insight'; -import {DEFAULT_MOBILE_BREAKPOINT} from '@/src/utils/replace-breakpoint'; +import {DEFAULT_MOBILE_BREAKPOINT} from '@/src/utils/replace-breakpoint-utils'; import type { FacetInfo, FacetStore, diff --git a/packages/atomic/src/components/insight/smart-snippets/atomic-insight-smart-snippet-feedback-modal/atomic-insight-smart-snippet-feedback-modal.tsx b/packages/atomic/src/components/insight/smart-snippets/atomic-insight-smart-snippet-feedback-modal/atomic-insight-smart-snippet-feedback-modal.tsx index bdf32f8eb78..5304ad00199 100644 --- a/packages/atomic/src/components/insight/smart-snippets/atomic-insight-smart-snippet-feedback-modal/atomic-insight-smart-snippet-feedback-modal.tsx +++ b/packages/atomic/src/components/insight/smart-snippets/atomic-insight-smart-snippet-feedback-modal/atomic-insight-smart-snippet-feedback-modal.tsx @@ -8,7 +8,7 @@ import { SmartSnippetFeedbackModalOption, smartSnippetFeedbackOptions, } from '@/src/components/common/smart-snippets/atomic-smart-snippet-feedback-modal/smart-snippet-feedback-modal-common'; -import {updateBreakpoints} from '@/src/utils/replace-breakpoint'; +import {updateBreakpoints} from '@/src/utils/replace-breakpoint-utils'; import {randomID} from '@/src/utils/utils'; import { buildSmartSnippet as buildInsightSmartSnippet, diff --git a/packages/atomic/src/components/ipx/atomic-ipx-body/atomic-ipx-body.tsx b/packages/atomic/src/components/ipx/atomic-ipx-body/atomic-ipx-body.tsx index 346b16d9f26..9f90cf14e79 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-body/atomic-ipx-body.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-body/atomic-ipx-body.tsx @@ -11,7 +11,7 @@ import { InitializableComponent, InitializeBindings, } from '../../../utils/initialization-utils'; -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import {once, randomID} from '../../../utils/utils'; import {AnyBindings} from '../../common/interface/bindings'; diff --git a/packages/atomic/src/components/ipx/atomic-ipx-embedded/atomic-ipx-embedded.tsx b/packages/atomic/src/components/ipx/atomic-ipx-embedded/atomic-ipx-embedded.tsx index a3849c671d9..ed4c8bd2fa4 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-embedded/atomic-ipx-embedded.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-embedded/atomic-ipx-embedded.tsx @@ -12,7 +12,7 @@ import { InitializableComponent, InitializeBindings, } from '../../../utils/initialization-utils'; -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import {once, randomID} from '../../../utils/utils'; import {AnyBindings} from '../../common/interface/bindings'; diff --git a/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx b/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx index 3b354fe27ef..e3017717755 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-modal/atomic-ipx-modal.tsx @@ -14,7 +14,7 @@ import { InitializableComponent, InitializeBindings, } from '../../../utils/initialization-utils'; -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import {once, randomID} from '../../../utils/utils'; import {AnyBindings} from '../../common/interface/bindings'; diff --git a/packages/atomic/src/components/search/atomic-search-box/atomic-search-box.tsx b/packages/atomic/src/components/search/atomic-search-box/atomic-search-box.tsx index e726edce8de..d4d9f6c6b72 100644 --- a/packages/atomic/src/components/search/atomic-search-box/atomic-search-box.tsx +++ b/packages/atomic/src/components/search/atomic-search-box/atomic-search-box.tsx @@ -31,7 +31,7 @@ import { StandaloneSearchBoxData, StorageItems, } from '../../../utils/local-storage-utils'; -import {updateBreakpoints} from '../../../utils/replace-breakpoint'; +import {updateBreakpoints} from '../../../utils/replace-breakpoint-utils'; import {AriaLiveRegion} from '../../../utils/stencil-accessibility-utils'; import { isFocusingOut, diff --git a/packages/atomic/src/components/search/atomic-search-interface/store.ts b/packages/atomic/src/components/search/atomic-search-interface/store.ts index 8329d4a0cb5..eebe9547d4a 100644 --- a/packages/atomic/src/components/search/atomic-search-interface/store.ts +++ b/packages/atomic/src/components/search/atomic-search-interface/store.ts @@ -4,7 +4,7 @@ import type { SearchEngine, SortCriterion, } from '@coveo/headless'; -import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint'; +import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint-utils'; import type { FacetInfo, FacetStore, diff --git a/packages/atomic/src/components/search/atomic-search-layout/atomic-search-layout.ts b/packages/atomic/src/components/search/atomic-search-layout/atomic-search-layout.ts index 0473a9e39d6..65718535735 100644 --- a/packages/atomic/src/components/search/atomic-search-layout/atomic-search-layout.ts +++ b/packages/atomic/src/components/search/atomic-search-layout/atomic-search-layout.ts @@ -3,7 +3,7 @@ import {customElement, property, state} from 'lit/decorators.js'; import {ChildrenUpdateCompleteMixin} from '@/src/mixins/children-update-complete-mixin'; import {LightDomMixin} from '@/src/mixins/light-dom'; import {randomID} from '@/src/utils/utils'; -import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint'; +import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint-utils'; import styles from './atomic-search-layout.tw.css'; import {buildSearchLayout} from './search-layout'; diff --git a/packages/atomic/src/components/search/facets/color-facet-checkbox/color-facet-checkbox.tsx b/packages/atomic/src/components/search/facets/color-facet-checkbox/color-facet-checkbox.tsx index 7950d827ffb..89ad53a112d 100644 --- a/packages/atomic/src/components/search/facets/color-facet-checkbox/color-facet-checkbox.tsx +++ b/packages/atomic/src/components/search/facets/color-facet-checkbox/color-facet-checkbox.tsx @@ -1,5 +1,5 @@ import {FunctionalComponent, h} from '@stencil/core'; -import {createRipple} from '../../../../utils/ripple'; +import {createRipple} from '../../../../utils/ripple-utils'; import {randomID} from '../../../../utils/utils'; import {FacetValueProps} from '../../../common/facets/stencil-facet-common'; diff --git a/packages/atomic/src/components/search/smart-snippets/atomic-smart-snippet-feedback-modal/atomic-smart-snippet-feedback-modal.tsx b/packages/atomic/src/components/search/smart-snippets/atomic-smart-snippet-feedback-modal/atomic-smart-snippet-feedback-modal.tsx index 8c1cfe02a26..72e7dba5cd0 100644 --- a/packages/atomic/src/components/search/smart-snippets/atomic-smart-snippet-feedback-modal/atomic-smart-snippet-feedback-modal.tsx +++ b/packages/atomic/src/components/search/smart-snippets/atomic-smart-snippet-feedback-modal/atomic-smart-snippet-feedback-modal.tsx @@ -8,7 +8,7 @@ import { SmartSnippetFeedbackModalOption, smartSnippetFeedbackOptions, } from '@/src/components/common/smart-snippets/atomic-smart-snippet-feedback-modal/smart-snippet-feedback-modal-common'; -import {updateBreakpoints} from '@/src/utils/replace-breakpoint'; +import {updateBreakpoints} from '@/src/utils/replace-breakpoint-utils'; import {randomID} from '@/src/utils/utils'; import { buildSmartSnippet, diff --git a/packages/atomic/src/utils/asset-path-utils.ts b/packages/atomic/src/utils/asset-path-utils.ts index f56885905f3..d90310c14ca 100644 --- a/packages/atomic/src/utils/asset-path-utils.ts +++ b/packages/atomic/src/utils/asset-path-utils.ts @@ -1,4 +1,4 @@ -import {getResourceUrl} from './resource-url'; +import {getResourceUrl} from './resource-url-utils'; export function getAssetPath(path: string): string { const resourceUrl = getResourceUrl(); diff --git a/packages/atomic/src/utils/color-utils.spec.ts b/packages/atomic/src/utils/color-utils.spec.ts new file mode 100644 index 00000000000..148e70c71c4 --- /dev/null +++ b/packages/atomic/src/utils/color-utils.spec.ts @@ -0,0 +1,228 @@ +import {describe, expect, it} from 'vitest'; +import {hsvToRgb, rgbToHsv} from './color-utils'; + +describe('color-utils', () => { + describe('#rgbToHsv', () => { + it('should convert primary colors correctly', () => { + // Red + const red = rgbToHsv(255, 0, 0); + expect(red).toEqual({h: 0, s: 1, v: 255}); + + // Green + const green = rgbToHsv(0, 255, 0); + expect(green.h).toBeCloseTo(1 / 3, 5); + expect(green.s).toBe(1); + expect(green.v).toBe(255); + + // Blue + const blue = rgbToHsv(0, 0, 255); + expect(blue.h).toBeCloseTo(2 / 3, 5); + expect(blue.s).toBe(1); + expect(blue.v).toBe(255); + }); + + it('should handle grayscale colors', () => { + // Black + const black = rgbToHsv(0, 0, 0); + expect(black).toEqual({h: 0, s: 0, v: 0}); + + // White + const white = rgbToHsv(255, 255, 255); + expect(white).toEqual({h: 0, s: 0, v: 255}); + + // Gray + const gray = rgbToHsv(128, 128, 128); + expect(gray).toEqual({h: 0, s: 0, v: 128}); + }); + + it('should handle mixed colors correctly', () => { + // Purple (magenta) + const purple = rgbToHsv(255, 0, 255); + expect(purple.h).toBeCloseTo(5 / 6, 5); + expect(purple.s).toBe(1); + expect(purple.v).toBe(255); + + // Yellow + const yellow = rgbToHsv(255, 255, 0); + expect(yellow.h).toBeCloseTo(1 / 6, 5); + expect(yellow.s).toBe(1); + expect(yellow.v).toBe(255); + + // Cyan + const cyan = rgbToHsv(0, 255, 255); + expect(cyan.h).toBeCloseTo(0.5, 5); + expect(cyan.s).toBe(1); + expect(cyan.v).toBe(255); + }); + + it('should handle decimal input values', () => { + const result = rgbToHsv(127.5, 63.75, 191.25); + expect(typeof result.h).toBe('number'); + expect(typeof result.s).toBe('number'); + expect(typeof result.v).toBe('number'); + expect(result.h).toBeGreaterThanOrEqual(0); + expect(result.h).toBeLessThanOrEqual(1); + expect(result.s).toBeGreaterThanOrEqual(0); + expect(result.s).toBeLessThanOrEqual(1); + }); + + it('should maintain mathematical properties', () => { + // When max === min, saturation should be 0 + const equalValues = rgbToHsv(100, 100, 100); + expect(equalValues.s).toBe(0); + expect(equalValues.h).toBe(0); + expect(equalValues.v).toBe(100); + }); + }); + + describe('#hsvToRgb', () => { + it('should convert HSV primary colors to RGB correctly', () => { + // Red (h=0) + const red = hsvToRgb(0, 1, 255); + expect(red).toEqual({r: 255, g: 0, b: 0}); + + // Green (h=1/3) + const green = hsvToRgb(1 / 3, 1, 255); + expect(green).toEqual({r: 0, g: 255, b: 0}); + + // Blue (h=2/3) + const blue = hsvToRgb(2 / 3, 1, 255); + expect(blue).toEqual({r: 0, g: 0, b: 255}); + }); + + it('should handle grayscale conversions', () => { + // Black + const black = hsvToRgb(0, 0, 0); + expect(black).toEqual({r: 0, g: 0, b: 0}); + + // White + const white = hsvToRgb(0, 0, 255); + expect(white).toEqual({r: 255, g: 255, b: 255}); + + // Gray (any hue with 0 saturation) + const gray = hsvToRgb(0.5, 0, 128); + expect(gray).toEqual({r: 128, g: 128, b: 128}); + }); + + it('should handle secondary colors correctly', () => { + // Yellow (h=1/6) + const yellow = hsvToRgb(1 / 6, 1, 255); + expect(yellow).toEqual({r: 255, g: 255, b: 0}); + + // Magenta (h=5/6) + const magenta = hsvToRgb(5 / 6, 1, 255); + expect(magenta).toEqual({r: 255, g: 0, b: 255}); + + // Cyan (h=1/2) + const cyan = hsvToRgb(0.5, 1, 255); + expect(cyan).toEqual({r: 0, g: 255, b: 255}); + }); + + it('should handle boundary values', () => { + // Maximum hue (should wrap around) + const maxHue = hsvToRgb(1, 1, 255); + expect(maxHue).toEqual({r: 255, g: 0, b: 0}); + }); + + it('should handle different saturation levels', () => { + // Full saturation + const fullSat = hsvToRgb(0, 1, 255); + expect(fullSat).toEqual({r: 255, g: 0, b: 0}); + + // Half saturation + const halfSat = hsvToRgb(0, 0.5, 255); + expect(halfSat).toEqual({r: 255, g: 128, b: 128}); + + // No saturation + const noSat = hsvToRgb(0, 0, 255); + expect(noSat).toEqual({r: 255, g: 255, b: 255}); + }); + + it('should handle different value levels', () => { + // Full value + const fullValue = hsvToRgb(0, 1, 255); + expect(fullValue).toEqual({r: 255, g: 0, b: 0}); + + // Half value + const halfValue = hsvToRgb(0, 1, 127.5); + expect(halfValue).toEqual({r: 128, g: 0, b: 0}); + + // Zero value + const zeroValue = hsvToRgb(0, 1, 0); + expect(zeroValue).toEqual({r: 0, g: 0, b: 0}); + }); + + it('should return rounded integer values', () => { + const result = hsvToRgb(0.1, 0.7, 200.8); + expect(Number.isInteger(result.r)).toBe(true); + expect(Number.isInteger(result.g)).toBe(true); + expect(Number.isInteger(result.b)).toBe(true); + }); + + it('should handle all six sectors of the color wheel', () => { + // Test each sector (i % 6) in the switch statement + const sectors = [ + {h: 0 / 6, expected: 'red-like'}, // sector 0 + {h: 1 / 6, expected: 'yellow-like'}, // sector 1 + {h: 2 / 6, expected: 'green-like'}, // sector 2 + {h: 3 / 6, expected: 'cyan-like'}, // sector 3 + {h: 4 / 6, expected: 'blue-like'}, // sector 4 + {h: 5 / 6, expected: 'magenta-like'}, // sector 5 + ]; + + sectors.forEach(({h}) => { + const result = hsvToRgb(h, 1, 255); + expect(result.r).toBeGreaterThanOrEqual(0); + expect(result.r).toBeLessThanOrEqual(255); + expect(result.g).toBeGreaterThanOrEqual(0); + expect(result.g).toBeLessThanOrEqual(255); + expect(result.b).toBeGreaterThanOrEqual(0); + expect(result.b).toBeLessThanOrEqual(255); + }); + }); + }); + + describe('round-trip conversions', () => { + it('should maintain consistency for RGB→HSV→RGB conversions', () => { + const testColors = [ + {r: 255, g: 0, b: 0}, // Red + {r: 0, g: 255, b: 0}, // Green + {r: 0, g: 0, b: 255}, // Blue + {r: 255, g: 255, b: 0}, // Yellow + {r: 255, g: 0, b: 255}, // Magenta + {r: 0, g: 255, b: 255}, // Cyan + {r: 128, g: 128, b: 128}, // Gray + {r: 200, g: 150, b: 100}, // Brown-ish + ]; + + testColors.forEach(({r, g, b}) => { + const hsv = rgbToHsv(r, g, b); + const backToRgb = hsvToRgb(hsv.h, hsv.s, hsv.v); + + expect(backToRgb.r).toBe(r); + expect(backToRgb.g).toBe(g); + expect(backToRgb.b).toBe(b); + }); + }); + + it('should maintain consistency for HSV→RGB→HSV conversions', () => { + const testColors = [ + {h: 0, s: 1, v: 255}, // Red + {h: 1 / 3, s: 1, v: 255}, // Green + {h: 2 / 3, s: 1, v: 255}, // Blue + {h: 0.5, s: 0.5, v: 200}, // Medium cyan + {h: 0.8, s: 0.7, v: 150}, // Purple-ish + ]; + + testColors.forEach(({h, s, v}) => { + const rgb = hsvToRgb(h, s, v); + const backToHsv = rgbToHsv(rgb.r, rgb.g, rgb.b); + + // Use approximate equality for floating point comparisons + expect(backToHsv.h).toBeCloseTo(h, 5); + expect(backToHsv.s).toBeCloseTo(s, 5); + expect(backToHsv.v).toBeCloseTo(v, 5); + }); + }); + }); +}); diff --git a/packages/atomic/src/utils/date-utils.spec.ts b/packages/atomic/src/utils/date-utils.spec.ts new file mode 100644 index 00000000000..4e5d005511a --- /dev/null +++ b/packages/atomic/src/utils/date-utils.spec.ts @@ -0,0 +1,183 @@ +import {describe, expect, it} from 'vitest'; +import {parseDate, parseTimestampToDateDetails} from './date-utils'; + +describe('date-utils', () => { + describe('#parseDate', () => { + it('should parse valid ISO date string', () => { + const date = parseDate('2025-09-15'); + expect(date.isValid()).toBe(true); + expect(date.year()).toBe(2025); + expect(date.month()).toBe(8); // Months are zero-indexed in dayjs + expect(date.date()).toBe(15); + }); + + it('should parse date with time', () => { + const date = parseDate('2025-09-15T10:30:00'); + expect(date.isValid()).toBe(true); + expect(date.hour()).toBe(10); + expect(date.minute()).toBe(30); + }); + + it('should parse timestamp number', () => { + const timestamp = 1726401600000; // 2024-09-15T12:00:00Z + const date = parseDate(timestamp); + expect(date.isValid()).toBe(true); + }); + + it('should parse Date object', () => { + const dateObj = new Date('2025-09-15'); + const date = parseDate(dateObj); + expect(date.isValid()).toBe(true); + expect(date.year()).toBe(2025); + }); + + it('should handle invalid date strings', () => { + const date = parseDate('invalid-date'); + expect(date.isValid()).toBe(false); + }); + + it('should handle null input', () => { + const date = parseDate(null); + expect(date.isValid()).toBe(false); + }); + + it('should handle undefined input', () => { + const date = parseDate(undefined); + expect(date.isValid()).toBe(true); // dayjs treats undefined as current date/time + }); + + it('should handle empty string', () => { + const date = parseDate(''); + expect(date.isValid()).toBe(false); + }); + }); + + describe('#parseTimestampToDateDetails', () => { + it('should parse valid timestamp into date components', () => { + // Using a known timestamp: January 1, 2024, 12:00:00 UTC + const timestamp = 1704110400000; + const details = parseTimestampToDateDetails(timestamp); + + expect(details.year).toBe(2024); + expect(details.month).toBe('January'); + expect(details.day).toBe(1); + expect(typeof details.dayOfWeek).toBe('string'); + expect(typeof details.hours).toBe('number'); + expect(typeof details.minutes).toBe('number'); + }); + + it('should handle epoch timestamp (0)', () => { + const details = parseTimestampToDateDetails(0); + const expectedDate = new Date(0); + + expect(details.year).toBe(expectedDate.getFullYear()); + expect(details.month).toBe( + [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ][expectedDate.getMonth()] + ); + expect(details.day).toBe(expectedDate.getDate()); + expect(details.dayOfWeek).toBe( + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][expectedDate.getDay()] + ); + expect(typeof details.hours).toBe('number'); + expect(typeof details.minutes).toBe('number'); + }); + + it('should handle large timestamp values', () => { + // Year 2050 + const timestamp = 2524608000000; + const details = parseTimestampToDateDetails(timestamp); + const expectedDate = new Date(timestamp); + + expect(details.year).toBe(expectedDate.getFullYear()); + expect(typeof details.month).toBe('string'); + expect(typeof details.dayOfWeek).toBe('string'); + }); + + it('should return consistent structure for all valid timestamps', () => { + const timestamp = Date.now(); + const details = parseTimestampToDateDetails(timestamp); + + expect(details).toHaveProperty('year'); + expect(details).toHaveProperty('month'); + expect(details).toHaveProperty('dayOfWeek'); + expect(details).toHaveProperty('day'); + expect(details).toHaveProperty('hours'); + expect(details).toHaveProperty('minutes'); + }); + + it('should handle different months correctly', () => { + // December 25, 2024 + const timestamp = 1735084800000; + const details = parseTimestampToDateDetails(timestamp); + const expectedDate = new Date(timestamp); + + expect(details.year).toBe(expectedDate.getFullYear()); + expect(details.month).toBe( + [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ][expectedDate.getMonth()] + ); + expect(details.day).toBe(expectedDate.getDate()); + }); + + it('should handle invalid timestamp gracefully', () => { + const details = parseTimestampToDateDetails(NaN); + + expect(Number.isNaN(details.year)).toBe(true); + expect(details.month).toBeUndefined(); + expect(details.dayOfWeek).toBeUndefined(); + expect(Number.isNaN(details.day)).toBe(true); + expect(Number.isNaN(details.hours)).toBe(true); + expect(Number.isNaN(details.minutes)).toBe(true); + }); + + it('should handle negative timestamp values', () => { + const timestamp = -86400000; // One day before epoch + const details = parseTimestampToDateDetails(timestamp); + const expectedDate = new Date(timestamp); + + expect(details.year).toBe(expectedDate.getFullYear()); + expect(details.month).toBe( + [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ][expectedDate.getMonth()] + ); + expect(details.day).toBe(expectedDate.getDate()); + }); + }); +}); diff --git a/packages/atomic/src/utils/dayjs-locales.spec.ts b/packages/atomic/src/utils/dayjs-locales.spec.ts index d7c5e911464..e4517a27d71 100644 --- a/packages/atomic/src/utils/dayjs-locales.spec.ts +++ b/packages/atomic/src/utils/dayjs-locales.spec.ts @@ -45,7 +45,6 @@ describe('#loadDayjsLocale', () => { beforeEach(() => { vi.useFakeTimers(); Object.values(mockLocales).forEach((fn) => fn.mockReset?.()); - vi.clearAllMocks(); console.warn = vi.fn(); }); diff --git a/packages/atomic/src/utils/device-utils.spec.ts b/packages/atomic/src/utils/device-utils.spec.ts new file mode 100644 index 00000000000..9fab1cbecd8 --- /dev/null +++ b/packages/atomic/src/utils/device-utils.spec.ts @@ -0,0 +1,231 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {hasKeyboard, isIOS, isMacOS} from './device-utils'; + +describe('device-utils', () => { + let originalUserAgent: string; + let originalPlatform: string; + let originalMaxTouchPoints: number; + + beforeEach(() => { + // Store original values + originalUserAgent = navigator.userAgent; + originalPlatform = navigator.platform; + originalMaxTouchPoints = navigator.maxTouchPoints; + }); + + afterEach(() => { + // Restore original values + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: originalUserAgent, + }); + Object.defineProperty(navigator, 'platform', { + writable: true, + value: originalPlatform, + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + value: originalMaxTouchPoints, + }); + vi.restoreAllMocks(); + }); + + describe('#isIOS', () => { + it('should return true for iPad user agent', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (iPad; CPU OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15', + }); + + expect(isIOS()).toBe(true); + }); + + it('should return true for iPhone user agent', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15', + }); + + expect(isIOS()).toBe(true); + }); + + it('should return true for iPod user agent', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (iPod touch; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15', + }); + + expect(isIOS()).toBe(true); + }); + + it('should return true for Macintosh with touch screen', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + value: 5, + }); + + expect(isIOS()).toBe(true); + }); + + it('should return false for Android user agent', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36', + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + value: 0, + }); + + expect(isIOS()).toBe(false); + }); + + it('should return false for Windows user agent', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + value: 0, + }); + + expect(isIOS()).toBe(false); + }); + + it('should return false for Macintosh without touch screen', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + value: 0, + }); + + // Mock Audio constructor and volume behavior + const mockAudio = { + volume: 0.5, + }; + vi.stubGlobal( + 'Audio', + vi.fn(() => mockAudio) + ); + + expect(isIOS()).toBe(false); + }); + + it('should handle iOS quirk for older versions', () => { + Object.defineProperty(navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + value: 0, + }); + + // Mock Audio with iOS quirk (volume stays at 1) + const mockAudio = {}; + Object.defineProperty(mockAudio, 'volume', { + get: () => 1, + set: () => {}, // Volume cannot be changed on iOS 12 and below + }); + vi.stubGlobal( + 'Audio', + vi.fn(() => mockAudio) + ); + + expect(isIOS()).toBe(true); + }); + }); + + describe('#isMacOS', () => { + it('should return true for Mac platform', () => { + Object.defineProperty(navigator, 'platform', { + writable: true, + value: 'MacIntel', + }); + + expect(isMacOS()).toBe(true); + }); + + it('should return true for MacPPC platform', () => { + Object.defineProperty(navigator, 'platform', { + writable: true, + value: 'MacPPC', + }); + + expect(isMacOS()).toBe(true); + }); + + it('should return false for Windows platform', () => { + Object.defineProperty(navigator, 'platform', { + writable: true, + value: 'Win32', + }); + + expect(isMacOS()).toBe(false); + }); + + it('should return false for Linux platform', () => { + Object.defineProperty(navigator, 'platform', { + writable: true, + value: 'Linux x86_64', + }); + + expect(isMacOS()).toBe(false); + }); + + it('should return false for empty platform', () => { + Object.defineProperty(navigator, 'platform', { + writable: true, + value: '', + }); + + expect(isMacOS()).toBe(false); + }); + }); + + describe('#hasKeyboard', () => { + it('should return true when hover is supported', () => { + const mockMatchMedia = vi.fn(() => ({ + matches: true, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })); + vi.stubGlobal('matchMedia', mockMatchMedia); + + expect(hasKeyboard()).toBe(true); + expect(mockMatchMedia).toHaveBeenCalledWith('(any-hover: hover)'); + }); + + it('should return false when hover is not supported', () => { + const mockMatchMedia = vi.fn(() => ({ + matches: false, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })); + vi.stubGlobal('matchMedia', mockMatchMedia); + + expect(hasKeyboard()).toBe(false); + expect(mockMatchMedia).toHaveBeenCalledWith('(any-hover: hover)'); + }); + + it('should handle matchMedia not being available', () => { + vi.stubGlobal('matchMedia', undefined); + + expect(() => hasKeyboard()).toThrow(); + }); + }); +}); diff --git a/packages/atomic/src/utils/dom-utils.spec.ts b/packages/atomic/src/utils/dom-utils.spec.ts new file mode 100644 index 00000000000..792df5ecb58 --- /dev/null +++ b/packages/atomic/src/utils/dom-utils.spec.ts @@ -0,0 +1,280 @@ +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import {closest, parentNodeToString, rectEquals} from './dom-utils'; + +describe('dom-utils', () => { + let container: HTMLElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + describe('#rectEquals', () => { + it('should return true for identical rectangles', () => { + const rect1 = new DOMRect(10, 20, 100, 200); + const rect2 = new DOMRect(10, 20, 100, 200); + + expect(rectEquals(rect1, rect2)).toBe(true); + }); + + it('should return false for rectangles with different x coordinates', () => { + const rect1 = new DOMRect(10, 20, 100, 200); + const rect2 = new DOMRect(15, 20, 100, 200); + + expect(rectEquals(rect1, rect2)).toBe(false); + }); + + it('should return false for rectangles with different y coordinates', () => { + const rect1 = new DOMRect(10, 20, 100, 200); + const rect2 = new DOMRect(10, 25, 100, 200); + + expect(rectEquals(rect1, rect2)).toBe(false); + }); + + it('should return false for rectangles with different width', () => { + const rect1 = new DOMRect(10, 20, 100, 200); + const rect2 = new DOMRect(10, 20, 150, 200); + + expect(rectEquals(rect1, rect2)).toBe(false); + }); + + it('should return false for rectangles with different height', () => { + const rect1 = new DOMRect(10, 20, 100, 200); + const rect2 = new DOMRect(10, 20, 100, 250); + + expect(rectEquals(rect1, rect2)).toBe(false); + }); + + it('should handle zero values correctly', () => { + const rect1 = new DOMRect(0, 0, 0, 0); + const rect2 = new DOMRect(0, 0, 0, 0); + + expect(rectEquals(rect1, rect2)).toBe(true); + }); + + it('should handle negative values correctly', () => { + const rect1 = new DOMRect(-10, -20, 100, 200); + const rect2 = new DOMRect(-10, -20, 100, 200); + + expect(rectEquals(rect1, rect2)).toBe(true); + }); + + it('should handle floating point values correctly', () => { + const rect1 = new DOMRect(10.5, 20.7, 100.3, 200.9); + const rect2 = new DOMRect(10.5, 20.7, 100.3, 200.9); + + expect(rectEquals(rect1, rect2)).toBe(true); + }); + }); + + describe('#parentNodeToString', () => { + it('should return empty string for node with no children', () => { + const emptyDiv = document.createElement('div'); + + expect(parentNodeToString(emptyDiv)).toBe(''); + }); + + it('should return HTML string for node with single child', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + child.textContent = 'Hello'; + parent.appendChild(child); + + expect(parentNodeToString(parent)).toBe('Hello'); + }); + + it('should return concatenated HTML for node with multiple children', () => { + const parent = document.createElement('div'); + + const child1 = document.createElement('span'); + child1.textContent = 'First'; + + const child2 = document.createElement('p'); + child2.textContent = 'Second'; + + parent.appendChild(child1); + parent.appendChild(child2); + + expect(parentNodeToString(parent)).toBe( + 'First

Second

' + ); + }); + + it('should handle children with attributes', () => { + const parent = document.createElement('div'); + const child = document.createElement('a'); + child.href = 'https://example.com'; + child.className = 'link'; + child.textContent = 'Link'; + parent.appendChild(child); + + expect(parentNodeToString(parent)).toBe( + 'Link' + ); + }); + + it('should handle nested children', () => { + const parent = document.createElement('div'); + const child = document.createElement('div'); + const grandchild = document.createElement('span'); + grandchild.textContent = 'Nested'; + child.appendChild(grandchild); + parent.appendChild(child); + + expect(parentNodeToString(parent)).toBe('
Nested
'); + }); + + it('should handle document fragment', () => { + const fragment = document.createDocumentFragment(); + const child1 = document.createElement('span'); + child1.textContent = 'Fragment child 1'; + const child2 = document.createElement('div'); + child2.textContent = 'Fragment child 2'; + + fragment.appendChild(child1); + fragment.appendChild(child2); + + expect(parentNodeToString(fragment)).toBe( + 'Fragment child 1
Fragment child 2
' + ); + }); + }); + + describe('#closest', () => { + it('should return the element itself when it matches the selector', () => { + const div = document.createElement('div'); + div.className = 'target'; + container.appendChild(div); + + expect(closest(div, '.target')).toBe(div); + }); + + it('should return the element itself when both element and parent match selector', () => { + const parent = document.createElement('div'); + parent.className = 'target'; + const child = document.createElement('div'); + child.className = 'target'; + parent.appendChild(child); + container.appendChild(parent); + + expect(closest(child, '.target')).toBe(child); + }); + + it('should return null for null element', () => { + expect(closest(null, '.target')).toBe(null); + }); + + it('should return parent element when it matches selector', () => { + const parent = document.createElement('div'); + parent.className = 'parent'; + const child = document.createElement('span'); + parent.appendChild(child); + container.appendChild(parent); + + expect(closest(child, '.parent')).toBe(parent); + }); + + it('should return ancestor element when it matches selector', () => { + const grandparent = document.createElement('div'); + grandparent.className = 'grandparent'; + const parent = document.createElement('div'); + const child = document.createElement('span'); + + parent.appendChild(child); + grandparent.appendChild(parent); + container.appendChild(grandparent); + + expect(closest(child, '.grandparent')).toBe(grandparent); + }); + + it('should return null when no ancestor matches selector', () => { + const parent = document.createElement('div'); + parent.className = 'parent'; + const child = document.createElement('span'); + parent.appendChild(child); + container.appendChild(parent); + + expect(closest(child, '.nonexistent')).toBe(null); + }); + + it('should work with tag name selectors', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + div.appendChild(span); + container.appendChild(div); + + expect(closest(span, 'div')).toBe(div); + }); + + it('should work with ID selectors', () => { + const parent = document.createElement('div'); + parent.id = 'unique-id'; + const child = document.createElement('span'); + parent.appendChild(child); + container.appendChild(parent); + + expect(closest(child, '#unique-id')).toBe(parent); + }); + + it('should work with attribute selectors', () => { + const parent = document.createElement('div'); + parent.setAttribute('data-test', 'value'); + const child = document.createElement('span'); + parent.appendChild(child); + container.appendChild(parent); + + expect(closest(child, '[data-test="value"]')).toBe(parent); + }); + + it('should handle complex selectors', () => { + const parent = document.createElement('div'); + parent.className = 'parent active'; + parent.setAttribute('data-role', 'container'); + const child = document.createElement('span'); + parent.appendChild(child); + container.appendChild(parent); + + expect(closest(child, 'div.parent.active[data-role="container"]')).toBe( + parent + ); + }); + + it('should traverse through shadow root boundaries', () => { + // Create a custom element with shadow DOM + class TestElement extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + const div = document.createElement('div'); + div.className = 'shadow-content'; + shadow.appendChild(div); + } + } + + customElements.define('test-element', TestElement); + + const hostElement = document.createElement('test-element'); + hostElement.className = 'host'; + container.appendChild(hostElement); + + const shadowContent = + hostElement.shadowRoot!.querySelector('.shadow-content')!; + + expect(closest(shadowContent, '.host')).toBe(hostElement); + }); + + it('should stop at document boundary', () => { + const element = document.createElement('div'); + document.body.appendChild(element); + + // Should not find html element + expect(closest(element, 'body')).toBe(document.body); + + document.body.removeChild(element); + }); + }); +}); diff --git a/packages/atomic/src/utils/initialization-utils.spec.ts b/packages/atomic/src/utils/initialization-utils.spec.ts index 5f84c50eb53..4028062b55b 100644 --- a/packages/atomic/src/utils/initialization-utils.spec.ts +++ b/packages/atomic/src/utils/initialization-utils.spec.ts @@ -19,7 +19,7 @@ import { MissingInterfaceParentError, } from './initialization-utils'; -jest.mock('./replace-breakpoint.ts', () => ({ +jest.mock('./replace-breakpoint-utils.ts', () => ({ ...jest.requireActual('./replace-breakpoint.ts'), updateBreakpoints: () => {}, })); diff --git a/packages/atomic/src/utils/item-section-utils.spec.ts b/packages/atomic/src/utils/item-section-utils.spec.ts new file mode 100644 index 00000000000..2903fe792d5 --- /dev/null +++ b/packages/atomic/src/utils/item-section-utils.spec.ts @@ -0,0 +1,140 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {hideEmptySection} from './item-section-utils'; + +vi.mock('./utils', () => ({ + containsVisualElement: vi.fn(), +})); + +import {containsVisualElement} from './utils'; + +describe('item-section-utils', () => { + let element: HTMLElement; + + beforeEach(() => { + element = document.createElement('div'); + document.body.appendChild(element); + }); + + afterEach(() => { + document.body.removeChild(element); + }); + + describe('#hideEmptySection', () => { + it('should hide element when it contains no visual elements', () => { + vi.mocked(containsVisualElement).mockReturnValue(false); + + hideEmptySection(element); + + expect(element.style.display).toBe('none'); + expect(containsVisualElement).toHaveBeenCalledWith(element); + }); + + it('should show element when it contains visual elements', () => { + vi.mocked(containsVisualElement).mockReturnValue(true); + + hideEmptySection(element); + + expect(element.style.display).toBe(''); + expect(containsVisualElement).toHaveBeenCalledWith(element); + }); + + it('should reset display style when element has visual elements', () => { + element.style.display = 'none'; + vi.mocked(containsVisualElement).mockReturnValue(true); + + hideEmptySection(element); + + expect(element.style.display).toBe(''); + expect(containsVisualElement).toHaveBeenCalledWith(element); + }); + + it('should preserve existing display style when showing element', () => { + element.style.display = 'flex'; + vi.mocked(containsVisualElement).mockReturnValue(false); + + hideEmptySection(element); + + expect(element.style.display).toBe('none'); + + // Now show it again + vi.mocked(containsVisualElement).mockReturnValue(true); + hideEmptySection(element); + + expect(element.style.display).toBe(''); + }); + + it('should handle element with existing inline styles', () => { + element.style.color = 'red'; + element.style.fontSize = '16px'; + vi.mocked(containsVisualElement).mockReturnValue(false); + + hideEmptySection(element); + + expect(element.style.display).toBe('none'); + expect(element.style.color).toBe('red'); + expect(element.style.fontSize).toBe('16px'); + }); + + it('should call containsVisualElement exactly once', () => { + vi.mocked(containsVisualElement).mockReturnValue(true); + + hideEmptySection(element); + + expect(containsVisualElement).toHaveBeenCalledTimes(1); + expect(containsVisualElement).toHaveBeenCalledWith(element); + }); + + it('should work with different types of HTML elements', () => { + const testElements = [ + document.createElement('section'), + document.createElement('article'), + document.createElement('aside'), + document.createElement('header'), + document.createElement('footer'), + ]; + + testElements.forEach((testElement, index) => { + document.body.appendChild(testElement); + vi.mocked(containsVisualElement).mockReturnValue(index % 2 === 0); + + hideEmptySection(testElement); + + if (index % 2 === 0) { + expect(testElement.style.display).toBe(''); + } else { + expect(testElement.style.display).toBe('none'); + } + + document.body.removeChild(testElement); + }); + + expect(containsVisualElement).toHaveBeenCalledTimes(testElements.length); + }); + + it('should handle elements with complex content', () => { + element.innerHTML = ` +
+ Some text + test +
+ `; + + vi.mocked(containsVisualElement).mockReturnValue(true); + + hideEmptySection(element); + + expect(element.style.display).toBe(''); + expect(containsVisualElement).toHaveBeenCalledWith(element); + }); + + it('should handle elements with only whitespace content', () => { + element.innerHTML = ' \n\t '; + vi.mocked(containsVisualElement).mockReturnValue(false); + + hideEmptySection(element); + + expect(element.style.display).toBe('none'); + expect(containsVisualElement).toHaveBeenCalledWith(element); + }); + }); +}); diff --git a/packages/atomic/src/utils/object-utils.spec.ts b/packages/atomic/src/utils/object-utils.spec.ts new file mode 100644 index 00000000000..37ed9a6d433 --- /dev/null +++ b/packages/atomic/src/utils/object-utils.spec.ts @@ -0,0 +1,222 @@ +import {describe, expect, it} from 'vitest'; +import {readFromObject} from './object-utils'; + +describe('object-utils', () => { + describe('#readFromObject', () => { + it('should return string value for simple property', () => { + const obj = {name: 'John'}; + + expect(readFromObject(obj, 'name')).toBe('John'); + }); + + it('should return string value for nested property', () => { + const obj = { + user: { + profile: { + name: 'Jane', + }, + }, + }; + + expect(readFromObject(obj, 'user.profile.name')).toBe('Jane'); + }); + + it('should return undefined for non-existent property', () => { + const obj = {name: 'John'}; + + expect(readFromObject(obj, 'age')).toBeUndefined(); + }); + + it('should return undefined for non-existent nested property', () => { + const obj = { + user: { + name: 'John', + }, + }; + + expect(readFromObject(obj, 'user.profile.name')).toBeUndefined(); + }); + + it('should return undefined when intermediate property is null', () => { + const obj = { + user: null, + }; + + expect(readFromObject(obj, 'user.name')).toBeUndefined(); + }); + + it('should return undefined when intermediate property is undefined', () => { + const obj = { + user: undefined, + }; + + expect(readFromObject(obj, 'user.name')).toBeUndefined(); + }); + + it('should return undefined for non-string values', () => { + const obj = { + count: 42, + isActive: true, + items: ['a', 'b', 'c'], + config: {setting: 'value'}, + }; + + expect(readFromObject(obj, 'count')).toBeUndefined(); + expect(readFromObject(obj, 'isActive')).toBeUndefined(); + expect(readFromObject(obj, 'items')).toBeUndefined(); + expect(readFromObject(obj, 'config')).toBeUndefined(); + }); + + it('should handle empty string values', () => { + const obj = {empty: ''}; + + expect(readFromObject(obj, 'empty')).toBe(''); + }); + + it('should handle string values in nested objects', () => { + const obj = { + level1: { + level2: { + level3: { + value: 'deep value', + }, + }, + }, + }; + + expect(readFromObject(obj, 'level1.level2.level3.value')).toBe( + 'deep value' + ); + }); + + it('should handle mixed data types in path', () => { + const obj = { + user: { + id: 123, + name: 'Alice', + active: true, + metadata: { + role: 'admin', + }, + }, + }; + + expect(readFromObject(obj, 'user.name')).toBe('Alice'); + expect(readFromObject(obj, 'user.metadata.role')).toBe('admin'); + expect(readFromObject(obj, 'user.id')).toBeUndefined(); + expect(readFromObject(obj, 'user.active')).toBeUndefined(); + }); + + it('should handle arrays as intermediate values', () => { + const obj = { + items: ['item1', 'item2'], + users: [{name: 'John'}, {name: 'Jane'}], + }; + + // Arrays are objects with numeric string keys, so this should work + expect(readFromObject(obj, 'items.0')).toBe('item1'); + // The function can traverse into array elements that are objects + expect(readFromObject(obj, 'users.0.name')).toBe('John'); + }); + + it('should handle special characters in property names', () => { + const obj = { + 'special-key': 'value1', + 'key with spaces': 'value2', + }; + + expect(readFromObject(obj, 'special-key')).toBe('value1'); + expect(readFromObject(obj, 'key with spaces')).toBe('value2'); + + // This key contains dots which are used as separators, so it won't work as expected + const objWithDots = { + 'key.with.dots': 'value3', + }; + expect(readFromObject(objWithDots, 'key.with.dots')).toBeUndefined(); + }); + + it('should handle numeric string keys', () => { + const obj = { + '123': 'numeric key', + nested: { + '456': 'nested numeric key', + }, + }; + + expect(readFromObject(obj, '123')).toBe('numeric key'); + expect(readFromObject(obj, 'nested.456')).toBe('nested numeric key'); + }); + + it('should handle empty object', () => { + const obj = {}; + + expect(readFromObject(obj, 'any.key')).toBeUndefined(); + }); + + it('should handle single character keys', () => { + const obj = { + a: { + b: { + c: 'alphabet', + }, + }, + }; + + expect(readFromObject(obj, 'a.b.c')).toBe('alphabet'); + }); + + it('should handle prototype pollution attempts safely', () => { + const obj = { + user: { + name: 'John', + }, + }; + + expect(readFromObject(obj, '__proto__.polluted')).toBeUndefined(); + expect( + readFromObject(obj, 'constructor.prototype.polluted') + ).toBeUndefined(); + }); + + it('should handle objects with toString methods', () => { + const objWithToString = { + toString: () => 'custom toString', + value: 'actual value', + }; + + expect(readFromObject(objWithToString, 'value')).toBe('actual value'); + expect(readFromObject(objWithToString, 'toString')).toBeUndefined(); + }); + + it('should handle deeply nested objects', () => { + const deepObj = { + l1: { + l2: { + l3: { + l4: { + l5: { + l6: { + l7: { + value: 'very deep', + }, + }, + }, + }, + }, + }, + }, + }; + + expect(readFromObject(deepObj, 'l1.l2.l3.l4.l5.l6.l7.value')).toBe( + 'very deep' + ); + }); + + it('should handle objects with null prototype', () => { + const obj = Object.create(null); + obj.key = 'value'; + + expect(readFromObject(obj, 'key')).toBe('value'); + }); + }); +}); diff --git a/packages/atomic/src/utils/promise-utils.spec.ts b/packages/atomic/src/utils/promise-utils.spec.ts new file mode 100644 index 00000000000..38c365cfdee --- /dev/null +++ b/packages/atomic/src/utils/promise-utils.spec.ts @@ -0,0 +1,196 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {promiseTimeout} from './promise-utils'; + +describe('promise-utils', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + describe('#promiseTimeout', () => { + it('should resolve when promise resolves before timeout', async () => { + const fastPromise = Promise.resolve('success'); + + const result = promiseTimeout(fastPromise, 1000); + + await expect(result).resolves.toBeUndefined(); + }); + + it('should resolve when non-promise value is provided', async () => { + const value = 'immediate value'; + + const result = promiseTimeout(value, 1000); + + await expect(result).resolves.toBeUndefined(); + }); + + it('should reject with timeout error when promise takes too long', async () => { + const slowPromise = new Promise((resolve) => { + setTimeout(() => resolve('too late'), 2000); + }); + + const result = promiseTimeout(slowPromise, 1000); + + // Advance time past timeout + vi.advanceTimersByTime(1000); + + await expect(result).rejects.toThrow('Promise timed out.'); + }); + + it('should clear timeout when promise resolves in time', async () => { + const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout'); + const fastPromise = Promise.resolve('quick'); + + const result = promiseTimeout(fastPromise, 1000); + + await expect(result).resolves.toBeUndefined(); + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); + + it('should handle promise rejection before timeout', async () => { + const rejectedPromise = Promise.reject(new Error('Promise failed')); + + const result = promiseTimeout(rejectedPromise, 1000); + + await expect(result).rejects.toThrow('Promise failed'); + }); + + it('should handle zero timeout', async () => { + const promise = new Promise((resolve) => { + setTimeout(() => resolve('delayed'), 100); + }); + + const result = promiseTimeout(promise, 0); + + // Advance time to trigger immediate timeout + vi.advanceTimersByTime(0); + + await expect(result).rejects.toThrow('Promise timed out.'); + }); + + it('should handle negative timeout', async () => { + const promise = Promise.resolve('immediate'); + + const result = promiseTimeout(promise, -100); + + // Negative timeout should still create a timeout that fires immediately + vi.advanceTimersByTime(0); + + // Should either resolve immediately or timeout immediately + await expect(result).resolves.toBeUndefined(); + }); + + it('should handle very large timeout values', async () => { + const promise = Promise.resolve('quick'); + + const result = promiseTimeout(promise, Number.MAX_SAFE_INTEGER); + + await expect(result).resolves.toBeUndefined(); + }); + + it('should handle multiple concurrent timeouts', async () => { + const promise1 = new Promise((resolve) => + setTimeout(() => resolve('p1'), 500) + ); + const promise2 = new Promise((resolve) => + setTimeout(() => resolve('p2'), 1500) + ); + const promise3 = new Promise((resolve) => + setTimeout(() => resolve('p3'), 800) + ); + + const result1 = promiseTimeout(promise1, 1000); + const result2 = promiseTimeout(promise2, 1000); + const result3 = promiseTimeout(promise3, 1000); + + // Advance time to 500ms - promise1 should resolve + vi.advanceTimersByTime(500); + await expect(result1).resolves.toBeUndefined(); + + // Advance time to 1000ms total - promise2 should timeout, promise3 should resolve + vi.advanceTimersByTime(500); + await expect(result2).rejects.toThrow('Promise timed out.'); + await expect(result3).resolves.toBeUndefined(); + }); + + it('should handle promise that resolves exactly at timeout', async () => { + const promise = new Promise((resolve) => { + setTimeout(() => resolve('on time'), 1000); + }); + + const result = promiseTimeout(promise, 1000); + + vi.advanceTimersByTime(1000); + + // This is a race condition - could go either way depending on implementation + // But the function should handle it gracefully + await expect(result).resolves.toBeUndefined(); + }); + + it('should work with async/await pattern', async () => { + const asyncFunction = async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return 'async result'; + }; + + const result = promiseTimeout(asyncFunction(), 1000); + + vi.advanceTimersByTime(500); + + await expect(result).resolves.toBeUndefined(); + }); + + it('should handle promises that throw synchronously', async () => { + const throwingPromise = Promise.resolve().then(() => { + throw new Error('Synchronous error'); + }); + + const result = promiseTimeout(throwingPromise, 1000); + + await expect(result).rejects.toThrow('Synchronous error'); + }); + + it('should properly clean up timeout IDs', async () => { + const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); + const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout'); + + const promise = Promise.resolve('success'); + + await promiseTimeout(promise, 1000); + + expect(setTimeoutSpy).toHaveBeenCalled(); + expect(clearTimeoutSpy).toHaveBeenCalled(); + + const timeoutId = setTimeoutSpy.mock.results[0].value; + expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId); + }); + + it('should handle different promise types', async () => { + const stringPromise = Promise.resolve('string value'); + const numberPromise = Promise.resolve(42); + const objectPromise = Promise.resolve({key: 'value'}); + const arrayPromise = Promise.resolve([1, 2, 3]); + const nullPromise = Promise.resolve(null); + const undefinedPromise = Promise.resolve(undefined); + + await expect( + promiseTimeout(stringPromise, 1000) + ).resolves.toBeUndefined(); + await expect( + promiseTimeout(numberPromise, 1000) + ).resolves.toBeUndefined(); + await expect( + promiseTimeout(objectPromise, 1000) + ).resolves.toBeUndefined(); + await expect(promiseTimeout(arrayPromise, 1000)).resolves.toBeUndefined(); + await expect(promiseTimeout(nullPromise, 1000)).resolves.toBeUndefined(); + await expect( + promiseTimeout(undefinedPromise, 1000) + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/packages/atomic/src/utils/replace-breakpoint-utils.spec.ts b/packages/atomic/src/utils/replace-breakpoint-utils.spec.ts new file mode 100644 index 00000000000..2db630227b3 --- /dev/null +++ b/packages/atomic/src/utils/replace-breakpoint-utils.spec.ts @@ -0,0 +1,237 @@ +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import * as domUtils from './dom-utils'; +import { + DEFAULT_MOBILE_BREAKPOINT, + updateBreakpoints, +} from './replace-breakpoint-utils'; + +vi.mock('./dom-utils', () => ({ + closest: vi.fn(), +})); + +describe('replace-breakpoint-utils', () => { + let mockElement: HTMLElement; + let mockShadowRoot: ShadowRoot; + let mockStyleSheet: CSSStyleSheet; + let mockStyleTag: HTMLStyleElement; + + beforeEach(() => { + mockStyleSheet = { + cssRules: [ + { + cssText: `@media (min-width: ${DEFAULT_MOBILE_BREAKPOINT}) { .test { color: red; } }`, + }, + {cssText: '.other { color: blue; }'}, + ], + replaceSync: vi.fn(), + } as unknown as CSSStyleSheet; + + mockStyleTag = { + textContent: `@media (min-width: ${DEFAULT_MOBILE_BREAKPOINT}) { .mobile { display: block; } }`, + } as HTMLStyleElement; + + mockShadowRoot = { + adoptedStyleSheets: [mockStyleSheet], + querySelector: vi.fn().mockReturnValue(mockStyleTag), + } as unknown as ShadowRoot; + + mockElement = { + shadowRoot: mockShadowRoot, + } as HTMLElement; + }); + + describe('#updateBreakpoints', () => { + it('should do nothing when no layout element is found', () => { + vi.mocked(domUtils.closest).mockReturnValue(null); + + updateBreakpoints(mockElement); + + expect(mockStyleSheet.replaceSync).not.toHaveBeenCalled(); + }); + + it('should do nothing when layout element has no mobileBreakpoint', () => { + const mockLayout = {} as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(mockElement); + + expect(mockStyleSheet.replaceSync).not.toHaveBeenCalled(); + }); + + it('should do nothing when mobileBreakpoint equals DEFAULT_MOBILE_BREAKPOINT', () => { + const mockLayout = { + mobileBreakpoint: DEFAULT_MOBILE_BREAKPOINT, + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(mockElement); + + expect(mockStyleSheet.replaceSync).not.toHaveBeenCalled(); + }); + + it('should replace breakpoints in stylesheet when different mobileBreakpoint is provided', () => { + const customBreakpoint = '768px'; + const mockLayout = { + mobileBreakpoint: customBreakpoint, + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(mockElement); + + expect(mockStyleSheet.replaceSync).toHaveBeenCalledWith( + `@media (width >= ${customBreakpoint}) { .test { color: red; } }.other { color: blue; }` + ); + }); + + it('should replace breakpoints in style tag when different mobileBreakpoint is provided', () => { + const customBreakpoint = '768px'; + const mockLayout = { + mobileBreakpoint: customBreakpoint, + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(mockElement); + + expect(mockStyleTag.textContent).toBe( + `@media (width >= ${customBreakpoint}) { .mobile { display: block; } }` + ); + }); + + it('should handle element with no shadow root', () => { + const elementWithoutShadow = {} as HTMLElement; + const mockLayout = { + mobileBreakpoint: '768px', + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + expect(() => updateBreakpoints(elementWithoutShadow)).not.toThrow(); + }); + + it('should handle shadow root with no adopted stylesheets', () => { + const shadowWithoutStylesheets = { + adoptedStyleSheets: [], + querySelector: vi.fn().mockReturnValue(mockStyleTag), + } as unknown as ShadowRoot; + const elementWithEmptyStylesheets = { + shadowRoot: shadowWithoutStylesheets, + } as HTMLElement; + const mockLayout = { + mobileBreakpoint: '768px', + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(elementWithEmptyStylesheets); + + expect(mockStyleTag.textContent).toBe( + '@media (width >= 768px) { .mobile { display: block; } }' + ); + }); + + it('should handle shadow root with no style tag', () => { + const shadowWithoutStyleTag = { + adoptedStyleSheets: [mockStyleSheet], + querySelector: vi.fn().mockReturnValue(null), + } as unknown as ShadowRoot; + const elementWithoutStyleTag = { + shadowRoot: shadowWithoutStyleTag, + } as HTMLElement; + const mockLayout = { + mobileBreakpoint: '768px', + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(elementWithoutStyleTag); + + expect(mockStyleSheet.replaceSync).toHaveBeenCalled(); + }); + + it('should call closest with layout selectors', () => { + const mockLayout = { + mobileBreakpoint: '768px', + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + updateBreakpoints(mockElement); + + expect(domUtils.closest).toHaveBeenCalledWith( + mockElement, + 'atomic-search-layout, atomic-insight-layout' + ); + }); + + it('should replace both min-width and width >= media queries', () => { + const customBreakpoint = '768px'; + const mockLayout = { + mobileBreakpoint: customBreakpoint, + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + // Test with both query formats + const mockStyleSheetWithBothQueries = { + cssRules: [ + { + cssText: `@media (min-width: ${DEFAULT_MOBILE_BREAKPOINT}) { .test1 { color: red; } }`, + }, + { + cssText: `@media (width >= ${DEFAULT_MOBILE_BREAKPOINT}) { .test2 { color: blue; } }`, + }, + ], + replaceSync: vi.fn(), + } as unknown as CSSStyleSheet; + + mockShadowRoot.adoptedStyleSheets = [mockStyleSheetWithBothQueries]; + + updateBreakpoints(mockElement); + + expect(mockStyleSheetWithBothQueries.replaceSync).toHaveBeenCalledWith( + `@media (width >= ${customBreakpoint}) { .test1 { color: red; } }@media (width >= ${customBreakpoint}) { .test2 { color: blue; } }` + ); + }); + + it('should handle style tag with null textContent', () => { + const styleTagWithNullContent = { + textContent: null, + } as unknown; + const shadowWithNullContent = { + adoptedStyleSheets: [], + querySelector: vi.fn().mockReturnValue(styleTagWithNullContent), + } as unknown as ShadowRoot; + const elementWithNullContent = { + shadowRoot: shadowWithNullContent, + } as HTMLElement; + const mockLayout = { + mobileBreakpoint: '768px', + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + // The implementation uses textContent! so it will throw on null + expect(() => updateBreakpoints(elementWithNullContent)).toThrow(); + }); + + it('should preserve non-matching media queries unchanged', () => { + const customBreakpoint = '768px'; + const mockLayout = { + mobileBreakpoint: customBreakpoint, + } as HTMLElement & {mobileBreakpoint: string}; + vi.mocked(domUtils.closest).mockReturnValue(mockLayout); + + const mockStyleSheetWithMixedQueries = { + cssRules: [ + {cssText: '@media (max-width: 500px) { .small { display: none; } }'}, + { + cssText: `@media (min-width: ${DEFAULT_MOBILE_BREAKPOINT}) { .large { display: block; } }`, + }, + ], + replaceSync: vi.fn(), + } as unknown as CSSStyleSheet; + + mockShadowRoot.adoptedStyleSheets = [mockStyleSheetWithMixedQueries]; + + updateBreakpoints(mockElement); + + expect(mockStyleSheetWithMixedQueries.replaceSync).toHaveBeenCalledWith( + `@media (max-width: 500px) { .small { display: none; } }@media (width >= ${customBreakpoint}) { .large { display: block; } }` + ); + }); + }); +}); diff --git a/packages/atomic/src/utils/replace-breakpoint.ts b/packages/atomic/src/utils/replace-breakpoint-utils.ts similarity index 100% rename from packages/atomic/src/utils/replace-breakpoint.ts rename to packages/atomic/src/utils/replace-breakpoint-utils.ts diff --git a/packages/atomic/src/utils/resource-url-utils.spec.ts b/packages/atomic/src/utils/resource-url-utils.spec.ts new file mode 100644 index 00000000000..c2bb0618fa3 --- /dev/null +++ b/packages/atomic/src/utils/resource-url-utils.spec.ts @@ -0,0 +1,103 @@ +import {beforeEach, describe, expect, it, vi} from 'vitest'; + +vi.mock('./resource-url-utils', async () => { + const actual = await vi.importActual('./resource-url-utils'); + + const mockIsCoveoCDN = vi.fn(); + const mockGetCoveoCdnResourceUrl = vi.fn(); + + const getResourceUrl = () => { + return mockIsCoveoCDN() ? mockGetCoveoCdnResourceUrl() : undefined; + }; + + return { + ...actual, + getResourceUrl, + __mockIsCoveoCDN: mockIsCoveoCDN, + __mockGetCoveoCdnResourceUrl: mockGetCoveoCdnResourceUrl, + }; +}); + +describe('resource-url-utils', () => { + let mockModule: { + getResourceUrl: () => string | undefined; + __mockIsCoveoCDN: ReturnType; + __mockGetCoveoCdnResourceUrl: ReturnType; + }; + + beforeEach(async () => { + mockModule = (await import('./resource-url-utils')) as typeof mockModule; + mockModule.__mockGetCoveoCdnResourceUrl.mockReturnValue( + './mocked-resource-path/' + ); + }); + + describe('#getResourceUrl', () => { + it('should return CDN resource URL when isCoveoCDN returns true', () => { + mockModule.__mockIsCoveoCDN.mockReturnValue(true); + + const result = mockModule.getResourceUrl(); + + expect(result).toBe('./mocked-resource-path/'); + expect(mockModule.__mockGetCoveoCdnResourceUrl).toHaveBeenCalled(); + }); + + it('should return undefined when isCoveoCDN returns false', () => { + mockModule.__mockIsCoveoCDN.mockReturnValue(false); + + const result = mockModule.getResourceUrl(); + + expect(result).toBeUndefined(); + expect(mockModule.__mockGetCoveoCdnResourceUrl).not.toHaveBeenCalled(); + }); + + it('should call isCoveoCDN to determine if current origin is a CDN', () => { + mockModule.__mockIsCoveoCDN.mockReturnValue(false); + + mockModule.getResourceUrl(); + + expect(mockModule.__mockIsCoveoCDN).toHaveBeenCalled(); + }); + + it('should return CDN resource URL when CDN check passes', () => { + mockModule.__mockIsCoveoCDN.mockReturnValue(true); + mockModule.__mockGetCoveoCdnResourceUrl.mockReturnValue('./custom-path/'); + + const result = mockModule.getResourceUrl(); + + expect(result).toBe('./custom-path/'); + }); + + it('should handle different CDN resource URLs', () => { + const testPaths = ['./path1/', './assets/', './resources/v2/', undefined]; + + for (const path of testPaths) { + mockModule.__mockIsCoveoCDN.mockReturnValue(!!path); + mockModule.__mockGetCoveoCdnResourceUrl.mockReturnValue(path); + + const result = mockModule.getResourceUrl(); + + expect(result).toBe(path || undefined); + } + }); + + it('should maintain consistent behavior across multiple calls', () => { + mockModule.__mockIsCoveoCDN.mockReturnValue(true); + + const result1 = mockModule.getResourceUrl(); + const result2 = mockModule.getResourceUrl(); + + expect(result1).toBe(result2); + expect(result1).toBe('./mocked-resource-path/'); + }); + + it('should handle edge cases with empty or falsy return values', () => { + mockModule.__mockIsCoveoCDN.mockReturnValue(true); + mockModule.__mockGetCoveoCdnResourceUrl.mockReturnValue(''); + + const result = mockModule.getResourceUrl(); + + expect(result).toBe(''); + }); + }); +}); diff --git a/packages/atomic/src/utils/resource-url.ts b/packages/atomic/src/utils/resource-url-utils.ts similarity index 100% rename from packages/atomic/src/utils/resource-url.ts rename to packages/atomic/src/utils/resource-url-utils.ts diff --git a/packages/atomic/src/utils/ripple-utils.spec.ts b/packages/atomic/src/utils/ripple-utils.spec.ts new file mode 100644 index 00000000000..fcb5247aca1 --- /dev/null +++ b/packages/atomic/src/utils/ripple-utils.spec.ts @@ -0,0 +1,344 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import * as eventUtils from './event-utils'; +import {createRipple} from './ripple-utils'; + +vi.mock('./event-utils', () => ({ + listenOnce: vi.fn(), +})); + +describe('ripple-utils', () => { + let mockButton: HTMLElement; + let mockEvent: MouseEvent; + + beforeEach(() => { + vi.clearAllTimers(); + vi.useFakeTimers(); + + mockButton = document.createElement('button'); + Object.defineProperties(mockButton, { + clientWidth: {value: 100, writable: true, configurable: true}, + clientHeight: {value: 80, writable: true, configurable: true}, + getBoundingClientRect: { + value: vi.fn().mockReturnValue({ + top: 50, + left: 100, + width: 100, + height: 80, + }), + writable: true, + configurable: true, + }, + }); + document.body.appendChild(mockButton); + + mockEvent = { + currentTarget: mockButton, + clientX: 150, + clientY: 90, + } as unknown as MouseEvent; + + Object.defineProperty(globalThis, 'getComputedStyle', { + value: vi.fn().mockReturnValue({ + position: 'static', + }), + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + vi.useRealTimers(); + document.body.innerHTML = ''; + }); + + describe('#createRipple', () => { + it('should create a ripple element when called', async () => { + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple'); + expect(ripple).toBeTruthy(); + expect(ripple?.tagName).toBe('SPAN'); + expect(ripple?.classList.contains('ripple')).toBe(true); + + // Fast-forward timers to complete the animation + vi.runAllTimers(); + await ripplePromise; + }); + + it('should remove existing ripple before creating new one', async () => { + const options = {color: 'primary'}; + + const firstRipplePromise = createRipple(mockEvent, options); + const firstRipple = mockButton.querySelector('.ripple'); + expect(firstRipple).toBeTruthy(); + + const secondRipplePromise = createRipple(mockEvent, options); + const ripples = mockButton.querySelectorAll('.ripple'); + expect(ripples.length).toBe(1); + + vi.runAllTimers(); + await Promise.all([firstRipplePromise, secondRipplePromise]); + }); + + it('should add ripple-parent class to button', async () => { + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + expect(mockButton.classList.contains('ripple-parent')).toBe(true); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should add ripple-relative class when position is static', async () => { + const options = {color: 'primary'}; + Object.defineProperty(globalThis, 'getComputedStyle', { + value: vi.fn().mockReturnValue({ + position: 'static', + }), + writable: true, + configurable: true, + }); + + const ripplePromise = createRipple(mockEvent, options); + + expect(mockButton.classList.contains('ripple-relative')).toBe(true); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should not add ripple-relative class when position is not static', async () => { + const options = {color: 'primary'}; + Object.defineProperty(globalThis, 'getComputedStyle', { + value: vi.fn().mockReturnValue({ + position: 'relative', + }), + writable: true, + configurable: true, + }); + + const ripplePromise = createRipple(mockEvent, options); + + expect(mockButton.classList.contains('ripple-relative')).toBe(false); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should add ripple-relative class to child elements with static position', async () => { + const child = document.createElement('span'); + mockButton.appendChild(child); + + Object.defineProperty(globalThis, 'getComputedStyle', { + value: vi.fn().mockImplementation((element) => ({ + position: element === child ? 'static' : 'relative', + })), + writable: true, + configurable: true, + }); + + const options = {color: 'primary'}; + const ripplePromise = createRipple(mockEvent, options); + + expect(child.classList.contains('ripple-relative')).toBe(true); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should set correct ripple styles', async () => { + const options = {color: 'secondary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple') as HTMLElement; + expect(ripple?.style.backgroundColor).toBe('var(--atomic-secondary)'); + expect(ripple?.getAttribute('part')).toBe('ripple'); + + // Diameter should be max of width and height (100px) + expect(ripple?.style.width).toBe('100px'); + expect(ripple?.style.height).toBe('100px'); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should position ripple correctly based on click coordinates', async () => { + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple') as HTMLElement; + + // Click at (150, 90), button at (100, 50), radius = 50 + // Left: 150 - (100 + 50) = 0 + // Top: 90 - (50 + 50) = -10 + expect(ripple?.style.left).toBe('0px'); + expect(ripple?.style.top).toBe('-10px'); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should use parent element when provided in options', async () => { + const parentElement = document.createElement('div'); + Object.defineProperties(parentElement, { + clientWidth: {value: 200, writable: true, configurable: true}, + clientHeight: {value: 150, writable: true, configurable: true}, + getBoundingClientRect: { + value: vi.fn().mockReturnValue({ + top: 0, + left: 0, + width: 200, + height: 150, + }), + writable: true, + configurable: true, + }, + }); + document.body.appendChild(parentElement); + + const options = {color: 'primary', parent: parentElement}; + + const ripplePromise = createRipple(mockEvent, options); + + expect(parentElement.classList.contains('ripple-parent')).toBe(true); + expect(parentElement.querySelector('.ripple')).toBeTruthy(); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should calculate animation duration based on radius', async () => { + Object.defineProperties(mockButton, { + clientWidth: {value: 318, writable: true, configurable: true}, + clientHeight: {value: 318, writable: true, configurable: true}, + }); + + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple') as HTMLElement; + + // Diameter = 318, radius = 159 + // Duration = Math.cbrt(159) * 129.21 ≈ 700ms + const expectedDuration = Math.cbrt(159) * 129.21; + expect(ripple?.style.getPropertyValue('--animation-duration')).toBe( + `${expectedDuration}ms` + ); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should set up event listener for animationend', async () => { + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + expect(eventUtils.listenOnce).toHaveBeenCalled(); + const [element, eventType] = vi.mocked(eventUtils.listenOnce).mock + .calls[0]; + expect(element).toBe(mockButton.querySelector('.ripple')); + expect(eventType).toBe('animationend'); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should prepend ripple to button', async () => { + const existingChild = document.createElement('span'); + existingChild.textContent = 'Button Text'; + mockButton.appendChild(existingChild); + + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + expect(mockButton.firstChild).toBe(mockButton.querySelector('.ripple')); + + vi.runAllTimers(); + await ripplePromise; + }); + + it("should handle cleanup with timeout even if animation doesn't end", async () => { + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple'); + expect(ripple).toBeTruthy(); + + vi.runAllTimers(); + + await ripplePromise; + }); + + it('should use diameter as max of width and height', async () => { + // Set different width and height + Object.defineProperties(mockButton, { + clientWidth: {value: 80, writable: true, configurable: true}, + clientHeight: {value: 120, writable: true, configurable: true}, + }); + + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple') as HTMLElement; + + // Diameter should be max(80, 120) = 120 + expect(ripple?.style.width).toBe('120px'); + expect(ripple?.style.height).toBe('120px'); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should handle edge case with zero dimensions', async () => { + Object.defineProperties(mockButton, { + clientWidth: {value: 0, writable: true, configurable: true}, + clientHeight: {value: 0, writable: true, configurable: true}, + }); + + const options = {color: 'primary'}; + + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple') as HTMLElement; + expect(ripple?.style.width).toBe('0px'); + expect(ripple?.style.height).toBe('0px'); + + vi.runAllTimers(); + await ripplePromise; + }); + + it('should handle different color options correctly', async () => { + const testColors = [ + 'primary', + 'secondary', + 'success', + 'warning', + 'error', + ]; + + for (const color of testColors) { + const options = {color}; + const ripplePromise = createRipple(mockEvent, options); + + const ripple = mockButton.querySelector('.ripple') as HTMLElement; + expect(ripple?.style.backgroundColor).toBe(`var(--atomic-${color})`); + + vi.runAllTimers(); + await ripplePromise; + + ripple?.remove(); + } + }); + }); +}); diff --git a/packages/atomic/src/utils/ripple.ts b/packages/atomic/src/utils/ripple-utils.ts similarity index 100% rename from packages/atomic/src/utils/ripple.ts rename to packages/atomic/src/utils/ripple-utils.ts diff --git a/packages/auth/package.json b/packages/auth/package.json index 30ad35415ed..95a162e48d1 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -43,8 +43,8 @@ "devDependencies": { "jest": "29.7.0", "rimraf": "6.0.1", - "ts-jest": "29.4.0", - "vite": "7.0.6" + "ts-jest": "29.4.1", + "vite": "7.1.5" }, "engines": { "node": "^20.9.0 || ^22.11.0" diff --git a/packages/create-atomic-component-project/template/package.json b/packages/create-atomic-component-project/template/package.json index 3c67fdf522d..1b0bad74201 100644 --- a/packages/create-atomic-component-project/template/package.json +++ b/packages/create-atomic-component-project/template/package.json @@ -20,7 +20,7 @@ "@types/jest": "29.5.14", "jest": "29.7.0", "jest-cli": "29.7.0", - "puppeteer": "24.15.0", + "puppeteer": "24.20.0", "rollup-plugin-html": "0.2.1", "rimraf": "6.0.1", "@coveo/create-atomic-rollup-plugin": "1.2.0" diff --git a/packages/create-atomic-component/template/src/components/sample-component/package.json b/packages/create-atomic-component/template/src/components/sample-component/package.json index fc4850f3b2a..1badacdc5a0 100644 --- a/packages/create-atomic-component/template/src/components/sample-component/package.json +++ b/packages/create-atomic-component/template/src/components/sample-component/package.json @@ -33,7 +33,7 @@ "@types/jest": "29.5.14", "jest": "29.7.0", "jest-cli": "29.7.0", - "puppeteer": "24.15.0", + "puppeteer": "24.20.0", "rollup-plugin-html": "0.2.1", "rimraf": "6.0.1", "@coveo/create-atomic-rollup-plugin": "1.2.0" diff --git a/packages/create-atomic-rollup-plugin/package.json b/packages/create-atomic-rollup-plugin/package.json index a2fea0acbdf..e66f1c12a87 100644 --- a/packages/create-atomic-rollup-plugin/package.json +++ b/packages/create-atomic-rollup-plugin/package.json @@ -35,6 +35,6 @@ "directory": "packages/create-atomic-rollup-plugin" }, "devDependencies": { - "typescript": "5.8.3" + "typescript": "5.9.2" } } diff --git a/packages/create-atomic-template/package.json b/packages/create-atomic-template/package.json index 14f7b9e1cd0..d7aa1d3bf68 100644 --- a/packages/create-atomic-template/package.json +++ b/packages/create-atomic-template/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@coveo/create-atomic-rollup-plugin": "1.2.0", "@rollup/plugin-replace": "5.0.2", - "esbuild": "0.25.8", + "esbuild": "0.25.9", "gts": "3.1.1", "prettier": "2.8.8", "rollup-plugin-dotenv": "0.5.1", diff --git a/packages/create-atomic/package.json b/packages/create-atomic/package.json index 53155ca609f..ff7353ecc8e 100644 --- a/packages/create-atomic/package.json +++ b/packages/create-atomic/package.json @@ -44,10 +44,10 @@ "isomorphic-fetch": "3.0.0", "minimist": "1.2.8", "node-plop": "^0.32.0", - "plop": "4.0.1" + "plop": "4.0.2" }, "devDependencies": { - "fs-extra": "11.3.0", - "typescript": "5.8.3" + "fs-extra": "11.3.1", + "typescript": "5.9.2" } } diff --git a/packages/headless-react/package.json b/packages/headless-react/package.json index 0f3dbe2e9ab..53299810363 100644 --- a/packages/headless-react/package.json +++ b/packages/headless-react/package.json @@ -47,7 +47,7 @@ "@testing-library/react": "16.3.0", "live-server": "1.2.2", "rimraf": "6.0.1", - "typescript": "5.8.3", + "typescript": "5.9.2", "typedoc": "0.28.7", "vitest": "3.2.4", "@coveo/documentation": "1.0.0" diff --git a/packages/headless/package.json b/packages/headless/package.json index fe1fcb7d6b8..bd4ec7928de 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -111,22 +111,22 @@ "dependencies": { "@coveo/bueno": "1.1.2", "@coveo/relay": "1.2.7", - "@coveo/relay-event-types": "15.1.0", + "@coveo/relay-event-types": "15.3.0", "@reduxjs/toolkit": "2.6.0", "abortcontroller-polyfill": "1.7.8", "coveo.analytics": "2.30.45", - "dayjs": "1.11.13", + "dayjs": "1.11.18", "exponential-backoff": "3.1.2", "fast-equals": "5.2.2", "headers-polyfill": "4.0.3", "navigator.sendbeacon": "0.0.20", - "pino": "9.7.0", + "pino": "9.9.5", "redux-thunk": "3.1.0", "ts-debounce": "4.0.0" }, "devDependencies": { "@coveo/documentation": "1.0.0", - "chalk": "5.4.1", + "chalk": "5.6.2", "live-server": "1.2.2", "typedoc": "0.28.7", "vitest": "3.2.4" diff --git a/packages/quantic/package.json b/packages/quantic/package.json index 64a7c58a4a8..32d21fb6213 100644 --- a/packages/quantic/package.json +++ b/packages/quantic/package.json @@ -50,30 +50,30 @@ "@coveo/bueno": "1.1.2", "@coveo/headless": "3.30.2", "dompurify": "3.2.6", - "fs-extra": "11.3.0", + "fs-extra": "11.3.1", "marked": "12.0.2" }, "engines": { "node": "^20.9.0 || ^22.11.0" }, "devDependencies": { - "@babel/cli": "7.28.0", + "@babel/cli": "7.28.3", "@babel/plugin-transform-async-to-generator": "7.27.1", "@coveo/ci": "1.0.0", - "@coveo/relay-event-types": "15.1.0", + "@coveo/relay-event-types": "15.3.0", "@octokit/graphql": "9.0.1", "@octokit/graphql-schema": "15.26.0", - "@playwright/test": "1.54.1", - "@salesforce-ux/slds-linter": "0.3.0", + "@playwright/test": "1.55.0", + "@salesforce-ux/slds-linter": "0.5.3", "@salesforce/eslint-config-lwc": "3.7.2", "@salesforce/sfdx-lwc-jest": "5.1.0", - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/strip-color": "0.1.2", "@types/wait-on": "5.3.4", "@typescript-eslint/parser": "8.44.0", "chalk": "4.1.2", "change-case": "4.1.2", - "dotenv": "17.2.1", + "dotenv": "17.2.2", "eslint": "8.57", "eslint-config-prettier": "10.1.8", "jest": "29.7.0", diff --git a/samples/atomic/search-commerce-angular/package.json b/samples/atomic/search-commerce-angular/package.json index b5b633ac192..ac2fd069838 100644 --- a/samples/atomic/search-commerce-angular/package.json +++ b/samples/atomic/search-commerce-angular/package.json @@ -28,8 +28,8 @@ "@angular/build": "19.2.3", "@angular/cli": "19.2.3", "@angular/compiler-cli": "19.2.2", - "@playwright/test": "1.54.1", - "@types/node": "22.16.5", - "typescript": "5.8.3" + "@playwright/test": "1.55.0", + "@types/node": "22.18.2", + "typescript": "5.9.2" } } diff --git a/samples/atomic/search-commerce-react/package.json b/samples/atomic/search-commerce-react/package.json index 0ea4836200d..f0bf65e3247 100644 --- a/samples/atomic/search-commerce-react/package.json +++ b/samples/atomic/search-commerce-react/package.json @@ -18,13 +18,13 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@playwright/test": "1.54.1", - "@types/node": "22.16.5", + "@playwright/test": "1.55.0", + "@types/node": "22.18.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@vitejs/plugin-react": "4.7.0", "ncp": "2.0.0", - "typescript": "5.8.3", - "vite": "7.0.6" + "typescript": "5.9.2", + "vite": "7.1.5" } } diff --git a/samples/atomic/search-nextjs-app-router/package.json b/samples/atomic/search-nextjs-app-router/package.json index 53328323281..f0181f7da21 100644 --- a/samples/atomic/search-nextjs-app-router/package.json +++ b/samples/atomic/search-nextjs-app-router/package.json @@ -18,8 +18,8 @@ "@coveo/headless": "3.30.2" }, "devDependencies": { - "typescript": "5.8.3", - "@types/node": "22.16.5", + "typescript": "5.9.2", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", "ncp": "2.0.0" diff --git a/samples/atomic/search-nextjs-pages-router/package.json b/samples/atomic/search-nextjs-pages-router/package.json index 442eb201a8b..312852b77f3 100644 --- a/samples/atomic/search-nextjs-pages-router/package.json +++ b/samples/atomic/search-nextjs-pages-router/package.json @@ -12,12 +12,12 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", "cypress": "13.7.3", "ncp": "2.0.0", - "typescript": "5.8.3" + "typescript": "5.9.2" }, "scripts": { "build": "npm run build:next && npm run build:ts-esm && npm run build:ts-cjs", diff --git a/samples/atomic/search-vuejs/package.json b/samples/atomic/search-vuejs/package.json index 95646177d33..a9f2092a039 100644 --- a/samples/atomic/search-vuejs/package.json +++ b/samples/atomic/search-vuejs/package.json @@ -13,13 +13,13 @@ }, "dependencies": { "@coveo/atomic": "3.33.6", - "vue": "3.5.18" + "vue": "3.5.21" }, "devDependencies": { "@vitejs/plugin-vue": "6.0.1", "cypress": "13.7.3", "ncp": "2.0.0", - "typescript": "5.8.3", - "vite": "7.0.6" + "typescript": "5.9.2", + "vite": "7.1.5" } } diff --git a/samples/headless-ssr/commerce-express/package.json b/samples/headless-ssr/commerce-express/package.json index b9d7291dba4..1679127e285 100644 --- a/samples/headless-ssr/commerce-express/package.json +++ b/samples/headless-ssr/commerce-express/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@playwright/test": "^1.40.0", "@types/express": "^4.17.17", - "@types/node": "^20.0.0", + "@types/node": "^22.18.2", "esbuild": "^0.25.9", "tsx": "^4.7.0", "typescript": "^5.3.0" diff --git a/samples/headless-ssr/commerce-nextjs/package.json b/samples/headless-ssr/commerce-nextjs/package.json index 9869d2dbbca..cf0750129c4 100644 --- a/samples/headless-ssr/commerce-nextjs/package.json +++ b/samples/headless-ssr/commerce-nextjs/package.json @@ -16,10 +16,10 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", - "@playwright/test": "1.54.1", - "typescript": "5.8.3" + "@playwright/test": "1.55.0", + "typescript": "5.9.2" } } diff --git a/samples/headless-ssr/commerce-react-router/package.json b/samples/headless-ssr/commerce-react-router/package.json index f0c42a7118f..a3a687c7c3c 100644 --- a/samples/headless-ssr/commerce-react-router/package.json +++ b/samples/headless-ssr/commerce-react-router/package.json @@ -20,12 +20,12 @@ "tiny-invariant": "1.3.3" }, "devDependencies": { - "@coveo/relay-event-types": "15.1.0", + "@coveo/relay-event-types": "15.3.0", "@react-router/dev": "7.8.2", "@types/react": "18.3.23", "@types/react-dom": "18.3.7", - "typescript": "5.8.3", - "vite": "7.0.6", + "typescript": "5.9.2", + "vite": "7.1.5", "vite-tsconfig-paths": "5.1.4" } } diff --git a/samples/headless-ssr/search-nextjs/app-router/package.json b/samples/headless-ssr/search-nextjs/app-router/package.json index fcb137c0056..dad50bc83d3 100644 --- a/samples/headless-ssr/search-nextjs/app-router/package.json +++ b/samples/headless-ssr/search-nextjs/app-router/package.json @@ -18,9 +18,9 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", - "typescript": "5.8.3" + "typescript": "5.9.2" } } diff --git a/samples/headless-ssr/search-nextjs/package.json b/samples/headless-ssr/search-nextjs/package.json index f19f32d45fd..a2f91450e4b 100644 --- a/samples/headless-ssr/search-nextjs/package.json +++ b/samples/headless-ssr/search-nextjs/package.json @@ -15,11 +15,11 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", "cypress": "13.7.3", "cypress-web-vitals": "4.2.0", - "typescript": "5.8.3" + "typescript": "5.9.2" } } diff --git a/samples/headless-ssr/search-nextjs/pages-router/package.json b/samples/headless-ssr/search-nextjs/pages-router/package.json index 4f0b0128303..af81609689a 100644 --- a/samples/headless-ssr/search-nextjs/pages-router/package.json +++ b/samples/headless-ssr/search-nextjs/pages-router/package.json @@ -18,9 +18,9 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", - "typescript": "5.8.3" + "typescript": "5.9.2" } } diff --git a/samples/headless/commerce-react/package.json b/samples/headless/commerce-react/package.json index f01e2fa68dd..44e3912d80e 100644 --- a/samples/headless/commerce-react/package.json +++ b/samples/headless/commerce-react/package.json @@ -27,12 +27,12 @@ ] }, "devDependencies": { - "@playwright/test": "1.54.1", - "@testing-library/jest-dom": "6.6.3", + "@playwright/test": "1.55.0", + "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", "@vitejs/plugin-react": "4.7.0", - "vite": "7.0.6", - "vite-plugin-static-copy": "3.1.1", + "vite": "7.1.5", + "vite-plugin-static-copy": "3.1.2", "vitest": "3.2.4" } } diff --git a/samples/headless/search-react/package.json b/samples/headless/search-react/package.json index 3e5977507ab..53bdc7034a5 100644 --- a/samples/headless/search-react/package.json +++ b/samples/headless/search-react/package.json @@ -10,20 +10,20 @@ "dependencies": { "@coveo/auth": "2.1.0", "@coveo/headless": "3.30.2", - "@testing-library/jest-dom": "6.6.3", + "@testing-library/jest-dom": "6.8.0", "@testing-library/react": "16.3.0", "@types/escape-html": "1.0.4", "@types/express": "5.0.3", - "@types/node": "22.16.5", + "@types/node": "22.18.2", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", - "dayjs": "1.11.13", + "dayjs": "1.11.18", "escape-html": "1.0.3", "express": "5.1.0", "react": "19.1.1", "react-dom": "19.1.1", "react-router": "7.8.2", - "typescript": "5.8.3" + "typescript": "5.9.2" }, "scripts": { "dev": "vite", @@ -47,7 +47,7 @@ "devDependencies": { "@vitejs/plugin-react": "4.7.0", "cypress": "13.7.3", - "vite": "7.0.6", + "vite": "7.1.5", "vitest": "3.2.4" } } diff --git a/utils/cdn/package.json b/utils/cdn/package.json index d69086a6dad..60489af52fa 100644 --- a/utils/cdn/package.json +++ b/utils/cdn/package.json @@ -13,6 +13,6 @@ }, "devDependencies": { "ncp": "2.0.0", - "chalk": "5.4.1" + "chalk": "5.6.2" } } diff --git a/utils/ci/package.json b/utils/ci/package.json index fa10bbe7728..792ac495325 100644 --- a/utils/ci/package.json +++ b/utils/ci/package.json @@ -5,7 +5,7 @@ "version": "1.0.0", "type": "module", "dependencies": { - "@coveo/semantic-monorepo-tools": "2.6.11", + "@coveo/semantic-monorepo-tools": "2.6.14", "@npmcli/arborist": "9.1.4", "conventional-changelog-conventionalcommits": "8.0.0", "dependency-graph": "1.0.0", @@ -16,7 +16,7 @@ "devDependencies": { "@types/conventional-changelog-writer": "4.0.10", "@types/npmcli__arborist": "6.3.1", - "typescript": "5.8.3" + "typescript": "5.9.2" }, "scripts": { "add-generated-files": "node ./add-generated-files.mjs",