diff --git a/.cursor/rules/test-driven-development.mdc b/.cursor/rules/test-driven-development.mdc index fb03801a9c..dde07f4f43 100644 --- a/.cursor/rules/test-driven-development.mdc +++ b/.cursor/rules/test-driven-development.mdc @@ -2,8 +2,8 @@ description: Standards for Test-Driven Development in the Browser SDK project globs: tags: - - jasmine - - karma + - vitest + - browser-mode alwaysApply: true --- @@ -29,9 +29,11 @@ touch packages/some-package/src/path/to/feature.spec.ts **Spec file must exist BEFORE implementation file** -## 3. Spec Structure (Jasmine/Karma) +## 3. Spec Structure (Vitest Browser Mode) ```typescript +import { beforeEach, describe, expect, it, vi } from 'vitest' + describe('FeatureName', () => { // Setup variables let feature: FeatureType @@ -39,7 +41,7 @@ let mockDependency: MockType beforeEach(() => { // Setup for each test - mockDependency = jasmine.createSpy() + mockDependency = vi.fn() feature = new FeatureName(mockDependency) // Use registerCleanupTask for cleanup diff --git a/.cursor/rules/unit-test-best-practices.mdc b/.cursor/rules/unit-test-best-practices.mdc index 8cefa3b4f6..1d59c361c7 100644 --- a/.cursor/rules/unit-test-best-practices.mdc +++ b/.cursor/rules/unit-test-best-practices.mdc @@ -9,7 +9,7 @@ alwaysApply: false - **Run all unit tests**: `yarn test:unit` - **Run a specific spec file**: `yarn test:unit --spec ` -- **Focus on specific tests**: Prepend `f` to `describe` and `it` (e.g., `fdescribe`, `fit`) +- **Focus on specific tests**: Use `describe.only` or `it.only` temporarily ### Examples @@ -33,7 +33,7 @@ yarn test:unit --spec "packages/browser-core/**/browser/*.spec.ts" #### Focus on Specific Tests ```typescript // Run only this describe block -fdescribe('addEventListener', () => { +describe.only('addEventListener', () => { // All tests in this block will run it('should add event listener', () => {}) it('should remove event listener', () => {}) @@ -41,7 +41,7 @@ fdescribe('addEventListener', () => { // Run only this single test describe('addEventListener', () => { - fit('should add event listener', () => { + it.only('should add event listener', () => { // Only this test will run }) @@ -64,24 +64,10 @@ describe('addEventListener', () => { ```bash # 1. Check test output for randomization seed yarn test:unit -# Look for: "Randomized with seed 65781" - -# 2. Set seed in karma configuration -# Edit test/unit/karma.base.conf.js: -module.exports = { - // ... other config - client: { - jasmine: { - // ... other jasmine config - seed: 65781 // Use the seed from step 1 - } - } -} - -# 3. Run tests with the same order -yarn test:unit +# Look for: Running tests with seed "65781" -# 4. Remove seed after debugging +# 2. Run tests with the same order +yarn test:unit --seed 65781 ``` ### File Organization @@ -169,7 +155,7 @@ beforeEach(() => { clock = mockClock() }) it('should handle delayed operations', () => { - const callback = jasmine.createSpy() + const callback = vi.fn() setTimeout(callback, 1000) clock.tick(1000) // Instant! diff --git a/.github/dependabot.yml b/.github/dependabot.yml index af8c9271f7..0863a747a4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,6 +17,3 @@ updates: open-pull-requests-limit: 0 commit-message: prefix: 'πŸ‘·' - ignore: - # update karma-webpack: RUM-3130 - - dependency-name: 'karma-webpack' diff --git a/.gitignore b/.gitignore index e6eb5498fc..c6816a497d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ test/e2e/.pinned-browsers/ *.local.md .claude/settings.local.json .claude/worktrees/ +**/__screenshots__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 916cbcf42b..641f243cdb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -231,6 +231,7 @@ unit: junit: test-report/unit/*.xml script: - yarn + - yarn playwright install chromium --with-deps - yarn test:unit after_script: - node ./scripts/test/export-test-result.ts unit @@ -314,13 +315,14 @@ unit-bs: interruptible: true parallel: matrix: - - BS_BROWSER: [edge, firefox, safari-desktop, chrome-desktop, chrome-mobile] + - BS_BROWSER: [edge, firefox, webkit-desktop, chrome-desktop] artifacts: + when: always reports: junit: test-report/unit-bs/*.xml script: - yarn - - node scripts/test/ci-bs.ts test:unit + - FORCE_COLOR=1 node scripts/test/ci-bs.ts test:unit after_script: - node ./scripts/test/export-test-result.ts unit-bs diff --git a/AGENTS.md b/AGENTS.md index 9ba815e04a..5ef279f2a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,7 +76,7 @@ test/ β”œβ”€β”€ apps/ # Test apps for E2E and performance testing β”œβ”€β”€ e2e/ # Playwright E2E test scenarios β”œβ”€β”€ performance/ # Performance benchmarking tests -└── unit/ # Karma/Jasmine unit test configuration +└── unit/ # Vitest Browser Mode unit test configuration scripts/ # Build, deploy, release automation ``` @@ -97,8 +97,8 @@ For deeper context, see: ### Unit Tests -- Test framework: Jasmine + Karma. Spec files co-located with implementation: `feature.ts` β†’ `feature.spec.ts` -- Focus tests with `fit()` / `fdescribe()`, skip with `xit()` / `xdescribe()` +- Test framework: Vitest Browser Mode. Spec files co-located with implementation: `feature.ts` β†’ `feature.spec.ts` +- Focus tests with `it.only()` / `describe.only()`, skip with `it.skip()` / `describe.skip()` - Use `registerCleanupTask()` for cleanup, NOT `afterEach()` - Prefer `collectAsyncCalls(spy, n)` over `waitFor(() => spy.calls.count() > 0)` for waiting on spy calls - Don't destructure methods from `spy.calls` (e.g., `argsFor`, `mostRecent`) - use `calls.argsFor()` to avoid `@typescript-eslint/unbound-method` errors diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 95888f76b8..ac1b31a8bb 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -18,7 +18,6 @@ dev,@angular/core,MIT,Copyright (c) 2010-2024 Google LLC dev,@angular/platform-browser,MIT,Copyright (c) 2010-2024 Google LLC dev,@angular/router,MIT,Copyright (c) 2010-2024 Google LLC dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, -dev,@jsdevtools/coverage-istanbul-loader,MIT,Copyright (c) 2015 James Messinger dev,@ngtools/webpack,MIT,Copyright (c) 2017 Google LLC dev,@playwright/test,Apache-2.0,Copyright Microsoft Corporation dev,@swc/core,Apache-2.0,Copyright (c) SWC Contributors @@ -27,7 +26,6 @@ dev,@types/chrome,MIT,Copyright Microsoft Corporation dev,@types/busboy,MIT,Copyright Microsoft Corporation dev,@types/cors,MIT,Copyright Microsoft Corporation dev,@types/express,MIT,Copyright Microsoft Corporation -dev,@types/jasmine,MIT,Copyright Microsoft Corporation dev,@types/node,MIT,Copyright Microsoft Corporation dev,@types/node-forge,MIT,Copyright Microsoft Corporation dev,@types/ws,MIT,Copyright Microsoft Corporation @@ -39,6 +37,10 @@ dev,@wxt-dev/module-react,MIT,Copyright (c) 2023 Aaron dev,@vitejs/plugin-react,MIT,Copyright (c) 2019-present Evan You & Vite Contributors dev,@vitejs/plugin-vue,MIT,Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors dev,@microsoft/api-extractor,MIT,Copyright (c) Microsoft Corporation. All rights reserved. +dev,@vitest/browser,MIT,Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors +dev,@vitest/browser-playwright,MIT,Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors +dev,@vitest/coverage-istanbul,MIT,Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors +dev,@vitest/eslint-plugin,MIT,Copyright (c) 2022 - PRESENT VeritΓ© Mugabo Makuza dev,@module-federation/enhanced,MIT, Copyright (c) 2020 ScriptedAlchemy LLC (Zack Jackson) Zhou Shaw (zhouxiao) dev,@vue/compiler-dom,MIT,Copyright (c) 2018-present, Yuxi (Evan) You dev,@vue/test-utils,MIT,Copyright (c) 2021-present vuejs @@ -53,28 +55,18 @@ dev,emoji-name-map,MIT,Copyright 2016-19 IonicΔƒ BizΔƒu dev,eslint,MIT,Copyright JS Foundation and other contributors dev,eslint-plugin-import-x,MIT,Copyright (c) 2015 Ben Mosher dev,eslint-import-resolver-typescript,ISC,Copyright 2021 Alex Gorbatchev -dev,eslint-plugin-jasmine,MIT,Copyright (c) 2021 Tom Vincent dev,eslint-plugin-jsdoc,BSD-3-Clause,Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) dev,eslint-plugin-unicorn,MIT,Copyright (c) Sindre Sorhus (https://sindresorhus.com) dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson dev,globals,MIT,Copyright (c) Sindre Sorhus (sindresorhus.com) dev,html-webpack-plugin,MIT,Copyright JS Foundation and other contributors dev,http-server,MIT,Copyright http-party contributors -dev,jasmine-core,MIT,Copyright 2008-2017 Pivotal Labs dev,json-schema-to-typescript,MIT, -dev,karma,MIT,Copyright 2011-2019 Google Inc. -dev,karma-browserstack-launcher,MIT,Copyright 2011-2013 Google, Inc. -dev,karma-chrome-launcher,MIT,Copyright 2011-2013 Google Inc. -dev,karma-coverage-istanbul-reporter,MIT,Copyright 2017 Matt Lewis -dev,karma-jasmine,MIT,Copyright 2011-2013 Google Inc. -dev,karma-junit-reporter,MIT,Copyright (C) 2011-2013 Google, Inc. -dev,karma-sourcemap-loader,MIT,Copyright 2013 Sergey Todyshev -dev,karma-spec-reporter,MIT,Copyright 2015 Michael Lex -dev,karma-webpack,MIT,Copyright JS Foundation and other contributors dev,minimatch,ISC,Copyright (c) Isaac Z. Schlueter and Contributors dev,next,MIT,Copyright (c) 2025 Vercel, Inc. dev,node-forge,BSD,Copyright (c) 2010, Digital Bazaar, Inc. dev,pako,MIT,(C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin +dev,playwright,Apache-2.0,Copyright Microsoft Corporation dev,prettier,MIT,Copyright James Long and contributors dev,puppeteer,Apache-2.0,Copyright 2017 Google Inc. dev,react-is,MIT,Copyright (c) Meta Platforms, Inc. and affiliates @@ -92,6 +84,7 @@ dev,typescript,Apache-2.0,Copyright Microsoft Corporation dev,typescript-eslint,MIT,Copyright (c) 2019 typescript-eslint and other contributors dev,undici,MIT,Copyright (c) Matteo Collina and Undici contributors dev,vite,MIT,Copyright (c) 2019-present, VoidZero Inc. and Vite contributors +dev,vitest,MIT,Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors dev,vue,MIT,Copyright (c) 2018-present, Yuxi (Evan) You dev,nuxt,MIT,Copyright (c) 2016-present Nuxt Team dev,vue-router,MIT,Copyright (c) 2019-present Eduardo San Martin Morote diff --git a/developer-extension/src/panel/components/tabs/eventsTab/computeFacetState.spec.ts b/developer-extension/src/panel/components/tabs/eventsTab/computeFacetState.spec.ts index db2f06ed58..b3f5847282 100644 --- a/developer-extension/src/panel/components/tabs/eventsTab/computeFacetState.spec.ts +++ b/developer-extension/src/panel/components/tabs/eventsTab/computeFacetState.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { RumActionEvent, RumResourceEvent } from '@datadog/browser-rum' import { FacetRegistry } from '../../../hooks/useEvents' import type { FacetValuesFilter } from '../../../hooks/useEvents' diff --git a/developer-extension/src/panel/components/tabs/eventsTab/copyEvent.spec.ts b/developer-extension/src/panel/components/tabs/eventsTab/copyEvent.spec.ts index 4c2cd5105b..1a42509f5e 100644 --- a/developer-extension/src/panel/components/tabs/eventsTab/copyEvent.spec.ts +++ b/developer-extension/src/panel/components/tabs/eventsTab/copyEvent.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { TelemetryEvent } from '../../../../../../packages/browser-core/src/domain/telemetry' import type { LogsEvent } from '../../../../../../packages/browser-logs/src/logsEvent.types' import type { RumEvent } from '../../../../../../packages/browser-rum-core/src/rumEvent.types' diff --git a/developer-extension/src/panel/flushEvents.spec.ts b/developer-extension/src/panel/flushEvents.spec.ts index 95b6f7cbf8..e81918489c 100644 --- a/developer-extension/src/panel/flushEvents.spec.ts +++ b/developer-extension/src/panel/flushEvents.spec.ts @@ -1,13 +1,14 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { registerCleanupTask } from '../../../packages/browser-core/test' import type { PageMayExitEvent } from '../../../packages/browser-core/src/browser/pageMayExitObservable' import { createPageMayExitObservable } from '../../../packages/browser-core/src/browser/pageMayExitObservable' import { flushScript } from './flushEvents' describe('flushEvents', () => { - let onExitSpy: jasmine.Spy<(event: PageMayExitEvent) => void> + let onExitSpy: Mock<(event: PageMayExitEvent) => void> beforeEach(() => { - onExitSpy = jasmine.createSpy() + onExitSpy = vi.fn() registerCleanupTask(createPageMayExitObservable().subscribe(onExitSpy).unsubscribe) }) diff --git a/developer-extension/src/panel/hooks/useEvents/eventFilters.spec.ts b/developer-extension/src/panel/hooks/useEvents/eventFilters.spec.ts index ae4dcd1bf1..7236dab3c1 100644 --- a/developer-extension/src/panel/hooks/useEvents/eventFilters.spec.ts +++ b/developer-extension/src/panel/hooks/useEvents/eventFilters.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { RumEvent } from '../../../../../packages/browser-rum-core/src/rumEvent.types' import type { LogsEvent } from '../../../../../packages/browser-logs/src/logsEvent.types' import { isSafari } from '../../../../../packages/browser-core/src/tools/utils/browserDetection' diff --git a/developer-extension/src/panel/hooks/useEvents/facetRegistry.spec.ts b/developer-extension/src/panel/hooks/useEvents/facetRegistry.spec.ts index c89d319433..7f87e6ac98 100644 --- a/developer-extension/src/panel/hooks/useEvents/facetRegistry.spec.ts +++ b/developer-extension/src/panel/hooks/useEvents/facetRegistry.spec.ts @@ -1,10 +1,12 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { isChromium } from '../../../../../packages/browser-core/src/tools/utils/browserDetection' import { getAllFields } from './facetRegistry' describe('getAllFields', () => { - beforeEach(() => { + beforeEach((ctx) => { if (!isChromium()) { - pending('Extension only supported in chromium') + ctx.skip(true, 'extension only supported in Chromium') + return } }) diff --git a/developer-extension/tsconfig.json b/developer-extension/tsconfig.json index 694fdcf3fa..1b06390212 100644 --- a/developer-extension/tsconfig.json +++ b/developer-extension/tsconfig.json @@ -7,7 +7,7 @@ "moduleResolution": "bundler", "jsx": "react", "lib": ["ES2022", "DOM"], - "types": ["chrome", "react", "react-dom", "jasmine"], + "types": ["chrome", "react", "react-dom"], "skipLibCheck": true, "allowImportingTsExtensions": true, "noEmit": true diff --git a/docs/TESTING.md b/docs/TESTING.md index e258d78cfc..67b0968abd 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -21,16 +21,16 @@ Use this strategy to test the integration on Real applications. ## Work with unit tests 1. Write a unit test. -2. Run only your tests by temporarily replacing `describe(...)` with `fdescribe(...)` or `it(...)` with `fit(...)`. -3. Run `yarn test` in the project root. It will launch the tests in watch mode, and also expose [a local URL](http://localhost:9876/) that you can open in any browser. -4. Look at your terminal to see your test results. -5. For deeper investigation, open [the "debug" page](http://localhost:9876/debug.html#) to inspect test execution via the devtools. +2. Run the spec directly with `yarn test:unit --spec packages/path/to/feature.spec.ts`. +3. Use `it.only(...)` or `describe.only(...)` temporarily when you need to focus further. ESLint prevents focused tests from being checked in. +4. Run `yarn test` for Vitest watch mode, or `yarn test:unit` for a single full run. +5. Look at the terminal output; failure screenshots are written next to the failing spec under `__screenshots__`. ## Debug flaky unit tests Top-level `describe` blocks are run in a random order. Sometimes, an issue can only be reproduced with a given order. To reproduce a test run order: -1. Check the `Randomized with seed XXXX` message from the test output. +1. Check the `Running tests with seed "XXXX"` message from the test output. 2. Run `yarn test:unit --seed XXXX`. ## Work with E2E tests diff --git a/eslint.config.ts b/eslint.config.ts index 3be09ff2f1..a4d5608fcf 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,11 +1,10 @@ import eslint from '@eslint/js' +import vitest from '@vitest/eslint-plugin' import { defineConfig } from 'eslint/config' import * as tseslint from 'typescript-eslint' import { importX } from 'eslint-plugin-import-x' import unicornPlugin from 'eslint-plugin-unicorn' import jsdocPlugin from 'eslint-plugin-jsdoc' -// @ts-expect-error -- eslint-plugin-jasmine is not typed -import jasmine from 'eslint-plugin-jasmine' import globals from 'globals' // eslint-disable-next-line local-rules/disallow-protected-directory-import import eslintLocalRules from './eslint-local-rules/index.ts' @@ -36,6 +35,7 @@ export default defineConfig( ignores: [ ...SCHEMAS.map((schema) => schema.typesPath), 'packages/*/bundle', + 'bundle', 'packages/*/cjs', 'packages/*/esm', 'test/**/dist', @@ -75,7 +75,7 @@ export default defineConfig( unicorn: unicornPlugin, 'local-rules': { rules: eslintLocalRules as Record }, jsdoc: jsdocPlugin, - jasmine, + vitest, }, languageOptions: { @@ -246,7 +246,6 @@ export default defineConfig( }, ], - 'jasmine/no-focused-tests': 'error', 'jsdoc/check-alignment': 'error', 'jsdoc/check-indentation': 'error', 'jsdoc/no-blank-blocks': 'error', @@ -475,6 +474,16 @@ export default defineConfig( }, }, + { + files: [SPEC_FILES, 'packages/*/test/**/*.ts'], + rules: { + // vitest is a root devDependency, not listed in each sub-package + 'import-x/no-extraneous-dependencies': 'off', + 'vitest/no-focused-tests': 'error', + 'vitest/no-identical-title': 'error', + }, + }, + { files: ['packages/browser-core/src/tools/**/*.ts'], ignores: [SPEC_FILES], diff --git a/package.json b/package.json index 0a3537fbf5..3a54208da4 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ "dev-server": "node scripts/dev-server/index.ts", "release": "node ./scripts/release/prepare-release.ts --push", "test": "yarn test:unit:watch", - "test:unit": "karma start ./test/unit/karma.local.conf.js", + "test:unit": "node scripts/test/run-unit-tests.ts", "test:script": "node --test --experimental-test-module-mocks './scripts/**/*.spec.*'", - "test:unit:watch": "yarn test:unit --no-single-run", - "test:unit:bs": "node --env-file-if-exists=.env ./scripts/test/bs-wrapper.ts karma start test/unit/karma.bs.conf.js", + "test:unit:watch": "node scripts/test/run-unit-tests.ts --watch", + "test:unit:bs": "node ./scripts/test/bs-wrapper.ts vitest run --config vitest.bs.config.ts", "test:e2e:setup": "yarn build && yarn build:apps && yarn playwright install --with-deps", "test:e2e:setup:pinned": "volta run --node 20 yarn dlx -p playwright@1.40.1 playwright install chromium firefox webkit", "test:e2e": "playwright test --config test/e2e/playwright.config.ts", @@ -48,7 +48,6 @@ "devDependencies": { "@datadog/rum-events-format": "DataDog/rum-events-format#commit=31cc845ba1cb52086a25424de86731c316b2385e", "@eslint/js": "10.0.1", - "@jsdevtools/coverage-istanbul-loader": "3.0.5", "@microsoft/api-extractor": "7.58.9", "@playwright/test": "1.61.1", "@swc/core": "1.15.43", @@ -56,11 +55,14 @@ "@types/chrome": "0.2.2", "@types/cors": "2.8.19", "@types/express": "5.0.6", - "@types/jasmine": "3.10.19", "@types/node": "26.1.0", "@types/node-forge": "1.3.14", "@types/ws": "8.18.1", "@typescript-eslint/utils": "8.62.1", + "@vitest/browser": "4.1.8", + "@vitest/browser-playwright": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/eslint-plugin": "1.6.22", "ajv": "8.20.0", "browserstack-local": "1.5.13", "busboy": "1.6.0", @@ -70,26 +72,17 @@ "eslint": "10.6.0", "eslint-import-resolver-typescript": "4.4.5", "eslint-plugin-import-x": "4.17.1", - "eslint-plugin-jasmine": "4.2.2", "eslint-plugin-jsdoc": "63.0.12", "eslint-plugin-unicorn": "65.0.1", "express": "5.2.1", "globals": "17.7.0", "html-webpack-plugin": "5.6.7", "http-server": "14.1.1", - "jasmine-core": "3.99.1", "json-schema-to-typescript": "bcaudan/json-schema-to-typescript#bcaudan/add-readonly-support", - "karma": "6.4.4", - "karma-browserstack-launcher": "1.6.0", - "karma-chrome-launcher": "3.2.0", - "karma-coverage-istanbul-reporter": "3.0.3", - "karma-jasmine": "4.0.2", - "karma-junit-reporter": "2.0.1", - "karma-sourcemap-loader": "0.4.0", - "karma-spec-reporter": "0.0.36", - "karma-webpack": "5.0.0", "minimatch": "10.2.5", "node-forge": "1.4.0", + "playwright": "1.61.1", + "playwright-browserstack": "npm:playwright@1.59.1", "prettier": "3.9.4", "puppeteer": "25.3.0", "swc-loader": "0.2.7", @@ -101,6 +94,7 @@ "typescript": "6.0.3", "typescript-eslint": "8.62.1", "undici": "8.7.0", + "vitest": "4.1.8", "webpack": "5.108.4", "webpack-cli": "7.2.1", "webpack-dev-middleware": "8.0.3", diff --git a/packages/browser-core/src/boot/displayAlreadyInitializedError.spec.ts b/packages/browser-core/src/boot/displayAlreadyInitializedError.spec.ts index c2a884cf4e..fcc7e08e54 100644 --- a/packages/browser-core/src/boot/displayAlreadyInitializedError.spec.ts +++ b/packages/browser-core/src/boot/displayAlreadyInitializedError.spec.ts @@ -1,17 +1,18 @@ +import { vi, describe, expect, it } from 'vitest' import type { InitConfiguration } from '../domain/configuration' import { display } from '../tools/display' import { displayAlreadyInitializedError } from './displayAlreadyInitializedError' describe('displayAlreadyInitializedError', () => { it('should display an error', () => { - const displayErrorSpy = spyOn(display, 'error') + const displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) displayAlreadyInitializedError('DD_RUM', {} as InitConfiguration) expect(displayErrorSpy).toHaveBeenCalledTimes(1) expect(displayErrorSpy).toHaveBeenCalledWith('DD_RUM is already initialized.') }) it('should not display an error if the "silentMultipleInit" option is used', () => { - const displayErrorSpy = spyOn(display, 'error') + const displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) displayAlreadyInitializedError('DD_RUM', { silentMultipleInit: true } as InitConfiguration) expect(displayErrorSpy).not.toHaveBeenCalled() }) diff --git a/packages/browser-core/src/boot/init.spec.ts b/packages/browser-core/src/boot/init.spec.ts index e3661f772b..76a2adaf0c 100644 --- a/packages/browser-core/src/boot/init.spec.ts +++ b/packages/browser-core/src/boot/init.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { display } from '../tools/display' import { defineGlobal } from './init' @@ -17,8 +18,8 @@ describe('defineGlobal', () => { }) it('run the queued callbacks on the old value', () => { - const fn1 = jasmine.createSpy() - const fn2 = jasmine.createSpy() + const fn1 = vi.fn() + const fn2 = vi.fn() const myGlobal: any = { foo: { q: [fn1, fn2], @@ -42,7 +43,7 @@ describe('defineGlobal', () => { q: [onReady], }, } - const displaySpy = spyOn(display, 'error') + const displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) defineGlobal(myGlobal, 'foo', {}) expect(displaySpy).toHaveBeenCalledWith('onReady callback threw an error:', myError) diff --git a/packages/browser-core/src/browser/addEventListener.spec.ts b/packages/browser-core/src/browser/addEventListener.spec.ts index 97f2fe63f0..a7e43e9cf9 100644 --- a/packages/browser-core/src/browser/addEventListener.spec.ts +++ b/packages/browser-core/src/browser/addEventListener.spec.ts @@ -1,7 +1,14 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, mockZoneJs, registerCleanupTask } from '../../test' import type { MockZoneJs } from '../../test' import { noop } from '../tools/utils/functionUtils' -import { addEventListener, DOM_EVENT, isEventSupported, setAllowUntrustedEvents } from './addEventListener' +import { + addEventListener, + DOM_EVENT, + isEventSupported, + resetAllowUntrustedEvents, + setAllowUntrustedEvents, +} from './addEventListener' describe('addEventListener', () => { describe('Zone.js support', () => { @@ -12,7 +19,7 @@ describe('addEventListener', () => { }) it('uses the original addEventListener method instead of the method patched by Zone.js', () => { - const zoneJsPatchedAddEventListener = jasmine.createSpy() + const zoneJsPatchedAddEventListener = vi.fn() const eventTarget = document.createElement('div') zoneJs.replaceProperty(eventTarget, 'addEventListener', zoneJsPatchedAddEventListener) @@ -21,7 +28,7 @@ describe('addEventListener', () => { }) it('uses the original removeEventListener method instead of the method patched by Zone.js', () => { - const zoneJsPatchedRemoveEventListener = jasmine.createSpy() + const zoneJsPatchedRemoveEventListener = vi.fn() const eventTarget = document.createElement('div') zoneJs.replaceProperty(eventTarget, 'removeEventListener', zoneJsPatchedRemoveEventListener) @@ -37,8 +44,8 @@ describe('addEventListener', () => { // eslint-disable-next-line @typescript-eslint/unbound-method const originalRemoveEventListener = EventTarget.prototype.removeEventListener - EventTarget.prototype.addEventListener = jasmine.createSpy() - EventTarget.prototype.removeEventListener = jasmine.createSpy() + EventTarget.prototype.addEventListener = vi.fn() + EventTarget.prototype.removeEventListener = vi.fn() registerCleanupTask(() => { EventTarget.prototype.addEventListener = originalAddEventListener @@ -46,8 +53,8 @@ describe('addEventListener', () => { }) const htmlDivElement = document.createElement('div') - htmlDivElement.addEventListener = jasmine.createSpy() - htmlDivElement.removeEventListener = jasmine.createSpy() + htmlDivElement.addEventListener = vi.fn() + htmlDivElement.removeEventListener = vi.fn() const { stop } = addEventListener(htmlDivElement, DOM_EVENT.CLICK, noop) @@ -67,11 +74,11 @@ describe('addEventListener', () => { }) it('Use the addEventListener method when the eventTarget is not an instance of EventTarget', () => { - const listener = jasmine.createSpy() + const listener = vi.fn() const customEventTarget = { - addEventListener: jasmine.createSpy(), - removeEventListener: jasmine.createSpy(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), } as unknown as HTMLElement const { stop } = addEventListener(customEventTarget, 'change', listener) @@ -86,10 +93,11 @@ describe('addEventListener', () => { describe('Untrusted event', () => { beforeEach(() => { setAllowUntrustedEvents(false) + registerCleanupTask(resetAllowUntrustedEvents) }) it('should be ignored if __ddIsTrusted is absent', () => { - const listener = jasmine.createSpy() + const listener = vi.fn() const eventTarget = document.createElement('div') addEventListener(eventTarget, DOM_EVENT.CLICK, listener) @@ -99,7 +107,7 @@ describe('addEventListener', () => { }) it('should be ignored if __ddIsTrusted is false', () => { - const listener = jasmine.createSpy() + const listener = vi.fn() const eventTarget = document.createElement('div') addEventListener(eventTarget, DOM_EVENT.CLICK, listener) @@ -109,7 +117,7 @@ describe('addEventListener', () => { }) it('should not be ignored if __ddIsTrusted is true', () => { - const listener = jasmine.createSpy() + const listener = vi.fn() const eventTarget = document.createElement('div') addEventListener(eventTarget, DOM_EVENT.CLICK, listener) @@ -120,7 +128,7 @@ describe('addEventListener', () => { }) it('should not be ignored if setAllowUntrustedEvents(true) was called', () => { - const listener = jasmine.createSpy() + const listener = vi.fn() const eventTarget = document.createElement('div') setAllowUntrustedEvents(true) @@ -134,13 +142,14 @@ describe('addEventListener', () => { }) describe('setAllowUntrustedEvents', () => { - let listener: jasmine.Spy + let listener: Mock<(event: PointerEvent & { type: DOM_EVENT.CLICK }) => void> let eventTarget: HTMLElement beforeEach(() => { - listener = jasmine.createSpy() + listener = vi.fn() eventTarget = document.createElement('div') addEventListener(eventTarget, DOM_EVENT.CLICK, listener) + registerCleanupTask(resetAllowUntrustedEvents) }) function dispatchUntrustedClick() { @@ -193,7 +202,9 @@ describe('addEventListener', () => { it('should return false if the event listener cannot be added', () => { const eventTarget = { - addEventListener: jasmine.createSpy().and.throwError('unsupported'), + addEventListener: vi.fn().mockImplementation(() => { + throw new Error('unsupported') + }), } as unknown as HTMLElement expect(isEventSupported(eventTarget, DOM_EVENT.CLICK)).toBe(false) diff --git a/packages/browser-core/src/browser/cookie.spec.ts b/packages/browser-core/src/browser/cookie.spec.ts index fe4b2f750f..bcf2a8acef 100644 --- a/packages/browser-core/src/browser/cookie.spec.ts +++ b/packages/browser-core/src/browser/cookie.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mockCookies } from '../../test' import { getCurrentSite } from './cookie' diff --git a/packages/browser-core/src/browser/cookieAccess.spec.ts b/packages/browser-core/src/browser/cookieAccess.spec.ts index 7d508994bc..66300bf381 100644 --- a/packages/browser-core/src/browser/cookieAccess.spec.ts +++ b/packages/browser-core/src/browser/cookieAccess.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { ONE_MINUTE, dateNow } from '@datadog/js-core/time' import { globalObject } from '@datadog/js-core/util' import type { Clock } from '../../test' @@ -22,10 +23,10 @@ function disableCookieStore() { replaceMockable(globalObject.cookieStore, undefined) } -interface setupResult { +interface SetupResult { clock: Clock createCookieAccess: (name: string, options: CookieOptions) => CookieAccess - flushObservable: (spy: jasmine.Spy) => Promise + flushObservable: (spy: Mock) => Promise setCookieWithCleanup: ( this: void, name: string, @@ -36,7 +37,7 @@ interface setupResult { } describe('cookieAccess', () => { - const setups = [ + const setups: Array<{ title: string; setup: () => SetupResult }> = [ { title: 'document.cookie fallback', setup: () => { @@ -46,7 +47,7 @@ describe('cookieAccess', () => { return { clock, createCookieAccess: (name: string, options: CookieOptions) => createDocumentCookieAccess(name, options), - flushObservable(this: void, _spy: jasmine.Spy) { + flushObservable(this: void, _spy: Mock) { clock.tick(WATCH_COOKIE_INTERVAL_DELAY) return Promise.resolve() }, @@ -70,17 +71,16 @@ describe('cookieAccess', () => { const clock = mockClock() if (!globalObject.cookieStore) { - pending('CookieStore API not available') - return {} as setupResult + return {} as SetupResult } return { clock, createCookieAccess: (name: string, options: CookieOptions) => createCookieStoreAccess(name, options), - async flushObservable(this: void, spy: jasmine.Spy) { + async flushObservable(this: void, spy: Mock) { await collectAsyncCalls(spy, 1) // Reset the spy calls to avoid throwing on unexpected calls during teardown - registerCleanupTask(() => spy.calls.reset()) + registerCleanupTask(() => spy.mockClear()) }, async setCookieWithCleanup( this: void, @@ -114,9 +114,25 @@ describe('cookieAccess', () => { for (const { title, setup } of setups) { describe(title, () => { + let setupResult: SetupResult + + beforeEach((ctx) => { + // Skip CookieStore API tests when the API is not available. + if (title === 'CookieStore API' && !globalObject.cookieStore) { + ctx.skip(true, 'CookieStore API not available') + } + setupResult = setup() + }) + describe('getAllAndSet', () => { + beforeEach(async (ctx) => { + if (!(await areCookiesAuthorized(setupResult.createCookieAccess, COOKIE_OPTIONS))) { + ctx.skip(true, 'cookies are not available in this browser test context') + } + }) + it('should pass current cookie values to callback', async () => { - const { createCookieAccess, setCookieWithCleanup } = setup() + const { createCookieAccess, setCookieWithCleanup } = setupResult await setCookieWithCleanup(COOKIE_NAME, 'value1', ONE_MINUTE) const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) @@ -131,7 +147,7 @@ describe('cookieAccess', () => { }) it('should pass empty array when cookie does not exist', async () => { - const { createCookieAccess } = setup() + const { createCookieAccess } = setupResult const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) let capturedValues: string[] | undefined @@ -144,7 +160,7 @@ describe('cookieAccess', () => { }) it('should write the value returned by the callback', async () => { - const { createCookieAccess } = setup() + const { createCookieAccess } = setupResult const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) await cookieAccess.getAllAndSet(() => ({ value: 'hello', expireDelay: ONE_MINUTE })) @@ -152,13 +168,16 @@ describe('cookieAccess', () => { expect(getCookie(COOKIE_NAME)).toBe('hello') }) - it('should pass all cookie values to callback', async () => { + it('should pass all cookie values to callback', async (ctx) => { const browserVersion = detectVersion() - if (!isChromium() || (browserVersion !== undefined && browserVersion < 145)) { - pending('Only Recent Chromium supports multiple cookies with the same name with different options') + if (!isChromium() || browserVersion === undefined || browserVersion < 145) { + ctx.skip(true, 'Only recent Chromium (>=145) supports multiple cookies with the same name') + } + if (location.protocol !== 'https:') { + ctx.skip(true, 'Partitioned cookies require a secure (HTTPS) context') } - const { createCookieAccess, setCookieWithCleanup } = setup() + const { createCookieAccess, setCookieWithCleanup } = setupResult await setCookieWithCleanup(COOKIE_NAME, 'value1', ONE_MINUTE) await setCookieWithCleanup(COOKIE_NAME, 'value2', ONE_MINUTE, { secure: true, partitioned: true }) @@ -175,10 +194,16 @@ describe('cookieAccess', () => { }) describe('observable', () => { + beforeEach(async (ctx) => { + if (!(await areCookiesAuthorized(setupResult.createCookieAccess, COOKIE_OPTIONS))) { + ctx.skip(true, 'cookies are not available in this browser test context') + } + }) + it('should notify when cookie is changed externally', async () => { - const { createCookieAccess, flushObservable, setCookieWithCleanup } = setup() + const { createCookieAccess, flushObservable, setCookieWithCleanup } = setupResult const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = cookieAccess.observable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) @@ -189,11 +214,11 @@ describe('cookieAccess', () => { }) it('should notify when cookie is deleted externally', async () => { - const { createCookieAccess, flushObservable, setCookieWithCleanup } = setup() + const { createCookieAccess, flushObservable, setCookieWithCleanup } = setupResult await setCookieWithCleanup(COOKIE_NAME, 'existing', ONE_MINUTE) const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = cookieAccess.observable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) @@ -204,11 +229,11 @@ describe('cookieAccess', () => { }) it('should not notify when cookie value is unchanged', async () => { - const { createCookieAccess, clock, setCookieWithCleanup } = setup() + const { createCookieAccess, clock, setCookieWithCleanup } = setupResult await setCookieWithCleanup(COOKIE_NAME, 'stable', ONE_MINUTE) const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = cookieAccess.observable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) @@ -218,9 +243,9 @@ describe('cookieAccess', () => { }) it('should notify the observable after writing', async () => { - const { createCookieAccess, flushObservable } = setup() + const { createCookieAccess, flushObservable } = setupResult const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS) - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = cookieAccess.observable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) @@ -236,16 +261,16 @@ describe('cookieAccess', () => { describe('areCookiesAuthorized', () => { it('returns true when the access can write and read back the test cookie', async () => { const access: CookieAccess = { - getAll: jasmine.createSpy('getAll').and.returnValue(Promise.resolve(['test'])), - getAllAndSet: jasmine.createSpy('getAllAndSet').and.returnValue(Promise.resolve()), + getAll: vi.fn().mockReturnValue(Promise.resolve(['test'])), + getAllAndSet: vi.fn().mockReturnValue(Promise.resolve()), observable: null as any, } - const factory = jasmine.createSpy('factory').and.returnValue(access) + const factory = vi.fn().mockReturnValue(access) const result = await areCookiesAuthorized(factory, COOKIE_OPTIONS) expect(result).toBe(true) - expect(factory).toHaveBeenCalledWith(jasmine.any(String), COOKIE_OPTIONS) + expect(factory).toHaveBeenCalledWith(expect.any(String), COOKIE_OPTIONS) }) it('returns false when the access cannot read back the test cookie', async () => { @@ -261,7 +286,7 @@ describe('cookieAccess', () => { }) it('returns false and logs when the access throws', async () => { - const displayErrorSpy = spyOn(display, 'error') + const displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const access: CookieAccess = { getAll: () => Promise.resolve([]), getAllAndSet: () => Promise.reject(new Error('boom')), @@ -288,7 +313,7 @@ describe('cookieAccess', () => { await areCookiesAuthorized(() => access, COOKIE_OPTIONS) expect(calls).toEqual([ - { value: 'test', expireDelay: jasmine.any(Number) as unknown as number }, + { value: 'test', expireDelay: expect.any(Number) }, { value: '', expireDelay: 0 }, ]) }) @@ -299,16 +324,19 @@ describe('cookieAccess', () => { expect(result).toBe(true) }) - it('works with the real createCookieStoreAccess', async () => { + it('works with the real createCookieStoreAccess', async (ctx) => { if (!globalObject.cookieStore) { - pending('CookieStore API not available') + ctx.skip(true, 'CookieStore API not available') } const result = await areCookiesAuthorized(createCookieStoreAccess, COOKIE_OPTIONS) + if (!result) { + ctx.skip(true, 'CookieStore is not available in this browser test context') + } expect(result).toBe(true) }) it('returns false when document.cookie is empty', async () => { - spyOnProperty(document, 'cookie', 'get').and.returnValue('') + vi.spyOn(document, 'cookie', 'get').mockReturnValue('') const result = await areCookiesAuthorized(createDocumentCookieAccess, COOKIE_OPTIONS) expect(result).toBe(false) }) @@ -330,7 +358,9 @@ describe('cookieAccess', () => { it('returns false when the CookieStore API does not support change events', () => { const cookieStore = { - addEventListener: jasmine.createSpy().and.throwError('unsupported'), + addEventListener: vi.fn().mockImplementation(() => { + throw new Error('unsupported') + }), } replaceMockable(globalObject.cookieStore, cookieStore as unknown as typeof globalObject.cookieStore) diff --git a/packages/browser-core/src/browser/fetch.spec.ts b/packages/browser-core/src/browser/fetch.spec.ts index aecba40b8f..084f07684f 100644 --- a/packages/browser-core/src/browser/fetch.spec.ts +++ b/packages/browser-core/src/browser/fetch.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { type MockZoneJs, mockZoneJs } from '../../test' import { fetch } from './fetch' @@ -9,8 +10,8 @@ describe('fetch', () => { }) it('does not use the Zone.js function', async () => { - const nativeFetchSpy = spyOn(window, 'fetch') - const zoneJsFetchSpy = jasmine.createSpy('zoneJsFetch') + const nativeFetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(new Response()) + const zoneJsFetchSpy = vi.fn() zoneJs.replaceProperty(window, 'fetch', zoneJsFetchSpy) @@ -21,8 +22,8 @@ describe('fetch', () => { }) it('calls the native fetch function with correct arguments', async () => { - const nativeFetchSpy = spyOn(window, 'fetch') - const zoneJsFetchSpy = jasmine.createSpy('zoneJsFetch') + const nativeFetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(new Response()) + const zoneJsFetchSpy = vi.fn() zoneJs.replaceProperty(window, 'fetch', zoneJsFetchSpy) @@ -33,8 +34,8 @@ describe('fetch', () => { it('returns the response from native fetch', async () => { const mockResponse = new Response('test response', { status: 200 }) - spyOn(window, 'fetch').and.returnValue(Promise.resolve(mockResponse)) - const zoneJsFetchSpy = jasmine.createSpy('zoneJsFetch').and.returnValue(Promise.resolve(new Response())) + vi.spyOn(window, 'fetch').mockReturnValue(Promise.resolve(mockResponse)) + const zoneJsFetchSpy = vi.fn().mockReturnValue(Promise.resolve(new Response())) zoneJs.replaceProperty(window, 'fetch', zoneJsFetchSpy) diff --git a/packages/browser-core/src/browser/fetchObservable.spec.ts b/packages/browser-core/src/browser/fetchObservable.spec.ts index 6955731026..a081460322 100644 --- a/packages/browser-core/src/browser/fetchObservable.spec.ts +++ b/packages/browser-core/src/browser/fetchObservable.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it } from 'vitest' import type { MockFetch, MockFetchManager } from '../../test' import { registerCleanupTask, mockFetch } from '../../test' import type { Subscription } from '../tools/observable' @@ -34,226 +35,245 @@ describe('fetch proxy', () => { }) }) - it('should track server error', (done) => { - fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) - - mockFetchManager.whenAllComplete(() => { - const request = requests[0] - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(500) - expect(request.isAborted).toBe(false) - expect(request.handlingStack).toBeDefined() - done() - }) - }) - - it('should track refused fetch', (done) => { - fetch(FAKE_URL).rejectWith(new Error('fetch error')) - - mockFetchManager.whenAllComplete(() => { - const request = requests[0] - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(0) - expect(request.isAborted).toBe(false) - expect(request.error).toEqual(new Error('fetch error')) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + it('should track server error', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) - it('should track aborted fetch', (done) => { - fetch(FAKE_URL).abort() - - mockFetchManager.whenAllComplete(() => { - const request = requests[0] - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(0) - expect(request.isAborted).toBe(true) - expect(request.error).toEqual(new DOMException('The user aborted a request', 'AbortError')) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + const request = requests[0] + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(500) + expect(request.isAborted).toBe(false) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should track fetch aborted by AbortController', (done) => { - const controller = new AbortController() - void fetch(FAKE_URL, { signal: controller.signal }) - controller.abort('AbortError') - - mockFetchManager.whenAllComplete(() => { - const request = requests[0] - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(0) - expect(request.isAborted).toBe(true) - expect(request.error).toEqual(controller.signal.reason) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + it('should track refused fetch', () => + new Promise((resolve) => { + fetch(FAKE_URL).rejectWith(new Error('fetch error')) - it('should track opaque fetch', (done) => { - // https://fetch.spec.whatwg.org/#concept-filtered-response-opaque - fetch(FAKE_URL).resolveWith({ status: 0, type: 'opaque' }) - - mockFetchManager.whenAllComplete(() => { - const request = requests[0] - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(0) - expect(request.isAborted).toBe(false) - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + const request = requests[0] + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(0) + expect(request.isAborted).toBe(false) + expect(request.error).toEqual(new Error('fetch error')) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should track client error', (done) => { - fetch(FAKE_URL).resolveWith({ status: 400, responseText: 'Not found' }) + it('should track aborted fetch', () => + new Promise((resolve) => { + fetch(FAKE_URL).abort() - mockFetchManager.whenAllComplete(() => { - const request = requests[0] - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(400) - expect(request.isAborted).toBe(false) - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + const request = requests[0] + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(0) + expect(request.isAborted).toBe(true) + expect(request.error).toEqual(new DOMException('The user aborted a request', 'AbortError')) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should get method from input', (done) => { - fetch(FAKE_URL).resolveWith({ status: 500 }) - fetch(new Request(FAKE_URL)).resolveWith({ status: 500 }) - fetch(new Request(FAKE_URL, { method: 'PUT' })).resolveWith({ status: 500 }) - fetch(new Request(FAKE_URL, { method: 'PUT' }), { method: 'POST' }).resolveWith({ status: 500 }) - fetch(new Request(FAKE_URL), { method: 'POST' }).resolveWith({ status: 500 }) - fetch(FAKE_URL, { method: 'POST' }).resolveWith({ status: 500 }) - fetch(FAKE_URL, { method: 'post' }).resolveWith({ status: 500 }) - fetch(null as any).resolveWith({ status: 500 }) - fetch({ method: 'POST' } as any).resolveWith({ status: 500 }) - fetch(FAKE_URL, { method: null as any }).resolveWith({ status: 500 }) - fetch(FAKE_URL, { method: undefined }).resolveWith({ status: 500 }) - - mockFetchManager.whenAllComplete(() => { - expect(requests[0].method).toEqual('GET') - expect(requests[1].method).toEqual('GET') - expect(requests[2].method).toEqual('PUT') - expect(requests[3].method).toEqual('POST') - expect(requests[4].method).toEqual('POST') - expect(requests[5].method).toEqual('POST') - expect(requests[6].method).toEqual('POST') - expect(requests[7].method).toEqual('GET') - expect(requests[8].method).toEqual('GET') - expect(requests[9].method).toEqual('NULL') - expect(requests[10].method).toEqual('GET') - - done() - }) - }) + it('should track fetch aborted by AbortController', (ctx) => + new Promise((resolve) => { + if (!window.AbortController) { + ctx.skip(true, 'AbortController not supported') + return resolve() + } - it('should get the normalized url from input', (done) => { - fetch(FAKE_URL).rejectWith(new Error('fetch error')) - fetch(new Request(FAKE_URL)).rejectWith(new Error('fetch error')) - fetch(null as any).rejectWith(new Error('fetch error')) - fetch({ - toString() { - return FAKE_RELATIVE_URL - }, - } as any).rejectWith(new Error('fetch error')) - fetch(FAKE_RELATIVE_URL).rejectWith(new Error('fetch error')) - fetch(new Request(FAKE_RELATIVE_URL)).rejectWith(new Error('fetch error')) - - mockFetchManager.whenAllComplete(() => { - expect(requests[0].url).toEqual(FAKE_URL) - expect(requests[1].url).toEqual(FAKE_URL) - expect(requests[2].url).toMatch(/\/null$/) - expect(requests[3].url).toEqual(NORMALIZED_FAKE_RELATIVE_URL) - expect(requests[4].url).toEqual(NORMALIZED_FAKE_RELATIVE_URL) - expect(requests[5].url).toEqual(NORMALIZED_FAKE_RELATIVE_URL) - done() - }) - }) + const controller = new AbortController() + void fetch(FAKE_URL, { signal: controller.signal }) + controller.abort('AbortError') - it('should keep promise resolved behavior for Response', (done) => { - const mockFetchPromise = fetch(FAKE_URL) - const spy = jasmine.createSpy() - mockFetchPromise.then(spy).catch(() => { - fail('Should not have thrown an error!') - }) - mockFetchPromise.resolveWith({ status: 500 }) + mockFetchManager.whenAllComplete(() => { + const request = requests[0] + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(0) + expect(request.isAborted).toBe(true) + expect(request.error).toEqual(controller.signal.reason) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }) - }) + it('should track opaque fetch', () => + new Promise((resolve) => { + // https://fetch.spec.whatwg.org/#concept-filtered-response-opaque + fetch(FAKE_URL).resolveWith({ status: 0, type: 'opaque' }) - it('should keep promise resolved behavior for any other type', (done) => { - const mockFetchPromise = fetch(FAKE_URL) - const spy = jasmine.createSpy() - mockFetchPromise.then(spy).catch(() => { - fail('Should not have thrown an error!') - }) - mockFetchPromise.resolveWith('response' as any) + mockFetchManager.whenAllComplete(() => { + const request = requests[0] + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(0) + expect(request.isAborted).toBe(false) + resolve() + }) + })) - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }) - }) + it('should track client error', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 400, responseText: 'Not found' }) - it('should keep promise rejected behavior for Error', (done) => { - const mockFetchPromise = fetch(FAKE_URL) - const spy = jasmine.createSpy() - mockFetchPromise.catch(spy) - mockFetchPromise.rejectWith(new Error('fetch error')) + mockFetchManager.whenAllComplete(() => { + const request = requests[0] + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(400) + expect(request.isAborted).toBe(false) + resolve() + }) + })) + + it('should get method from input', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 500 }) + fetch(new Request(FAKE_URL)).resolveWith({ status: 500 }) + fetch(new Request(FAKE_URL, { method: 'PUT' })).resolveWith({ status: 500 }) + fetch(new Request(FAKE_URL, { method: 'PUT' }), { method: 'POST' }).resolveWith({ status: 500 }) + fetch(new Request(FAKE_URL), { method: 'POST' }).resolveWith({ status: 500 }) + fetch(FAKE_URL, { method: 'POST' }).resolveWith({ status: 500 }) + fetch(FAKE_URL, { method: 'post' }).resolveWith({ status: 500 }) + fetch(null as any).resolveWith({ status: 500 }) + fetch({ method: 'POST' } as any).resolveWith({ status: 500 }) + fetch(FAKE_URL, { method: null as any }).resolveWith({ status: 500 }) + fetch(FAKE_URL, { method: undefined }).resolveWith({ status: 500 }) - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].method).toEqual('GET') + expect(requests[1].method).toEqual('GET') + expect(requests[2].method).toEqual('PUT') + expect(requests[3].method).toEqual('POST') + expect(requests[4].method).toEqual('POST') + expect(requests[5].method).toEqual('POST') + expect(requests[6].method).toEqual('POST') + expect(requests[7].method).toEqual('GET') + expect(requests[8].method).toEqual('GET') + expect(requests[9].method).toEqual('NULL') + expect(requests[10].method).toEqual('GET') + + resolve() + }) + })) + + it('should get the normalized url from input', () => + new Promise((resolve) => { + fetch(FAKE_URL).rejectWith(new Error('fetch error')) + fetch(new Request(FAKE_URL)).rejectWith(new Error('fetch error')) + fetch(null as any).rejectWith(new Error('fetch error')) + fetch({ + toString() { + return FAKE_RELATIVE_URL + }, + } as any).rejectWith(new Error('fetch error')) + fetch(FAKE_RELATIVE_URL).rejectWith(new Error('fetch error')) + fetch(new Request(FAKE_RELATIVE_URL)).rejectWith(new Error('fetch error')) - it('should keep promise rejected behavior for any other type', (done) => { - const mockFetchPromise = fetch(FAKE_URL) - const spy = jasmine.createSpy() - mockFetchPromise.catch(spy) - mockFetchPromise.rejectWith('fetch error' as any) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].url).toEqual(FAKE_URL) + expect(requests[1].url).toEqual(FAKE_URL) + expect(requests[2].url).toMatch(/\/null$/) + expect(requests[3].url).toEqual(NORMALIZED_FAKE_RELATIVE_URL) + expect(requests[4].url).toEqual(NORMALIZED_FAKE_RELATIVE_URL) + expect(requests[5].url).toEqual(NORMALIZED_FAKE_RELATIVE_URL) + resolve() + }) + })) + + it('should keep promise resolved behavior for Response', () => + new Promise((resolve) => { + const mockFetchPromise = fetch(FAKE_URL) + const spy = vi.fn() + mockFetchPromise.then(spy).catch(() => { + throw new Error('Should not have thrown an error!') + }) + mockFetchPromise.resolveWith({ status: 500 }) - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }) - }) + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }) + })) + + it('should keep promise resolved behavior for any other type', () => + new Promise((resolve) => { + const mockFetchPromise = fetch(FAKE_URL) + const spy = vi.fn() + mockFetchPromise.then(spy).catch(() => { + throw new Error('Should not have thrown an error!') + }) + mockFetchPromise.resolveWith('response' as any) - it('should allow to enhance the context', (done) => { - type CustomContext = FetchContext & { foo: string } - contextEditionSubscription = initFetchObservable().subscribe((rawContext) => { - const context = rawContext as CustomContext - if (context.state === 'start') { - context.foo = 'bar' - } - }) - fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }) + })) + + it('should keep promise rejected behavior for Error', () => + new Promise((resolve) => { + const mockFetchPromise = fetch(FAKE_URL) + const spy = vi.fn() + mockFetchPromise.catch(spy) + mockFetchPromise.rejectWith(new Error('fetch error')) + + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }) + })) + + it('should keep promise rejected behavior for any other type', () => + new Promise((resolve) => { + const mockFetchPromise = fetch(FAKE_URL) + const spy = vi.fn() + mockFetchPromise.catch(spy) + mockFetchPromise.rejectWith('fetch error' as any) + + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }) + })) + + it('should allow to enhance the context', () => + new Promise((resolve) => { + type CustomContext = FetchContext & { foo: string } + contextEditionSubscription = initFetchObservable().subscribe((rawContext) => { + const context = rawContext as CustomContext + if (context.state === 'start') { + context.foo = 'bar' + } + }) + fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) - mockFetchManager.whenAllComplete(() => { - expect((requests[0] as CustomContext).foo).toBe('bar') - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect((requests[0] as CustomContext).foo).toBe('bar') + resolve() + }) + })) describe('when unsubscribing', () => { - it('should stop tracking requests', (done) => { - requestsTrackingSubscription.unsubscribe() + it('should stop tracking requests', () => + new Promise((resolve) => { + requestsTrackingSubscription.unsubscribe() - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'ok' }) + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'ok' }) - mockFetchManager.whenAllComplete(() => { - expect(requests).toEqual([]) - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests).toEqual([]) + resolve() + }) + })) it('should restore original window.fetch', () => { requestsTrackingSubscription.unsubscribe() @@ -286,67 +306,72 @@ describe('fetch proxy with ResponseBodyAction', () => { resetFetchObservable() }) - it('should collect response body with COLLECT action', (done) => { - setupFetchTracking(() => ResponseBodyAction.COLLECT) + it('should collect response body with COLLECT action', () => + new Promise((resolve) => { + setupFetchTracking(() => ResponseBodyAction.COLLECT) - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' }) + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' }) - mockFetchManager.whenAllComplete(() => { - expect(requests[0].responseBody).toBe('response body content') - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].responseBody).toBe('response body content') + resolve() + }) + })) - it('should not collect response body with IGNORE action', (done) => { - setupFetchTracking(() => ResponseBodyAction.IGNORE) + it('should not collect response body with IGNORE action', () => + new Promise((resolve) => { + setupFetchTracking((): ResponseBodyAction => ResponseBodyAction.IGNORE) - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' }) + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' }) - mockFetchManager.whenAllComplete(() => { - expect(requests[0].responseBody).toBeUndefined() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].responseBody).toBeUndefined() + resolve() + }) + })) - it('should not collect response body when body is already used', (done) => { - setupFetchTracking(() => ResponseBodyAction.COLLECT) + it('should not collect response body when body is already used', () => + new Promise((resolve) => { + setupFetchTracking(() => ResponseBodyAction.COLLECT) - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content', bodyUsed: true }) + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content', bodyUsed: true }) - mockFetchManager.whenAllComplete(() => { - expect(requests[0].responseBody).toBeUndefined() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].responseBody).toBeUndefined() + resolve() + }) + })) - it('should not collect response body when body is disturbed', (done) => { - setupFetchTracking(() => ResponseBodyAction.COLLECT) + it('should not collect response body when body is disturbed', () => + new Promise((resolve) => { + setupFetchTracking(() => ResponseBodyAction.COLLECT) - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content', bodyDisturbed: true }) + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content', bodyDisturbed: true }) - mockFetchManager.whenAllComplete(() => { - expect(requests[0].responseBody).toBeUndefined() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].responseBody).toBeUndefined() + resolve() + }) + })) - it('should use the highest priority action when multiple getters are registered', (done) => { - setupFetchTracking(() => ResponseBodyAction.IGNORE) + it('should use the highest priority action when multiple getters are registered', () => + new Promise((resolve) => { + setupFetchTracking(() => ResponseBodyAction.IGNORE) - initFetchObservable({ - responseBodyAction: () => ResponseBodyAction.COLLECT, - }) + initFetchObservable({ + responseBodyAction: () => ResponseBodyAction.COLLECT, + }) - registerCleanupTask(() => { - requestsTrackingSubscription.unsubscribe() - }) + registerCleanupTask(() => { + requestsTrackingSubscription.unsubscribe() + }) - fetch = window.fetch as MockFetch - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' }) + fetch = window.fetch as MockFetch + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'response body content' }) - mockFetchManager.whenAllComplete(() => { - expect(requests[0].responseBody).toBe('response body content') - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(requests[0].responseBody).toBe('response body content') + resolve() + }) + })) }) diff --git a/packages/browser-core/src/browser/pageMayExitObservable.spec.ts b/packages/browser-core/src/browser/pageMayExitObservable.spec.ts index 6b407323ed..62a8a2bd12 100644 --- a/packages/browser-core/src/browser/pageMayExitObservable.spec.ts +++ b/packages/browser-core/src/browser/pageMayExitObservable.spec.ts @@ -1,12 +1,13 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, restorePageVisibility, setPageVisibility, registerCleanupTask } from '../../test' import type { PageMayExitEvent } from './pageMayExitObservable' import { PageExitReason, createPageMayExitObservable } from './pageMayExitObservable' describe('createPageMayExitObservable', () => { - let onExitSpy: jasmine.Spy<(event: PageMayExitEvent) => void> + let onExitSpy: Mock<(event: PageMayExitEvent) => void> beforeEach(() => { - onExitSpy = jasmine.createSpy() + onExitSpy = vi.fn() registerCleanupTask(createPageMayExitObservable().subscribe(onExitSpy).unsubscribe) }) @@ -17,19 +18,19 @@ describe('createPageMayExitObservable', () => { it('notifies when the page fires beforeunload', () => { window.dispatchEvent(createNewEvent('beforeunload')) - expect(onExitSpy).toHaveBeenCalledOnceWith({ reason: PageExitReason.UNLOADING }) + expect(onExitSpy).toHaveBeenCalledExactlyOnceWith({ reason: PageExitReason.UNLOADING }) }) it('notifies when the page becomes hidden', () => { emulatePageVisibilityChange('hidden') - expect(onExitSpy).toHaveBeenCalledOnceWith({ reason: PageExitReason.HIDDEN }) + expect(onExitSpy).toHaveBeenCalledExactlyOnceWith({ reason: PageExitReason.HIDDEN }) }) it('notifies when the page becomes frozen', () => { window.dispatchEvent(createNewEvent('freeze')) - expect(onExitSpy).toHaveBeenCalledOnceWith({ reason: PageExitReason.FROZEN }) + expect(onExitSpy).toHaveBeenCalledExactlyOnceWith({ reason: PageExitReason.FROZEN }) }) it('notifies multiple times', () => { diff --git a/packages/browser-core/src/browser/webSocketObservable.spec.ts b/packages/browser-core/src/browser/webSocketObservable.spec.ts index 05ef1ccf96..2db8f1a755 100644 --- a/packages/browser-core/src/browser/webSocketObservable.spec.ts +++ b/packages/browser-core/src/browser/webSocketObservable.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' import { registerCleanupTask } from '../../test' import type { Subscription } from '../tools/observable' import { setAllowUntrustedEvents } from './addEventListener' @@ -117,7 +118,7 @@ describe('webSocketObservable', () => { expect(connectingContexts.length).toBe(1) expect(connectingContexts[0].url).toBe(url) expect(connectingContexts[0].instance).toBe(ws as unknown as WebSocket) - expect(connectingContexts[0].startClocks.timeStamp).toEqual(jasmine.any(Number)) + expect(connectingContexts[0].startClocks.timeStamp).toEqual(expect.any(Number)) }) it('reports the resolved instance.url rather than the raw constructor argument', () => { @@ -154,7 +155,7 @@ describe('webSocketObservable', () => { describe('preservation of native behavior', () => { it('does not clobber a customer-set onmessage handler', () => { const ws = new windowAsWebSocketHost.WebSocket('wss://example.com/socket') - const customerHandler = jasmine.createSpy() + const customerHandler = vi.fn() ws.onmessage = customerHandler ws.simulateMessage('hello') @@ -165,7 +166,7 @@ describe('webSocketObservable', () => { it('does not clobber a customer-set onopen handler', () => { const ws = new windowAsWebSocketHost.WebSocket('wss://example.com/socket') - const customerHandler = jasmine.createSpy() + const customerHandler = vi.fn() ws.onopen = customerHandler ws.simulateOpen() @@ -176,7 +177,7 @@ describe('webSocketObservable', () => { it('does not clobber a customer-set onclose handler', () => { const ws = new windowAsWebSocketHost.WebSocket('wss://example.com/socket') - const customerHandler = jasmine.createSpy() + const customerHandler = vi.fn() ws.onclose = customerHandler ws.simulateClose(1000, 'bye', true) @@ -197,7 +198,7 @@ describe('webSocketObservable', () => { expect(openContexts.length).toBe(1) expect(openContexts[0].protocol).toBe(negotiatedProtocol) expect(openContexts[0].instance).toBe(ws as unknown as WebSocket) - expect(openContexts[0].openClocks.timeStamp).toEqual(jasmine.any(Number)) + expect(openContexts[0].openClocks.timeStamp).toEqual(expect.any(Number)) }) it('emits an "open" context with empty protocol when no sub-protocol negotiated', () => { @@ -272,7 +273,7 @@ describe('webSocketObservable', () => { expect(messageOutContexts.length).toBe(1) expect(messageOutContexts[0].size).toBe(payload.length) expect(messageOutContexts[0].bufferedAmountPreSend).toBe(bufferedAmountPreSend) - expect(messageOutContexts[0].at.timeStamp).toEqual(jasmine.any(Number)) + expect(messageOutContexts[0].at.timeStamp).toEqual(expect.any(Number)) }) it('emits "message-out" with byteLength for ArrayBuffer payloads', () => { @@ -313,7 +314,7 @@ describe('webSocketObservable', () => { expect(closeContexts[0].code).toBe(closeCode) expect(closeContexts[0].reason).toBe(closeReason) expect(closeContexts[0].wasClean).toBe(wasClean) - expect(closeContexts[0].at.timeStamp).toEqual(jasmine.any(Number)) + expect(closeContexts[0].at.timeStamp).toEqual(expect.any(Number)) }) }) diff --git a/packages/browser-core/src/browser/xhrObservable.spec.ts b/packages/browser-core/src/browser/xhrObservable.spec.ts index 605751e2fd..3a23c91392 100644 --- a/packages/browser-core/src/browser/xhrObservable.spec.ts +++ b/packages/browser-core/src/browser/xhrObservable.spec.ts @@ -1,9 +1,10 @@ +import { vi, afterEach, beforeEach, describe, expect, it } from 'vitest' import { withXhr, mockXhr } from '../../test' import type { Subscription } from '../tools/observable' import { noop } from '../tools/utils/functionUtils' import type { XhrCompleteContext, XhrContext } from './xhrObservable' +import { resetAllowUntrustedEvents, setAllowUntrustedEvents } from './addEventListener' import { initXhrObservable, resetXhrObservable } from './xhrObservable' -import { setAllowUntrustedEvents } from './addEventListener' describe('xhr observable', () => { let requestsTrackingSubscription: Subscription @@ -33,346 +34,362 @@ describe('xhr observable', () => { }) } - it('should track successful request', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200, 'ok') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/ok') - expect(request.status).toBe(200) - expect(request.isAborted).toBe(false) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) + it('should track successful request', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/ok') + expect(request.status).toBe(200) + expect(request.isAborted).toBe(false) + expect(request.duration).toEqual(expect.any(Number)) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should sanitize request method', (done) => { - withXhr({ - setup(xhr) { - xhr.open('get', '/ok') - xhr.send() - xhr.complete(200, 'ok') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) + it('should sanitize request method', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('get', '/ok') + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should track client error', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/expected-404') - xhr.send() - xhr.complete(404, 'NOT FOUND') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/expected-404') - expect(request.status).toBe(404) - expect(request.isAborted).toBe(false) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) + it('should track client error', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/expected-404') + xhr.send() + xhr.complete(404, 'NOT FOUND') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/expected-404') + expect(request.status).toBe(404) + expect(request.isAborted).toBe(false) + expect(request.duration).toEqual(expect.any(Number)) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should track server error', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/throw') - xhr.send() - xhr.complete(500, 'expected server error') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/throw') - expect(request.status).toBe(500) - expect(request.isAborted).toBe(false) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) + it('should track server error', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/throw') + xhr.send() + xhr.complete(500, 'expected server error') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/throw') + expect(request.status).toBe(500) + expect(request.isAborted).toBe(false) + expect(request.duration).toEqual(expect.any(Number)) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should track network error', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', 'http://foo.bar/qux') - xhr.send() - xhr.complete(0, '') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toBe('http://foo.bar/qux') - expect(request.status).toBe(0) - expect(request.isAborted).toBe(false) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) + it('should track network error', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', 'http://foo.bar/qux') + xhr.send() + xhr.complete(0, '') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toBe('http://foo.bar/qux') + expect(request.status).toBe(0) + expect(request.isAborted).toBe(false) + expect(request.duration).toEqual(expect.any(Number)) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should track successful request aborted', (done) => { - withXhr({ - setup(xhr) { - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - xhr.abort() + it('should track successful request aborted', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + xhr.abort() + } } - } - spyOn(xhr, 'onreadystatechange').and.callThrough() - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200, 'ok') - }, - onComplete(xhr) { - const request = requests[0] - expect(requests.length).toBe(1) - expect(request.method).toBe('GET') - expect(request.url).toContain('/ok') - expect(request.status).toBe(200) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(request.isAborted).toBe(false) - expect(xhr.status).toBe(0) - expect(xhr.onreadystatechange).toHaveBeenCalledTimes(1) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) - - it('should track aborted requests', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.abort() - }, - onComplete(xhr) { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/ok') - expect(request.status).toBe(0) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(request.isAborted).toBe(true) - expect(xhr.status).toBe(0) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) - - it('should track request with onreadystatechange overridden before open', (done) => { - withXhr({ - setup(xhr) { - xhr.onreadystatechange = jasmine.createSpy() - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200, 'ok') - }, - onComplete(xhr) { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/ok') - expect(request.status).toBe(200) - expect(request.isAborted).toBe(false) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(xhr.onreadystatechange).toHaveBeenCalled() - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) - - it('should track request with onreadystatechange overridden after open', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.onreadystatechange = jasmine.createSpy() - xhr.complete(200, 'ok') - }, - onComplete(xhr) { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/ok') - expect(request.status).toBe(200) - expect(request.isAborted).toBe(false) - expect(request.duration).toEqual(jasmine.any(Number)) - expect(xhr.onreadystatechange).toHaveBeenCalled() - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) + vi.spyOn(xhr, 'onreadystatechange') + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete(xhr) { + const request = requests[0] + expect(requests.length).toBe(1) + expect(request.method).toBe('GET') + expect(request.url).toContain('/ok') + expect(request.status).toBe(200) + expect(request.duration).toEqual(expect.any(Number)) + expect(request.isAborted).toBe(false) + expect(xhr.status).toBe(0) + expect(xhr.onreadystatechange).toHaveBeenCalledTimes(1) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should allow to enhance the context', (done) => { - type CustomContext = XhrContext & { foo: string } - contextEditionSubscription = initXhrObservable().subscribe((rawContext) => { - const context = rawContext as CustomContext - if (context.state === 'start') { - context.foo = 'bar' - } - }) - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200) - }, - onComplete() { - const request = requests[0] - expect((request as CustomContext).foo).toBe('bar') - done() - }, - }) - }) + it('should track aborted requests', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.abort() + }, + onComplete(xhr) { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/ok') + expect(request.status).toBe(0) + expect(request.duration).toEqual(expect.any(Number)) + expect(request.isAborted).toBe(true) + expect(xhr.status).toBe(0) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should not break xhr opened before the instrumentation', (done) => { - requestsTrackingSubscription.unsubscribe() - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - startTrackingRequests() - xhr.send() - xhr.complete(200) - }, - onComplete() { - expect(requests.length).toBe(0) - done() - }, - }) - }) + it('should track request with onreadystatechange overridden before open', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.onreadystatechange = vi.fn() + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete(xhr) { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/ok') + expect(request.status).toBe(200) + expect(request.isAborted).toBe(false) + expect(request.duration).toEqual(expect.any(Number)) + expect(xhr.onreadystatechange).toHaveBeenCalled() + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - it('should track multiple requests with the same xhr instance', (done) => { - let listeners: { [k: string]: Array<(event: Event) => void> } - withXhr({ - setup(xhr) { - const secondOnload = () => { - xhr.removeEventListener('load', secondOnload) - } - const onLoad = () => { - xhr.removeEventListener('load', onLoad) - xhr.addEventListener('load', secondOnload) - xhr.open('GET', '/ok?request=2') + it('should track request with onreadystatechange overridden after open', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') xhr.send() - xhr.complete(400, 'ok') + xhr.onreadystatechange = vi.fn() + xhr.complete(200, 'ok') + }, + onComplete(xhr) { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/ok') + expect(request.status).toBe(200) + expect(request.isAborted).toBe(false) + expect(request.duration).toEqual(expect.any(Number)) + expect(xhr.onreadystatechange).toHaveBeenCalled() + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) + + it('should allow to enhance the context', () => + new Promise((resolve) => { + type CustomContext = XhrContext & { foo: string } + contextEditionSubscription = initXhrObservable().subscribe((rawContext) => { + const context = rawContext as CustomContext + if (context.state === 'start') { + context.foo = 'bar' } - xhr.onreadystatechange = jasmine.createSpy() - xhr.addEventListener('load', onLoad) - xhr.open('GET', '/ok?request=1') - xhr.send() - xhr.complete(200, 'ok') - listeners = xhr.listeners - }, - onComplete(xhr) { - const firstRequest = requests[0] - expect(firstRequest.method).toBe('GET') - expect(firstRequest.url).toContain('/ok?request=1') - expect(firstRequest.status).toBe(200) - expect(firstRequest.isAborted).toBe(false) - expect(firstRequest.duration).toEqual(jasmine.any(Number)) - expect(firstRequest.handlingStack).toBeDefined() - - const secondRequest = requests[1] - expect(secondRequest.method).toBe('GET') - expect(secondRequest.url).toContain('/ok?request=2') - expect(secondRequest.status).toBe(400) - expect(secondRequest.isAborted).toBe(false) - expect(secondRequest.duration).toEqual(jasmine.any(Number)) - expect(secondRequest.handlingStack).toBeDefined() - - expect(xhr.onreadystatechange).toHaveBeenCalledTimes(2) - expect(listeners.load.length).toBe(0) - expect(listeners.loadend.length).toBe(0) - done() - }, - }) - }) - - it('should track request to undefined url', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', undefined) - xhr.send() - xhr.complete(404, 'NOT FOUND') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/undefined') - expect(request.status).toBe(404) - done() - }, - }) - }) - - it('should track request to null url', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', null) - xhr.send() - xhr.complete(404, 'NOT FOUND') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toContain('/null') - expect(request.status).toBe(404) - done() - }, - }) - }) - - it('should track request to URL object', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', new URL('http://example.com/path')) - xhr.send() - xhr.complete(200, 'ok') - }, - onComplete() { - const request = requests[0] - expect(request.method).toBe('GET') - expect(request.url).toBe('http://example.com/path') - done() - }, - }) - }) + }) + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200) + }, + onComplete() { + const request = requests[0] + expect((request as CustomContext).foo).toBe('bar') + resolve() + }, + }) + })) - describe('when unsubscribing', () => { - it('should stop tracking requests', (done) => { + it('should not break xhr opened before the instrumentation', () => + new Promise((resolve) => { requestsTrackingSubscription.unsubscribe() - withXhr({ setup(xhr) { xhr.open('GET', '/ok') + startTrackingRequests() xhr.send() xhr.complete(200) }, onComplete() { expect(requests.length).toBe(0) - done() + resolve() }, }) - }) + })) + + it('should track multiple requests with the same xhr instance', () => + new Promise((resolve) => { + let listeners: { [k: string]: Array<(event: Event) => void> } + withXhr({ + setup(xhr) { + const secondOnload = () => { + xhr.removeEventListener('load', secondOnload) + } + const onLoad = () => { + xhr.removeEventListener('load', onLoad) + xhr.addEventListener('load', secondOnload) + xhr.open('GET', '/ok?request=2') + xhr.send() + xhr.complete(400, 'ok') + } + xhr.onreadystatechange = vi.fn() + xhr.addEventListener('load', onLoad) + xhr.open('GET', '/ok?request=1') + xhr.send() + xhr.complete(200, 'ok') + listeners = xhr.listeners + }, + onComplete(xhr) { + const firstRequest = requests[0] + expect(firstRequest.method).toBe('GET') + expect(firstRequest.url).toContain('/ok?request=1') + expect(firstRequest.status).toBe(200) + expect(firstRequest.isAborted).toBe(false) + expect(firstRequest.duration).toEqual(expect.any(Number)) + expect(firstRequest.handlingStack).toBeDefined() + + const secondRequest = requests[1] + expect(secondRequest.method).toBe('GET') + expect(secondRequest.url).toContain('/ok?request=2') + expect(secondRequest.status).toBe(400) + expect(secondRequest.isAborted).toBe(false) + expect(secondRequest.duration).toEqual(expect.any(Number)) + expect(secondRequest.handlingStack).toBeDefined() + + expect(xhr.onreadystatechange).toHaveBeenCalledTimes(2) + expect(listeners.load.length).toBe(0) + expect(listeners.loadend.length).toBe(0) + resolve() + }, + }) + })) + + it('should track request to undefined url', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', undefined) + xhr.send() + xhr.complete(404, 'NOT FOUND') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/undefined') + expect(request.status).toBe(404) + resolve() + }, + }) + })) + + it('should track request to null url', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', null) + xhr.send() + xhr.complete(404, 'NOT FOUND') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toContain('/null') + expect(request.status).toBe(404) + resolve() + }, + }) + })) + + it('should track request to URL object', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', new URL('http://example.com/path')) + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + expect(request.url).toBe('http://example.com/path') + resolve() + }, + }) + })) + + describe('when unsubscribing', () => { + it('should stop tracking requests', () => + new Promise((resolve) => { + requestsTrackingSubscription.unsubscribe() + + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200) + }, + onComplete() { + expect(requests.length).toBe(0) + resolve() + }, + }) + })) it('should restore original XMLHttpRequest methods', () => { requestsTrackingSubscription.unsubscribe() @@ -382,27 +399,32 @@ describe('xhr observable', () => { }) }) - it('should track request with undefined or null methods', (done) => { - withXhr({ - setup(xhr) { - xhr.open(null, '/ok') - xhr.send() - xhr.onreadystatechange = jasmine.createSpy() - xhr.complete(200, 'ok') - xhr.open(undefined, '/ok') - xhr.send() - xhr.onreadystatechange = jasmine.createSpy() - xhr.complete(200, 'ok') - }, - onComplete() { - expect(requests[0].method).toBe('NULL') - expect(requests[1].method).toBe('UNDEFINED') - done() - }, - }) - }) + it('should track request with undefined or null methods', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open(null, '/ok') + xhr.send() + xhr.onreadystatechange = vi.fn() + xhr.complete(200, 'ok') + xhr.open(undefined, '/ok') + xhr.send() + xhr.onreadystatechange = vi.fn() + xhr.complete(200, 'ok') + }, + onComplete() { + expect(requests[0].method).toBe('NULL') + expect(requests[1].method).toBe('UNDEFINED') + resolve() + }, + }) + })) - describe('untrusted loadend events', () => { + describe('with conflicting allowUntrustedEvents policies across callers', () => { + // Reproduces the bug where the early bufferedData call to + // initXhrObservable({ allowUntrustedEvents: true }) creates the singleton + // and a later initXhrObservable(customerConfig) silently reuses it, + // discarding the customer's stricter policy. let policySubscription: Subscription let policyContexts: XhrContext[] @@ -415,50 +437,62 @@ describe('xhr observable', () => { afterEach(() => { policySubscription.unsubscribe() resetXhrObservable() + resetAllowUntrustedEvents() // The outer afterEach calls requestsTrackingSubscription.unsubscribe(); we already // unsubscribed it in beforeEach, so give it a no-op stub. requestsTrackingSubscription = { unsubscribe: noop } }) - it('does not emit completion for an untrusted loadend when disallowed', (done) => { - setAllowUntrustedEvents(false) - policySubscription = initXhrObservable().subscribe((context) => { - policyContexts.push(context) - }) - - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - // Untrusted Event (constructor-built, no __ddIsTrusted marker) - xhr.dispatchEvent(new Event('loadend')) - }, - onComplete() { - const completions = policyContexts.filter((context) => context.state === 'complete') - expect(completions).toEqual([]) - done() - }, - }) - }) - - it('emits completion for an untrusted loadend when allowed', (done) => { - setAllowUntrustedEvents(true) - policySubscription = initXhrObservable().subscribe((context) => { - policyContexts.push(context) - }) - - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.dispatchEvent(new Event('loadend')) - }, - onComplete() { - const completions = policyContexts.filter((context) => context.state === 'complete') - expect(completions.length).toBe(1) - done() - }, - }) - }) + it('does not emit completion for an untrusted loadend when the customer disallows it', () => + new Promise((resolve) => { + // First caller (bufferedData) opts into untrusted events. + setAllowUntrustedEvents(true) + initXhrObservable() + // Second caller (customer config) opts out. + setAllowUntrustedEvents(false) + policySubscription = initXhrObservable().subscribe((context) => { + policyContexts.push(context) + }) + + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + // Untrusted Event (constructor-built, no __ddIsTrusted marker) + xhr.dispatchEvent(new Event('loadend')) + // Send the real (trusted) loadend so onComplete fires and we can assert + xhr.complete(200, 'ok') + }, + onComplete() { + const completions = policyContexts.filter((context) => context.state === 'complete') + // Only the trusted loadend should produce a completion, not the untrusted one + expect(completions.length).toBe(1) + resolve() + }, + }) + })) + + it('emits completion for an untrusted loadend when every caller allows it', () => + new Promise((resolve) => { + setAllowUntrustedEvents(true) + initXhrObservable() + setAllowUntrustedEvents(true) + policySubscription = initXhrObservable().subscribe((context) => { + policyContexts.push(context) + }) + + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.dispatchEvent(new Event('loadend')) + }, + onComplete() { + const completions = policyContexts.filter((context) => context.state === 'complete') + expect(completions.length).toBe(1) + resolve() + }, + }) + })) }) }) diff --git a/packages/browser-core/src/domain/allowedTrackingOrigins.spec.ts b/packages/browser-core/src/domain/allowedTrackingOrigins.spec.ts index 2f99af291a..f4af4aafdd 100644 --- a/packages/browser-core/src/domain/allowedTrackingOrigins.spec.ts +++ b/packages/browser-core/src/domain/allowedTrackingOrigins.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { replaceMockable, STACK_WITH_INIT_IN_EXTENSION, STACK_WITH_INIT_IN_PAGE } from '../../test' import { display } from '../tools/display' import { @@ -13,14 +14,14 @@ const DEFAULT_CONFIG = { } describe('checkForAllowedTrackingOrigins', () => { - let displayErrorSpy: jasmine.Spy + let displayErrorSpy: Mock function mockOrigin(origin: string) { replaceMockable(location, { origin } as Location) } beforeEach(() => { - displayErrorSpy = spyOn(display, 'error') + displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) }) it('should not warn if not in extension environment', () => { diff --git a/packages/browser-core/src/domain/bufferedData.spec.ts b/packages/browser-core/src/domain/bufferedData.spec.ts index fc89a8851b..374cb3f0b9 100644 --- a/packages/browser-core/src/domain/bufferedData.spec.ts +++ b/packages/browser-core/src/domain/bufferedData.spec.ts @@ -1,7 +1,16 @@ +import { vi, describe, expect, it } from 'vitest' import { clocksNow } from '@datadog/js-core/time' import { ConsoleApiName } from '@datadog/js-core/util' import type { MockFetch } from '../../test' -import { collectAsyncCalls, mockFetch, mockXhr, registerCleanupTask, replaceMockable, withXhr } from '../../test' +import { + allowCurrentTestConsoleLogs, + collectAsyncCalls, + mockFetch, + mockXhr, + registerCleanupTask, + replaceMockable, + withXhr, +} from '../../test' import { Observable } from '../tools/observable' import { resetFetchObservable } from '../browser/fetchObservable' import { resetXhrObservable } from '../browser/xhrObservable' @@ -13,40 +22,41 @@ import { ErrorHandling, ErrorSource, type RawError } from './error/error.types' import { trackRuntimeError } from './error/trackRuntimeError' describe('startBufferingData', () => { - it('collects runtime errors', (done) => { - const runtimeErrorObservable = new Observable() - replaceMockable(trackRuntimeError, () => runtimeErrorObservable) - const { observable, stop } = startBufferingData() - registerCleanupTask(stop) - - const rawError = { - startClocks: clocksNow(), - source: ErrorSource.SOURCE, - type: 'Error', - stack: 'Error: error!', - handling: ErrorHandling.UNHANDLED, - causes: undefined, - fingerprint: undefined, - message: 'error!', - } + it('collects runtime errors', () => + new Promise((resolve) => { + const runtimeErrorObservable = new Observable() + replaceMockable(trackRuntimeError, () => runtimeErrorObservable) + const { observable, stop } = startBufferingData() + registerCleanupTask(stop) + + const rawError = { + startClocks: clocksNow(), + source: ErrorSource.SOURCE, + type: 'Error', + stack: 'Error: error!', + handling: ErrorHandling.UNHANDLED, + causes: undefined, + fingerprint: undefined, + message: 'error!', + } - runtimeErrorObservable.notify(rawError) + runtimeErrorObservable.notify(rawError) - observable.subscribe((data) => { - expect(data).toEqual({ - type: BufferedDataType.RUNTIME_ERROR, - data: rawError, + observable.subscribe((data) => { + expect(data).toEqual({ + type: BufferedDataType.RUNTIME_ERROR, + data: rawError, + }) + resolve() }) - done() - }) - }) + })) it('collects fetch requests', async () => { mockFetch() const { observable, stop } = startBufferingData() const fetch = window.fetch as MockFetch const collected: BufferedData[] = [] - const bufferedDataCollectedSpy = jasmine.createSpy() + const bufferedDataCollectedSpy = vi.fn() registerCleanupTask(() => { stop() @@ -67,7 +77,7 @@ describe('startBufferingData', () => { expect(collected).toEqual([ { type: BufferedDataType.FETCH, - data: jasmine.objectContaining({ + data: expect.objectContaining({ state: 'start', url: 'http://fake-url/', method: 'GET', @@ -75,7 +85,7 @@ describe('startBufferingData', () => { }, { type: BufferedDataType.FETCH, - data: jasmine.objectContaining({ + data: expect.objectContaining({ state: 'resolve', url: 'http://fake-url/', method: 'GET', @@ -89,7 +99,7 @@ describe('startBufferingData', () => { mockXhr() const { observable, stop } = startBufferingData() const collected: BufferedData[] = [] - const bufferedDataCollectedSpy = jasmine.createSpy() + const bufferedDataCollectedSpy = vi.fn() registerCleanupTask(() => { stop() @@ -117,7 +127,7 @@ describe('startBufferingData', () => { expect(collected).toEqual([ { type: BufferedDataType.XHR, - data: jasmine.objectContaining({ + data: expect.objectContaining({ state: 'start', url: 'http://fake-url/', method: 'GET', @@ -125,7 +135,7 @@ describe('startBufferingData', () => { }, { type: BufferedDataType.XHR, - data: jasmine.objectContaining({ + data: expect.objectContaining({ state: 'complete', url: 'http://fake-url/', method: 'GET', @@ -135,24 +145,25 @@ describe('startBufferingData', () => { ]) }) - it('collects console logs', (done) => { - spyOn(console, 'error').and.callFake(noop) - const { observable, stop } = startBufferingData() + it('collects console logs', () => + new Promise((resolve) => { + allowCurrentTestConsoleLogs() + const { observable, stop } = startBufferingData() - registerCleanupTask(() => { - stop() - resetConsoleObservable() - }) + registerCleanupTask(() => { + stop() + resetConsoleObservable() + }) - observable.subscribe((data) => { - if (data.type === BufferedDataType.CONSOLE && data.data.api === ConsoleApiName.error) { - expect(data.data.message).toContain('buffered data test error') - done() - } - }) + observable.subscribe((data) => { + if (data.type === BufferedDataType.CONSOLE && data.data.api === ConsoleApiName.error) { + expect(data.data.message).toContain('buffered data test error') + resolve() + } + }) - /* eslint-disable no-console */ - console.error('buffered data test error') - /* eslint-enable no-console */ - }) + /* eslint-disable no-console */ + console.error('buffered data test error') + /* eslint-enable no-console */ + })) }) diff --git a/packages/browser-core/src/domain/configuration/configuration.spec.ts b/packages/browser-core/src/domain/configuration/configuration.spec.ts index d076a1c178..0f22ec5aa7 100644 --- a/packages/browser-core/src/domain/configuration/configuration.spec.ts +++ b/packages/browser-core/src/domain/configuration/configuration.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RumEvent } from '../../../../browser-rum-core/src' import { EXHAUSTIVE_INIT_CONFIGURATION, SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION } from '../../../test' import type { ExtractTelemetryConfiguration, MapInitConfigurationKey } from '../../../test' @@ -10,10 +11,10 @@ import { buildCookieOptions, serializeConfiguration, validateAndBuildConfigurati describe('validateAndBuildConfiguration', () => { const clientToken = 'some_client_token' - let displaySpy: jasmine.Spy + let displaySpy: Mock beforeEach(() => { - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) }) describe('experimentalFeatures', () => { @@ -32,22 +33,22 @@ describe('validateAndBuildConfiguration', () => { clientToken, enableExperimentalFeatures: ['bar', undefined as any, null as any, 11 as any], }) - expect(isExperimentalFeatureEnabled('bar' as any)).toBeFalse() - expect(isExperimentalFeatureEnabled(undefined as any)).toBeFalse() - expect(isExperimentalFeatureEnabled(null as any)).toBeFalse() - expect(isExperimentalFeatureEnabled(11 as any)).toBeFalse() + expect(isExperimentalFeatureEnabled('bar' as any)).toBe(false) + expect(isExperimentalFeatureEnabled(undefined as any)).toBe(false) + expect(isExperimentalFeatureEnabled(null as any)).toBe(false) + expect(isExperimentalFeatureEnabled(11 as any)).toBe(false) }) }) describe('validate init configuration', () => { it('requires the InitConfiguration to be defined', () => { expect(validateAndBuildConfiguration(undefined as unknown as InitConfiguration)).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Client Token is not configured, we will not send any data.') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Client Token is not configured, we will not send any data.') }) it('requires clientToken to be defined', () => { expect(validateAndBuildConfiguration({} as unknown as InitConfiguration)).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Client Token is not configured, we will not send any data.') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Client Token is not configured, we will not send any data.') }) it("shouldn't display any error if the configuration is correct", () => { @@ -59,13 +60,13 @@ describe('validateAndBuildConfiguration', () => { expect( validateAndBuildConfiguration({ clientToken, sessionSampleRate: 'foo' } as unknown as InitConfiguration) ).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Session Sample Rate should be a number between 0 and 100') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Session Sample Rate should be a number between 0 and 100') - displaySpy.calls.reset() + displaySpy.mockClear() expect(validateAndBuildConfiguration({ clientToken, sessionSampleRate: 200 })).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Session Sample Rate should be a number between 0 and 100') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Session Sample Rate should be a number between 0 and 100') - displaySpy.calls.reset() + displaySpy.mockClear() validateAndBuildConfiguration({ clientToken: 'yes', sessionSampleRate: 1 }) expect(displaySpy).not.toHaveBeenCalled() }) @@ -74,13 +75,13 @@ describe('validateAndBuildConfiguration', () => { expect( validateAndBuildConfiguration({ clientToken, telemetrySampleRate: 'foo' } as unknown as InitConfiguration) ).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Telemetry Sample Rate should be a number between 0 and 100') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Telemetry Sample Rate should be a number between 0 and 100') - displaySpy.calls.reset() + displaySpy.mockClear() expect(validateAndBuildConfiguration({ clientToken, telemetrySampleRate: 200 })).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Telemetry Sample Rate should be a number between 0 and 100') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Telemetry Sample Rate should be a number between 0 and 100') - displaySpy.calls.reset() + displaySpy.mockClear() validateAndBuildConfiguration({ clientToken: 'yes', telemetrySampleRate: 1 }) expect(displaySpy).not.toHaveBeenCalled() }) @@ -128,7 +129,7 @@ describe('validateAndBuildConfiguration', () => { } } const configuration = validateAndBuildConfiguration({ clientToken, beforeSend })! - expect(configuration.beforeSend!({ view: { url: '/foo' } }, {})).toBeFalse() + expect(configuration.beforeSend!({ view: { url: '/foo' } }, {})).toBe(false) expect(configuration.beforeSend!({ view: { url: '/bar' } }, {})).toBeUndefined() }) @@ -161,14 +162,14 @@ describe('validateAndBuildConfiguration', () => { it('rejects invalid values', () => { expect(validateAndBuildConfiguration({ clientToken: 'yes', trackingConsent: 'foo' as any })).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Tracking Consent should be either "granted" or "not-granted"') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Tracking Consent should be either "granted" or "not-granted"') }) }) describe('site parameter validation', () => { it('should validate the site parameter', () => { validateAndBuildConfiguration({ clientToken, site: 'foo.com' }) - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( `Site should be a valid Datadog site. ${MORE_DETAILS} ${DOCS_ORIGIN}/getting_started/site/.` ) }) @@ -177,14 +178,14 @@ describe('validateAndBuildConfiguration', () => { describe('env parameter validation', () => { it('should validate the env parameter', () => { validateAndBuildConfiguration({ clientToken, env: false as any }) - expect(displaySpy).toHaveBeenCalledOnceWith('Env must be defined as a string') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Env must be defined as a string') }) }) describe('service parameter validation', () => { it('should validate the service parameter', () => { validateAndBuildConfiguration({ clientToken, service: 1 as any }) - expect(displaySpy).toHaveBeenCalledOnceWith('Service must be defined as a string') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Service must be defined as a string') }) it('should not reject null', () => { @@ -197,14 +198,14 @@ describe('validateAndBuildConfiguration', () => { describe('version parameter validation', () => { it('should validate the version parameter', () => { validateAndBuildConfiguration({ clientToken, version: 0 as any }) - expect(displaySpy).toHaveBeenCalledOnceWith('Version must be defined as a string') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Version must be defined as a string') }) }) describe('allowedTrackingOrigins parameter validation', () => { it('should validate the allowedTrackingOrigins parameter', () => { validateAndBuildConfiguration({ clientToken, allowedTrackingOrigins: 'foo' as any }) - expect(displaySpy).toHaveBeenCalledOnceWith('Allowed Tracking Origins must be an array') + expect(displaySpy).toHaveBeenCalledExactlyOnceWith('Allowed Tracking Origins must be an array') }) }) @@ -244,7 +245,7 @@ describe('buildCookieOptions', () => { secure: false, crossSite: false, partitioned: false, - domain: jasmine.any(String), + domain: expect.any(String), }) }) }) diff --git a/packages/browser-core/src/domain/configuration/endpointBuilder.spec.ts b/packages/browser-core/src/domain/configuration/endpointBuilder.spec.ts index 031f74794c..ca22386416 100644 --- a/packages/browser-core/src/domain/configuration/endpointBuilder.spec.ts +++ b/packages/browser-core/src/domain/configuration/endpointBuilder.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { EndpointPayload } from '@datadog/js-core/transport' import { buildEndpointUrl, createEndpointBuilder, createReplicaEndpointBuilder } from '@datadog/js-core/transport' import type { InitConfiguration } from './configuration' @@ -16,7 +17,7 @@ describe('endpointBuilder', () => { describe('query parameters', () => { it('should add intake query parameters', () => { expect(createEndpointBuilder(initConfiguration, 'rum').build('fetch', DEFAULT_PAYLOAD)).toMatch( - `&dd-api-key=${clientToken}&dd-evp-origin-version=(.*)&dd-evp-origin=browser&dd-request-id=(.*)` + new RegExp(`&dd-api-key=${clientToken}&dd-evp-origin-version=(.*)&dd-evp-origin=browser&dd-request-id=(.*)`) ) }) @@ -49,17 +50,14 @@ describe('endpointBuilder', () => { describe('proxy configuration', () => { it('should replace the intake endpoint by the proxy and set the intake path and parameters in the attribute ddforward', () => { - expect( - createEndpointBuilder({ ...initConfiguration, proxy: 'https://proxy.io/path' }, 'rum').build( - 'fetch', - DEFAULT_PAYLOAD - ) - ).toMatch( - `https://proxy.io/path\\?ddforward=${encodeURIComponent( - `/api/v2/rum?ddsource=(.*)&dd-api-key=${clientToken}` + - '&dd-evp-origin-version=(.*)&dd-evp-origin=browser&dd-request-id=(.*)&batch_time=(.*)' - )}` + const url = createEndpointBuilder({ ...initConfiguration, proxy: 'https://proxy.io/path' }, 'rum').build( + 'fetch', + DEFAULT_PAYLOAD ) + expect(url).toContain('https://proxy.io/path?ddforward=') + expect(url).toContain(encodeURIComponent('/api/v2/rum?ddsource=')) + expect(url).toContain(encodeURIComponent(`&dd-api-key=${clientToken}`)) + expect(url).toContain(encodeURIComponent('&dd-evp-origin=browser')) }) it('normalizes the proxy url', () => { @@ -67,17 +65,17 @@ describe('endpointBuilder', () => { 'fetch', DEFAULT_PAYLOAD ) - expect(endpoint.startsWith(`${location.origin}/path?ddforward`)).toBeTrue() + expect(endpoint.startsWith(`${location.origin}/path?ddforward`)).toBe(true) }) it('should allow to fully control the proxy url', () => { const proxyFn = (options: { path: string; parameters: string }) => `https://proxy.io/prefix${options.path}/suffix?${options.parameters}` - expect( - createEndpointBuilder({ ...initConfiguration, proxy: proxyFn }, 'rum').build('fetch', DEFAULT_PAYLOAD) - ).toMatch( - `https://proxy.io/prefix/api/v2/rum/suffix\\?ddsource=(.*)&dd-api-key=${clientToken}&dd-evp-origin-version=(.*)&dd-evp-origin=browser&dd-request-id=(.*)&batch_time=(.*)` - ) + const url = createEndpointBuilder({ ...initConfiguration, proxy: proxyFn }, 'rum').build('fetch', DEFAULT_PAYLOAD) + expect(url).toContain('https://proxy.io/prefix/api/v2/rum/suffix?ddsource=') + expect(url).toContain(`&dd-api-key=${clientToken}`) + expect(url).toContain('&dd-evp-origin=browser') + expect(url).toContain('&batch_time=') }) }) diff --git a/packages/browser-core/src/domain/connectivity/connectivity.spec.ts b/packages/browser-core/src/domain/connectivity/connectivity.spec.ts index 42d9c0445e..f0bbf108df 100644 --- a/packages/browser-core/src/domain/connectivity/connectivity.spec.ts +++ b/packages/browser-core/src/domain/connectivity/connectivity.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { setNavigatorOnLine, setNavigatorConnection } from '../../../test' import { getConnectivity } from './connectivity' diff --git a/packages/browser-core/src/domain/console/consoleObservable.spec.ts b/packages/browser-core/src/domain/console/consoleObservable.spec.ts index 1b1ce6b5e1..fdc3c8d67f 100644 --- a/packages/browser-core/src/domain/console/consoleObservable.spec.ts +++ b/packages/browser-core/src/domain/console/consoleObservable.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' /* eslint-disable no-console */ import { ConsoleApiName } from '@datadog/js-core/util' import { ignoreConsoleLogs } from '../../../test' @@ -15,13 +16,13 @@ import { initConsoleObservable } from './consoleObservable' { api: ConsoleApiName.error }, ].forEach(({ api }) => { describe(`console ${api} observable`, () => { - let consoleSpy: jasmine.Spy + let consoleSpy: Mock let consoleSubscription: Subscription - let notifyLog: jasmine.Spy + let notifyLog: Mock beforeEach(() => { - consoleSpy = spyOn(console, api) - notifyLog = jasmine.createSpy('notifyLog') + consoleSpy = vi.spyOn(console, api).mockImplementation(() => undefined) + notifyLog = vi.fn() consoleSubscription = initConsoleObservable([api]).subscribe(notifyLog) }) @@ -33,10 +34,10 @@ import { initConsoleObservable } from './consoleObservable' it(`should notify ${api}`, () => { console[api]('foo', 'bar') - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ message: 'foo bar', api, }) @@ -51,18 +52,18 @@ import { initConsoleObservable } from './consoleObservable' it('should format error instance', () => { console[api](new TypeError('hello')) - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.message).toBe('TypeError: hello') }) it('should stringify object parameters', () => { console[api]('Hello', { foo: 'bar' }) - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.message).toBe('Hello {\n "foo": "bar"\n}') }) it('should allow multiple callers', () => { - const notifyOtherCaller = jasmine.createSpy('notifyOtherCaller') + const notifyOtherCaller = vi.fn() const instrumentedConsoleApi = console[api] const otherConsoleSubscription = initConsoleObservable([api]).subscribe(notifyOtherCaller) @@ -79,12 +80,12 @@ import { initConsoleObservable } from './consoleObservable' describe('console error observable', () => { let consoleSubscription: Subscription - let notifyLog: jasmine.Spy<(consoleLog: ErrorConsoleLog) => void> + let notifyLog: Mock<(consoleLog: ErrorConsoleLog) => void> beforeEach(() => { ignoreConsoleLogs('error', 'Error: foo') ignoreConsoleLogs('error', 'foo bar') - notifyLog = jasmine.createSpy('notifyLog') + notifyLog = vi.fn() consoleSubscription = initConsoleObservable([ConsoleApiName.error]).subscribe(notifyLog) }) @@ -98,13 +99,13 @@ describe('console error observable', () => { console.error('foo', 'bar') } triggerError() - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.handlingStack).toMatch(/^HandlingStack: console error\s+at triggerError (.|\n)*$/) }) it('should extract stack from first error', () => { console.error(new TypeError('foo'), new TypeError('bar')) - const stack = notifyLog.calls.mostRecent().args[0].error.stack + const stack = notifyLog.mock.lastCall![0].error.stack expect(stack).toContain('TypeError: foo') }) @@ -117,7 +118,7 @@ describe('console error observable', () => { console.error(error) - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.error.fingerprint).toBe('my-fingerprint') }) @@ -127,7 +128,7 @@ describe('console error observable', () => { console.error(error) - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.error.fingerprint).toBe('2') }) @@ -138,14 +139,14 @@ describe('console error observable', () => { const error = new Error('foo') ;(error as DatadogError).dd_context = { foo: 'bar' } console.error(error) - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.error.context).toEqual({ foo: 'bar' }) }) it('should report original error', () => { const error = new Error('foo') console.error(error) - const consoleLog = notifyLog.calls.mostRecent().args[0] + const consoleLog = notifyLog.mock.lastCall![0] expect(consoleLog.error.originalError).toBe(error) }) }) diff --git a/packages/browser-core/src/domain/context/contextManager.spec.ts b/packages/browser-core/src/domain/context/contextManager.spec.ts index 714a271bde..5e4c253951 100644 --- a/packages/browser-core/src/domain/context/contextManager.spec.ts +++ b/packages/browser-core/src/domain/context/contextManager.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { display } from '../../tools/display' import type { PropertiesConfig } from './contextManager' import { createContextManager } from './contextManager' @@ -75,7 +76,7 @@ describe('createContextManager', () => { }) it('should prevent setting non object values', () => { - spyOn(display, 'error') + vi.spyOn(display, 'error').mockImplementation(() => undefined) const manager = createContextManagerWithDefaults() manager.setContext(null) expect(manager.getContext()).toEqual({}) @@ -112,7 +113,7 @@ describe('createContextManager', () => { NULLISH_VALUES.forEach((value) => { it(`should warn when required property is ${value}`, () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const manager = createContextManagerWithDefaults({ id: { required: true }, @@ -120,7 +121,8 @@ describe('createContextManager', () => { manager.setContext({ id: value }) - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( 'The property id of test is required; context will not be sent to the intake.' ) }) @@ -129,7 +131,7 @@ describe('createContextManager', () => { describe('changeObservable', () => { it('should notify on context changes', () => { - const changeSpy = jasmine.createSpy('change') + const changeSpy = vi.fn() const manager = createContextManagerWithDefaults() manager.changeObservable.subscribe(changeSpy) diff --git a/packages/browser-core/src/domain/context/contextUtils.spec.ts b/packages/browser-core/src/domain/context/contextUtils.spec.ts index d2332f25dc..1c5f3296c4 100644 --- a/packages/browser-core/src/domain/context/contextUtils.spec.ts +++ b/packages/browser-core/src/domain/context/contextUtils.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { display } from '../../tools/display' import type { Context } from '../../tools/serialisation/context' import type { Account } from '../contexts/accountContext' @@ -6,7 +7,7 @@ import { checkContext } from './contextUtils' describe('checkContext function', () => { it('should only accept valid objects', () => { - spyOn(display, 'error') + vi.spyOn(display, 'error').mockImplementation(() => undefined) const obj: any = { id: 42, name: true, email: null } // Valid, even though not sanitized const user: User = { id: '42', name: 'John', email: 'john@doe.com' } diff --git a/packages/browser-core/src/domain/context/storeContextManager.spec.ts b/packages/browser-core/src/domain/context/storeContextManager.spec.ts index f3e16ad362..edfca59740 100644 --- a/packages/browser-core/src/domain/context/storeContextManager.spec.ts +++ b/packages/browser-core/src/domain/context/storeContextManager.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, describe, expect, it } from 'vitest' import { createNewEvent } from '../../../test' import { DOM_EVENT } from '../../browser/addEventListener' import type { Context } from '../../tools/serialisation/context' diff --git a/packages/browser-core/src/domain/contexts/accountContext.spec.ts b/packages/browser-core/src/domain/contexts/accountContext.spec.ts index 3ae73c1527..b3d6b1cdfc 100644 --- a/packages/browser-core/src/domain/contexts/accountContext.spec.ts +++ b/packages/browser-core/src/domain/contexts/accountContext.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import type { Hook } from '@datadog/js-core/assembly' import { createHook } from '@datadog/js-core/assembly' @@ -10,12 +11,12 @@ import { startAccountContext } from './accountContext' describe('account context', () => { let accountContext: ContextManager - let displaySpy: jasmine.Spy + let displaySpy: Mock let hook: Hook beforeEach(() => { hook = createHook() - displaySpy = spyOn(display, 'warn') + displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) accountContext = startAccountContext(hook, mockRumConfiguration(), 'some_product_key') }) diff --git a/packages/browser-core/src/domain/contexts/globalContext.spec.ts b/packages/browser-core/src/domain/contexts/globalContext.spec.ts index 0c78eb9cdf..2bfa6e1ac5 100644 --- a/packages/browser-core/src/domain/contexts/globalContext.spec.ts +++ b/packages/browser-core/src/domain/contexts/globalContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import type { Hook } from '@datadog/js-core/assembly' import { createHook } from '@datadog/js-core/assembly' diff --git a/packages/browser-core/src/domain/contexts/tabContext.spec.ts b/packages/browser-core/src/domain/contexts/tabContext.spec.ts index e9f162db39..4a695ec1c3 100644 --- a/packages/browser-core/src/domain/contexts/tabContext.spec.ts +++ b/packages/browser-core/src/domain/contexts/tabContext.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import type { Hook } from '@datadog/js-core/assembly' import { createHook } from '@datadog/js-core/assembly' @@ -26,9 +27,9 @@ describe('tabContext', () => { }) expect(event).toEqual( - jasmine.objectContaining({ - tab: jasmine.objectContaining({ - id: jasmine.stringMatching(UUID_PATTERN), + expect.objectContaining({ + tab: expect.objectContaining({ + id: expect.stringMatching(UUID_PATTERN), }), }) ) @@ -61,7 +62,9 @@ describe('tabContext', () => { }) it('should generate a tab ID when sessionStorage.getItem throws', () => { - spyOn(sessionStorage, 'getItem').and.throwError('SecurityError') + vi.spyOn(sessionStorage, 'getItem').mockImplementation(() => { + throw new Error('SecurityError') + }) startTabContext(hook) const event = hook.trigger({ startTime: 0 as RelativeTime }) @@ -70,8 +73,10 @@ describe('tabContext', () => { }) it('should generate a tab ID when sessionStorage.setItem throws', () => { - spyOn(sessionStorage, 'getItem').and.returnValue(null) - spyOn(sessionStorage, 'setItem').and.throwError('QuotaExceededError') + vi.spyOn(sessionStorage, 'getItem').mockReturnValue(null) + vi.spyOn(sessionStorage, 'setItem').mockImplementation(() => { + throw new Error('QuotaExceededError') + }) startTabContext(hook) const event = hook.trigger({ startTime: 0 as RelativeTime }) @@ -80,7 +85,9 @@ describe('tabContext', () => { }) it('should return the same tab ID across multiple startTabContext calls when sessionStorage is unavailable', () => { - spyOn(sessionStorage, 'getItem').and.throwError('SecurityError') + vi.spyOn(sessionStorage, 'getItem').mockImplementation(() => { + throw new Error('SecurityError') + }) const hook1 = createHook() startTabContext(hook1) diff --git a/packages/browser-core/src/domain/contexts/telemetrySessionContext.spec.ts b/packages/browser-core/src/domain/contexts/telemetrySessionContext.spec.ts index b9446f246b..22bfc5efe9 100644 --- a/packages/browser-core/src/domain/contexts/telemetrySessionContext.spec.ts +++ b/packages/browser-core/src/domain/contexts/telemetrySessionContext.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import type { Hook } from '@datadog/js-core/assembly' import { createHook } from '@datadog/js-core/assembly' @@ -44,7 +45,7 @@ describe('telemetrySessionContext', () => { it('should pass startTime to findTrackedSession', () => { const sessionManager = createSessionManagerMock() - spyOn(sessionManager, 'findTrackedSession').and.callThrough() + vi.spyOn(sessionManager, 'findTrackedSession') startTelemetrySessionContext(hook, sessionManager) diff --git a/packages/browser-core/src/domain/contexts/userContext.spec.ts b/packages/browser-core/src/domain/contexts/userContext.spec.ts index 704529d0d9..850e20621f 100644 --- a/packages/browser-core/src/domain/contexts/userContext.spec.ts +++ b/packages/browser-core/src/domain/contexts/userContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { createHook, type Hook } from '@datadog/js-core/assembly' import { registerCleanupTask } from '../../../test' diff --git a/packages/browser-core/src/domain/error/error.spec.ts b/packages/browser-core/src/domain/error/error.spec.ts index 5c33b84d3e..7ae5b07ae0 100644 --- a/packages/browser-core/src/domain/error/error.spec.ts +++ b/packages/browser-core/src/domain/error/error.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { clocksNow } from '@datadog/js-core/time' import type { StackTrace } from '../../tools/stackTrace/computeStackTrace' import { mockSourceCodeContext, registerCleanupTask } from '../../../test' diff --git a/packages/browser-core/src/domain/error/trackRuntimeError.spec.ts b/packages/browser-core/src/domain/error/trackRuntimeError.spec.ts index 74be354176..c5a996de91 100644 --- a/packages/browser-core/src/domain/error/trackRuntimeError.spec.ts +++ b/packages/browser-core/src/domain/error/trackRuntimeError.spec.ts @@ -1,4 +1,5 @@ -import { disableJasmineUncaughtExceptionTracking, wait } from '../../../test' +import { vi, beforeEach, describe, expect, it } from 'vitest' +import { disableUncaughtExceptionTracking, registerCleanupTask, wait } from '../../../test' import type { UnhandledErrorCallback } from './trackRuntimeError' import { instrumentOnError, instrumentUnhandledRejection, trackRuntimeError } from './trackRuntimeError' import type { RawError } from './error.types' @@ -7,7 +8,7 @@ describe('trackRuntimeError', () => { const ERROR_MESSAGE = 'foo' const errorViaTrackRuntimeError = async (callback: () => void): Promise => { - disableJasmineUncaughtExceptionTracking() + disableUncaughtExceptionTracking() const errorObservable = trackRuntimeError() const errorNotification = new Promise((resolve) => { @@ -29,18 +30,18 @@ describe('trackRuntimeError', () => { expect(error.message).toEqual(ERROR_MESSAGE) }) - it('should collect unhandled rejection', async () => { + it('should collect unhandled rejection', async (ctx) => { + if (!('onunhandledrejection' in window)) { + ctx.skip(true, 'onunhandledrejection not supported') + return + } + const error = await errorViaTrackRuntimeError(() => { - // Reject with a string instead of an Error here because Jasmine forwards the - // unhandled rejection to the onerror handler with the wrong argument structure if - // you use an Error. (It uses the argument structure you'd use for - // addEventListener('error'). We could make our error processing code robust to - // that, but since it's a Jasmine-specific issue with a simple workaround, it makes - // sense to just work around it here. + // Use a string to cover non-Error rejection values. // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors void Promise.reject(ERROR_MESSAGE) }) - expect(error.message).toEqual(jasmine.stringContaining(ERROR_MESSAGE)) + expect(error.message).toEqual(expect.stringContaining(ERROR_MESSAGE)) }) }) @@ -50,13 +51,16 @@ describe('instrumentOnError', () => { const ERROR_MESSAGE = 'foo' const spyViaInstrumentOnError = async (callback: () => void) => { - const onErrorSpy = spyOn(window as any, 'onerror') - const callbackSpy = jasmine.createSpy() + disableUncaughtExceptionTracking() + const originalHandlerSpy = vi.fn() + window.onerror = originalHandlerSpy + const callbackSpy = vi.fn() const { stop } = instrumentOnError(callbackSpy) try { await invokeAndWaitForErrorHandlers(callback) - expect(onErrorSpy).toHaveBeenCalled() + expect(originalHandlerSpy).toHaveBeenCalled() + expect(callbackSpy).toHaveBeenCalled() return callbackSpy } finally { stop() @@ -77,7 +81,7 @@ describe('instrumentOnError', () => { throw error }) - const [originalError, stack] = spy.calls.mostRecent().args + const [originalError, stack] = spy.mock.lastCall! expect(originalError).toBe(error) expect(stack).toBeUndefined() }) @@ -88,7 +92,7 @@ describe('instrumentOnError', () => { throw error }) - const [originalError, stack] = spy.calls.mostRecent().args + const [originalError, stack] = spy.mock.lastCall! expect(originalError).toBe(error) expect(stack).toBeDefined() }) @@ -99,7 +103,7 @@ describe('instrumentOnError', () => { throw error }) - const [originalError, stack] = spy.calls.mostRecent().args + const [originalError, stack] = spy.mock.lastCall! expect(originalError).toBe(error) expect(stack).toBeDefined() }) @@ -124,7 +128,7 @@ describe('instrumentOnError', () => { expect(spy).toHaveBeenCalledTimes(1) await wait(1000) expect(spy).toHaveBeenCalledTimes(1) - const [reportedError] = spy.calls.mostRecent().args + const [reportedError] = spy.mock.lastCall! expect(reportedError).toEqual(exception) }) }) @@ -136,7 +140,7 @@ describe('instrumentOnError', () => { window.onerror!(error, 'http://example.com', testLineNo, testColNo) }) - const [originalError, stack] = spy.calls.mostRecent().args + const [originalError, stack] = spy.mock.lastCall! expect(originalError).toBe(error) expect(stack).toBeDefined() }) @@ -149,7 +153,7 @@ describe('instrumentOnError', () => { window.onerror!(undefined!, undefined, testLineNo) }) - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack).toBeUndefined() }) }) @@ -160,7 +164,7 @@ describe('instrumentOnError', () => { window.onerror!('ReferenceError: foo is undefined', 'http://example.com', testLineNo) }) - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack!.name).toEqual('ReferenceError') expect(stack!.message).toEqual('foo is undefined') }) @@ -171,7 +175,7 @@ describe('instrumentOnError', () => { window.onerror!('Uncaught ReferenceError: foo is undefined', 'http://example.com', testLineNo) }) - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack!.name).toEqual('ReferenceError') expect(stack!.message).toEqual('foo is undefined') }) @@ -185,7 +189,7 @@ describe('instrumentOnError', () => { ) }) - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack!.name).toEqual('ReferenceError') expect(stack!.message).toEqual('Undefined variable: foo') }) @@ -199,7 +203,7 @@ describe('instrumentOnError', () => { ) }) - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack!.message).toEqual("foo is not a function. (In 'my.function(\n foo)") expect(stack!.name).toEqual('TypeError') }) @@ -210,7 +214,7 @@ describe('instrumentOnError', () => { }) // TODO: should we attempt to parse this? - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack!.name).toEqual(undefined) expect(stack!.message).toEqual('CustomError: woo scary') }) @@ -220,7 +224,7 @@ describe('instrumentOnError', () => { window.onerror!('all work and no play makes homer: something something', 'http://example.com', testLineNo) }) - const [, stack] = spy.calls.mostRecent().args + const [, stack] = spy.mock.lastCall! expect(stack!.name).toEqual(undefined) expect(stack!.message).toEqual('all work and no play makes homer: something something') }) @@ -230,7 +234,7 @@ describe('instrumentOnError', () => { window.onerror!({ foo: 'bar' } as any, 'http://example.com', testLineNo, testColNo) }) - const [error, stack] = spy.calls.mostRecent().args + const [error, stack] = spy.mock.lastCall! expect(stack!.message).toBeUndefined() expect(error).toEqual({ foo: 'bar' }) // consider the message as initial error }) @@ -248,7 +252,7 @@ describe('instrumentOnError', () => { ) }) - const [error, stack] = spy.calls.mostRecent().args + const [error, stack] = spy.mock.lastCall! expect(stack!.message).toBe('Any error message') expect(stack!.stack).toEqual([{ url: 'https://example.com', column: testColNo, line: testLineNo }]) expect(error).toEqual('Actual Error Message') @@ -262,7 +266,7 @@ describe('instrumentOnError', () => { } as any) }) - const [error, stack] = spy.calls.mostRecent().args + const [error, stack] = spy.mock.lastCall! expect(stack!.message).toBe('Any error message') expect(stack!.stack).toEqual([{ url: 'https://example.com', column: testColNo, line: testLineNo }]) expect(error).toEqual({ message: 'SyntaxError', data: 'foo' }) @@ -274,14 +278,27 @@ describe('instrumentOnError', () => { describe('instrumentUnhandledRejection', () => { const ERROR_MESSAGE = 'foo' + beforeEach((ctx) => { + if (!('onunhandledrejection' in window)) { + ctx.skip(true, 'onunhandledrejection not supported') + } + }) + const spyViaInstrumentOnUnhandledRejection = async (callback: () => void) => { - const onUnhandledRejectionSpy = spyOn(window as any, 'onunhandledrejection') - const callbackSpy = jasmine.createSpy() + const originalHandler = window.onunhandledrejection + const originalHandlerSpy = vi.fn() + window.onunhandledrejection = originalHandlerSpy + registerCleanupTask(() => { + window.onunhandledrejection = originalHandler + }) + + const callbackSpy = vi.fn() const { stop } = instrumentUnhandledRejection(callbackSpy) try { await invokeAndWaitForErrorHandlers(callback) - expect(onUnhandledRejectionSpy).toHaveBeenCalled() + expect(originalHandlerSpy).toHaveBeenCalled() + expect(callbackSpy).toHaveBeenCalled() return callbackSpy } finally { stop() @@ -305,7 +322,7 @@ describe('instrumentUnhandledRejection', () => { window.onunhandledrejection!({ reason } as PromiseRejectionEvent) }) - const [originalError, stack] = spy.calls.mostRecent().args + const [originalError, stack] = spy.mock.lastCall! expect(originalError).toBe(reason) expect(stack).toBeUndefined() }) diff --git a/packages/browser-core/src/domain/eventRateLimiter/createEventRateLimiter.spec.ts b/packages/browser-core/src/domain/eventRateLimiter/createEventRateLimiter.spec.ts index f72971525b..e58d36447d 100644 --- a/packages/browser-core/src/domain/eventRateLimiter/createEventRateLimiter.spec.ts +++ b/packages/browser-core/src/domain/eventRateLimiter/createEventRateLimiter.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { ONE_MINUTE } from '@datadog/js-core/time' import type { Clock } from '../../../test' import { mockClock } from '../../../test' @@ -36,12 +37,12 @@ describe('createEventRateLimiter', () => { }) it('calls the "onLimitReached" callback with the "limit reached" message when the limit is reached', () => { - const onLimitReachedSpy = jasmine.createSpy<(message: string) => void>() + const onLimitReachedSpy = vi.fn<(message: string) => void>() eventLimiter = createEventRateLimiter('error', onLimitReachedSpy, limit) eventLimiter.isLimitReached() eventLimiter.isLimitReached() - expect(onLimitReachedSpy).toHaveBeenCalledOnceWith('Reached max number of errors by minute: 1') + expect(onLimitReachedSpy).toHaveBeenCalledExactlyOnceWith('Reached max number of errors by minute: 1') }) it('returns false when called from the "onLimitReached" callback to bypass the limit for the "limit reached" error', () => { @@ -58,7 +59,7 @@ describe('createEventRateLimiter', () => { }) it('does not call the "onLimitReached" callback more than once when the limit is reached', () => { - const onLimitReachedSpy = jasmine.createSpy<(message: string) => void>() + const onLimitReachedSpy = vi.fn<(message: string) => void>() eventLimiter = createEventRateLimiter('error', onLimitReachedSpy, limit) eventLimiter.isLimitReached() diff --git a/packages/browser-core/src/domain/extension/extensionUtils.spec.ts b/packages/browser-core/src/domain/extension/extensionUtils.spec.ts index 6c4334b22e..4d7ef94865 100644 --- a/packages/browser-core/src/domain/extension/extensionUtils.spec.ts +++ b/packages/browser-core/src/domain/extension/extensionUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { STACK_WITH_INIT_IN_EXTENSION, STACK_WITH_INIT_IN_EXTENSION_FIREFOX, diff --git a/packages/browser-core/src/domain/intakeSites.spec.ts b/packages/browser-core/src/domain/intakeSites.spec.ts index 222e77f610..18fe504332 100644 --- a/packages/browser-core/src/domain/intakeSites.spec.ts +++ b/packages/browser-core/src/domain/intakeSites.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { isIntakeUrl } from '@datadog/js-core/transport' describe('intakeSites', () => { diff --git a/packages/browser-core/src/domain/report/reportObservable.spec.ts b/packages/browser-core/src/domain/report/reportObservable.spec.ts index 95da8f37d0..de0670fb82 100644 --- a/packages/browser-core/src/domain/report/reportObservable.spec.ts +++ b/packages/browser-core/src/domain/report/reportObservable.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { MockCspEventListener, MockReportingObserver } from '../../../test' import { mockReportingObserver, mockCspEventListener, FAKE_CSP_VIOLATION_EVENT } from '../../../test' import type { Subscription } from '../../tools/observable' @@ -9,15 +10,15 @@ describe('report observable', () => { let reportingObserver: MockReportingObserver let cspEventListener: MockCspEventListener let consoleSubscription: Subscription - let notifyReport: jasmine.Spy<(reportError: RawReportError) => void> + let notifyReport: Mock<(reportError: RawReportError) => void> - beforeEach(() => { + beforeEach((ctx) => { if (!window.ReportingObserver) { - pending('ReportingObserver not supported') + ctx.skip(true, 'ReportingObserver not supported') } reportingObserver = mockReportingObserver() cspEventListener = mockCspEventListener() - notifyReport = jasmine.createSpy('notifyReport') + notifyReport = vi.fn() }) afterEach(() => { @@ -28,10 +29,10 @@ describe('report observable', () => { consoleSubscription = initReportObservable([type]).subscribe(notifyReport) reportingObserver.raiseReport(type) - const [report] = notifyReport.calls.mostRecent().args + const [report] = notifyReport.mock.calls[notifyReport.mock.calls.length - 1] expect(report).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ message: `${type}: foo bar`, type: 'NavigatorVibrate', }) @@ -43,7 +44,7 @@ describe('report observable', () => { consoleSubscription = initReportObservable([RawReportType.intervention]).subscribe(notifyReport) reportingObserver.raiseReport(RawReportType.intervention) - const [report] = notifyReport.calls.mostRecent().args + const [report] = notifyReport.mock.calls[notifyReport.mock.calls.length - 1] expect(report.stack).toEqual(`NavigatorVibrate: foo bar at @ http://foo.bar/index.js:20:10`) @@ -53,8 +54,9 @@ describe('report observable', () => { consoleSubscription = initReportObservable([RawReportType.cspViolation]).subscribe(notifyReport) cspEventListener.dispatchEvent() - expect(notifyReport).toHaveBeenCalledOnceWith({ - startClocks: jasmine.any(Object), + expect(notifyReport).toHaveBeenCalledTimes(1) + expect(notifyReport).toHaveBeenCalledExactlyOnceWith({ + startClocks: expect.any(Object), source: ErrorSource.REPORT, message: "csp_violation: 'blob' blocked by 'worker-src' directive", type: 'worker-src', @@ -67,9 +69,15 @@ describe('report observable', () => { }) it(`should not notify ${RawReportType.cspViolation} when the event is not supported`, () => { - ;(EventTarget.prototype.addEventListener as jasmine.Spy).and.throwError('unsupported') + const addEventListenerSpy = vi.spyOn(EventTarget.prototype, 'addEventListener').mockImplementation(() => { + throw new Error('unsupported') + }) - consoleSubscription = initReportObservable([RawReportType.cspViolation]).subscribe(notifyReport) + try { + consoleSubscription = initReportObservable([RawReportType.cspViolation]).subscribe(notifyReport) + } finally { + addEventListenerSpy.mockRestore() + } cspEventListener.dispatchEvent() expect(notifyReport).not.toHaveBeenCalled() diff --git a/packages/browser-core/src/domain/sampler.spec.ts b/packages/browser-core/src/domain/sampler.spec.ts index 90ac480f7f..2d1224df93 100644 --- a/packages/browser-core/src/domain/sampler.spec.ts +++ b/packages/browser-core/src/domain/sampler.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { HIGH_HASH_UUID, LOW_HASH_UUID } from '../../test' import { correctedChildSampleRate, isSampled, resetSampleDecisionCache, sampleUsingKnuthFactor } from './sampler' @@ -6,39 +7,39 @@ const ARBITRARY_UUID = '1ff81c8c-6e32-473b-869b-55af08048323' describe('isSampled', () => { it('returns true when sampleRate is 100', () => { - expect(isSampled(ARBITRARY_UUID, 100)).toBeTrue() + expect(isSampled(ARBITRARY_UUID, 100)).toBe(true) }) it('returns false when sampleRate is 0', () => { - expect(isSampled(ARBITRARY_UUID, 0)).toBeFalse() + expect(isSampled(ARBITRARY_UUID, 0)).toBe(false) }) it('a session id with a low hash value should be sampled with a rate close to 0%', () => { - expect(isSampled(LOW_HASH_UUID, 0.1)).toBeTrue() + expect(isSampled(LOW_HASH_UUID, 0.1)).toBe(true) resetSampleDecisionCache() - expect(isSampled(LOW_HASH_UUID, 0.01)).toBeTrue() + expect(isSampled(LOW_HASH_UUID, 0.01)).toBe(true) resetSampleDecisionCache() - expect(isSampled(LOW_HASH_UUID, 0.001)).toBeTrue() + expect(isSampled(LOW_HASH_UUID, 0.001)).toBe(true) resetSampleDecisionCache() - expect(isSampled(LOW_HASH_UUID, 0.0001)).toBeTrue() + expect(isSampled(LOW_HASH_UUID, 0.0001)).toBe(true) resetSampleDecisionCache() // At some point the sample rate is so low that the session is not sampled even if the hash // is low. This is not an error: we can probably find a UUID with an even lower hash. - expect(isSampled(LOW_HASH_UUID, 0.0000000001)).toBeFalse() + expect(isSampled(LOW_HASH_UUID, 0.0000000001)).toBe(false) }) it('a session id with a high hash value should not be sampled even if the rate is close to 100%', () => { - expect(isSampled(HIGH_HASH_UUID, 99.9)).toBeFalse() + expect(isSampled(HIGH_HASH_UUID, 99.9)).toBe(false) resetSampleDecisionCache() - expect(isSampled(HIGH_HASH_UUID, 99.99)).toBeFalse() + expect(isSampled(HIGH_HASH_UUID, 99.99)).toBe(false) resetSampleDecisionCache() - expect(isSampled(HIGH_HASH_UUID, 99.999)).toBeFalse() + expect(isSampled(HIGH_HASH_UUID, 99.999)).toBe(false) resetSampleDecisionCache() - expect(isSampled(HIGH_HASH_UUID, 99.9999)).toBeFalse() + expect(isSampled(HIGH_HASH_UUID, 99.9999)).toBe(false) resetSampleDecisionCache() // At some point the sample rate is so high that the session is sampled even if the hash is // high. This is not an error: we can probably find a UUID with an even higher hash. - expect(isSampled(HIGH_HASH_UUID, 99.9999999999)).toBeTrue() + expect(isSampled(HIGH_HASH_UUID, 99.9999999999)).toBe(true) }) }) @@ -82,16 +83,16 @@ describe('sampleUsingKnuthFactor', () => { ] for (const [identifier, sampleRate, expected] of inputs) { - expect(sampleUsingKnuthFactor(identifier, sampleRate)) - .withContext(`identifier=${identifier}, sampleRate=${sampleRate}`) - .toBe(expected) + expect(sampleUsingKnuthFactor(identifier, sampleRate), `identifier=${identifier}, sampleRate=${sampleRate}`).toBe( + expected + ) } }) it('should cache sampling decision per sampling rate', () => { // For the same session id, the sampling decision should be different for trace and profiling, eg. trace should not cache profiling decisions and vice versa - expect(isSampled(HIGH_HASH_UUID, 99.9999999999)).toBeTrue() - expect(isSampled(HIGH_HASH_UUID, 0.0000001)).toBeFalse() - expect(isSampled(HIGH_HASH_UUID, 99.9999999999)).toBeTrue() + expect(isSampled(HIGH_HASH_UUID, 99.9999999999)).toBe(true) + expect(isSampled(HIGH_HASH_UUID, 0.0000001)).toBe(false) + expect(isSampled(HIGH_HASH_UUID, 99.9999999999)).toBe(true) }) }) diff --git a/packages/browser-core/src/domain/session/sessionManager.spec.ts b/packages/browser-core/src/domain/session/sessionManager.spec.ts index 67a4985ebd..e1dcf4c43d 100644 --- a/packages/browser-core/src/domain/session/sessionManager.spec.ts +++ b/packages/browser-core/src/domain/session/sessionManager.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { ONE_SECOND } from '@datadog/js-core/time' import { collectAsyncCalls, @@ -50,7 +51,7 @@ describe('startSessionManager', () => { } let fakeStrategy: ReturnType let clock: Clock - let sessionObservableSpy!: jasmine.Spy + let sessionObservableSpy: Mock<(...args: any[]) => any> /** * Creates a fresh fake strategy and updates the mockable reference. @@ -64,7 +65,7 @@ describe('startSessionManager', () => { let currentStoreType: SessionStoreStrategyType | undefined beforeEach(() => { - sessionObservableSpy = jasmine.createSpy('sessionObservable') + sessionObservableSpy = vi.fn() clock = mockClock() fakeStrategy = createFakeSessionStoreStrategy() fakeStrategy.sessionObservable.subscribe(sessionObservableSpy) @@ -98,7 +99,7 @@ describe('startSessionManager', () => { describe('initialization', () => { it('should not start if no session store strategy type is configured', async () => { - const displayWarnSpy = spyOn(display, 'warn') + const displayWarnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) currentStoreType = undefined const sessionManager = await startSessionManager( @@ -117,7 +118,7 @@ describe('startSessionManager', () => { }) it('should resolve with undefined if session initialization fails', async () => { - fakeStrategy.setSessionState.and.returnValue(Promise.reject(new Error('storage failure'))) + fakeStrategy.setSessionState.mockReturnValue(Promise.reject(new Error('storage failure'))) const sessionManager = await startSessionManager( { sessionSampleRate: 100, trackAnonymousUser: false } as Configuration, @@ -186,7 +187,7 @@ describe('startSessionManager', () => { describe('session renewal', () => { it('should renew on user activity after expiration', async () => { const sessionManager = await startSessionManagerWithDefaults() - const renewSpy = jasmine.createSpy('renew') + const renewSpy = vi.fn() sessionManager.renewObservable.subscribe(renewSpy) const initialId = sessionManager.findSession()!.id @@ -214,7 +215,7 @@ describe('startSessionManager', () => { registerCleanupTask(restorePageVisibility) const sessionManager = await startSessionManagerWithDefaults() - const renewSpy = jasmine.createSpy('renew') + const renewSpy = vi.fn() sessionManager.renewObservable.subscribe(renewSpy) sessionManager.expire() @@ -231,7 +232,7 @@ describe('startSessionManager', () => { // Wait for throttle to clear. clock.tick(ONE_SECOND) - const callCountBefore = fakeStrategy.setSessionState.calls.count() + const callCountBefore = fakeStrategy.setSessionState.mock.calls.length // Multiple rapid clicks within the throttle window document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) @@ -239,20 +240,20 @@ describe('startSessionManager', () => { document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) // Only one call (leading edge) should have fired immediately - expect(fakeStrategy.setSessionState.calls.count() - callCountBefore).toBe(1) + expect(fakeStrategy.setSessionState.mock.calls.length - callCountBefore).toBe(1) // After throttle delay, the trailing call fires (from the queued clicks) clock.tick(ONE_SECOND) // Leading (1) + trailing (1) = 2 calls total - expect(fakeStrategy.setSessionState.calls.count() - callCountBefore).toBe(2) + expect(fakeStrategy.setSessionState.mock.calls.length - callCountBefore).toBe(2) }) }) describe('session expiration', () => { it('should fire expireObservable when session expires', async () => { const sessionManager = await startSessionManagerWithDefaults() - const expireSpy = jasmine.createSpy('expire') + const expireSpy = vi.fn() sessionManager.expireObservable.subscribe(expireSpy) sessionManager.expire() @@ -262,7 +263,7 @@ describe('startSessionManager', () => { it('should only fire expireObservable once for multiple expire calls', async () => { const sessionManager = await startSessionManagerWithDefaults() - const expireSpy = jasmine.createSpy('expire') + const expireSpy = vi.fn() sessionManager.expireObservable.subscribe(expireSpy) sessionManager.expire() @@ -379,7 +380,7 @@ describe('startSessionManager', () => { it('should expire session after SESSION_EXPIRATION_DELAY without any activity in a hidden tab', async () => { const sessionManager = await startSessionManagerWithDefaults() - const expireSpy = jasmine.createSpy('expire') + const expireSpy = vi.fn() sessionManager.expireObservable.subscribe(expireSpy) expect(sessionManager.findSession()).toBeDefined() @@ -396,7 +397,7 @@ describe('startSessionManager', () => { it('should expire session after SESSION_TIME_OUT_DELAY even on a continuously visible page', async () => { setPageVisibility('visible') const sessionManager = await startSessionManagerWithDefaults() - const expireSpy = jasmine.createSpy('expire') + const expireSpy = vi.fn() sessionManager.expireObservable.subscribe(expireSpy) expect(sessionManager.findSession()).toBeDefined() @@ -420,7 +421,7 @@ describe('startSessionManager', () => { describe('cross-tab changes (simulateExternalChange)', () => { it('should not adopt a session created by another tab when it replaces our session directly', async () => { const sessionManager = await startSessionManagerWithDefaults() - const renewSpy = jasmine.createSpy('renew') + const renewSpy = vi.fn() sessionManager.renewObservable.subscribe(renewSpy) // Another tab expires our session and immediately starts a new one @@ -452,7 +453,7 @@ describe('startSessionManager', () => { it('should fire expireObservable when external change removes the session', async () => { const sessionManager = await startSessionManagerWithDefaults() - const expireSpy = jasmine.createSpy('expire') + const expireSpy = vi.fn() sessionManager.expireObservable.subscribe(expireSpy) fakeStrategy.simulateExternalChange({ isExpired: EXPIRED }) @@ -463,7 +464,7 @@ describe('startSessionManager', () => { it('should not adopt a session created by another tab after expiry', async () => { const sessionManager = await startSessionManagerWithDefaults() - const renewSpy = jasmine.createSpy('renew') + const renewSpy = vi.fn() sessionManager.renewObservable.subscribe(renewSpy) // First expire @@ -542,9 +543,9 @@ describe('startSessionManager', () => { const initResolvers: Array<() => void> = [] const delayedStrategy = createFakeSessionStoreStrategy() - delayedStrategy.setSessionState = jasmine - .createSpy('setSessionState') - .and.callFake((fn: (state: SessionState) => SessionState): Promise => { + delayedStrategy.setSessionState = vi + .fn() + .mockImplementation((fn: (state: SessionState) => SessionState): Promise => { fn({}) return new Promise((resolve) => { initResolvers.push(resolve) @@ -553,7 +554,7 @@ describe('startSessionManager', () => { fakeStrategy = delayedStrategy - const sessionManagerResolution = jasmine.createSpy('sessionManagerResolution') + const sessionManagerResolution = vi.fn() void startSessionManager( { sessionSampleRate: 100, @@ -577,9 +578,9 @@ describe('startSessionManager', () => { const initResolvers: Array<() => void> = [] const delayedStrategy = createFakeSessionStoreStrategy() - delayedStrategy.setSessionState = jasmine - .createSpy('setSessionState') - .and.callFake((fn: (state: SessionState) => SessionState): Promise => { + delayedStrategy.setSessionState = vi + .fn() + .mockImplementation((fn: (state: SessionState) => SessionState): Promise => { fn({}) return new Promise((resolve) => { initResolvers.push(resolve) @@ -620,9 +621,9 @@ describe('startSessionManager', () => { const initResolvers: Array<() => void> = [] const setStateCalls: Array<(state: SessionState) => SessionState> = [] const delayedStrategy = createFakeSessionStoreStrategy() - delayedStrategy.setSessionState = jasmine - .createSpy('setSessionState') - .and.callFake((fn: (state: SessionState) => SessionState): Promise => { + delayedStrategy.setSessionState = vi + .fn() + .mockImplementation((fn: (state: SessionState) => SessionState): Promise => { setStateCalls.push(fn) fn({}) return new Promise((resolve) => { @@ -849,11 +850,11 @@ describe('startSessionManager', () => { describe('updateSessionState', () => { it('should merge partial state via setSessionState', async () => { const sessionManager = await startSessionManagerWithDefaults() - const callCountBefore = fakeStrategy.setSessionState.calls.count() + const callCountBefore = fakeStrategy.setSessionState.mock.calls.length sessionManager.updateSessionState({ extra: 'value' }) - expect(fakeStrategy.setSessionState.calls.count()).toBe(callCountBefore + 1) + expect(fakeStrategy.setSessionState.mock.calls.length).toBe(callCountBefore + 1) expect(fakeStrategy.getInternalState().extra).toBe('value') }) @@ -907,8 +908,8 @@ describe('startSessionManager', () => { const firstManager = await startSessionManagerWithDefaults() const secondManager = await startSessionManagerWithDefaults() - const expireSpy1 = jasmine.createSpy('expire1') - const expireSpy2 = jasmine.createSpy('expire2') + const expireSpy1 = vi.fn() + const expireSpy2 = vi.fn() firstManager?.expireObservable.subscribe(expireSpy1) secondManager?.expireObservable.subscribe(expireSpy2) @@ -950,16 +951,16 @@ describe('startSessionManager', () => { // Wait for throttle to clear clock.tick(ONE_SECOND) - const callCountAfterStop = fakeStrategy.setSessionState.calls.count() + const callCountAfterStop = fakeStrategy.setSessionState.mock.calls.length document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) - expect(fakeStrategy.setSessionState.calls.count()).toBe(callCountAfterStop) + expect(fakeStrategy.setSessionState.mock.calls.length).toBe(callCountAfterStop) }) it('should unsubscribe from strategy observable after stopSessionManager', async () => { const sessionManager = await startSessionManagerWithDefaults() - const renewSpy = jasmine.createSpy('renew') + const renewSpy = vi.fn() sessionManager.renewObservable.subscribe(renewSpy) stopSessionManager() diff --git a/packages/browser-core/src/domain/session/sessionState.spec.ts b/packages/browser-core/src/domain/session/sessionState.spec.ts index 3ff7594483..87af020f88 100644 --- a/packages/browser-core/src/domain/session/sessionState.spec.ts +++ b/packages/browser-core/src/domain/session/sessionState.spec.ts @@ -1,5 +1,6 @@ -import { dateNow } from '@datadog/js-core/time' +import { describe, expect, it } from 'vitest' import type { TimeStamp } from '@datadog/js-core/time' +import { dateNow } from '@datadog/js-core/time' import { SESSION_EXPIRATION_DELAY, SESSION_NOT_TRACKED, SESSION_TIME_OUT_DELAY } from './sessionConstants' import type { SessionState } from './sessionState' import { @@ -114,7 +115,7 @@ describe('session state utilities', () => { const session = { ...LIVE_SESSION } const now = dateNow() expandSessionState(session) - expect(session.expire).toBeGreaterThanOrEqual(now + SESSION_EXPIRATION_DELAY) + expect(Number(session.expire)).toBeGreaterThanOrEqual(now + SESSION_EXPIRATION_DELAY) }) }) }) diff --git a/packages/browser-core/src/domain/session/sessionStore.spec.ts b/packages/browser-core/src/domain/session/sessionStore.spec.ts index 2941c8b626..d0bb08622c 100644 --- a/packages/browser-core/src/domain/session/sessionStore.spec.ts +++ b/packages/browser-core/src/domain/session/sessionStore.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { globalObject } from '@datadog/js-core/util' import { replaceMockable } from '../../../test' import type { Configuration, InitConfiguration } from '../configuration' @@ -22,7 +23,7 @@ describe('session store', () => { const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration(DEFAULT_INIT_CONFIGURATION) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.COOKIE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.COOKIE })) }) it('returns undefined when cookies are not available', async () => { @@ -37,7 +38,7 @@ describe('session store', () => { const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionPersistence: SessionPersistence.COOKIE }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.COOKIE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.COOKIE })) }) }) @@ -46,7 +47,7 @@ describe('session store', () => { const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionPersistence: SessionPersistence.LOCAL_STORAGE }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) }) it('returns undefined when local storage is not available', async () => { @@ -63,18 +64,19 @@ describe('session store', () => { const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionPersistence: SessionPersistence.MEMORY }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.MEMORY })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.MEMORY })) }) }) it('returns undefined when sessionPersistence is invalid', async () => { - const displayErrorSpy = spyOn(display, 'error') + const displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionPersistence: 'invalid' as SessionPersistence }) ) expect(sessionStoreStrategyType).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith("Invalid session persistence 'invalid'") + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith("Invalid session persistence 'invalid'") }) describe('sessionPersistence as array', () => { @@ -85,7 +87,7 @@ describe('session store', () => { sessionPersistence: [SessionPersistence.COOKIE, SessionPersistence.LOCAL_STORAGE], }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.COOKIE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.COOKIE })) }) it('falls back to next strategy when first is unavailable', async () => { @@ -96,7 +98,7 @@ describe('session store', () => { sessionPersistence: [SessionPersistence.COOKIE, SessionPersistence.LOCAL_STORAGE], }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) }) it('falls back to memory when cookie and local storage are unavailable', async () => { @@ -112,7 +114,7 @@ describe('session store', () => { ], }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.MEMORY })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.MEMORY })) }) it('returns undefined when no strategy in array is available', async () => { @@ -138,7 +140,7 @@ describe('session store', () => { const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionPersistence: [SessionPersistence.LOCAL_STORAGE] }) ) - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) }) it('stops at first available strategy and does not try subsequent ones', async () => { @@ -149,11 +151,11 @@ describe('session store', () => { }) ) // Should return local storage (first available), not cookie - expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) + expect(sessionStoreStrategyType).toEqual(expect.objectContaining({ type: SessionPersistence.LOCAL_STORAGE })) }) it('returns undefined and logs error if array contains invalid persistence type', async () => { - const displayErrorSpy = spyOn(display, 'error') + const displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const sessionStoreStrategyType = await selectSessionStoreStrategyType( makeConfiguration({ ...DEFAULT_INIT_CONFIGURATION, @@ -161,16 +163,19 @@ describe('session store', () => { }) ) expect(sessionStoreStrategyType).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith("Invalid session persistence 'invalid'") + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith("Invalid session persistence 'invalid'") }) }) function disableCookies() { - spyOnProperty(document, 'cookie', 'get').and.returnValue('') + vi.spyOn(document, 'cookie', 'get').mockReturnValue('') replaceMockable(globalObject.cookieStore, undefined) } function disableLocalStorage() { - spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('unavailable') + }) } }) }) diff --git a/packages/browser-core/src/domain/session/storeStrategies/sessionInCookie.spec.ts b/packages/browser-core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index 1bb72770f9..ac9e9029e4 100644 --- a/packages/browser-core/src/domain/session/storeStrategies/sessionInCookie.spec.ts +++ b/packages/browser-core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,10 +1,11 @@ +import { vi, describe, expect, it } from 'vitest' import { globalObject } from '@datadog/js-core/util' import { registerCleanupTask, replaceMockable, mockCookies, collectAsyncCalls } from '../../../../test' import { Observable } from '../../../tools/observable' import type { SessionState } from '../sessionState' import type { Configuration, InitConfiguration } from '../../configuration' import { buildCookieOptions } from '../../configuration' -import { SESSION_COOKIE_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY, SessionPersistence } from '../sessionConstants' +import { SESSION_COOKIE_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from '../sessionConstants' import { CookieApi, LEGACY_SESSION_STORE_KEY } from './sessionStoreStrategy' import { createCookieAccess, selectCookieStrategy, initCookieStrategy } from './sessionInCookie' @@ -54,14 +55,16 @@ function setupCookieStrategy(partialInitConfiguration: Partial mockCookieAccess) + const cookiesMock = mockCookies() return { strategy: initCookieStrategy( - { type: SessionPersistence.COOKIE, cookieOptions, cookieApi: CookieApi.DOCUMENT_COOKIE }, + { type: 'cookie', cookieOptions, cookieApi: CookieApi.DOCUMENT_COOKIE }, configuration ), cookieOptions, mockCookie, + cookiesMock, } } @@ -132,38 +135,38 @@ describe('session in cookie strategy', () => { it('should strip c from state emitted via observable', async () => { const { strategy, mockCookie } = setupCookieStrategy() - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = strategy.sessionObservable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) mockCookie.simulateExternalChange('id=test&c=0') await collectAsyncCalls(spy, 1) - expect(spy.calls.mostRecent().args[0].c).toBeUndefined() + expect(spy.mock.lastCall![0].c).toBeUndefined() }) it('should notify observable when cookie is cleared', async () => { const { strategy, mockCookie } = setupCookieStrategy() - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = strategy.sessionObservable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) mockCookie.simulateExternalChange('') await collectAsyncCalls(spy, 1) - expect(spy.calls.mostRecent().args[0]).toEqual({}) + expect(spy.mock.lastCall![0]).toEqual({}) }) it('should notify sessionObservable after write', async () => { const { strategy } = setupCookieStrategy() - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = strategy.sessionObservable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) await strategy.setSessionState(() => ({ id: '123' }), 'updateState') await collectAsyncCalls(spy, 1) - expect(spy.calls.mostRecent().args[0]).toEqual({ id: '123' }) + expect(spy.mock.lastCall![0]).toEqual({ id: '123' }) }) it('should queue setSessionState calls and process them sequentially', async () => { @@ -242,23 +245,27 @@ describe('session in cookie strategy', () => { } function disableDocumentCookie() { - spyOnProperty(document, 'cookie', 'get').and.returnValue('') + vi.spyOn(document, 'cookie', 'get').mockReturnValue('') } - it('returns cookieStore strategy when both APIs are available', async () => { + it('returns cookieStore strategy when both APIs are available', async (ctx) => { if (!globalObject.cookieStore) { - pending('CookieStore API not available') + ctx.skip(true, 'CookieStore API not available') + return } mockCookies() const strategy = await selectCookieStrategy(makeConfiguration()) - expect(strategy).toEqual(jasmine.objectContaining({ cookieApi: CookieApi.COOKIE_STORE })) + if (strategy?.type !== 'cookie' || strategy.cookieApi !== CookieApi.COOKIE_STORE) { + ctx.skip(true, 'CookieStore is not available in this browser test context') + } + expect(strategy).toEqual(expect.objectContaining({ cookieApi: CookieApi.COOKIE_STORE })) }) it('falls back to document.cookie when CookieStore is unavailable', async () => { disableCookieStore() mockCookies() const strategy = await selectCookieStrategy(makeConfiguration()) - expect(strategy).toEqual(jasmine.objectContaining({ cookieApi: CookieApi.DOCUMENT_COOKIE })) + expect(strategy).toEqual(expect.objectContaining({ cookieApi: CookieApi.DOCUMENT_COOKIE })) }) it('returns undefined when both APIs are unavailable', async () => { @@ -275,9 +282,8 @@ describe('session in cookie strategy', () => { }) describe('migration from legacy cookie', () => { - function setLegacyCookie(value: string) { - const mock = mockCookies() - mock.getCookies().push({ + function setLegacyCookie(cookiesMock: ReturnType, value: string) { + cookiesMock.getCookies().push({ name: LEGACY_SESSION_STORE_KEY, value, expires: Date.now() + 60_000, @@ -285,8 +291,8 @@ describe('session in cookie strategy', () => { } it('should read from legacy cookie on first call when new cookie is empty', async () => { - setLegacyCookie('id=legacy-id&created=123&c=0') - const { strategy } = setupCookieStrategy() + const { strategy, cookiesMock } = setupCookieStrategy() + setLegacyCookie(cookiesMock, 'id=legacy-id&created=123&c=0') let capturedState: SessionState | undefined await strategy.setSessionState((state) => { @@ -299,8 +305,8 @@ describe('session in cookie strategy', () => { }) it('should not read from legacy cookie when new cookie has data', async () => { - setLegacyCookie('id=legacy-id&c=0') - const { strategy, mockCookie } = setupCookieStrategy() + const { strategy, mockCookie, cookiesMock } = setupCookieStrategy() + setLegacyCookie(cookiesMock, 'id=legacy-id&c=0') mockCookie.setAllValues(['id=new-id&c=0']) let capturedState: SessionState | undefined @@ -313,8 +319,8 @@ describe('session in cookie strategy', () => { }) it('should not read from legacy cookie on subsequent calls', async () => { - setLegacyCookie('id=legacy-id&c=0') - const { strategy, mockCookie } = setupCookieStrategy() + const { strategy, mockCookie, cookiesMock } = setupCookieStrategy() + setLegacyCookie(cookiesMock, 'id=legacy-id&c=0') // First call triggers migration await strategy.setSessionState((state) => state, 'updateState') diff --git a/packages/browser-core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts b/packages/browser-core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts index 4bc7553c51..d36829e319 100644 --- a/packages/browser-core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts +++ b/packages/browser-core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { registerCleanupTask } from '../../../../test' import type { TrustableEvent } from '../../../browser/addEventListener' import type { SessionState } from '../sessionState' @@ -47,19 +48,20 @@ describe('LocalStorage SessionStoreStrategy', () => { }) it('should notify sessionObservable after write', async () => { - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = strategy.sessionObservable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) await strategy.setSessionState((state) => ({ ...state, id: 'test-id' }), 'updateState') - expect(spy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ id: 'test-id' })) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ id: 'test-id' })) }) }) describe('sessionObservable', () => { it('should emit on storage event from other tabs', () => { - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = strategy.sessionObservable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) @@ -72,11 +74,12 @@ describe('LocalStorage SessionStoreStrategy', () => { ;(event as TrustableEvent).__ddIsTrusted = true window.dispatchEvent(event) - expect(spy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ id: 'from-other-tab' })) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ id: 'from-other-tab' })) }) it('should ignore storage events for other keys', () => { - const spy = jasmine.createSpy('observer') + const spy = vi.fn() const subscription = strategy.sessionObservable.subscribe(spy) registerCleanupTask(() => subscription.unsubscribe()) diff --git a/packages/browser-core/src/domain/session/storeStrategies/sessionInMemory.spec.ts b/packages/browser-core/src/domain/session/storeStrategies/sessionInMemory.spec.ts index 653d187edf..365d41ccc3 100644 --- a/packages/browser-core/src/domain/session/storeStrategies/sessionInMemory.spec.ts +++ b/packages/browser-core/src/domain/session/storeStrategies/sessionInMemory.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { globalObject } from '@datadog/js-core/util' import { registerCleanupTask } from '../../../../test' import type { SessionState } from '../sessionState' @@ -32,24 +33,26 @@ describe('Memory SessionStoreStrategy', () => { }) it('should notify sessionObservable after write', async () => { - const spy = jasmine.createSpy('observer') + const spy = vi.fn() strategy.sessionObservable.subscribe(spy) await strategy.setSessionState((state) => ({ ...state, id: 'test-id' }), 'updateState') - expect(spy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ id: 'test-id' })) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ id: 'test-id' })) }) }) describe('sessionObservable', () => { it('should be shared across strategy instances', async () => { const strategy2 = initMemorySessionStoreStrategy() - const spy = jasmine.createSpy('observer') + const spy = vi.fn() strategy.sessionObservable.subscribe(spy) await strategy2.setSessionState((state) => ({ ...state, id: 'from-strategy2' }), 'updateState') - expect(spy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ id: 'from-strategy2' })) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ id: 'from-strategy2' })) }) }) diff --git a/packages/browser-core/src/domain/sourceCodeContext.spec.ts b/packages/browser-core/src/domain/sourceCodeContext.spec.ts index 96b5b29f12..11f33e800a 100644 --- a/packages/browser-core/src/domain/sourceCodeContext.spec.ts +++ b/packages/browser-core/src/domain/sourceCodeContext.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mockSourceCodeContext, startMockTelemetry } from '../../test' import { buildDebugIdByUrl, getSourceCodeContext } from './sourceCodeContext' @@ -103,7 +104,7 @@ describe('source code context telemetry usage', () => { const events = await telemetry.getEvents() expect(events).toEqual([ - jasmine.objectContaining({ + expect.objectContaining({ type: 'usage', usage: { feature: 'source-code-context', use_debug_id: true, use_service: true, use_version: false }, }), diff --git a/packages/browser-core/src/domain/synthetics/syntheticsWorkerValues.spec.ts b/packages/browser-core/src/domain/synthetics/syntheticsWorkerValues.spec.ts index c18c5ad63a..b1d25d997c 100644 --- a/packages/browser-core/src/domain/synthetics/syntheticsWorkerValues.spec.ts +++ b/packages/browser-core/src/domain/synthetics/syntheticsWorkerValues.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mockSyntheticsWorkerValues } from '../../../test' import { getSyntheticsContext, willSyntheticsInjectRum } from './syntheticsWorkerValues' @@ -6,25 +7,25 @@ describe('syntheticsWorkerValues', () => { it('returns false if nothing is defined', () => { mockSyntheticsWorkerValues({}, 'globals') - expect(willSyntheticsInjectRum()).toBeFalse() + expect(willSyntheticsInjectRum()).toBe(false) }) it('returns false if the INJECTS_RUM global variable is false', () => { mockSyntheticsWorkerValues({ injectsRum: false }, 'globals') - expect(willSyntheticsInjectRum()).toBeFalse() + expect(willSyntheticsInjectRum()).toBe(false) }) it('returns true if the INJECTS_RUM global variable is truthy', () => { mockSyntheticsWorkerValues({ injectsRum: true }, 'globals') - expect(willSyntheticsInjectRum()).toBeTrue() + expect(willSyntheticsInjectRum()).toBe(true) }) it('returns true if the INJECTS_RUM cookie is truthy', () => { mockSyntheticsWorkerValues({ injectsRum: true }, 'cookies') - expect(willSyntheticsInjectRum()).toBeTrue() + expect(willSyntheticsInjectRum()).toBe(true) }) }) diff --git a/packages/browser-core/src/domain/tags.spec.ts b/packages/browser-core/src/domain/tags.spec.ts index 1fd4526382..9b20a9998b 100644 --- a/packages/browser-core/src/domain/tags.spec.ts +++ b/packages/browser-core/src/domain/tags.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { display } from '../tools/display' import type { Configuration } from './configuration' import { buildTag, buildTags, TAG_SIZE_LIMIT } from './tags' @@ -18,9 +19,9 @@ describe('buildTags', () => { }) describe('buildTag warning', () => { - let displaySpy: jasmine.Spy + let displaySpy: Mock beforeEach(() => { - displaySpy = spyOn(display, 'warn') + displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) }) ;( [ @@ -63,8 +64,9 @@ describe('buildTag warning', () => { }) function expectWarning() { - expect(displaySpy).toHaveBeenCalledOnceWith( - jasmine.stringMatching("Tag .* doesn't meet tag requirements and will be sanitized") + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching("Tag .* doesn't meet tag requirements and will be sanitized") ) } }) diff --git a/packages/browser-core/src/domain/telemetry/telemetry.spec.ts b/packages/browser-core/src/domain/telemetry/telemetry.spec.ts index de7cbbb4bb..850b10e4ca 100644 --- a/packages/browser-core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/browser-core/src/domain/telemetry/telemetry.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { createHook } from '@datadog/js-core/assembly' import { INTAKE_SITE_US1_FED, INTAKE_SITE_US2_FED, INTAKE_SITE_US1 } from '@datadog/js-core/transport' import { NO_ERROR_STACK_PRESENT_MESSAGE } from '../error/error' @@ -75,8 +76,8 @@ describe('telemetry', () => { }) expect(await getTelemetryEvents()).toEqual([ - jasmine.objectContaining({ - telemetry: jasmine.objectContaining({ + expect.objectContaining({ + telemetry: expect.objectContaining({ type: TelemetryType.LOG, status: StatusType.error, }), @@ -94,10 +95,10 @@ describe('telemetry', () => { addTelemetryConfiguration({}) expect(await getTelemetryEvents()).toEqual([ - jasmine.objectContaining({ - telemetry: jasmine.objectContaining({ + expect.objectContaining({ + telemetry: expect.objectContaining({ type: TelemetryType.CONFIGURATION, - configuration: jasmine.anything(), + configuration: expect.anything(), }), }), ]) @@ -133,10 +134,10 @@ describe('telemetry', () => { addTelemetryUsage({ feature: 'set-tracking-consent', tracking_consent: 'granted' }) expect(await getTelemetryEvents()).toEqual([ - jasmine.objectContaining({ - telemetry: jasmine.objectContaining({ + expect.objectContaining({ + telemetry: expect.objectContaining({ type: TelemetryType.USAGE, - usage: jasmine.anything(), + usage: expect.anything(), }), }), ]) @@ -166,8 +167,8 @@ describe('telemetry', () => { addTelemetryMetrics(TelemetryMetrics.CUSTOMER_DATA_METRIC_NAME, { speed: 1000 }) expect(await getTelemetryEvents()).toEqual([ - jasmine.objectContaining({ - telemetry: jasmine.objectContaining({ + expect.objectContaining({ + telemetry: expect.objectContaining({ type: TelemetryType.LOG, message: TelemetryMetrics.CUSTOMER_DATA_METRIC_NAME, status: StatusType.debug, @@ -210,8 +211,8 @@ describe('telemetry', () => { }) expect((await getTelemetryEvents())[0].telemetry.runtime_env).toEqual({ - is_local_file: jasmine.any(Boolean), - is_worker: jasmine.any(Boolean), + is_local_file: expect.any(Boolean), + is_worker: expect.any(Boolean), }) }) @@ -307,7 +308,7 @@ describe('telemetry', () => { describe('sampling', () => { it('should notify when sampled', async () => { - spyOn(Math, 'random').and.callFake(() => 0) + vi.spyOn(Math, 'random').mockImplementation(() => 0) const { getTelemetryEvents } = startAndSpyTelemetry({ telemetrySampleRate: 50 }) callMonitored(() => { @@ -318,7 +319,7 @@ describe('telemetry', () => { }) it('should not notify when not sampled', async () => { - spyOn(Math, 'random').and.callFake(() => 1) + vi.spyOn(Math, 'random').mockImplementation(() => 1) const { getTelemetryEvents } = startAndSpyTelemetry({ telemetrySampleRate: 50 }) callMonitored(() => { @@ -403,18 +404,18 @@ describe('telemetry', () => { expect((await getTelemetryEvents()).map((event) => event.telemetry)).toEqual([ // Group 1. - jasmine.objectContaining({ message: 'debug 1' }), - jasmine.objectContaining({ message: 'error 1' }), - jasmine.objectContaining({ message: TelemetryMetrics.SEGMENT_METRICS_TELEMETRY_NAME, bandwidth: 500 }), - jasmine.objectContaining({ message: TelemetryMetrics.CUSTOMER_DATA_METRIC_NAME, speed: 1000 }), - jasmine.objectContaining({ usage: jasmine.objectContaining({ feature: 'stop-session' }) }), + expect.objectContaining({ message: 'debug 1' }), + expect.objectContaining({ message: 'error 1' }), + expect.objectContaining({ message: TelemetryMetrics.SEGMENT_METRICS_TELEMETRY_NAME, bandwidth: 500 }), + expect.objectContaining({ message: TelemetryMetrics.CUSTOMER_DATA_METRIC_NAME, speed: 1000 }), + expect.objectContaining({ usage: expect.objectContaining({ feature: 'stop-session' }) }), // Group 2. - jasmine.objectContaining({ message: 'debug 2' }), - jasmine.objectContaining({ message: 'error 2' }), - jasmine.objectContaining({ message: TelemetryMetrics.SEGMENT_METRICS_TELEMETRY_NAME, latency: 50 }), - jasmine.objectContaining({ message: TelemetryMetrics.CUSTOMER_DATA_METRIC_NAME, jank: 50 }), - jasmine.objectContaining({ usage: jasmine.objectContaining({ feature: 'start-session-replay-recording' }) }), + expect.objectContaining({ message: 'debug 2' }), + expect.objectContaining({ message: 'error 2' }), + expect.objectContaining({ message: TelemetryMetrics.SEGMENT_METRICS_TELEMETRY_NAME, latency: 50 }), + expect.objectContaining({ message: TelemetryMetrics.CUSTOMER_DATA_METRIC_NAME, jank: 50 }), + expect.objectContaining({ usage: expect.objectContaining({ feature: 'start-session-replay-recording' }) }), ]) }) }) @@ -489,7 +490,7 @@ describe('formatError', () => { message: 'message', error: { kind: 'Error', - stack: jasmine.stringMatching(/^Error: message(\n|$)/) as unknown as string, + stack: expect.stringMatching(/^Error: message(\n|$)/), }, }) }) @@ -528,7 +529,7 @@ describe('scrubCustomerFrames', () => { const candidate: Partial = { stack: [{ url }], } - expect(scrubCustomerFrames(candidate as StackTrace).stack.length).toBe(scrub ? 0 : 1, `for url: ${url!}`) + expect(scrubCustomerFrames(candidate as StackTrace).stack.length).toBe(scrub ? 0 : 1) }) }) }) diff --git a/packages/browser-core/src/domain/trackingConsent.spec.ts b/packages/browser-core/src/domain/trackingConsent.spec.ts index 35381e6d56..547ca8d94f 100644 --- a/packages/browser-core/src/domain/trackingConsent.spec.ts +++ b/packages/browser-core/src/domain/trackingConsent.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { TrackingConsent, createTrackingConsentState } from './trackingConsent' describe('createTrackingConsentState', () => { @@ -8,22 +9,22 @@ describe('createTrackingConsentState', () => { it('defaults to not granted', () => { const trackingConsentState = createTrackingConsentState() - expect(trackingConsentState.isGranted()).toBeFalse() + expect(trackingConsentState.isGranted()).toBe(false) }) it('can be created with a default consent state', () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - expect(trackingConsentState.isGranted()).toBeTrue() + expect(trackingConsentState.isGranted()).toBe(true) }) it('can be updated to granted', () => { const trackingConsentState = createTrackingConsentState() trackingConsentState.update(TrackingConsent.GRANTED) - expect(trackingConsentState.isGranted()).toBeTrue() + expect(trackingConsentState.isGranted()).toBe(true) }) it('notifies when the consent is updated', () => { - const spy = jasmine.createSpy() + const spy = vi.fn() const trackingConsentState = createTrackingConsentState() trackingConsentState.observable.subscribe(spy) trackingConsentState.update(TrackingConsent.GRANTED) @@ -33,26 +34,26 @@ describe('createTrackingConsentState', () => { it('can init a consent state if not defined yet', () => { const trackingConsentState = createTrackingConsentState() trackingConsentState.tryToInit(TrackingConsent.GRANTED) - expect(trackingConsentState.isGranted()).toBeTrue() + expect(trackingConsentState.isGranted()).toBe(true) }) it('does not init a consent state if already defined', () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) trackingConsentState.tryToInit(TrackingConsent.NOT_GRANTED) - expect(trackingConsentState.isGranted()).toBeTrue() + expect(trackingConsentState.isGranted()).toBe(true) }) describe('onGrantedOnce', () => { it('calls onGrantedOnce when consent was already granted', () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - const spy = jasmine.createSpy() + const spy = vi.fn() trackingConsentState.onGrantedOnce(spy) expect(spy).toHaveBeenCalledTimes(1) }) it('calls onGrantedOnce when consent is granted', () => { const trackingConsentState = createTrackingConsentState() - const spy = jasmine.createSpy() + const spy = vi.fn() trackingConsentState.onGrantedOnce(spy) expect(spy).toHaveBeenCalledTimes(0) trackingConsentState.update(TrackingConsent.GRANTED) diff --git a/packages/browser-core/src/tools/abstractLifeCycle.spec.ts b/packages/browser-core/src/tools/abstractLifeCycle.spec.ts index 49db891859..0815d2ecd9 100644 --- a/packages/browser-core/src/tools/abstractLifeCycle.spec.ts +++ b/packages/browser-core/src/tools/abstractLifeCycle.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { AbstractLifeCycle } from './abstractLifeCycle' describe('AbstractLifeCycle', () => { @@ -13,30 +14,33 @@ describe('AbstractLifeCycle', () => { it('notifies subscribers', () => { const lifeCycle = new LifeCycle() - const subscriber1Spy = jasmine.createSpy() - const subscriber2Spy = jasmine.createSpy() + const subscriber1Spy = vi.fn() + const subscriber2Spy = vi.fn() lifeCycle.subscribe('foo', subscriber1Spy) lifeCycle.subscribe('foo', subscriber2Spy) lifeCycle.notify('foo', 'bar') - expect(subscriber1Spy).toHaveBeenCalledOnceWith('bar') - expect(subscriber2Spy).toHaveBeenCalledOnceWith('bar') + expect(subscriber1Spy).toHaveBeenCalledTimes(1) + expect(subscriber1Spy).toHaveBeenCalledExactlyOnceWith('bar') + expect(subscriber2Spy).toHaveBeenCalledTimes(1) + expect(subscriber2Spy).toHaveBeenCalledExactlyOnceWith('bar') }) it('notifies subscribers for events without data', () => { const lifeCycle = new LifeCycle() - const subscriberSpy = jasmine.createSpy() + const subscriberSpy = vi.fn() lifeCycle.subscribe('no_data', subscriberSpy) lifeCycle.notify('no_data') - expect(subscriberSpy).toHaveBeenCalledOnceWith(undefined) + expect(subscriberSpy).toHaveBeenCalledTimes(1) + expect(subscriberSpy).toHaveBeenCalledExactlyOnceWith(undefined) }) it('does not notify unsubscribed subscribers', () => { const lifeCycle = new LifeCycle() - const subscriberSpy = jasmine.createSpy() + const subscriberSpy = vi.fn() lifeCycle.subscribe('foo', subscriberSpy).unsubscribe() lifeCycle.notify('foo', 'bar') diff --git a/packages/browser-core/src/tools/catchUserErrors.spec.ts b/packages/browser-core/src/tools/catchUserErrors.spec.ts index 9c833985cc..544fc61e8d 100644 --- a/packages/browser-core/src/tools/catchUserErrors.spec.ts +++ b/packages/browser-core/src/tools/catchUserErrors.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { catchUserErrors } from './catchUserErrors' import { display } from './display' @@ -8,7 +9,7 @@ describe('catchUserErrors', () => { }) it('logs errors using console.error and returns undefined', () => { - const displaySpy = spyOn(display, 'error') + const displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const myError = 'Ooops!' const wrappedFn = catchUserErrors(() => { // eslint-disable-next-line @typescript-eslint/only-throw-error diff --git a/packages/browser-core/src/tools/encoder.spec.ts b/packages/browser-core/src/tools/encoder.spec.ts index 5f177b815d..2553344dfd 100644 --- a/packages/browser-core/src/tools/encoder.spec.ts +++ b/packages/browser-core/src/tools/encoder.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { createIdentityEncoder } from './encoder' import { noop } from './utils/functionUtils' @@ -27,11 +28,12 @@ describe('createIdentityEncoder', () => { it('calls the callback when writing data with a callback', () => { const encoder = createIdentityEncoder() const data = 'Callback test' - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() encoder.write(data, callbackSpy) - expect(callbackSpy).toHaveBeenCalledOnceWith(data.length) + expect(callbackSpy).toHaveBeenCalledTimes(1) + expect(callbackSpy).toHaveBeenCalledExactlyOnceWith(data.length) }) }) @@ -40,7 +42,7 @@ describe('createIdentityEncoder', () => { const encoder = createIdentityEncoder() const data = 'Final data' encoder.write(data) - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() encoder.finish(callbackSpy) diff --git a/packages/browser-core/src/tools/experimentalFeatures.spec.ts b/packages/browser-core/src/tools/experimentalFeatures.spec.ts index 0655d567b5..f52fb5d0ea 100644 --- a/packages/browser-core/src/tools/experimentalFeatures.spec.ts +++ b/packages/browser-core/src/tools/experimentalFeatures.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { ExperimentalFeature, addExperimentalFeatures, @@ -10,22 +11,22 @@ const TEST_FEATURE_FLAG_TWO = 'bar' as ExperimentalFeature describe('experimentalFeatures', () => { it('initial state is empty', () => { - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeFalse() - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeFalse() + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBe(false) + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBe(false) }) it('should define enabled experimental features', () => { addExperimentalFeatures([TEST_FEATURE_FLAG_ONE]) - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue() - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeFalse() + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBe(true) + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBe(false) }) it('should allow to be shared between products', () => { addExperimentalFeatures([TEST_FEATURE_FLAG_ONE]) addExperimentalFeatures([TEST_FEATURE_FLAG_TWO]) - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue() - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeTrue() + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBe(true) + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBe(true) }) }) @@ -41,14 +42,14 @@ describe('initFeatureFlags', () => { it('ignores unknown experimental features', () => { initFeatureFlags(['bar', undefined as any, null as any, 11 as any]) - expect(isExperimentalFeatureEnabled('bar' as any)).toBeFalse() - expect(isExperimentalFeatureEnabled(undefined as any)).toBeFalse() - expect(isExperimentalFeatureEnabled(null as any)).toBeFalse() - expect(isExperimentalFeatureEnabled(11 as any)).toBeFalse() + expect(isExperimentalFeatureEnabled('bar' as any)).toBe(false) + expect(isExperimentalFeatureEnabled(undefined as any)).toBe(false) + expect(isExperimentalFeatureEnabled(null as any)).toBe(false) + expect(isExperimentalFeatureEnabled(11 as any)).toBe(false) }) it('updates experimental feature flags', () => { initFeatureFlags(['foo']) - expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue() + expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBe(true) }) }) diff --git a/packages/browser-core/src/tools/getZoneJsOriginalValue.spec.ts b/packages/browser-core/src/tools/getZoneJsOriginalValue.spec.ts index e4a774c41f..9dbff02b0f 100644 --- a/packages/browser-core/src/tools/getZoneJsOriginalValue.spec.ts +++ b/packages/browser-core/src/tools/getZoneJsOriginalValue.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mockZoneJs } from '../../test' import type { BrowserWindowWithZoneJs } from './getZoneJsOriginalValue' diff --git a/packages/browser-core/src/tools/instrumentMethod.spec.ts b/packages/browser-core/src/tools/instrumentMethod.spec.ts index a3eab89a0b..655a0a6595 100644 --- a/packages/browser-core/src/tools/instrumentMethod.spec.ts +++ b/packages/browser-core/src/tools/instrumentMethod.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { mockClock, mockZoneJs, registerCleanupTask } from '../../test' import type { Clock, MockZoneJs } from '../../test' import type { InstrumentedMethodCall } from './instrumentMethod' @@ -17,8 +18,8 @@ describe('instrumentMethod', () => { }) it('calls the instrumentation before the original method', () => { - const originalSpy = jasmine.createSpy() - const instrumentationSpy = jasmine.createSpy() + const originalSpy = vi.fn() + const instrumentationSpy = vi.fn() const object = { method: originalSpy } instrumentMethod(object, 'method', instrumentationSpy) @@ -39,7 +40,7 @@ describe('instrumentMethod', () => { it('sets an event handler even if it was originally undefined', () => { const object: { onevent?: () => void } = { onevent: undefined } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentMethod(object, 'onevent', instrumentationSpy) expect(object.onevent).toBeDefined() @@ -51,27 +52,28 @@ describe('instrumentMethod', () => { it('do not set an event handler even if the event is not supported (i.e. property does not exist on object)', () => { const object: { onevent?: () => void } = {} - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentMethod(object, 'onevent', instrumentationSpy) - expect('onevent' in object).toBeFalse() + expect('onevent' in object).toBe(false) }) it('calls the instrumentation with method target and parameters', () => { const object = { method: (a: number, b: number) => a + b } - const instrumentationSpy = jasmine.createSpy<(call: InstrumentedMethodCall) => void>() + const instrumentationSpy = vi.fn<(call: InstrumentedMethodCall) => void>() instrumentMethod(object, 'method', instrumentationSpy) object.method(2, 3) - expect(instrumentationSpy).toHaveBeenCalledOnceWith({ + expect(instrumentationSpy).toHaveBeenCalledTimes(1) + expect(instrumentationSpy).toHaveBeenCalledExactlyOnceWith({ target: object, - parameters: jasmine.any(Object), - onPostCall: jasmine.any(Function), + parameters: expect.any(Object), + onPostCall: expect.any(Function), handlingStack: undefined, }) - expect(instrumentationSpy.calls.mostRecent().args[0].parameters[0]).toBe(2) - expect(instrumentationSpy.calls.mostRecent().args[0].parameters[1]).toBe(3) + expect(instrumentationSpy.mock.lastCall![0].parameters[0]).toBe(2) + expect(instrumentationSpy.mock.lastCall![0].parameters[1]).toBe(3) }) it('allows replacing a parameter', () => { @@ -94,17 +96,18 @@ describe('instrumentMethod', () => { it('calls the "onPostCall" callback with the original method result', () => { const object = { method: () => 1 } - const onPostCallSpy = jasmine.createSpy() + const onPostCallSpy = vi.fn() instrumentMethod(object, 'method', ({ onPostCall }) => onPostCall(onPostCallSpy)) object.method() - expect(onPostCallSpy).toHaveBeenCalledOnceWith(1) + expect(onPostCallSpy).toHaveBeenCalledTimes(1) + expect(onPostCallSpy).toHaveBeenCalledExactlyOnceWith(1) }) it('allows other instrumentations from third parties', () => { const object = { method: () => 1 } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentMethod(object, 'method', instrumentationSpy) thirdPartyInstrumentation(object) @@ -115,7 +118,7 @@ describe('instrumentMethod', () => { it('computes the handling stack', () => { const object = { method: () => 1 } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentMethod(object, 'method', instrumentationSpy, { computeHandlingStack: true }) function foo() { @@ -124,15 +127,15 @@ describe('instrumentMethod', () => { foo() - expect(instrumentationSpy.calls.mostRecent().args[0].handlingStack).toEqual( - jasmine.stringMatching(/^HandlingStack: instrumented method\n {2}at foo @/) + expect(instrumentationSpy.mock.lastCall![0].handlingStack).toEqual( + expect.stringMatching(/^HandlingStack: instrumented method\n {2}at foo @/) ) }) describe('stop()', () => { it('does not call the instrumentation anymore', () => { const object = { method: () => 1 } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() const { stop } = instrumentMethod(object, 'method', () => instrumentationSpy) stop() @@ -156,7 +159,7 @@ describe('instrumentMethod', () => { it('does not call the instrumentation', () => { const object = { method: () => 1 } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() const { stop } = instrumentMethod(object, 'method', instrumentationSpy) thirdPartyInstrumentation(object) @@ -209,14 +212,15 @@ describe('instrumentConstructor', () => { it('calls the instrumentation when the constructor is called with new', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentConstructor(container, 'MyClass', instrumentationSpy) const instance = new container.MyClass(42) - expect(instrumentationSpy).toHaveBeenCalledOnceWith({ + expect(instrumentationSpy).toHaveBeenCalledOnce() + expect(instrumentationSpy).toHaveBeenCalledExactlyOnceWith({ parameters: [42], - onPostCall: jasmine.any(Function), + onPostCall: expect.any(Function), handlingStack: undefined, }) expect(instance.value).toBe(42) @@ -225,7 +229,7 @@ describe('instrumentConstructor', () => { it('allows other instrumentations from third parties', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentConstructor(container, 'MyClass', instrumentationSpy) thirdPartyConstructorWrap(container) @@ -239,7 +243,7 @@ describe('instrumentConstructor', () => { it('computes the handling stack', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentConstructor(container, 'MyClass', instrumentationSpy, { computeHandlingStack: true }) function foo() { @@ -248,14 +252,14 @@ describe('instrumentConstructor', () => { foo() - expect(instrumentationSpy.calls.mostRecent().args[0].handlingStack).toEqual( - jasmine.stringMatching(/^HandlingStack: instrumented constructor\n {2}at foo @/) + expect(instrumentationSpy.mock.calls[instrumentationSpy.mock.calls.length - 1][0].handlingStack).toEqual( + expect.stringMatching(/^HandlingStack: instrumented constructor\n {2}at foo @/) ) }) it('does not call the instrumentation when the constructor is invoked without new', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() instrumentConstructor(container, 'MyClass', instrumentationSpy) // Bare `[[Call]]` has no `new.target`; the original class constructor throws before any work. @@ -273,20 +277,20 @@ describe('instrumentConstructor', () => { const instance = new container.MyClass(1) // The instance is recognized both as the original class and as the instrumented value. - expect(instance instanceof MyClass).toBeTrue() - expect(instance instanceof container.MyClass).toBeTrue() + expect(instance instanceof MyClass).toBe(true) + expect(instance instanceof container.MyClass).toBe(true) }) it('keeps the constructor prototype property non-writable after instrumentation', () => { const originalPrototypeDescriptor = Object.getOwnPropertyDescriptor(MyClass, 'prototype')! - expect(originalPrototypeDescriptor.writable).toBeFalse() + expect(originalPrototypeDescriptor.writable).toBe(false) const container = { MyClass } const { stop } = instrumentConstructor(container, 'MyClass', noop) registerCleanupTask(stop) const instrumentedPrototypeDescriptor = Object.getOwnPropertyDescriptor(container.MyClass, 'prototype')! - expect(instrumentedPrototypeDescriptor.writable).toBeFalse() + expect(instrumentedPrototypeDescriptor.writable).toBe(false) }) it('exposes the instrumented constructor as instance.constructor', () => { @@ -365,18 +369,21 @@ describe('instrumentConstructor', () => { container.MyClass = class extends OurInstrumentation {} const instance = new container.MyClass(99) - expect(instance instanceof container.MyClass).toBeTrue() + expect(instance instanceof container.MyClass).toBe(true) }) it('passes the constructed instance to onPostCall', () => { const container = { MyClass } - const postCallCallbackSpy = jasmine.createSpy() + const postCallCallbackSpy = vi.fn() instrumentConstructor(container, 'MyClass', ({ onPostCall }) => onPostCall(postCallCallbackSpy)) const instance = new container.MyClass(7) - expect(postCallCallbackSpy).toHaveBeenCalledOnceWith(instance) - expect((postCallCallbackSpy.calls.mostRecent().args[0] as MyClassInstance).value).toBe(7) + expect(postCallCallbackSpy).toHaveBeenCalledOnce() + expect(postCallCallbackSpy).toHaveBeenCalledExactlyOnceWith(instance) + expect( + (postCallCallbackSpy.mock.calls[postCallCallbackSpy.mock.calls.length - 1][0] as MyClassInstance).value + ).toBe(7) }) it('does not instrument a constructor that does not exist', () => { @@ -391,7 +398,7 @@ describe('instrumentConstructor', () => { describe('stop()', () => { it('constructs without calling the instrumentation when calling new Original()', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() const { stop } = instrumentConstructor(container, 'MyClass', instrumentationSpy) stop() @@ -399,13 +406,13 @@ describe('instrumentConstructor', () => { const instance = new container.MyClass(5) expect(instrumentationSpy).not.toHaveBeenCalled() - expect(instance instanceof MyClass).toBeTrue() + expect(instance instanceof MyClass).toBe(true) expect(instance.value).toBe(5) }) it('constructs without calling the instrumentation when calling the instrumentation reference', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() const { stop } = instrumentConstructor(container, 'MyClass', instrumentationSpy) const OurInstrumentation = container.MyClass @@ -415,7 +422,7 @@ describe('instrumentConstructor', () => { const instance = new OurInstrumentation(5) expect(instrumentationSpy).not.toHaveBeenCalled() - expect(instance instanceof MyClass).toBeTrue() + expect(instance instanceof MyClass).toBe(true) expect(instance.value).toBe(5) }) @@ -434,7 +441,7 @@ describe('instrumentConstructor', () => { it('does not call the instrumentation when constructing after stop()', () => { const container = { MyClass } - const instrumentationSpy = jasmine.createSpy() + const instrumentationSpy = vi.fn() const { stop } = instrumentConstructor(container, 'MyClass', instrumentationSpy) thirdPartyConstructorWrap(container) @@ -446,7 +453,7 @@ describe('instrumentConstructor', () => { expect(instrumentationSpy).not.toHaveBeenCalled() expect(instance.value).toBe(5) expect(instance.thirdPartyTag).toBe(THIRD_PARTY_CONSTRUCTOR_TAG) - expect(instance instanceof MyClass).toBeTrue() + expect(instance instanceof MyClass).toBe(true) }) }) @@ -468,7 +475,7 @@ describe('instrumentConstructor', () => { // and loses the subclass prototype methods. const instance = new Sub(5) - expect(instance instanceof Sub).toBeTrue() + expect(instance instanceof Sub).toBe(true) expect(instance.extra()).toBe('sub') expect(instance.value).toBe(5) }) @@ -595,18 +602,19 @@ describe('instrumentSetter', () => { }) it('calls the original setter', () => { - const originalSetterSpy = jasmine.createSpy() + const originalSetterSpy = vi.fn() const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: originalSetterSpy, configurable: true }) instrumentSetter(object, 'foo', noop) object.foo = 1 - expect(originalSetterSpy).toHaveBeenCalledOnceWith(1) + expect(originalSetterSpy).toHaveBeenCalledTimes(1) + expect(originalSetterSpy).toHaveBeenCalledExactlyOnceWith(1) }) it('calls the instrumentation asynchronously', () => { - const instrumentationSetterSpy = jasmine.createSpy() + const instrumentationSetterSpy = vi.fn() const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: noop, configurable: true }) @@ -615,12 +623,13 @@ describe('instrumentSetter', () => { object.foo = 1 expect(instrumentationSetterSpy).not.toHaveBeenCalled() clock.tick(0) - expect(instrumentationSetterSpy).toHaveBeenCalledOnceWith(object, 1) + expect(instrumentationSetterSpy).toHaveBeenCalledTimes(1) + expect(instrumentationSetterSpy).toHaveBeenCalledExactlyOnceWith(object, 1) }) it('does not use the Zone.js setTimeout function', () => { - const zoneJsSetTimeoutSpy = jasmine.createSpy() - zoneJs.replaceProperty(window, 'setTimeout', zoneJsSetTimeoutSpy) + const zoneJsSetTimeoutSpy = vi.fn() + zoneJs.replaceProperty(window, 'setTimeout', zoneJsSetTimeoutSpy as any) const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: noop, configurable: true }) @@ -636,15 +645,17 @@ describe('instrumentSetter', () => { it('allows other instrumentations from third parties', () => { const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: noop, configurable: true }) - const instrumentationSetterSpy = jasmine.createSpy() + const instrumentationSetterSpy = vi.fn() instrumentSetter(object, 'foo', instrumentationSetterSpy) const thirdPartyInstrumentationSpy = thirdPartyInstrumentation(object) object.foo = 2 - expect(thirdPartyInstrumentationSpy).toHaveBeenCalledOnceWith(2) + expect(thirdPartyInstrumentationSpy).toHaveBeenCalledTimes(1) + expect(thirdPartyInstrumentationSpy).toHaveBeenCalledExactlyOnceWith(2) clock.tick(0) - expect(instrumentationSetterSpy).toHaveBeenCalledOnceWith(object, 2) + expect(instrumentationSetterSpy).toHaveBeenCalledTimes(1) + expect(instrumentationSetterSpy).toHaveBeenCalledExactlyOnceWith(object, 2) }) describe('stop()', () => { @@ -665,7 +676,7 @@ describe('instrumentSetter', () => { it('does not call the instrumentation anymore', () => { const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: noop, configurable: true }) - const instrumentationSetterSpy = jasmine.createSpy() + const instrumentationSetterSpy = vi.fn() const { stop } = instrumentSetter(object, 'foo', instrumentationSetterSpy) stop() @@ -679,7 +690,7 @@ describe('instrumentSetter', () => { it('does not call instrumentation pending in the event loop via setTimeout', () => { const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: noop, configurable: true }) - const instrumentationSetterSpy = jasmine.createSpy() + const instrumentationSetterSpy = vi.fn() const { stop } = instrumentSetter(object, 'foo', instrumentationSetterSpy) object.foo = 2 @@ -706,7 +717,7 @@ describe('instrumentSetter', () => { it('does not call the instrumentation', () => { const object = {} as { foo: number } Object.defineProperty(object, 'foo', { set: noop, configurable: true }) - const instrumentationSetterSpy = jasmine.createSpy() + const instrumentationSetterSpy = vi.fn() const { stop } = instrumentSetter(object, 'foo', instrumentationSetterSpy) thirdPartyInstrumentation(object) @@ -724,7 +735,7 @@ describe('instrumentSetter', () => { function thirdPartyInstrumentation(object: { foo: number }) { // eslint-disable-next-line @typescript-eslint/unbound-method const originalSetter = Object.getOwnPropertyDescriptor(object, 'foo')!.set - const thirdPartyInstrumentationSpy = jasmine.createSpy().and.callFake(function (this: any, value) { + const thirdPartyInstrumentationSpy = vi.fn().mockImplementation(function (this: any, value) { if (originalSetter) { originalSetter.call(this, value) } diff --git a/packages/browser-core/src/tools/matchOption.spec.ts b/packages/browser-core/src/tools/matchOption.spec.ts index 767e0b30e9..1dfe4e4357 100644 --- a/packages/browser-core/src/tools/matchOption.spec.ts +++ b/packages/browser-core/src/tools/matchOption.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { display } from './display' import { matchList } from './matchOption' @@ -36,7 +37,7 @@ describe('matchList', () => { }) it('should catch error from provided function', () => { - spyOn(display, 'error') + vi.spyOn(display, 'error').mockImplementation(() => undefined) const list = [ (_: string) => { throw new Error('oops') diff --git a/packages/browser-core/src/tools/monitor.spec.ts b/packages/browser-core/src/tools/monitor.spec.ts index 549ddeffd2..c4fd808600 100644 --- a/packages/browser-core/src/tools/monitor.spec.ts +++ b/packages/browser-core/src/tools/monitor.spec.ts @@ -1,10 +1,11 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { callMonitored, monitored, startMonitorErrorCollection } from './monitor' describe('monitor', () => { - let onMonitorErrorCollectedSpy: jasmine.Spy<(error: unknown) => void> + let onMonitorErrorCollectedSpy: Mock<(error: unknown) => void> beforeEach(() => { - onMonitorErrorCollectedSpy = jasmine.createSpy() + onMonitorErrorCollectedSpy = vi.fn() }) it('catches monitored errors but does not report them before startMonitorErrorCollection', () => { @@ -24,6 +25,6 @@ describe('monitor', () => { callMonitored(() => { throw new Error('error') }) - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith(new Error('error')) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith(new Error('error')) }) }) diff --git a/packages/browser-core/src/tools/observable.spec.ts b/packages/browser-core/src/tools/observable.spec.ts index 2cbf77583a..a01773385e 100644 --- a/packages/browser-core/src/tools/observable.spec.ts +++ b/packages/browser-core/src/tools/observable.spec.ts @@ -1,13 +1,14 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { waitNextMicrotask } from '../../test' import { BufferedObservable, mergeObservables, Observable } from './observable' describe('observable', () => { let observable: Observable - let subscriber: jasmine.Spy + let subscriber: Mock<(...args: any[]) => any> beforeEach(() => { observable = new Observable() - subscriber = jasmine.createSpy('sub') + subscriber = vi.fn() }) it('should allow to subscribe and be notified', () => { @@ -22,7 +23,7 @@ describe('observable', () => { }) it('should notify multiple clients', () => { - const otherSubscriber = jasmine.createSpy('sub2') + const otherSubscriber = vi.fn() observable.subscribe(subscriber) observable.subscribe(otherSubscriber) @@ -42,8 +43,8 @@ describe('observable', () => { }) it('should execute onFirstSubscribe callback', () => { - const onFirstSubscribe = jasmine.createSpy('callback') - const otherSubscriber = jasmine.createSpy('sub2') + const onFirstSubscribe = vi.fn() + const otherSubscriber = vi.fn() observable = new Observable(onFirstSubscribe) expect(onFirstSubscribe).not.toHaveBeenCalled() @@ -55,7 +56,7 @@ describe('observable', () => { }) it('should notify the first subscriber if the onFirstSubscribe callback notifies synchronously ', () => { - const onFirstSubscribe = jasmine.createSpy('callback').and.callFake((observable: Observable) => { + const onFirstSubscribe = vi.fn().mockImplementation((observable: Observable) => { observable.notify() }) observable = new Observable(onFirstSubscribe) @@ -66,7 +67,7 @@ describe('observable', () => { }) it('should pass the observable instance to the onFirstSubscribe callback', () => { - const onFirstSubscribe = jasmine.createSpy('callback') + const onFirstSubscribe = vi.fn() observable = new Observable(onFirstSubscribe) observable.subscribe(subscriber) @@ -74,8 +75,8 @@ describe('observable', () => { }) it('should execute onLastUnsubscribe callback', () => { - const onLastUnsubscribe = jasmine.createSpy('callback') - const otherSubscriber = jasmine.createSpy('sub2') + const onLastUnsubscribe = vi.fn() + const otherSubscriber = vi.fn() observable = new Observable(() => onLastUnsubscribe) const subscription = observable.subscribe(subscriber) const otherSubscription = observable.subscribe(otherSubscriber) @@ -93,13 +94,13 @@ describe('mergeObservables', () => { let observableOne: Observable let observableTwo: Observable let mergedObservable: Observable - let subscriber: jasmine.Spy + let subscriber: Mock<(...args: any[]) => any> beforeEach(() => { observableOne = new Observable() observableTwo = new Observable() mergedObservable = mergeObservables(observableOne, observableTwo) - subscriber = jasmine.createSpy('subscriber') + subscriber = vi.fn() }) it('should notify when one of the merged observable notifies', () => { @@ -127,7 +128,7 @@ describe('BufferedObservable', () => { observable.notify('first') observable.notify('second') - const observer = jasmine.createSpy('observer') + const observer = vi.fn() observable.subscribe(observer) await waitNextMicrotask() @@ -139,7 +140,7 @@ describe('BufferedObservable', () => { const observable = new BufferedObservable(100) observable.notify('first') - const observer = jasmine.createSpy('observer') + const observer = vi.fn() observable.subscribe(observer) expect(observer).not.toHaveBeenCalled() @@ -152,7 +153,7 @@ describe('BufferedObservable', () => { it('invokes the observer when new data is notified after subscription', async () => { const observable = new BufferedObservable(100) - const observer = jasmine.createSpy('observer') + const observer = vi.fn() observable.subscribe(observer) observable.notify('first') @@ -172,7 +173,7 @@ describe('BufferedObservable', () => { observable.notify('second') observable.notify('third') - const observer = jasmine.createSpy('observer') + const observer = vi.fn() observable.subscribe(observer) await waitNextMicrotask() @@ -187,7 +188,7 @@ describe('BufferedObservable', () => { observable.notify('first') observable.notify('second') - const observer = jasmine.createSpy('observer').and.callFake(() => { + const observer = vi.fn().mockImplementation(() => { subscription.unsubscribe() }) const subscription = observable.subscribe(observer) @@ -201,7 +202,7 @@ describe('BufferedObservable', () => { const observable = new BufferedObservable(100) observable.notify('first') - const observer = jasmine.createSpy('observer') + const observer = vi.fn() const subscription = observable.subscribe(observer) subscription.unsubscribe() @@ -214,7 +215,7 @@ describe('BufferedObservable', () => { it('allows to unsubscribe after the buffered data', async () => { const observable = new BufferedObservable(100) - const observer = jasmine.createSpy('observer') + const observer = vi.fn() const subscription = observable.subscribe(observer) await waitNextMicrotask() @@ -234,7 +235,7 @@ describe('BufferedObservable', () => { observable.unbuffer() await waitNextMicrotask() - const observer = jasmine.createSpy('observer') + const observer = vi.fn() observable.subscribe(observer) await waitNextMicrotask() @@ -246,7 +247,7 @@ describe('BufferedObservable', () => { observable.notify('first') observable.notify('second') - const observer = jasmine.createSpy('observer') + const observer = vi.fn() observable.subscribe(observer) observable.unbuffer() @@ -258,7 +259,7 @@ describe('BufferedObservable', () => { }) it('calls onDrop with the number of dropped items when unbuffering', async () => { - const onDrop = jasmine.createSpy('onDrop') + const onDrop = vi.fn() const observable = new BufferedObservable(2, onDrop) observable.notify('first') // dropped observable.notify('second') // dropped @@ -268,11 +269,12 @@ describe('BufferedObservable', () => { observable.unbuffer() await waitNextMicrotask() - expect(onDrop).toHaveBeenCalledOnceWith(2) + expect(onDrop).toHaveBeenCalledTimes(1) + expect(onDrop).toHaveBeenCalledExactlyOnceWith(2) }) it('does not call onDrop when no data was dropped', async () => { - const onDrop = jasmine.createSpy('onDrop') + const onDrop = vi.fn() const observable = new BufferedObservable(2, onDrop) observable.notify('first') observable.notify('second') diff --git a/packages/browser-core/src/tools/queueMicrotask.spec.ts b/packages/browser-core/src/tools/queueMicrotask.spec.ts index aa5abc8f3b..80a1c7e1ab 100644 --- a/packages/browser-core/src/tools/queueMicrotask.spec.ts +++ b/packages/browser-core/src/tools/queueMicrotask.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { startMockTelemetry, waitNextMicrotask } from '../../test' import { queueMicrotask } from './queueMicrotask' diff --git a/packages/browser-core/src/tools/readBytesFromStream.spec.ts b/packages/browser-core/src/tools/readBytesFromStream.spec.ts index 2cbd0f6ead..ac69bcbdbf 100644 --- a/packages/browser-core/src/tools/readBytesFromStream.spec.ts +++ b/packages/browser-core/src/tools/readBytesFromStream.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { readBytesFromStream } from './readBytesFromStream' describe('readBytesFromStream', () => { @@ -26,12 +27,7 @@ describe('readBytesFromStream', () => { }, }) - try { - await readBytesFromStream(stream) - fail('Should have thrown an error') - } catch (error) { - expect(error).toEqual(jasmine.any(Error)) - } + await expect(readBytesFromStream(stream)).rejects.toThrow('foo') }) it('should handle rejection error on cancel', async () => { diff --git a/packages/browser-core/src/tools/requestIdleCallback.spec.ts b/packages/browser-core/src/tools/requestIdleCallback.spec.ts index 55368e01b4..c586f4c433 100644 --- a/packages/browser-core/src/tools/requestIdleCallback.spec.ts +++ b/packages/browser-core/src/tools/requestIdleCallback.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { mockClock, registerCleanupTask, type Clock } from '../../test' import { MAX_TASK_TIME, requestIdleCallbackShim, requestIdleCallback } from './requestIdleCallback' @@ -6,7 +7,7 @@ describe('requestIdleCallback', () => { const clock = mockClock() removeGlobalRequestIdleCallback() - const spy = jasmine.createSpy<(deadline: IdleDeadline) => void>() + const spy = vi.fn<(deadline: IdleDeadline) => void>() requestIdleCallback(spy) expect(spy).not.toHaveBeenCalled() @@ -24,19 +25,20 @@ describe('requestIdleCallbackShim', () => { }) it('calls the callback asynchronously', () => { - const spy = jasmine.createSpy<(deadline: IdleDeadline) => void>() + const spy = vi.fn<(deadline: IdleDeadline) => void>() requestIdleCallbackShim(spy) expect(spy).not.toHaveBeenCalled() clock.tick(0) - expect(spy).toHaveBeenCalledOnceWith({ didTimeout: false, timeRemaining: jasmine.any(Function) }) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith({ didTimeout: false, timeRemaining: expect.any(Function) }) }) it('notifies the remaining time', () => { - const spy = jasmine.createSpy<(deadline: IdleDeadline) => void>() + const spy = vi.fn<(deadline: IdleDeadline) => void>() requestIdleCallbackShim(spy) clock.tick(10) - const deadline = spy.calls.mostRecent().args[0] + const deadline = spy.mock.lastCall![0] expect(deadline.timeRemaining()).toBe(MAX_TASK_TIME - 10) @@ -48,7 +50,7 @@ describe('requestIdleCallbackShim', () => { }) it('cancels the callback when calling the stop function', () => { - const spy = jasmine.createSpy<(deadline: IdleDeadline) => void>() + const spy = vi.fn<(deadline: IdleDeadline) => void>() const stop = requestIdleCallbackShim(spy) stop() clock.tick(0) diff --git a/packages/browser-core/src/tools/serialisation/jsonStringify.spec.ts b/packages/browser-core/src/tools/serialisation/jsonStringify.spec.ts index 0272106cff..e8c2cceb5d 100644 --- a/packages/browser-core/src/tools/serialisation/jsonStringify.spec.ts +++ b/packages/browser-core/src/tools/serialisation/jsonStringify.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, describe, expect, it } from 'vitest' import { jsonStringify } from './jsonStringify' describe('jsonStringify', () => { diff --git a/packages/browser-core/src/tools/serialisation/sanitize.spec.ts b/packages/browser-core/src/tools/serialisation/sanitize.spec.ts index 56834f7d44..8ec58bfd15 100644 --- a/packages/browser-core/src/tools/serialisation/sanitize.spec.ts +++ b/packages/browser-core/src/tools/serialisation/sanitize.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { display } from '../display' import { registerCleanupTask } from '../../../test' import { sanitize } from './sanitize' @@ -32,12 +33,23 @@ describe('sanitize', () => { expect(sanitize(testFunction)).toBe('[Function] testFunction') }) - it('should handle bigint', () => { - expect(sanitize(2n)).toEqual('[BigInt] 2') + it('should handle bigint', (ctx) => { + const bigIntFunction: (val: number) => any = (window as any).BigInt + if (typeof bigIntFunction !== 'function') { + ctx.skip(true, 'BigInt not supported') + return + } + const bigint = bigIntFunction(2) + expect(sanitize(bigint)).toEqual('[BigInt] 2') }) - it('shoud handle symbols', () => { - const symbol = Symbol('description') + it('shoud handle symbols', (ctx) => { + const symbolFunction: (description: string) => any = (window as any).Symbol + if (typeof symbolFunction !== 'function') { + ctx.skip(true, 'Symbol not supported') + return + } + const symbol = symbolFunction('description') expect(sanitize(symbol)).toMatch(/\[Symbol\] (?:Symbol\()?description\)?/) }) }) @@ -55,30 +67,31 @@ describe('sanitize', () => { expect(sanitize(node)).toBe('[HTMLDivElement]') }) - it('should serialize events', (done) => { - const button = document.createElement('button') - document.body.appendChild(button) - - registerCleanupTask(() => { - document.body.removeChild(button) - }) - - document.addEventListener( - 'click', - (event) => { - expect(sanitize(event)).toEqual({ - type: 'click', - isTrusted: false, - target: '[HTMLButtonElement]', - currentTarget: '[HTMLDocument]', - }) - done() - }, - { once: true } - ) - - button.click() - }) + it('should serialize events', () => + new Promise((resolve) => { + const button = document.createElement('button') + document.body.appendChild(button) + + registerCleanupTask(() => { + document.body.removeChild(button) + }) + + document.addEventListener( + 'click', + (event) => { + expect(sanitize(event)).toEqual({ + type: 'click', + isTrusted: false, + target: '[HTMLButtonElement]', + currentTarget: '[HTMLDocument]', + }) + resolve() + }, + { once: true } + ) + + button.click() + })) it('should serialize errors as JSON.stringify does', () => { // Explicitely keep the previous behavior to avoid breaking changes in 4.x @@ -183,7 +196,7 @@ describe('sanitize', () => { describe('toJson functions handling', () => { it('should use toJSON functions if available on root object', () => { - const toJSON = jasmine.createSpy('toJSON', () => 'Specific').and.callThrough() + const toJSON = vi.fn().mockImplementation(() => 'Specific') const obj = { a: 1, b: 2, toJSON } expect(sanitize(obj)).toEqual('Specific') @@ -191,7 +204,7 @@ describe('sanitize', () => { }) it('should use toJSON functions if available on nested objects', () => { - const toJSON = jasmine.createSpy('toJSON', () => ({ d: 4 })).and.callThrough() + const toJSON = vi.fn().mockImplementation(() => ({ d: 4 })) const obj = { a: 1, b: 2, c: { a: 3, toJSON } } expect(sanitize(obj)).toEqual({ a: 1, b: 2, c: { d: 4 } }) @@ -204,8 +217,8 @@ describe('sanitize', () => { }) it('should not use toJSON methods added to arrays and objects prototypes', () => { - const toJSONArray = jasmine.createSpy('toJSONArray', () => 'Array').and.callThrough() - const toJSONObject = jasmine.createSpy('toJSONObject', () => 'Object').and.callThrough() + const toJSONArray = vi.fn().mockImplementation(() => 'Array') + const toJSONObject = vi.fn().mockImplementation(() => 'Object') ;(Array.prototype as any).toJSON = toJSONArray ;(Object.prototype as any).toJSON = toJSONObject @@ -228,7 +241,7 @@ describe('sanitize', () => { }) it('should restore prototype toJSON methods when sanitization throws', () => { - const toJSON = jasmine.createSpy('toJSON', () => 'Object').and.callThrough() + const toJSON = vi.fn(() => 'Object') ;(Object.prototype as any).toJSON = toJSON const obj = { get x() { @@ -244,7 +257,7 @@ describe('sanitize', () => { describe('maxSize verification', () => { it('should return nothing if a simple type is over max size ', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const str = 'A not so long string...' expect(sanitize(str, 5)).toBe(undefined) @@ -252,7 +265,7 @@ describe('sanitize', () => { }) it('should stop cloning if an object container type reaches max size', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const obj = { a: 'abc', b: 'def', c: 'ghi' } // Length of 31 after JSON.stringify const sanitized = sanitize(obj, 21) expect(sanitized).toEqual({ a: 'abc', b: 'def' }) // Length of 21 after JSON.stringify @@ -260,7 +273,7 @@ describe('sanitize', () => { }) it('should stop cloning if an array container type reaches max size', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const obj = [1, 2, 3, 4] // Length of 9 after JSON.stringify const sanitized = sanitize(obj, 5) expect(sanitized).toEqual([1, 2]) // Length of 5 after JSON.stringify @@ -269,7 +282,7 @@ describe('sanitize', () => { it('should count size properly when array contains undefined values', () => { // This is a special case: JSON.stringify([undefined]) => '[null]' - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const arr = [undefined, undefined] // Length of 11 after JSON.stringify const sanitized = sanitize(arr, 10) expect(sanitized).toEqual([undefined]) @@ -277,7 +290,7 @@ describe('sanitize', () => { }) it('should count size properly when an object contains properties with undefined values', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const obj = { a: undefined, b: 42 } // Length of 8 after JSON.stringify const sanitized = sanitize(obj, 8) expect(sanitized).toEqual({ a: undefined, b: 42 }) diff --git a/packages/browser-core/src/tools/serialisation/stringify.spec.ts b/packages/browser-core/src/tools/serialisation/stringify.spec.ts index f326611085..1f40087505 100644 --- a/packages/browser-core/src/tools/serialisation/stringify.spec.ts +++ b/packages/browser-core/src/tools/serialisation/stringify.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { safeStringify, safeToString } from './stringify' describe('stringify', () => { diff --git a/packages/browser-core/src/tools/stackTrace/computeStackTrace.spec.ts b/packages/browser-core/src/tools/stackTrace/computeStackTrace.spec.ts index 3467d9b091..381bd91821 100644 --- a/packages/browser-core/src/tools/stackTrace/computeStackTrace.spec.ts +++ b/packages/browser-core/src/tools/stackTrace/computeStackTrace.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { isSafari } from '../utils/browserDetection' import * as CapturedExceptions from './capturedExceptions.specHelper' import { CHROME_141_HTML_ANONYMOUS_LISTENER } from './capturedExceptions.specHelper' @@ -108,9 +109,10 @@ Error: foo expect(computeStackTrace(null).message).toBeUndefined() }) - it('should get the order of functions called right', () => { + it('should get the order of functions called right', (ctx) => { if (isSafari()) { - pending() + ctx.skip(true, 'not supported in Safari') + return } function foo() { return bar() diff --git a/packages/browser-core/src/tools/stackTrace/handlingStack.spec.ts b/packages/browser-core/src/tools/stackTrace/handlingStack.spec.ts index 11c5ee40ca..4a507121ba 100644 --- a/packages/browser-core/src/tools/stackTrace/handlingStack.spec.ts +++ b/packages/browser-core/src/tools/stackTrace/handlingStack.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { createHandlingStack } from './handlingStack' describe('createHandlingStack', () => { diff --git a/packages/browser-core/src/tools/taskQueue.spec.ts b/packages/browser-core/src/tools/taskQueue.spec.ts index b2fd7ec077..c8fb386be5 100644 --- a/packages/browser-core/src/tools/taskQueue.spec.ts +++ b/packages/browser-core/src/tools/taskQueue.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { mockClock, mockRequestIdleCallback, registerCleanupTask } from '../../test' import { createTaskQueue, MAX_EXECUTION_TIME_ON_TIMEOUT } from './taskQueue' @@ -5,7 +6,7 @@ describe('createTaskQueue', () => { it('runs the task using an idle callback', () => { const requestIdleCallbackMock = mockRequestIdleCallback() const taskQueue = createTaskQueue() - const task = jasmine.createSpy('task') + const task = vi.fn() taskQueue.push(task) expect(task).not.toHaveBeenCalled() @@ -19,9 +20,9 @@ describe('createTaskQueue', () => { const requestIdleCallbackMock = mockRequestIdleCallback() // Each task takes 10ms to run - const task1 = jasmine.createSpy().and.callFake(() => clock.tick(10)) - const task2 = jasmine.createSpy().and.callFake(() => clock.tick(10)) - const task3 = jasmine.createSpy().and.callFake(() => clock.tick(10)) + const task1 = vi.fn().mockImplementation(() => clock.tick(10)) + const task2 = vi.fn().mockImplementation(() => clock.tick(10)) + const task3 = vi.fn().mockImplementation(() => clock.tick(10)) const taskQueue = createTaskQueue() @@ -42,9 +43,9 @@ describe('createTaskQueue', () => { const clock = mockClock() const requestIdleCallbackMock = mockRequestIdleCallback() - const task1 = jasmine.createSpy().and.callFake(() => clock.tick(MAX_EXECUTION_TIME_ON_TIMEOUT - 10)) - const task2 = jasmine.createSpy().and.callFake(() => clock.tick(20)) - const task3 = jasmine.createSpy().and.callFake(() => clock.tick(20)) + const task1 = vi.fn().mockImplementation(() => clock.tick(MAX_EXECUTION_TIME_ON_TIMEOUT - 10)) + const task2 = vi.fn().mockImplementation(() => clock.tick(20)) + const task3 = vi.fn().mockImplementation(() => clock.tick(20)) const taskQueue = createTaskQueue() @@ -63,9 +64,9 @@ describe('createTaskQueue', () => { replaceRequestIdleCallbackWithPolyfillShim() const taskQueue = createTaskQueue() - const task1 = jasmine.createSpy('task1') - const task2 = jasmine.createSpy('task2') - const task3 = jasmine.createSpy('task3') + const task1 = vi.fn() + const task2 = vi.fn() + const task3 = vi.fn() taskQueue.push(task1) taskQueue.push(task2) @@ -83,7 +84,8 @@ function replaceRequestIdleCallbackWithPolyfillShim() { const originalRequestIdleCallback = window.requestIdleCallback const originalCancelIdleCallback = window.cancelIdleCallback // Reproduces `(cb) => setTimeout(cb, 0)` β€” invokes the callback with no argument. - window.requestIdleCallback = ((cb: () => void) => setTimeout(() => cb(), 0)) as typeof window.requestIdleCallback + window.requestIdleCallback = ((cb: () => void) => + setTimeout(() => cb(), 0)) as unknown as typeof window.requestIdleCallback window.cancelIdleCallback = (id: number) => clearTimeout(id) registerCleanupTask(() => { window.requestIdleCallback = originalRequestIdleCallback diff --git a/packages/browser-core/src/tools/timer.spec.ts b/packages/browser-core/src/tools/timer.spec.ts index 5ced697864..5bacdb2b3c 100644 --- a/packages/browser-core/src/tools/timer.spec.ts +++ b/packages/browser-core/src/tools/timer.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { mockClock, mockZoneJs } from '../../test' import type { Clock, MockZoneJs } from '../../test' import { startMonitorErrorCollection } from './monitor' @@ -25,23 +26,23 @@ import { noop } from './utils/functionUtils' }) it('executes the callback asynchronously', () => { - const spy = jasmine.createSpy() + const spy = vi.fn() setTimer(spy) expect(spy).not.toHaveBeenCalled() clock.tick(0) - expect(spy).toHaveBeenCalledOnceWith() + expect(spy).toHaveBeenCalledTimes(1) }) it('schedules an asynchronous task', () => { - const spy = jasmine.createSpy() + const spy = vi.fn() setTimer(spy) expect(spy).not.toHaveBeenCalled() clock.tick(0) - expect(spy).toHaveBeenCalledOnceWith() + expect(spy).toHaveBeenCalledTimes(1) }) it('does not use the Zone.js function', () => { - const zoneJsSetTimerSpy = jasmine.createSpy() + const zoneJsSetTimerSpy = vi.fn() zoneJs.replaceProperty(window, name, zoneJsSetTimerSpy) setTimer(noop) @@ -51,7 +52,7 @@ import { noop } from './utils/functionUtils' }) it('monitors the callback', () => { - const onMonitorErrorCollectedSpy = jasmine.createSpy() + const onMonitorErrorCollectedSpy = vi.fn() startMonitorErrorCollection(onMonitorErrorCollectedSpy) setTimer(() => { @@ -59,11 +60,12 @@ import { noop } from './utils/functionUtils' }) clock.tick(0) - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith(new Error('foo')) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledTimes(1) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith(new Error('foo')) }) it('can be canceled', () => { - const spy = jasmine.createSpy() + const spy = vi.fn() const timerId = setTimer(spy) clearTimer(timerId) clock.tick(0) diff --git a/packages/browser-core/src/tools/utils/arrayUtils.spec.ts b/packages/browser-core/src/tools/utils/arrayUtils.spec.ts index 99ac886824..1e69ef9230 100644 --- a/packages/browser-core/src/tools/utils/arrayUtils.spec.ts +++ b/packages/browser-core/src/tools/utils/arrayUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mergeArrays } from './arrayUtils' describe('arrayUtils', () => { diff --git a/packages/browser-core/src/tools/utils/browserDetection.spec.ts b/packages/browser-core/src/tools/utils/browserDetection.spec.ts index 2bfa0da8cd..89fb7ff6e1 100644 --- a/packages/browser-core/src/tools/utils/browserDetection.spec.ts +++ b/packages/browser-core/src/tools/utils/browserDetection.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { combine } from '@datadog/js-core/util' import { Browser, detectBrowser } from './browserDetection' diff --git a/packages/browser-core/src/tools/utils/byteUtils.spec.ts b/packages/browser-core/src/tools/utils/byteUtils.spec.ts index 1fa1e952c1..65c84cacd7 100644 --- a/packages/browser-core/src/tools/utils/byteUtils.spec.ts +++ b/packages/browser-core/src/tools/utils/byteUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { computeBytesCount, concatBuffers } from './byteUtils' describe('byteUtils', () => { diff --git a/packages/browser-core/src/tools/utils/functionUtils.spec.ts b/packages/browser-core/src/tools/utils/functionUtils.spec.ts index 0b71ee50f8..5f9fc934b8 100644 --- a/packages/browser-core/src/tools/utils/functionUtils.spec.ts +++ b/packages/browser-core/src/tools/utils/functionUtils.spec.ts @@ -1,17 +1,18 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Clock } from '../../../test' import { mockClock } from '../../../test' import { throttle } from './functionUtils' describe('functionUtils', () => { describe('throttle', () => { - let spy: jasmine.Spy + let spy: Mock let throttled: () => void let cancel: () => void let clock: Clock beforeEach(() => { clock = mockClock() - spy = jasmine.createSpy() + spy = vi.fn() }) describe('when {leading: false, trailing:false}', () => { @@ -223,7 +224,7 @@ describe('functionUtils', () => { throttled(2) throttled(3) clock.tick(2) - expect(spy.calls.allArgs()).toEqual([[1], [3]]) + expect(spy.mock.calls).toEqual([[1], [3]]) }) }) }) diff --git a/packages/browser-core/src/tools/utils/numberUtils.spec.ts b/packages/browser-core/src/tools/utils/numberUtils.spec.ts index 43c0792870..e2d90d3409 100644 --- a/packages/browser-core/src/tools/utils/numberUtils.spec.ts +++ b/packages/browser-core/src/tools/utils/numberUtils.spec.ts @@ -1,9 +1,10 @@ +import { vi, describe, expect, it } from 'vitest' import { performDraw, round } from './numberUtils' describe('numberUtils', () => { it('should perform a draw', () => { let random = 0 - spyOn(Math, 'random').and.callFake(() => random) + vi.spyOn(Math, 'random').mockImplementation(() => random) expect(performDraw(0)).toBe(false) expect(performDraw(100)).toEqual(true) diff --git a/packages/browser-core/src/tools/utils/objectUtils.spec.ts b/packages/browser-core/src/tools/utils/objectUtils.spec.ts index cc3deda667..48d18943e8 100644 --- a/packages/browser-core/src/tools/utils/objectUtils.spec.ts +++ b/packages/browser-core/src/tools/utils/objectUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { getConstructorName, tryJsonParse } from './objectUtils' describe('objectUtils', () => { @@ -51,7 +52,7 @@ describe('objectUtils', () => { it('should strip __proto__ key', () => { const result = tryJsonParse('{"a":1,"__proto__":{"injected":true}}') - expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBeFalse() + expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false) expect(({} as any).injected).toBeUndefined() }) diff --git a/packages/browser-core/src/tools/utils/stringUtils.spec.ts b/packages/browser-core/src/tools/utils/stringUtils.spec.ts index 429f1a9ea6..0b59ca147b 100644 --- a/packages/browser-core/src/tools/utils/stringUtils.spec.ts +++ b/packages/browser-core/src/tools/utils/stringUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { safeTruncate, findCommaSeparatedValue, diff --git a/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts b/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts index 4ca746ffb5..62be60a06e 100644 --- a/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts +++ b/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { buildUrl, getPathName, isValidUrl, normalizeUrl, getPristineWindow } from '@datadog/js-core/util' describe('normalize url', () => { @@ -42,7 +43,9 @@ describe('isValidUrl', () => { it('should return the same result if the URL has been wrongfully overridden between calls', () => { expect(isValidUrl('http://www.datadoghq.com')).toBe(true) - spyOn(window, 'URL').and.throwError('wrong URL override') + vi.spyOn(window, 'URL').mockImplementation(() => { + throw new Error('wrong URL override') + }) expect(isValidUrl('http://www.datadoghq.com')).toBe(true) }) }) diff --git a/packages/browser-core/src/tools/valueHistory.spec.ts b/packages/browser-core/src/tools/valueHistory.spec.ts index 9165fe901a..662e9db113 100644 --- a/packages/browser-core/src/tools/valueHistory.spec.ts +++ b/packages/browser-core/src/tools/valueHistory.spec.ts @@ -1,5 +1,6 @@ -import { ONE_MINUTE, addDuration } from '@datadog/js-core/time' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { Duration, RelativeTime } from '@datadog/js-core/time' +import { addDuration, ONE_MINUTE } from '@datadog/js-core/time' import type { Clock } from '../../test' import { mockClock } from '../../test' import { CLEAR_OLD_VALUES_INTERVAL, type ValueHistory, createValueHistory } from './valueHistory' @@ -128,8 +129,8 @@ describe('valueHistory', () => { value: 'foo', startTime: 0 as RelativeTime, endTime: END_OF_TIMES, - remove: jasmine.any(Function), - close: jasmine.any(Function), + remove: expect.any(Function), + close: expect.any(Function), }, ]) expect(valueHistory.getEntries(5 as RelativeTime)).toEqual([ @@ -137,15 +138,15 @@ describe('valueHistory', () => { value: 'qux', startTime: 5 as RelativeTime, endTime: 15 as RelativeTime, - remove: jasmine.any(Function), - close: jasmine.any(Function), + remove: expect.any(Function), + close: expect.any(Function), }, { value: 'bar', startTime: 5 as RelativeTime, endTime: 10 as RelativeTime, - remove: jasmine.any(Function), - close: jasmine.any(Function), + remove: expect.any(Function), + close: expect.any(Function), }, ]) expect(valueHistory.getEntries(10 as RelativeTime)).toEqual([ @@ -153,8 +154,8 @@ describe('valueHistory', () => { value: 'baz', startTime: 10 as RelativeTime, endTime: END_OF_TIMES, - remove: jasmine.any(Function), - close: jasmine.any(Function), + remove: expect.any(Function), + close: expect.any(Function), }, ]) }) diff --git a/packages/browser-core/src/transport/batch.spec.ts b/packages/browser-core/src/transport/batch.spec.ts index 4ab0d70ec9..f3637c8674 100644 --- a/packages/browser-core/src/transport/batch.spec.ts +++ b/packages/browser-core/src/transport/batch.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { Observable } from '..' import type { MockFlushController } from '../../test' import { createMockFlushController, replaceMockable } from '../../test' @@ -19,8 +20,8 @@ describe('batch', () => { let batch: Batch let transport: { observable: Observable - send: jasmine.Spy - sendOnExit: jasmine.Spy + send: Mock + sendOnExit: Mock } let flushController: MockFlushController @@ -29,8 +30,8 @@ describe('batch', () => { beforeEach(() => { transport = { observable: new Observable(), - send: jasmine.createSpy(), - sendOnExit: jasmine.createSpy(), + send: vi.fn(), + sendOnExit: vi.fn(), } satisfies HttpRequest flushController = createMockFlushController() encoder = createIdentityEncoder() @@ -45,7 +46,7 @@ describe('batch', () => { flushController.notifyFlush() - expect(transport.send.calls.mostRecent().args[0]).toEqual({ + expect(transport.send.mock.lastCall![0]).toEqual({ data: '{"message":"hello"}', bytesCount: SMALL_MESSAGE_BYTES_COUNT, encoding: undefined, @@ -56,8 +57,10 @@ describe('batch', () => { it('should add message to the flush controller', () => { batch.add(SMALL_MESSAGE) - expect(flushController.notifyBeforeAddMessage).toHaveBeenCalledOnceWith(SMALL_MESSAGE_BYTES_COUNT) - expect(flushController.notifyAfterAddMessage).toHaveBeenCalledOnceWith(0) + expect(flushController.notifyBeforeAddMessage).toHaveBeenCalledTimes(1) + expect(flushController.notifyBeforeAddMessage).toHaveBeenCalledExactlyOnceWith(SMALL_MESSAGE_BYTES_COUNT) + expect(flushController.notifyAfterAddMessage).toHaveBeenCalledTimes(1) + expect(flushController.notifyAfterAddMessage).toHaveBeenCalledExactlyOnceWith(0) }) it('should consider separators when adding message', () => { @@ -78,13 +81,14 @@ describe('batch', () => { batch.add(SMALL_MESSAGE) batch.upsert(SMALL_MESSAGE, 'a') - flushController.notifyBeforeAddMessage.calls.reset() - flushController.notifyAfterAddMessage.calls.reset() + flushController.notifyBeforeAddMessage.mockClear() + flushController.notifyAfterAddMessage.mockClear() batch.upsert(SMALL_MESSAGE, 'a') expect(flushController.notifyBeforeAddMessage).not.toHaveBeenCalled() - expect(flushController.notifyAfterAddMessage).toHaveBeenCalledOnceWith(0) // same size, diff = 0 + expect(flushController.notifyAfterAddMessage).toHaveBeenCalledTimes(1) + expect(flushController.notifyAfterAddMessage).toHaveBeenCalledExactlyOnceWith(0) // same size, diff = 0 expect(flushController.bytesCount).toEqual( // Note: contrary to added messages (see test above), we don't take separators into account // when upserting messages, because it's irrelevant: upserted messages size are not yet @@ -94,7 +98,7 @@ describe('batch', () => { }) it('should not send a message with a bytes size above the limit', () => { - const warnSpy = spyOn(display, 'warn') + const warnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) batch.add(BIG_MESSAGE_OVER_BYTES_LIMIT) expect(warnSpy).toHaveBeenCalled() @@ -104,8 +108,10 @@ describe('batch', () => { it('should adjust the message size after the message has been added', () => { const message = { message: '😀' } // JS string length = 2, but 4 bytes once encoded to UTF-8 batch.add(message) - expect(flushController.notifyBeforeAddMessage).toHaveBeenCalledOnceWith(16) - expect(flushController.notifyAfterAddMessage).toHaveBeenCalledOnceWith(2) // 2 more bytes once encoded + expect(flushController.notifyBeforeAddMessage).toHaveBeenCalledTimes(1) + expect(flushController.notifyBeforeAddMessage).toHaveBeenCalledExactlyOnceWith(16) + expect(flushController.notifyAfterAddMessage).toHaveBeenCalledTimes(1) + expect(flushController.notifyAfterAddMessage).toHaveBeenCalledExactlyOnceWith(2) // 2 more bytes once encoded }) }) @@ -116,9 +122,9 @@ describe('batch', () => { batch.upsert({ message: '4' }, 'c') flushController.notifyFlush() - expect(transport.send.calls.mostRecent().args[0]).toEqual({ + expect(transport.send.mock.lastCall![0]).toEqual({ data: '{"message":"2"}\n{"message":"3"}\n{"message":"4"}', - bytesCount: jasmine.any(Number), + bytesCount: expect.any(Number), encoding: undefined, }) @@ -127,9 +133,9 @@ describe('batch', () => { batch.upsert({ message: '7' }, 'a') flushController.notifyFlush() - expect(transport.send.calls.mostRecent().args[0]).toEqual({ + expect(transport.send.mock.lastCall![0]).toEqual({ data: '{"message":"5"}\n{"message":"6"}\n{"message":"7"}', - bytesCount: jasmine.any(Number), + bytesCount: expect.any(Number), encoding: undefined, }) @@ -139,9 +145,9 @@ describe('batch', () => { batch.upsert({ message: '11' }, 'b') flushController.notifyFlush() - expect(transport.send.calls.mostRecent().args[0]).toEqual({ + expect(transport.send.mock.lastCall![0]).toEqual({ data: '{"message":"10"}\n{"message":"11"}', - bytesCount: jasmine.any(Number), + bytesCount: expect.any(Number), encoding: undefined, }) }) @@ -153,25 +159,26 @@ describe('batch', () => { flushController.notifyFlush() - expect(transport.send.calls.mostRecent().args[0]).toEqual({ + expect(transport.send.mock.lastCall![0]).toEqual({ data: '{"message":"1"}\n{"message":"2"}', - bytesCount: jasmine.any(Number), + bytesCount: expect.any(Number), encoding: undefined, }) }) it('should encode upserted messages', () => { - const encoderWriteSpy = spyOn(encoder, 'write') + const encoderWriteSpy = vi.spyOn(encoder, 'write').mockImplementation(() => undefined) batch.upsert({ message: '2' }, 'a') flushController.notifyFlush() - expect(encoderWriteSpy).toHaveBeenCalledOnceWith('{"message":"2"}') + expect(encoderWriteSpy).toHaveBeenCalledTimes(1) + expect(encoderWriteSpy).toHaveBeenCalledExactlyOnceWith('{"message":"2"}') }) it('should be able to use telemetry in the httpRequest.send', () => { - transport.send.and.callFake(() => { + transport.send.mockImplementation(() => { addTelemetryDebugFake() }) const addTelemetryDebugFake = () => batch.add({ message: 'telemetry message' }) @@ -235,7 +242,7 @@ describe('batch', () => { if (pending) { // eslint-disable-next-line @typescript-eslint/unbound-method const original = encoder.finishSync - spyOn(encoder, 'finishSync').and.callFake(() => ({ + vi.spyOn(encoder, 'finishSync').mockImplementation(() => ({ ...original(), pendingData: JSON.stringify(pending), })) @@ -243,12 +250,12 @@ describe('batch', () => { flushController.notifyFlush('before_unload') - expect(transport.sendOnExit.calls.allArgs().map(([payload]) => payload.data)).toEqual(expectedRequests) + expect(transport.sendOnExit.mock.calls.map(([payload]) => payload.data)).toEqual(expectedRequests) }) }) it('should be able to use telemetry in the httpRequest.sendOnExit', () => { - transport.sendOnExit.and.callFake(() => { + transport.sendOnExit.mockImplementation(() => { addTelemetryDebugFake() }) const addTelemetryDebugFake = () => batch.add({ message: 'telemetry message' }) diff --git a/packages/browser-core/src/transport/eventBridge.spec.ts b/packages/browser-core/src/transport/eventBridge.spec.ts index 043e58326f..fe3d00d730 100644 --- a/packages/browser-core/src/transport/eventBridge.spec.ts +++ b/packages/browser-core/src/transport/eventBridge.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { mockEventBridge } from '../../test' import { display } from '../tools/display' import { DefaultPrivacyLevel } from '../domain/configuration' @@ -9,32 +10,32 @@ describe('canUseEventBridge', () => { it('should detect when the bridge is present and the webView host is allowed', () => { mockEventBridge({ allowedWebViewHosts }) - expect(canUseEventBridge('foo.bar')).toBeTrue() - expect(canUseEventBridge('baz.foo.bar')).toBeTrue() - expect(canUseEventBridge('www.foo.bar')).toBeTrue() - expect(canUseEventBridge('www.qux.foo.bar')).toBeTrue() + expect(canUseEventBridge('foo.bar')).toBe(true) + expect(canUseEventBridge('baz.foo.bar')).toBe(true) + expect(canUseEventBridge('www.foo.bar')).toBe(true) + expect(canUseEventBridge('www.qux.foo.bar')).toBe(true) }) it('should not detect when the bridge is present and the webView host is not allowed', () => { mockEventBridge({ allowedWebViewHosts }) - expect(canUseEventBridge('foo.com')).toBeFalse() - expect(canUseEventBridge('foo.bar.baz')).toBeFalse() - expect(canUseEventBridge('bazfoo.bar')).toBeFalse() + expect(canUseEventBridge('foo.com')).toBe(false) + expect(canUseEventBridge('foo.bar.baz')).toBe(false) + expect(canUseEventBridge('bazfoo.bar')).toBe(false) }) it('should not detect when the bridge on the parent domain if only the subdomain is allowed', () => { mockEventBridge({ allowedWebViewHosts: ['baz.foo.bar'] }) - expect(canUseEventBridge('foo.bar')).toBeFalse() + expect(canUseEventBridge('foo.bar')).toBe(false) }) it('should not detect when the bridge is absent', () => { - expect(canUseEventBridge()).toBeFalse() + expect(canUseEventBridge()).toBe(false) }) }) describe('matchesHostEntry', () => { beforeEach(() => { - spyOn(display, 'error') + vi.spyOn(display, 'error').mockImplementation(() => undefined) }) // prettier-ignore @@ -66,39 +67,39 @@ describe('matchesHostEntry', () => { describe('canUseEventBridge with wildcard patterns', () => { it('should match wildcard entries in getAllowedWebViewHosts', () => { mockEventBridge({ allowedWebViewHosts: ['*.shopist.io', 'shopist.io'] }) - expect(canUseEventBridge('app.shopist.io')).toBeTrue() - expect(canUseEventBridge('shopist.io')).toBeTrue() - expect(canUseEventBridge('evil.com')).toBeFalse() + expect(canUseEventBridge('app.shopist.io')).toBe(true) + expect(canUseEventBridge('shopist.io')).toBe(true) + expect(canUseEventBridge('evil.com')).toBe(false) }) it('should match plain and wildcard entries together', () => { mockEventBridge({ allowedWebViewHosts: ['legacy.com', '*.shopist.io'] }) - expect(canUseEventBridge('legacy.com')).toBeTrue() - expect(canUseEventBridge('app.shopist.io')).toBeTrue() - expect(canUseEventBridge('evil.com')).toBeFalse() + expect(canUseEventBridge('legacy.com')).toBe(true) + expect(canUseEventBridge('app.shopist.io')).toBe(true) + expect(canUseEventBridge('evil.com')).toBe(false) }) it('should log an error and skip patterns with more than one wildcard', () => { - const displayErrorSpy = spyOn(display, 'error') + const displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) mockEventBridge({ allowedWebViewHosts: ['*.foo.*.bar', 'shopist.io'] }) - expect(canUseEventBridge('shopist.io')).toBeTrue() - expect(canUseEventBridge('anything.foo.anything.bar')).toBeFalse() + expect(canUseEventBridge('shopist.io')).toBe(true) + expect(canUseEventBridge('anything.foo.anything.bar')).toBe(false) expect(displayErrorSpy).toHaveBeenCalledWith( 'Invalid WebView host pattern "*.foo.*.bar": only one wildcard (*) is supported.' ) }) it('should not detect when bridge is absent', () => { - expect(canUseEventBridge('shopist.io')).toBeFalse() + expect(canUseEventBridge('shopist.io')).toBe(false) }) }) describe('event bridge send', () => { - let sendSpy: jasmine.Spy<(msg: string) => void> + let sendSpy: Mock<(msg: string) => void> beforeEach(() => { const eventBridge = mockEventBridge() - sendSpy = spyOn(eventBridge, 'send') + sendSpy = vi.spyOn(eventBridge, 'send').mockImplementation(() => undefined) }) it('should serialize sent events without view', () => { @@ -106,7 +107,8 @@ describe('event bridge send', () => { eventBridge.send('view', { foo: 'bar' }) - expect(sendSpy).toHaveBeenCalledOnceWith('{"eventType":"view","event":{"foo":"bar"}}') + expect(sendSpy).toHaveBeenCalledTimes(1) + expect(sendSpy).toHaveBeenCalledExactlyOnceWith('{"eventType":"view","event":{"foo":"bar"}}') }) it('should serialize sent events with view', () => { @@ -114,7 +116,8 @@ describe('event bridge send', () => { eventBridge.send('view', { foo: 'bar' }, '123') - expect(sendSpy).toHaveBeenCalledOnceWith('{"eventType":"view","event":{"foo":"bar"},"view":{"id":"123"}}') + expect(sendSpy).toHaveBeenCalledTimes(1) + expect(sendSpy).toHaveBeenCalledExactlyOnceWith('{"eventType":"view","event":{"foo":"bar"},"view":{"id":"123"}}') }) }) @@ -123,14 +126,14 @@ describe('event bridge getIsTraceSampled', () => { mockEventBridge({ isTraceSampled: true }) const eventBridge = getEventBridge()! - expect(eventBridge.getIsTraceSampled()).toBeTrue() + expect(eventBridge.getIsTraceSampled()).toBe(true) }) it("should return false when the bridge returns 'false'", () => { mockEventBridge({ isTraceSampled: false }) const eventBridge = getEventBridge()! - expect(eventBridge.getIsTraceSampled()).toBeFalse() + expect(eventBridge.getIsTraceSampled()).toBe(false) }) it("should return null when the bridge returns 'null'", () => { @@ -172,12 +175,12 @@ describe('event bridge getPrivacyLevel', () => { describe('bridgeSupports', () => { it('should returns true when the bridge supports a capability', () => { mockEventBridge({ capabilities: [BridgeCapability.RECORDS] }) - expect(bridgeSupports(BridgeCapability.RECORDS)).toBeTrue() + expect(bridgeSupports(BridgeCapability.RECORDS)).toBe(true) }) it('should returns false when the bridge does not support a capability', () => { mockEventBridge({ capabilities: [] }) - expect(bridgeSupports(BridgeCapability.RECORDS)).toBeFalse() + expect(bridgeSupports(BridgeCapability.RECORDS)).toBe(false) }) }) }) diff --git a/packages/browser-core/src/transport/flushController.spec.ts b/packages/browser-core/src/transport/flushController.spec.ts index 51912eca6e..f3c7757bc4 100644 --- a/packages/browser-core/src/transport/flushController.spec.ts +++ b/packages/browser-core/src/transport/flushController.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Clock } from '../../test' import { mockClock } from '../../test' import type { PageMayExitEvent } from '../browser/pageMayExitObservable' @@ -13,7 +14,7 @@ const SMALL_MESSAGE_BYTE_COUNT = 2 describe('flushController', () => { let clock: Clock let flushController: FlushController - let flushSpy: jasmine.Spy<(event: FlushEvent) => void> + let flushSpy: Mock<(event: FlushEvent) => void> let pageMayExitObservable: Observable beforeEach(() => { @@ -22,7 +23,7 @@ describe('flushController', () => { flushController = createFlushController({ pageMayExitObservable, }) - flushSpy = jasmine.createSpy() + flushSpy = vi.fn() flushController.flushObservable.subscribe(flushSpy) }) @@ -35,8 +36,9 @@ describe('flushController', () => { pageMayExitObservable.notify({ reason: 'before_unload' }) - expect(flushSpy).toHaveBeenCalledOnceWith({ - reason: jasmine.any(String), + expect(flushSpy).toHaveBeenCalledTimes(1) + expect(flushSpy).toHaveBeenCalledExactlyOnceWith({ + reason: expect.any(String), bytesCount: messagesCount * SMALL_MESSAGE_BYTE_COUNT, messagesCount, }) @@ -54,7 +56,7 @@ describe('flushController', () => { flushController.notifyBeforeAddMessage(SMALL_MESSAGE_BYTE_COUNT) flushController.notifyAfterAddMessage() pageMayExitObservable.notify({ reason: 'before_unload' }) - expect(flushSpy.calls.first().args[0].reason).toBe('before_unload') + expect(flushSpy.mock.calls[0][0].reason).toBe('before_unload') }) it('does not notify if no message was added', () => { @@ -81,7 +83,7 @@ describe('flushController', () => { flushController.notifyBeforeAddMessage(SMALL_MESSAGE_BYTE_COUNT) flushController.notifyAfterAddMessage() flushController.forceFlush('session_expire') - expect(flushSpy.calls.first().args[0].reason).toBe('session_expire') + expect(flushSpy.mock.calls[0][0].reason).toBe('session_expire') }) it('does not notify if no message was added', () => { @@ -107,7 +109,7 @@ describe('flushController', () => { pageMayExitObservable.notify({ reason: 'before_unload' }) - expect(flushSpy.calls.first().args[0].reason).toBe('before_unload') + expect(flushSpy.mock.calls[0][0].reason).toBe('before_unload') }) it('notifies when the bytes limit is reached after adding a message', () => { @@ -119,7 +121,7 @@ describe('flushController', () => { it('flush reason should be "bytes_limit"', () => { flushController.notifyBeforeAddMessage(BYTES_LIMIT) flushController.notifyAfterAddMessage() - expect(flushSpy.calls.first().args[0].reason).toBe('bytes_limit') + expect(flushSpy.mock.calls[0][0].reason).toBe('bytes_limit') }) it('notifies when the bytes limit will be reached before adding a message', () => { @@ -147,7 +149,7 @@ describe('flushController', () => { flushController.notifyBeforeAddMessage(BYTES_LIMIT) flushController.notifyAfterAddMessage() - flushSpy.calls.reset() + flushSpy.mockClear() flushController.notifyBeforeAddMessage(SMALL_MESSAGE_BYTE_COUNT) flushController.notifyAfterAddMessage() @@ -169,7 +171,7 @@ describe('flushController', () => { flushController.notifyBeforeAddMessage(SMALL_MESSAGE_BYTE_COUNT) flushController.notifyAfterAddMessage() } - expect(flushSpy.calls.first().args[0].reason).toBe('messages_limit') + expect(flushSpy.mock.calls[0][0].reason).toBe('messages_limit') }) it('does not flush when the message was not fully added yet', () => { @@ -198,7 +200,7 @@ describe('flushController', () => { flushController.notifyAfterAddMessage() } - flushSpy.calls.reset() + flushSpy.mockClear() flushController.notifyBeforeAddMessage(SMALL_MESSAGE_BYTE_COUNT) flushController.notifyAfterAddMessage() @@ -218,7 +220,7 @@ describe('flushController', () => { flushController.notifyBeforeAddMessage(SMALL_MESSAGE_BYTE_COUNT) flushController.notifyAfterAddMessage() clock.tick(FLUSH_DURATION_LIMIT) - expect(flushSpy.calls.first().args[0].reason).toBe('duration_limit') + expect(flushSpy.mock.calls[0][0].reason).toBe('duration_limit') }) it('does not postpone the duration limit when another message was added', () => { diff --git a/packages/browser-core/src/transport/httpRequest.spec.ts b/packages/browser-core/src/transport/httpRequest.spec.ts index 0f1efd1bb2..2edb8100a1 100644 --- a/packages/browser-core/src/transport/httpRequest.spec.ts +++ b/packages/browser-core/src/transport/httpRequest.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { EndpointBuilder } from '@datadog/js-core/transport' import { createEndpointBuilder } from '@datadog/js-core/transport' import type { Request } from '../../test' @@ -77,23 +78,25 @@ describe('httpRequest', () => { }) describe('fetchStrategy onResponse', () => { - it('should be called with intake response', (done) => { - interceptor.withFetch(DEFAULT_FETCH_MOCK) - - fetchStrategy(endpointBuilder, { data: '{"foo":"bar1"}\n{"foo":"bar2"}', bytesCount: 10 }, (response) => { - expect(response).toEqual({ status: 200, type: 'cors' }) - done() - }) - }) - - it('should be called with status 0 when fetch fails', (done) => { - interceptor.withFetch(NETWORK_ERROR_FETCH_MOCK) - - fetchStrategy(endpointBuilder, { data: '{"foo":"bar1"}\n{"foo":"bar2"}', bytesCount: 10 }, (response) => { - expect(response).toEqual({ status: 0 }) - done() - }) - }) + it('should be called with intake response', () => + new Promise((resolve) => { + interceptor.withFetch(DEFAULT_FETCH_MOCK) + + fetchStrategy(endpointBuilder, { data: '{"foo":"bar1"}\n{"foo":"bar2"}', bytesCount: 10 }, (response) => { + expect(response).toEqual({ status: 200, type: 'cors' }) + resolve() + }) + })) + + it('should be called with status 0 when fetch fails', () => + new Promise((resolve) => { + interceptor.withFetch(NETWORK_ERROR_FETCH_MOCK) + + fetchStrategy(endpointBuilder, { data: '{"foo":"bar1"}\n{"foo":"bar2"}', bytesCount: 10 }, (response) => { + expect(response).toEqual({ status: 0 }) + resolve() + }) + })) }) describe('sendOnExit', () => { @@ -110,7 +113,12 @@ describe('httpRequest', () => { expect(requests[0].body).toEqual('{"foo":"bar1"}\n{"foo":"bar2"}') }) - it('should use sendBeacon when the bytes count is correct', () => { + it('should use sendBeacon when the bytes count is correct', (ctx) => { + if (!interceptor.isSendBeaconSupported()) { + ctx.skip(true, 'sendBeacon not supported') + return + } + request.sendOnExit({ data: '{"foo":"bar1"}\n{"foo":"bar2"}', bytesCount: 10 }) expect(requests.length).toEqual(1) @@ -126,7 +134,12 @@ describe('httpRequest', () => { expect(requests[0].type).toBe('fetch') }) - it('should fallback to fetch when sendBeacon is not queued', async () => { + it('should fallback to fetch when sendBeacon is not queued', async (ctx) => { + if (!interceptor.isSendBeaconSupported()) { + ctx.skip(true, 'sendBeacon not supported') + return + } + interceptor.withSendBeacon(() => false) request.sendOnExit({ data: '{"foo":"bar1"}\n{"foo":"bar2"}', bytesCount: 10 }) @@ -137,7 +150,12 @@ describe('httpRequest', () => { expect(requests[0].type).toBe('fetch') }) - it('should fallback to fetch when sendBeacon throws', async () => { + it('should fallback to fetch when sendBeacon throws', async (ctx) => { + if (!interceptor.isSendBeaconSupported()) { + ctx.skip(true, 'sendBeacon not supported') + return + } + let sendBeaconCalled = false interceptor.withSendBeacon(() => { sendBeaconCalled = true diff --git a/packages/browser-core/src/transport/sendWithRetryStrategy.spec.ts b/packages/browser-core/src/transport/sendWithRetryStrategy.spec.ts index f14cd74250..277db7ab72 100644 --- a/packages/browser-core/src/transport/sendWithRetryStrategy.spec.ts +++ b/packages/browser-core/src/transport/sendWithRetryStrategy.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { mockClock, setNavigatorOnLine } from '../../test' import type { Clock } from '../../test' import { Observable } from '../tools/observable' @@ -19,7 +20,7 @@ describe('sendWithRetryStrategy', () => { let state: RetryState let sendRequest: (payload?: Partial) => Payload let clock: Clock - let reportErrorSpy: jasmine.Spy + let reportErrorSpy: Mock<(...args: any[]) => any> const observedEvents: HttpRequestEvent[] = [] function mockSend() { @@ -47,7 +48,7 @@ describe('sendWithRetryStrategy', () => { sendMock = mockSend() state = newRetryState() clock = mockClock() - reportErrorSpy = jasmine.createSpy('reportError') + reportErrorSpy = vi.fn() const observable = new Observable() observable.subscribe((event) => observedEvents.push(event)) sendRequest = (payload) => { @@ -168,7 +169,7 @@ describe('sendWithRetryStrategy', () => { expect(reportErrorSpy).toHaveBeenCalledWith( `Reached max logs events size queued for upload: ${MAX_QUEUE_BYTES_COUNT / ONE_MEBI_BYTE}MiB` ) - reportErrorSpy.calls.reset() + reportErrorSpy.mockClear() expect(latestEvents()).toEqual([ { type: 'success', bandwidth: { ongoingByteCount: 0, ongoingRequestCount: 0 }, payload: payload0 }, ]) diff --git a/packages/browser-core/test/collectAsyncCalls.spec.ts b/packages/browser-core/test/collectAsyncCalls.spec.ts new file mode 100644 index 0000000000..7f5314c58c --- /dev/null +++ b/packages/browser-core/test/collectAsyncCalls.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest' +import { collectAsyncCalls } from './collectAsyncCalls' + +describe('collectAsyncCalls', () => { + it('collects calls and preserves the mock implementation return value', async () => { + const spy = vi.fn((value: number) => value * 2) + const callsPromise = collectAsyncCalls(spy) + + expect(spy(21)).toBe(42) + + const calls = await callsPromise + expect(calls.count()).toBe(1) + expect(calls.mostRecent()).toEqual({ args: [21], returnValue: 42 }) + }) + + it('does not swallow errors from the mock implementation', async () => { + const expectedError = new Error('original error') + const spy = vi.fn(() => { + throw expectedError + }) + const callsPromise = collectAsyncCalls(spy) + + expect(() => spy()).toThrow(expectedError) + await expect(callsPromise).resolves.toBeDefined() + }) + + it('throws on an extra call after the expected calls were collected', async () => { + const spy = vi.fn<() => void>() + const callsPromise = collectAsyncCalls(spy) + spy() + await callsPromise + + expect(() => spy()).toThrow('Unexpected call count (expected 1, got 2)') + spy.mockClear() + }) + + it('can collect the same mock again', async () => { + const spy = vi.fn<() => void>() + const firstCalls = collectAsyncCalls(spy) + spy() + await firstCalls + + spy.mockClear() + const secondCalls = collectAsyncCalls(spy) + spy() + + await expect(secondCalls).resolves.toBeDefined() + }) +}) diff --git a/packages/browser-core/test/collectAsyncCalls.ts b/packages/browser-core/test/collectAsyncCalls.ts index a5c874af3a..ba34f2bbeb 100644 --- a/packages/browser-core/test/collectAsyncCalls.ts +++ b/packages/browser-core/test/collectAsyncCalls.ts @@ -1,42 +1,98 @@ -import { getCurrentJasmineSpec } from './getCurrentJasmineSpec' +import { onTestFinished, type Mock } from 'vitest' -const originalPlanForGuard = new WeakMap<() => void, () => void>() +const activeGuards = new WeakMap void>() +const originalImplementations = new WeakMap<(...args: any[]) => any, ((...args: any[]) => any) | undefined>() -export function collectAsyncCalls( - spy: jasmine.Spy, +export interface MockCalls any> { + all(): Array<{ args: Parameters; returnValue: ReturnType }> + count(): number + argsFor(index: number): Parameters + mostRecent(): { args: Parameters; returnValue: ReturnType } +} + +export function collectAsyncCalls any>( + spy: Mock, expectedCallsCount = 1 -): Promise> { +): Promise> { return new Promise((resolve, reject) => { - const currentSpec = getCurrentJasmineSpec() - if (!currentSpec) { - reject(new Error('collectAsyncCalls should be called within jasmine code')) - return + const currentImplementation = spy.getMockImplementation() as ((...args: any[]) => any) | undefined + const originalImplementation = + currentImplementation && originalImplementations.has(currentImplementation) + ? originalImplementations.get(currentImplementation) + : currentImplementation + activeGuards.get(spy)?.() + + let guardIsActive = true + activeGuards.set(spy, () => { + guardIsActive = false + }) + + onTestFinished(() => { + // Some callers explicitly clear the mock during cleanup to stop the guard before tearing + // down the observed component. Extra calls still throw synchronously when they happen. + if (guardIsActive && spy.mock.calls.length !== 0 && spy.mock.calls.length !== expectedCallsCount) { + throw createUnexpectedCallCountError(spy, expectedCallsCount) + } + }) + + function extraCallDetected(): Error { + const error = createUnexpectedCallCountError(spy, expectedCallsCount) + reject(error) + return error } const checkCalls = () => { - if (spy.calls.count() === expectedCallsCount) { - resolve(spy.calls) - } else if (spy.calls.count() > expectedCallsCount) { - const message = `Unexpected extra call for spec '${currentSpec.fullName}'` - fail(message) - reject(new Error(message)) + if (spy.mock.calls.length === expectedCallsCount) { + const guardImplementation = ((...args: Parameters) => { + originalImplementation?.(...args) + throw extraCallDetected() + }) as unknown as Parameters['mockImplementation']>[0] + originalImplementations.set(guardImplementation, originalImplementation) + spy.mockImplementation(guardImplementation) + resolve(wrapMockCalls(spy)) + } else if (spy.mock.calls.length > expectedCallsCount) { + extraCallDetected() } } - checkCalls() + const collectingImplementation = ((...args: Parameters) => { + let result: ReturnType | undefined + try { + result = originalImplementation?.(...args) + } finally { + checkCalls() + } + return result as ReturnType + }) as Parameters['mockImplementation']>[0] + originalImplementations.set(collectingImplementation, originalImplementation) + spy.mockImplementation(collectingImplementation) - const originalPlan = getOriginalPlan(spy) - const guard = ((...args: Parameters) => { - checkCalls() - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return originalPlan(...args) - }) as F - originalPlanForGuard.set(guard, originalPlan) - spy.and.callFake(guard) + checkCalls() }) } -function getOriginalPlan void>(spy: jasmine.Spy): F { - const originalPlanOrGuard: F = (spy.and as unknown as { plan: F }).plan - return (originalPlanForGuard.get(originalPlanOrGuard) as F | undefined) ?? originalPlanOrGuard +function createUnexpectedCallCountError any>( + spy: Mock, + expectedCallsCount: number +): Error { + return new Error(`Unexpected call count (expected ${expectedCallsCount}, got ${spy.mock.calls.length})`) +} + +function wrapMockCalls any>(spy: Mock): MockCalls { + return { + all: () => + spy.mock.calls.map((args, i) => ({ + args: args as Parameters, + returnValue: spy.mock.results[i]?.value as ReturnType, + })), + count: () => spy.mock.calls.length, + argsFor: (index: number) => spy.mock.calls[index] as Parameters, + mostRecent: () => { + const lastIndex = spy.mock.calls.length - 1 + return { + args: spy.mock.calls[lastIndex] as Parameters, + returnValue: spy.mock.results[lastIndex]?.value as ReturnType, + } + }, + } } diff --git a/packages/browser-core/test/consoleLog.ts b/packages/browser-core/test/consoleLog.ts index 048c3ef47c..c191a0626c 100644 --- a/packages/browser-core/test/consoleLog.ts +++ b/packages/browser-core/test/consoleLog.ts @@ -1,3 +1,5 @@ +import { afterEach, vi } from 'vitest' + const ignoreList: Array<{ level: string; match: string }> = [] afterEach(() => { @@ -11,10 +13,10 @@ afterEach(() => { export function ignoreConsoleLogs(level: 'error' | 'warn' | 'log', match: string) { ignoreList.push({ level, match }) - if (!jasmine.isSpy(console[level])) { + if (!vi.isMockFunction(console[level])) { const originalLogFunction = console[level].bind(console) - spyOn(console, level).and.callFake((...args: unknown[]) => { + vi.spyOn(console, level).mockImplementation((...args: unknown[]) => { // No need to be too precise with formating here, we just want something to match against const message = args.map((arg) => String(arg)).join(' ') if (ignoreList.some((ignoreEntry) => ignoreEntry.level === level && message.includes(ignoreEntry.match))) { diff --git a/packages/browser-core/test/cookie.ts b/packages/browser-core/test/cookie.ts index b500dc5841..4ee1e97ea3 100644 --- a/packages/browser-core/test/cookie.ts +++ b/packages/browser-core/test/cookie.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import { ONE_MINUTE } from '@datadog/js-core/time' import { getCookie, setCookie } from '../src/browser/cookie' import { toSessionState } from '../src/domain/session/sessionState' @@ -30,12 +31,18 @@ interface Cookie { export function mockCookies({ filter }: { filter?: (cookie: Cookie) => boolean } = {}) { let cookies: Cookie[] = [] - const getter = jasmine.createSpy('document.cookie getter').and.callFake(() => { + const documentPrototype = + 'cookie' in Document.prototype + ? Document.prototype + : // Firefox 67 doesn't define `cookie` on `Document.prototype` + HTMLDocument.prototype + + const getter = vi.spyOn(documentPrototype, 'cookie', 'get').mockImplementation(() => { removeExpiredCookies() return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join(';') }) - const setter = jasmine.createSpy('document.cookie setter').and.callFake((cookieString: string) => { + const setter = vi.spyOn(documentPrototype, 'cookie', 'set').mockImplementation((cookieString) => { const cookie = parseSingleCookieString(cookieString) if (filter && !filter(cookie)) { diff --git a/packages/browser-core/test/disableJasmineUncaughtExceptionTracking.ts b/packages/browser-core/test/disableJasmineUncaughtExceptionTracking.ts deleted file mode 100644 index f0d7f4c01e..0000000000 --- a/packages/browser-core/test/disableJasmineUncaughtExceptionTracking.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Disable Jasmine's uncaught error handling. This is useful for test cases throwing exceptions or - * unhandled rejections that are expected to be caught somehow, but Jasmine also catch them and - * fails the test. - */ -export function disableJasmineUncaughtExceptionTracking() { - spyOn(window as any, 'onerror') -} diff --git a/packages/browser-core/test/disableUncaughtExceptionTracking.ts b/packages/browser-core/test/disableUncaughtExceptionTracking.ts new file mode 100644 index 0000000000..a2ccd18a2a --- /dev/null +++ b/packages/browser-core/test/disableUncaughtExceptionTracking.ts @@ -0,0 +1,36 @@ +import { registerCleanupTask } from './registerCleanupTask' +import { allowCurrentTestConsoleLogs } from './getCurrentTest' + +/** + * Disable uncaught error handling. This is useful for test cases throwing exceptions or + * unhandled rejections that are expected to be caught somehow. + * + * In Vitest browser mode, setting window.onerror = null is not enough because Vitest + * registers its own 'error' and 'unhandledrejection' event listeners to track uncaught + * errors. We add capturing listeners that call preventDefault() to mark errors as handled + * without blocking window.onerror (which the tests need). + */ +export function disableUncaughtExceptionTracking() { + // Vitest's browser error catcher writes the expected exception to stderr even when the event is + // prevented. Mark the test so the CI reporter can distinguish it from accidental console calls. + allowCurrentTestConsoleLogs() + + const originalOnerror = window.onerror + window.onerror = null + + // Use only preventDefault() β€” NOT stopImmediatePropagation(). + // preventDefault() marks the error as "handled" without blocking other listeners. + // stopImmediatePropagation() would block window.onerror which the tests need. + const suppressError = (event: Event) => { + event.preventDefault() + } + + window.addEventListener('error', suppressError, true) + window.addEventListener('unhandledrejection', suppressError, true) + + registerCleanupTask(() => { + window.onerror = originalOnerror + window.removeEventListener('error', suppressError, true) + window.removeEventListener('unhandledrejection', suppressError, true) + }) +} diff --git a/packages/browser-core/test/emulate/mockClock.ts b/packages/browser-core/test/emulate/mockClock.ts index 51f12b566e..b93ee84c75 100644 --- a/packages/browser-core/test/emulate/mockClock.ts +++ b/packages/browser-core/test/emulate/mockClock.ts @@ -1,19 +1,41 @@ +import { vi } from 'vitest' import type { TimeStamp, RelativeTime } from '@datadog/js-core/time' import { registerCleanupTask } from '../registerCleanupTask' export type Clock = ReturnType export function mockClock() { - jasmine.clock().install() - jasmine.clock().mockDate() + // Use performance.timing.navigationStart as timeOrigin β€” it's an integer set once + // at page load, so it doesn't suffer from timing races between performance.now() + // and Date.now() captures. This matches getNavigationStart() in timeUtils.ts. + const timeOrigin = performance.timing.navigationStart - const timeOrigin = performance.timing.navigationStart // @see getTimeOrigin() in @datadog/js-core/time + // Exclude 'performance' from faking so our override sticks on the real object + // AND so performance.timing.navigationStart remains accessible. + // When performance IS faked, @sinonjs/fake-timers replaces the object and our + // override silently fails (the fake performance.now() starts at 0). + vi.useFakeTimers({ + toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'], + }) + + // Capture Date.now() AFTER vi.useFakeTimers() to get the exact frozen value. const timeStampStart = Date.now() const relativeStart = timeStampStart - timeOrigin - spyOn(performance, 'now').and.callFake(() => Date.now() - timeOrigin) + const originalPerformanceNowDescriptor = Object.getOwnPropertyDescriptor(performance, 'now') + Object.defineProperty(performance, 'now', { + configurable: true, + value: () => Date.now() - timeOrigin, + }) - registerCleanupTask(() => jasmine.clock().uninstall()) + registerCleanupTask(() => { + if (originalPerformanceNowDescriptor) { + Object.defineProperty(performance, 'now', originalPerformanceNowDescriptor) + } else { + delete (performance as unknown as { now?: () => number }).now + } + vi.useRealTimers() + }) return { /** @@ -26,7 +48,11 @@ export function mockClock() { * invokation (the start of the test). */ timeStamp: (duration: number) => (timeStampStart + duration) as TimeStamp, - tick: (ms: number) => jasmine.clock().tick(ms), - setDate: (date: Date) => jasmine.clock().mockDate(date), + // Wrap tick() to return void β€” vi.advanceTimersByTime returns the Vi instance, + // which breaks React useEffect cleanup that expects void. + tick: (ms: number) => { + vi.advanceTimersByTime(ms) + }, + setDate: (date: Date) => vi.setSystemTime(date), } } diff --git a/packages/browser-core/test/emulate/mockFlushController.ts b/packages/browser-core/test/emulate/mockFlushController.ts index c6a158c6c9..6526f0cb2c 100644 --- a/packages/browser-core/test/emulate/mockFlushController.ts +++ b/packages/browser-core/test/emulate/mockFlushController.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import { Observable } from '../../src/tools/observable' import type { FlushEvent, FlushController, FlushReason, UrgentFlushReason } from '../../src/transport' @@ -10,20 +11,20 @@ export function createMockFlushController() { let currentBytesCount = 0 const flushController = { - notifyBeforeAddMessage: jasmine - .createSpy() - .and.callFake((messageBytesCount) => { + notifyBeforeAddMessage: vi + .fn() + .mockImplementation((messageBytesCount) => { currentBytesCount += messageBytesCount currentMessagesCount += 1 }), - notifyAfterAddMessage: jasmine - .createSpy() - .and.callFake((messageBytesCountDiff = 0) => { + notifyAfterAddMessage: vi + .fn() + .mockImplementation((messageBytesCountDiff = 0) => { currentBytesCount += messageBytesCountDiff }), flushObservable, prepareUrgentFlushObservable, - forceFlush: jasmine.createSpy(), + forceFlush: vi.fn(), get messagesCount() { return currentMessagesCount }, diff --git a/packages/browser-core/test/emulate/mockReportingObserver.ts b/packages/browser-core/test/emulate/mockReportingObserver.ts index a278ece395..2231a0ef89 100644 --- a/packages/browser-core/test/emulate/mockReportingObserver.ts +++ b/packages/browser-core/test/emulate/mockReportingObserver.ts @@ -50,23 +50,9 @@ export function mockReportingObserver() { export type MockCspEventListener = ReturnType export function mockCspEventListener() { - // eslint-disable-next-line @typescript-eslint/unbound-method - const originalAddEventListener = EventTarget.prototype.addEventListener - EventTarget.prototype.addEventListener = jasmine - .createSpy() - .and.callFake((_type: string, listener: EventListener) => { - listeners.push(listener) - }) - - registerCleanupTask(() => { - EventTarget.prototype.addEventListener = originalAddEventListener - }) - - const listeners: EventListener[] = [] - return { dispatchEvent() { - listeners.forEach((listener) => listener(FAKE_CSP_VIOLATION_EVENT)) + document.dispatchEvent(FAKE_CSP_VIOLATION_EVENT) }, } } diff --git a/packages/browser-core/test/emulate/mockRequestIdleCallback.ts b/packages/browser-core/test/emulate/mockRequestIdleCallback.ts index 0ae32d982d..ecd85c299e 100644 --- a/packages/browser-core/test/emulate/mockRequestIdleCallback.ts +++ b/packages/browser-core/test/emulate/mockRequestIdleCallback.ts @@ -1,3 +1,4 @@ +import { vi, type Mock } from 'vitest' import { registerCleanupTask } from '../registerCleanupTask' // Arbitrary remaining time for tests that don't care about remaining time @@ -6,21 +7,21 @@ const DEFAULT_TIME_REMAINING = 10 export interface RequestIdleCallbackMock { idle(timeRemaining?: number): void timeout(): void - spy: jasmine.Spy + spy: Mock } export function mockRequestIdleCallback(): RequestIdleCallbackMock { let nextId = 1 const activeIds = new Set() - const requestSpy = jasmine.createSpy().and.callFake(() => { + const requestSpy = vi.fn().mockImplementation(() => { const id = nextId activeIds.add(id) nextId++ return id }) - const cancelSpy = jasmine.createSpy().and.callFake((id: number) => { + const cancelSpy = vi.fn().mockImplementation((id: number) => { activeIds.delete(id) }) @@ -35,12 +36,16 @@ export function mockRequestIdleCallback(): RequestIdleCallbackMock { }) function callAllActiveCallbacks(deadline: IdleDeadline) { - for (const call of requestSpy.calls.all().slice()) { - if (!activeIds.has(call.returnValue)) { + // Snapshot the call count: callbacks invoked during this loop may call scheduleNextRun() + // which adds new spy calls. Without snapshotting, the loop grows infinitely. + const callCount = requestSpy.mock.calls.length + for (let i = 0; i < callCount; i++) { + const returnValue = requestSpy.mock.results[i].value as number + if (!activeIds.has(returnValue)) { continue } - activeIds.delete(call.returnValue) - call.args[0](deadline) + activeIds.delete(returnValue) + requestSpy.mock.calls[i][0](deadline) } } diff --git a/packages/browser-core/test/fakeSessionStoreStrategy.ts b/packages/browser-core/test/fakeSessionStoreStrategy.ts index 24532889e5..bf42493382 100644 --- a/packages/browser-core/test/fakeSessionStoreStrategy.ts +++ b/packages/browser-core/test/fakeSessionStoreStrategy.ts @@ -1,9 +1,10 @@ +import { vi } from 'vitest' import { Observable } from '../src/tools/observable' import type { SessionState } from '../src/domain/session/sessionState' import type { SessionStoreStrategy } from '../src/domain/session/storeStrategies/sessionStoreStrategy' export type FakeSessionStoreStrategy = SessionStoreStrategy & { - setSessionState: jasmine.Spy<(fn: (state: SessionState) => SessionState) => Promise> + setSessionState: ReturnType SessionState) => Promise>> getInternalState: () => SessionState simulateExternalChange: (state: SessionState) => void } @@ -15,9 +16,9 @@ export function createFakeSessionStoreStrategy({ const sessionObservable = new Observable() return { - setSessionState: jasmine - .createSpy('setSessionState') - .and.callFake(async (fn: (state: SessionState) => SessionState): Promise => { + setSessionState: vi + .fn<(fn: (state: SessionState) => SessionState) => Promise>() + .mockImplementation(async (fn: (state: SessionState) => SessionState): Promise => { session = fn({ ...session }) await Promise.resolve() sessionObservable.notify({ ...session }) diff --git a/packages/browser-core/test/forEach.spec.ts b/packages/browser-core/test/forEach.spec.ts deleted file mode 100644 index 1c393d7960..0000000000 --- a/packages/browser-core/test/forEach.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { resetManageResourceTimingBufferFull } from '../../browser-rum-core/src/browser/performanceObservable' -import { resetExperimentalFeatures } from '../src/tools/experimentalFeatures' -import { resetValueHistoryGlobals } from '../src/tools/valueHistory' -import { resetFetchObservable } from '../src/browser/fetchObservable' -import { resetConsoleObservable } from '../src/domain/console/consoleObservable' -import { resetXhrObservable } from '../src/browser/xhrObservable' -import { resetWebSocketObservable } from '../src/browser/webSocketObservable' -import { resetGetCurrentSite } from '../src/browser/cookie' -import { resetReplayStats } from '../../browser-rum/src/domain/replayStats' -import { resetInteractionCountPolyfill } from '../../browser-rum-core/src/domain/view/viewMetrics/interactionCountPolyfill' -import { resetMonitor } from '../src/tools/monitor' -import { resetTelemetry } from '../src/domain/telemetry' -import { resetSampleDecisionCache } from '../src/domain/sampler' -import { resetAllowUntrustedEvents } from '../src/browser/addEventListener' -import { startLeakDetection } from './leakDetection' -import type { BuildEnvWindow } from './buildEnv' -;(window as unknown as BuildEnvWindow).__BUILD_ENV__SDK_VERSION__ = 'test' -;(window as any).IS_REACT_ACT_ENVIRONMENT = true - -beforeEach(() => { - // prevent 'Some of your tests did a full page reload!' issue - window.onbeforeunload = () => 'stop' - startLeakDetection() - // Note: clearing cookies should be done in `beforeEach` rather than `afterEach`, because in some - // cases the test patches the `document.cookie` getter (ex: `spyOnProperty(document, 'cookie', - // 'get')`), which would prevent the `clearAllCookies` function from working properly. - clearAllCookies() -}) - -afterEach(() => { - // reset globals - delete (window as any).DD_LOGS - delete (window as any).DD_RUM - resetValueHistoryGlobals() - resetFetchObservable() - resetConsoleObservable() - resetXhrObservable() - resetWebSocketObservable() - resetGetCurrentSite() - resetReplayStats() - resetMonitor() - resetTelemetry() - resetInteractionCountPolyfill() - resetSampleDecisionCache() - resetExperimentalFeatures() - resetManageResourceTimingBufferFull() - resetAllowUntrustedEvents() -}) - -function clearAllCookies() { - document.cookie.split(';').forEach((c) => { - document.cookie = c.replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/;samesite=strict`) - }) -} diff --git a/packages/browser-core/test/getCurrentJasmineSpec.ts b/packages/browser-core/test/getCurrentJasmineSpec.ts deleted file mode 100644 index 59a114abe3..0000000000 --- a/packages/browser-core/test/getCurrentJasmineSpec.ts +++ /dev/null @@ -1,14 +0,0 @@ -let currentSpec: jasmine.SpecResult | null = null - -export function getCurrentJasmineSpec() { - return currentSpec -} - -jasmine.getEnv().addReporter({ - specStarted(specResult) { - currentSpec = specResult - }, - specDone() { - currentSpec = null - }, -}) diff --git a/packages/browser-core/test/getCurrentTest.ts b/packages/browser-core/test/getCurrentTest.ts new file mode 100644 index 0000000000..3a917cd535 --- /dev/null +++ b/packages/browser-core/test/getCurrentTest.ts @@ -0,0 +1,31 @@ +import { beforeEach, onTestFinished } from 'vitest' + +export interface CurrentTest { + id: string + name: string + meta: Record +} + +let currentTest: CurrentTest | null = null + +export function getCurrentTest(): CurrentTest | null { + return currentTest +} + +beforeEach((context) => { + currentTest = { + id: context.task.id, + name: context.task.name, + meta: context.task.meta as Record, + } + onTestFinished(() => { + currentTest = null + }) +}) + +export function allowCurrentTestConsoleLogs(): void { + if (!currentTest) { + throw new Error('allowCurrentTestConsoleLogs() must be called from a test') + } + currentTest.meta.allowUnexpectedConsoleLogs = true +} diff --git a/packages/browser-core/test/index.ts b/packages/browser-core/test/index.ts index 7ddae1471a..d8f2dda29f 100644 --- a/packages/browser-core/test/index.ts +++ b/packages/browser-core/test/index.ts @@ -23,12 +23,14 @@ export * from './emulate/mockTelemetry' export * from './emulate/mockSourceCodeContext' export type * from './typeUtils' export * from './coreConfiguration' -export * from './disableJasmineUncaughtExceptionTracking' +export * from './disableUncaughtExceptionTracking' export * from './instrumentation' export * from './wait' export * from './consoleLog' export * from './fakeSessionStoreStrategy' +export * from './getCurrentTest' export * from './readFormData' export * from './replaceMockable' export * from './sampling' export * from './mockSessionManager' +export * from './leakDetection' diff --git a/packages/browser-core/test/interceptRequests.ts b/packages/browser-core/test/interceptRequests.ts index 6963329b62..018b49b0ae 100644 --- a/packages/browser-core/test/interceptRequests.ts +++ b/packages/browser-core/test/interceptRequests.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import type { EndpointBuilder } from '@datadog/js-core/transport' import { noop } from '../src' import { mockXhr, MockXhr } from './emulate/mockXhr' @@ -25,7 +26,7 @@ export function interceptRequests() { const originalFetch = window.fetch if (isSendBeaconSupported()) { - spyOn(navigator, 'sendBeacon').and.callFake((url, body) => { + vi.spyOn(navigator, 'sendBeacon').mockImplementation((url, body) => { requests.push({ type: 'sendBeacon', url: url as string, body: body as string }) return true }) @@ -38,7 +39,7 @@ export function interceptRequests() { resolveFetchCallReturns = resolve }) - const fetchSpy = spyOn(window, 'fetch').and.callFake((url, config) => { + const fetchSpy = vi.spyOn(window, 'fetch').mockImplementation((url, config) => { const fetchPromise = fetchMocks.shift() if (!fetchPromise) { diff --git a/packages/browser-core/test/leakDetection.ts b/packages/browser-core/test/leakDetection.ts index 957d8eb4d2..784b05148b 100644 --- a/packages/browser-core/test/leakDetection.ts +++ b/packages/browser-core/test/leakDetection.ts @@ -1,5 +1,5 @@ import { display } from '../src/tools/display' -import { getCurrentJasmineSpec } from './getCurrentJasmineSpec' +import { getCurrentTest } from './getCurrentTest' import { registerCleanupTask } from './registerCleanupTask' export function startLeakDetection() { @@ -34,10 +34,10 @@ export function startLeakDetection() { } function withLeakDetection(eventName: string, listener: EventListener) { - const specWhenAdded = getCurrentJasmineSpec() + const testWhenAdded = getCurrentTest() const stackWhenAdded = new Error().stack if ( - !specWhenAdded || + !testWhenAdded || // Ignore listeners added by React: React is adding listeners to DOM elements for synthetic events, and there is no way to remove them stackWhenAdded?.includes('listenToAllSupportedEvents') ) { @@ -45,12 +45,12 @@ function withLeakDetection(eventName: string, listener: EventListener) { } return (event: Event) => { - const currentSpec = getCurrentJasmineSpec() - if (specWhenAdded.fullName !== currentSpec?.fullName) { + const currentTest = getCurrentTest() + if (testWhenAdded.id !== currentTest?.id) { display.error(`Leaked listener event names: "${eventName}" - attached with: "${specWhenAdded.fullName}" - ${currentSpec ? `executed with: "${currentSpec.fullName}"` : 'executed outside of a spec'} + attached with: "${testWhenAdded.name}" + ${currentTest ? `executed with: "${currentTest.name}"` : 'executed outside of a test'} attachment stack: ${stackWhenAdded}`) } listener(event) diff --git a/packages/browser-core/test/mockGlobalContext.ts b/packages/browser-core/test/mockGlobalContext.ts index 2ab4eae847..4078e10e78 100644 --- a/packages/browser-core/test/mockGlobalContext.ts +++ b/packages/browser-core/test/mockGlobalContext.ts @@ -1,11 +1,12 @@ +import { vi } from 'vitest' import type { ContextManager } from '../src/domain/context/contextManager' export function mockContextManager() { return { getContext: () => ({}), - setContext: jasmine.createSpy(), - setContextProperty: jasmine.createSpy(), - removeContextProperty: jasmine.createSpy(), - clearContext: jasmine.createSpy(), + setContext: vi.fn(), + setContextProperty: vi.fn(), + removeContextProperty: vi.fn(), + clearContext: vi.fn(), } as unknown as ContextManager } diff --git a/packages/browser-core/test/registerCleanupTask.spec.ts b/packages/browser-core/test/registerCleanupTask.spec.ts new file mode 100644 index 0000000000..0947a74956 --- /dev/null +++ b/packages/browser-core/test/registerCleanupTask.spec.ts @@ -0,0 +1,21 @@ +import { afterAll, describe, expect, it } from 'vitest' +import { registerCleanupTask } from './registerCleanupTask' + +describe('registerCleanupTask', () => { + describe('when the current test is dynamically skipped', () => { + let cleanupCompleted = false + + afterAll(() => { + expect(cleanupCompleted).toBe(true) + }) + + it('runs and awaits its cleanup tasks', (ctx) => { + registerCleanupTask(async () => { + await Promise.resolve() + cleanupCompleted = true + }) + + ctx.skip(true, 'exercise dynamic skip cleanup') + }) + }) +}) diff --git a/packages/browser-core/test/registerCleanupTask.ts b/packages/browser-core/test/registerCleanupTask.ts index eaf1f8d4ed..9c94174436 100644 --- a/packages/browser-core/test/registerCleanupTask.ts +++ b/packages/browser-core/test/registerCleanupTask.ts @@ -1,3 +1,5 @@ +import { afterEach } from 'vitest' + type CleanupTask = () => unknown const cleanupTasks: CleanupTask[] = [] @@ -8,6 +10,12 @@ export function registerCleanupTask(task: CleanupTask) { afterEach(async () => { for (const task of cleanupTasks.splice(0)) { - await task() + try { + await task() + } catch (error) { + const cleanupError = new Error(`Cleanup task failed: ${String(error)}`) as Error & { cause?: unknown } + cleanupError.cause = error + throw cleanupError + } } }) diff --git a/packages/browser-core/test/replaceMockable.ts b/packages/browser-core/test/replaceMockable.ts index 04631aab34..88d97bd81a 100644 --- a/packages/browser-core/test/replaceMockable.ts +++ b/packages/browser-core/test/replaceMockable.ts @@ -1,3 +1,4 @@ +import { vi, type Mock } from 'vitest' import { mockableReplacements } from '../src/tools/mockable' import { registerCleanupTask } from './registerCleanupTask' @@ -27,11 +28,11 @@ export function replaceMockable(value: T, replacement: T): void { } /** - * Creates a Jasmine spy and registers it as a mock replacement for a mockable function. + * Creates a Vitest mock function and registers it as a mock replacement for a mockable function. * The mock is automatically cleaned up after each test via registerCleanupTask. * * @param value - The original function (must be the same reference passed to mockable()) - * @returns A Jasmine spy that can be used for assertions + * @returns A Vitest mock function that can be used for assertions * @example * import { replaceMockableWithSpy } from '@datadog/browser-core/test' * import { trackRuntimeError } from '../domain/error/trackRuntimeError' @@ -42,8 +43,8 @@ export function replaceMockable(value: T, replacement: T): void { * expect(spy).toHaveBeenCalled() * }) */ -export function replaceMockableWithSpy any>(value: T): jasmine.Spy { - const spy = jasmine.createSpy() +export function replaceMockableWithSpy any>(value: T): Mock { + const spy = vi.fn() replaceMockable(value, spy as unknown as T) return spy } diff --git a/packages/browser-debugger/src/domain/api.spec.ts b/packages/browser-debugger/src/domain/api.spec.ts index 98bf360208..d08247fd96 100644 --- a/packages/browser-debugger/src/domain/api.spec.ts +++ b/packages/browser-debugger/src/domain/api.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest' import { globalObject } from '@datadog/browser-core' import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' import { onEntry, onReturn, onThrow, initDebuggerTransport, resetDebuggerTransport } from './api' @@ -10,8 +11,8 @@ const DEFAULT_PROBE_FUNCTION_ID = 'test.js;testMethod' const thisArg = {} describe('api', () => { - let mockBatchAdd: jasmine.Spy - let warnSpy: jasmine.Spy + let mockBatchAdd: Mock + let warnSpy: Mock function initTransport(overrides: Record = {}) { resetDebuggerTransport() @@ -26,8 +27,8 @@ describe('api', () => { beforeEach(() => { clearProbes() - warnSpy = spyOn(display, 'warn') - mockBatchAdd = jasmine.createSpy('batchAdd') + warnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) + mockBatchAdd = vi.fn() initTransport() ;(window as any).DD_DEBUGGER = { version: '0.0.1', @@ -50,7 +51,7 @@ describe('api', () => { onEntry(probes, self, args) onReturn(probes, 'result', self, args) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot // Verify entry.arguments structure - now flat @@ -92,7 +93,7 @@ describe('api', () => { onEntry(probes, globalObject, args) onReturn(probes, 'result', globalObject, args) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.entry.arguments).toEqual({ @@ -116,10 +117,10 @@ describe('api', () => { expect(result).toBe('returnValue') expect(mockBatchAdd).toHaveBeenCalledTimes(1) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] expect(payload.message).toBe('Test message') expect(payload.debugger.snapshot).toEqual( - jasmine.objectContaining({ id: jasmine.any(String), captures: jasmine.any(Object) }) + expect.objectContaining({ id: expect.any(String), captures: expect.any(Object) }) ) }) @@ -208,7 +209,7 @@ describe('api', () => { clearProbes() addProbe(probe) - mockBatchAdd.calls.reset() + mockBatchAdd.mockClear() probes = getProbes(DEFAULT_PROBE_FUNCTION_ID)! // Should not fire when condition fails @@ -234,7 +235,7 @@ describe('api', () => { clearProbes() addProbe(probe) - mockBatchAdd.calls.reset() + mockBatchAdd.mockClear() probes = getProbes(DEFAULT_PROBE_FUNCTION_ID)! // Should not fire when return value <= 10 @@ -260,7 +261,7 @@ describe('api', () => { onEntry(probes, thisArg, { arg: { value: 42 }, longString: 'abcdef' }) onReturn(probes, null, thisArg, { arg: { value: 42 }, longString: 'abcdef' }) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures).toEqual({ entry: { @@ -289,7 +290,7 @@ describe('api', () => { onEntry(probes, thisArg) onReturn(probes, { nested: 'return' }, thisArg, {}, { local: { value: 'data' } }) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures).toEqual({ entry: undefined, @@ -322,7 +323,7 @@ describe('api', () => { onEntry(probes, thisArg, { existing: 'value' }) onReturn(probes, null, thisArg, { existing: 'value' }) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.entry.captureExpressions).toEqual({ existing: { type: 'string', value: 'value' }, @@ -330,7 +331,7 @@ describe('api', () => { expect(snapshot.evaluationErrors).toEqual([ { expr: 'missing.value', - message: jasmine.stringMatching(/^ReferenceError: /), + message: expect.stringMatching(/^ReferenceError: /), }, ]) }) @@ -342,7 +343,7 @@ describe('api', () => { onEntry(probes, { name: 'obj' }, { arg: 'value' }) onReturn(probes, 'result', { name: 'obj' }, { arg: 'value' }, { local: 'data' }) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.captures).toEqual({ entry: { @@ -371,7 +372,7 @@ describe('api', () => { onEntry(probes, { name: 'obj' }, { arg: 'value' }) onReturn(probes, 'result', { name: 'obj' }, { arg: 'value' }, { local: 'data' }) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.captures).toEqual({ entry: { @@ -407,7 +408,7 @@ describe('api', () => { onEntry(probes, thisArg, { arg: 'value' }) onReturn(probes, true, thisArg, { arg: 'value' }) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.entry).toBeUndefined() expect(snapshot.captures.return).toBeDefined() @@ -425,7 +426,7 @@ describe('api', () => { onReturn(probes, null, thisArg) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.duration).toBe(10_000_000) // Should be in nanoseconds (10ms) }) @@ -447,13 +448,13 @@ describe('api', () => { onReturn(probes, null, thisArg) expect(mockBatchAdd).toHaveBeenCalledTimes(1) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(payload.message).toBeUndefined() expect(snapshot.evaluationErrors).toEqual([ { expr: 'missing.value', - message: jasmine.stringMatching(/^ReferenceError: /), + message: expect.stringMatching(/^ReferenceError: /), }, ]) expect(snapshot.duration).toBeUndefined() @@ -477,13 +478,13 @@ describe('api', () => { onReturn(probes, null, thisArg) expect(mockBatchAdd).toHaveBeenCalledTimes(1) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(payload.message).toBeUndefined() expect(snapshot.evaluationErrors).toEqual([ { expr: 'missing.value', - message: jasmine.stringMatching(/^ReferenceError: /), + message: expect.stringMatching(/^ReferenceError: /), }, ]) expect(snapshot.captures).toBeUndefined() @@ -529,7 +530,7 @@ describe('api', () => { onEntry(probes, self, args) onThrow(probes, error, self, args) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot // Verify return.arguments structure - now flat @@ -547,15 +548,15 @@ describe('api', () => { // Verify throwable is still present expect(snapshot.captures.return.throwable).toEqual({ message: 'Test error', - stacktrace: jasmine.any(Array), + stacktrace: expect.any(Array), }) for (const frame of snapshot.captures.return.throwable.stacktrace) { expect(frame).toEqual( - jasmine.objectContaining({ - fileName: jasmine.any(String), - function: jasmine.any(String), - lineNumber: jasmine.any(Number), - columnNumber: jasmine.any(Number), + expect.objectContaining({ + fileName: expect.any(String), + function: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), }) ) } @@ -569,7 +570,7 @@ describe('api', () => { onEntry(probes, globalObject, args) onThrow(probes, new Error('Test error'), globalObject, args) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.arguments).toEqual({ @@ -577,7 +578,7 @@ describe('api', () => { }) expect(snapshot.captures.return.throwable).toEqual({ message: 'Test error', - stacktrace: jasmine.any(Array), + stacktrace: expect.any(Array), }) }) @@ -591,19 +592,19 @@ describe('api', () => { expect(mockBatchAdd).toHaveBeenCalledTimes(1) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.throwable).toEqual({ message: 'Test error', - stacktrace: jasmine.any(Array), + stacktrace: expect.any(Array), }) for (const frame of snapshot.captures.return.throwable.stacktrace) { expect(frame).toEqual( - jasmine.objectContaining({ - fileName: jasmine.any(String), - function: jasmine.any(String), - lineNumber: jasmine.any(Number), - columnNumber: jasmine.any(Number), + expect.objectContaining({ + fileName: expect.any(String), + function: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), }) ) } @@ -616,7 +617,7 @@ describe('api', () => { onEntry(probes, thisArg) onThrow(probes, 'Test error', thisArg) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.throwable).toEqual({ message: 'Test error', @@ -637,11 +638,11 @@ describe('api', () => { onEntry(probes, thisArg) onThrow(probes, error, thisArg) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.throwable).toEqual({ message: 'Iframe error', - stacktrace: jasmine.any(Array), + stacktrace: expect.any(Array), }) }) @@ -652,7 +653,7 @@ describe('api', () => { onEntry(probes, thisArg) expect(() => onThrow(probes, Object.create(null), thisArg)).not.toThrow() - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.throwable).toEqual({ message: '{}', @@ -675,7 +676,7 @@ describe('api', () => { onEntry(probes, {}, {}) expect(() => onThrow(probes, error, {}, {})).not.toThrow() - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.throwable).toEqual({ message: '', @@ -696,7 +697,7 @@ describe('api', () => { onEntry(probes, {}, {}) expect(() => onThrow(probes, error, {}, {})).not.toThrow() - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures.return.throwable).toEqual({ message: '[object Error]', @@ -742,7 +743,7 @@ describe('api', () => { onEntry(probes, thisArg) onThrow(probes, new Error('Test error'), thisArg) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.calls[mockBatchAdd.mock.calls.length - 1][0] const snapshot = payload.debugger.snapshot expect(snapshot.captures).toEqual({ entry: undefined, @@ -752,7 +753,7 @@ describe('api', () => { }, throwable: { message: 'Test error', - stacktrace: jasmine.any(Array), + stacktrace: expect.any(Array), }, }, }) @@ -774,13 +775,13 @@ describe('api', () => { onThrow(probes, new Error('Test error'), thisArg) expect(mockBatchAdd).toHaveBeenCalledTimes(1) - const payload = mockBatchAdd.calls.mostRecent().args[0] + const payload = mockBatchAdd.mock.lastCall![0] const snapshot = payload.debugger.snapshot expect(payload.message).toBeUndefined() expect(snapshot.evaluationErrors).toEqual([ { expr: 'missing.value', - message: jasmine.stringMatching(/^ReferenceError: /), + message: expect.stringMatching(/^ReferenceError: /), }, ]) expect(snapshot.captures).toBeUndefined() @@ -799,6 +800,10 @@ describe('api', () => { }) describe('global snapshot budget', () => { + beforeEach(() => { + mockClock() + }) + it('should respect global snapshot rate limit', () => { const probes: Probe[] = [] for (let i = 0; i < 30; i++) { @@ -947,7 +952,7 @@ describe('api', () => { it('should skip snapshot collection once the lifetime budget is exhausted', () => { initTransport({ maxSnapshotsPerProbeLifetime: 1 }) - const getterSpy = jasmine.createSpy('argGetter').and.returnValue('value') + const getterSpy = vi.fn().mockReturnValue('value') const args = {} Object.defineProperty(args, 'arg', { enumerable: true, @@ -1093,15 +1098,15 @@ describe('api', () => { // Second invocation: probeA's pre-call check now fails and it is queued for // removal. probeB must still be processed in the same iteration even though // probeA gets spliced out of the probes array. - mockBatchAdd.calls.reset() + mockBatchAdd.mockClear() const probesAfterFirst = getProbes(DEFAULT_PROBE_FUNCTION_ID)! onEntry(probesAfterFirst, thisArg) onReturn(probesAfterFirst, null, thisArg) expect(mockBatchAdd).toHaveBeenCalledTimes(1) - expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toEqual([jasmine.objectContaining({ id: 'sibling-probe-b' })]) + expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toEqual([expect.objectContaining({ id: 'sibling-probe-b' })]) // probeB's stack entry must not leak: a third onReturn without onEntry is a no-op. - mockBatchAdd.calls.reset() + mockBatchAdd.mockClear() const remainingProbes = getProbes(DEFAULT_PROBE_FUNCTION_ID)! onReturn(remainingProbes, null, thisArg) expect(mockBatchAdd).not.toHaveBeenCalled() @@ -1161,7 +1166,7 @@ describe('api', () => { onReturn(probes, null, thisArg) expect(mockBatchAdd).toHaveBeenCalledTimes(1) - mockBatchAdd.calls.reset() + mockBatchAdd.mockClear() // A second onReturn without onEntry should not produce a snapshot onReturn(probes, null, thisArg) @@ -1176,7 +1181,7 @@ describe('api', () => { onThrow(probes, new Error('test'), thisArg) expect(mockBatchAdd).toHaveBeenCalledTimes(1) - mockBatchAdd.calls.reset() + mockBatchAdd.mockClear() // A second onThrow without onEntry should not produce a snapshot onThrow(probes, new Error('test'), thisArg) @@ -1190,7 +1195,7 @@ describe('api', () => { let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ // Let the first few calls (start time, deadline creation) use real time, // then jump past the deadline to simulate slow capture. @@ -1224,7 +1229,7 @@ describe('api', () => { // Now make performance.now jump forward so the return capture times out let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 2) { return realNow() @@ -1248,7 +1253,7 @@ describe('api', () => { // Now make performance.now jump forward so the throw capture times out let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 2) { return realNow() @@ -1272,7 +1277,7 @@ describe('api', () => { // Spike performance.now to simulate slow execution let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 2) { return realNow() @@ -1293,7 +1298,7 @@ describe('api', () => { let shouldTimeout = true let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (!shouldTimeout || callCount <= 3) { return realNow() @@ -1333,7 +1338,7 @@ describe('api', () => { let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 3) { return realNow() @@ -1346,7 +1351,7 @@ describe('api', () => { onReturn(probes, null, thisArg, { x: 1 }) // The non-snapshot probe should still send, but both snapshot probes should be dropped - const calls = mockBatchAdd.calls.allArgs() + const calls = mockBatchAdd.mock.calls expect(calls.length).toBe(1) expect(calls[0][0].debugger.snapshot.probe.id).toBe(nonSnapshotProbe.id) }) @@ -1367,7 +1372,7 @@ describe('api', () => { let callCount = 0 const realNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 3) { return realNow() diff --git a/packages/browser-debugger/src/domain/capture.spec.ts b/packages/browser-debugger/src/domain/capture.spec.ts index 8f2ae0e6ae..f3703696e6 100644 --- a/packages/browser-debugger/src/domain/capture.spec.ts +++ b/packages/browser-debugger/src/domain/capture.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it, vi, type TestContext } from 'vitest' import { capture, captureFields } from './capture' import type { CaptureContext } from './capture' @@ -41,19 +42,17 @@ describe('capture', () => { expect(result).toEqual({ type: 'string', value: 'hello' }) }) - it('should capture bigint', () => { + it('should capture bigint', (ctx: TestContext) => { if (typeof BigInt === 'undefined') { - pending('BigInt is not supported in this browser') - return + ctx.skip(true, 'BigInt not supported') } const result = capture(BigInt(123), defaultOpts, noTimeout()) expect(result).toEqual({ type: 'bigint', value: '123' }) }) - it('should capture symbol', () => { + it('should capture symbol', (ctx: TestContext) => { if (!('description' in Symbol.prototype)) { - pending('Symbol.description is not supported in this browser') - return + ctx.skip(true, 'Symbol.description not supported') } const sym = Symbol('test') const result = capture(sym, defaultOpts, noTimeout()) @@ -114,7 +113,7 @@ describe('capture', () => { fields: { message: { type: 'string', value: 'test error' }, name: { type: 'string', value: 'Error' }, - stack: { type: 'string', value: jasmine.any(String), truncated: true, size: error.stack!.length }, + stack: { type: 'string', value: expect.any(String), truncated: true, size: error.stack!.length }, }, }) }) @@ -133,13 +132,12 @@ describe('capture', () => { expect(result.fields.name).toEqual({ type: 'string', value: 'CustomError' }) }) - it('should capture Error with cause', () => { + it('should capture Error with cause', (ctx: TestContext) => { const cause = new Error('cause error') // @ts-expect-error - cause is not a valid argument for Error constructor const error = new Error('main error', { cause }) if ((error as any).cause === undefined) { - pending('Error cause is not supported in this browser') - return + ctx.skip(true, 'Error cause not supported') } const result = capture(error, defaultOpts, noTimeout()) as any @@ -148,7 +146,7 @@ describe('capture', () => { fields: { message: { type: 'string', value: 'cause error' }, name: { type: 'string', value: 'Error' }, - stack: { type: 'string', value: jasmine.any(String), truncated: true, size: cause.stack!.length }, + stack: { type: 'string', value: expect.any(String), truncated: true, size: cause.stack!.length }, }, }) }) @@ -404,10 +402,9 @@ describe('capture', () => { expect(result.size).toBe(30) }) - it('should handle objects with symbol keys', () => { + it('should handle objects with symbol keys', (ctx: TestContext) => { if (!('description' in Symbol.prototype)) { - pending('Symbol.description is not supported in this browser') - return + ctx.skip(true, 'Symbol.description not supported') } const sym = Symbol('test') const obj = { [sym]: 'value' } @@ -628,10 +625,9 @@ describe('captureFields', () => { expect(result['some_property']).toEqual({ type: 'string', value: 'value' }) }) - it('should handle symbol keys', () => { + it('should handle symbol keys', (ctx: TestContext) => { if (!('description' in Symbol.prototype)) { - pending('Symbol.description is not supported in this browser') - return + ctx.skip(true, 'Symbol.description not supported') } const sym = Symbol('test') const obj = { [sym]: 'symbolValue' } @@ -677,7 +673,7 @@ describe('capture timeout', () => { it('should stop traversing object properties when deadline is exceeded', () => { let callCount = 0 const originalNow = performance.now.bind(performance) - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ // First call is the deadline check at the start of captureValue, // subsequent calls are from isTimedOut checks during property iteration. @@ -703,7 +699,7 @@ describe('capture timeout', () => { it('should stop traversing array elements when deadline is exceeded', () => { let callCount = 0 - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 4) { return 100 @@ -721,7 +717,7 @@ describe('capture timeout', () => { it('should stop captureFields traversal when deadline is exceeded', () => { let callCount = 0 - spyOn(performance, 'now').and.callFake(() => { + vi.spyOn(performance, 'now').mockImplementation(() => { callCount++ if (callCount <= 2) { return 100 diff --git a/packages/browser-debugger/src/domain/condition.spec.ts b/packages/browser-debugger/src/domain/condition.spec.ts index 0794afe6be..dac4837d41 100644 --- a/packages/browser-debugger/src/domain/condition.spec.ts +++ b/packages/browser-debugger/src/domain/condition.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { evaluateProbeCondition, compileCondition, isConditionEvaluationError } from './condition' describe('condition', () => { @@ -86,14 +87,19 @@ describe('condition', () => { condition: compileCondition('nonExistent.property'), } - expect(() => evaluateProbeCondition(probe, {})).toThrowMatching((error) => { - expect(isConditionEvaluationError(error)).toBeTrue() - expect(isConditionEvaluationError(error) && error.evaluationError).toEqual({ + let thrownError: unknown + try { + evaluateProbeCondition(probe, {}) + } catch (e) { + thrownError = e + } + expect(isConditionEvaluationError(thrownError)).toBe(true) + if (isConditionEvaluationError(thrownError)) { + expect(thrownError.evaluationError).toEqual({ expr: 'nonExistent.property', - message: jasmine.stringMatching(/^ReferenceError: /), + message: expect.stringMatching(/^ReferenceError: /), }) - return true - }) + } }) it('should handle syntax errors in condition', () => { @@ -104,14 +110,19 @@ describe('condition', () => { condition: compileCondition('invalid syntax !!!'), } - expect(() => evaluateProbeCondition(probe, {})).toThrowMatching((error) => { - expect(isConditionEvaluationError(error)).toBeTrue() - expect(isConditionEvaluationError(error) && error.evaluationError).toEqual({ + let thrownError: unknown + try { + evaluateProbeCondition(probe, {}) + } catch (e) { + thrownError = e + } + expect(isConditionEvaluationError(thrownError)).toBe(true) + if (isConditionEvaluationError(thrownError)) { + expect(thrownError.evaluationError).toEqual({ expr: 'invalid syntax !!!', - message: jasmine.stringMatching(/^SyntaxError: /), + message: expect.stringMatching(/^SyntaxError: /), }) - return true - }) + } }) it('should handle cross-realm Error condition failures', () => { @@ -131,14 +142,16 @@ describe('condition', () => { }, } - expect(() => evaluateProbeCondition(probe, {})).toThrowMatching((error) => { - expect(isConditionEvaluationError(error)).toBeTrue() + try { + evaluateProbeCondition(probe, {}) + expect.unreachable() + } catch (error) { + expect(isConditionEvaluationError(error)).toBe(true) expect(isConditionEvaluationError(error) && error.evaluationError).toEqual({ expr: 'throw iframe error', message: 'Error: Iframe error', }) - return true - }) + } }) it('should handle condition errors that cannot be coerced to strings', () => { @@ -158,14 +171,16 @@ describe('condition', () => { }, } - expect(() => evaluateProbeCondition(probe, {})).toThrowMatching((error) => { - expect(isConditionEvaluationError(error)).toBeTrue() + try { + evaluateProbeCondition(probe, {}) + expect.unreachable() + } catch (error) { + expect(isConditionEvaluationError(error)).toBe(true) expect(isConditionEvaluationError(error) && error.evaluationError).toEqual({ expr: 'throw non-coercible value', message: '', }) - return true - }) + } }) it('should handle Error-like condition errors with hostile name and message getters', () => { @@ -189,14 +204,16 @@ describe('condition', () => { }, } - expect(() => evaluateProbeCondition(probe, {})).toThrowMatching((error) => { - expect(isConditionEvaluationError(error)).toBeTrue() + try { + evaluateProbeCondition(probe, {}) + expect.unreachable() + } catch (error) { + expect(isConditionEvaluationError(error)).toBe(true) expect(isConditionEvaluationError(error) && error.evaluationError).toEqual({ expr: 'throw hostile error-like value', message: '[object Error]', }) - return true - }) + } }) it('should handle conditions with special variables', () => { diff --git a/packages/browser-debugger/src/domain/deliveryApi.spec.ts b/packages/browser-debugger/src/domain/deliveryApi.spec.ts index e6b864a35c..9d6f40eb75 100644 --- a/packages/browser-debugger/src/domain/deliveryApi.spec.ts +++ b/packages/browser-debugger/src/domain/deliveryApi.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest' import type { GlobalObject } from '@datadog/browser-core' import { globalObject } from '@datadog/browser-core' import { registerCleanupTask, mockClock, replaceMockable } from '@datadog/browser-core/test' @@ -74,9 +75,9 @@ describe('buildDeliveryApiUrl', () => { }) describe('deliveryApi', () => { - let fetchSpy: jasmine.Spy - let errorSpy: jasmine.Spy - let warnSpy: jasmine.Spy + let fetchSpy: Mock + let errorSpy: Mock + let warnSpy: Mock let clock: Clock function makeConfig(overrides: Partial = {}): DeliveryApiConfiguration { @@ -91,7 +92,7 @@ describe('deliveryApi', () => { } function respondWith(data: object, status = 200) { - fetchSpy.and.returnValue( + fetchSpy.mockReturnValue( Promise.resolve({ ok: status >= 200 && status < 300, status, @@ -105,9 +106,9 @@ describe('deliveryApi', () => { clock = mockClock() clearProbes() clearDeliveryApiState() - fetchSpy = spyOn(window, 'fetch') - errorSpy = spyOn(display, 'error') - warnSpy = spyOn(display, 'warn') + fetchSpy = vi.spyOn(window, 'fetch').mockImplementation(() => undefined as never) + errorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) + warnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) respondWith({ nextCursor: '', updates: [], deletions: [] }) registerCleanupTask(() => { @@ -128,7 +129,7 @@ describe('deliveryApi', () => { startDeliveryApiPolling(makeConfig()) expect(fetchSpy).toHaveBeenCalledTimes(1) - const [url, options] = fetchSpy.calls.mostRecent().args + const [url, options] = fetchSpy.mock.lastCall! expect(url).toBe('https://api.datadoghq.com/api/unstable/debugger/frontend/probes') expect(options.method).toBe('POST') expect(options.credentials).toBeUndefined() @@ -140,19 +141,19 @@ describe('deliveryApi', () => { it('should use the configured site for the request URL', () => { startDeliveryApiPolling(makeConfig({ site: 'datadoghq.eu' })) - const [url] = fetchSpy.calls.mostRecent().args + const [url] = fetchSpy.mock.lastCall! expect(url).toBe('https://api.datadoghq.eu/api/unstable/debugger/frontend/probes') }) it('should send the correct request body', () => { startDeliveryApiPolling(makeConfig()) - const [, options] = fetchSpy.calls.mostRecent().args + const [, options] = fetchSpy.mock.lastCall! const body = JSON.parse(options.body) expect(body).toEqual({ service: 'test-service', clientName: 'browser', - clientVersion: jasmine.stringMatching(/.+/), + clientVersion: expect.stringMatching(/.+/), env: 'staging', serviceVersion: '1.0.0', }) @@ -161,7 +162,7 @@ describe('deliveryApi', () => { it('should not include nextCursor in the first request', () => { startDeliveryApiPolling(makeConfig()) - const [, options] = fetchSpy.calls.mostRecent().args + const [, options] = fetchSpy.mock.lastCall! const body = JSON.parse(options.body) expect(body.nextCursor).toBeUndefined() }) @@ -170,7 +171,7 @@ describe('deliveryApi', () => { startDeliveryApiPolling(makeConfig()) startDeliveryApiPolling(makeConfig()) - expect(warnSpy).toHaveBeenCalledWith(jasmine.stringMatching(/already started/)) + expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/already started/)) }) it('should add probes from the updates array', async () => { @@ -253,7 +254,7 @@ describe('deliveryApi', () => { await flushPromises() expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toBeUndefined() - expect(errorSpy).toHaveBeenCalledWith('Failed to add probe invalid-capture-expression:', jasmine.any(Error)) + expect(errorSpy).toHaveBeenCalledWith('Failed to add probe invalid-capture-expression:', expect.any(Error)) }) it('should keep adding the other probes when one probe in the same response fails to initialize', async () => { @@ -284,7 +285,8 @@ describe('deliveryApi', () => { expect(getProbes('test.js;before')).toBeDefined() expect(getProbes('test.js;after')).toBeDefined() expect(getProbes('test.js;invalid')).toBeUndefined() - expect(errorSpy).toHaveBeenCalledOnceWith('Failed to add probe invalid:', jasmine.any(Error)) + expect(errorSpy).toHaveBeenCalledTimes(1) + expect(errorSpy).toHaveBeenCalledExactlyOnceWith('Failed to add probe invalid:', expect.any(Error)) }) it('should not track a probe that failed to initialize, so a later deletion is a no-op', async () => { @@ -307,9 +309,10 @@ describe('deliveryApi', () => { startDeliveryApiPolling(makeConfig()) await flushPromises() - expect(errorSpy).toHaveBeenCalledOnceWith('Failed to add probe invalid:', jasmine.any(Error)) + expect(errorSpy).toHaveBeenCalledTimes(1) + expect(errorSpy).toHaveBeenCalledExactlyOnceWith('Failed to add probe invalid:', expect.any(Error)) - errorSpy.calls.reset() + errorSpy.mockClear() // The failed probe was never tracked, so deleting it does nothing and must // not attempt a removal (which would log a "Failed to remove probe" error). @@ -344,16 +347,17 @@ describe('deliveryApi', () => { startDeliveryApiPolling(makeConfig({ pollInterval: 5000 })) await flushPromises() - expect(errorSpy).toHaveBeenCalledOnceWith('Failed to add probe invalid:', jasmine.any(Error)) + expect(errorSpy).toHaveBeenCalledTimes(1) + expect(errorSpy).toHaveBeenCalledExactlyOnceWith('Failed to add probe invalid:', expect.any(Error)) // A per-probe compilation error is a data error, not a transport failure: // it must not trip the circuit breaker or stop polling. - const callsBefore = fetchSpy.calls.count() + const callsBefore = fetchSpy.mock.calls.length clock.tick(5000) await flushPromises() - expect(fetchSpy.calls.count()).toBe(callsBefore + 1) - expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringMatching(/circuit breaker/i)) + expect(fetchSpy.mock.calls.length).toBe(callsBefore + 1) + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringMatching(/circuit breaker/i)) }) it('should ignore log probes without a where clause', async () => { @@ -425,7 +429,7 @@ describe('deliveryApi', () => { clock.tick(5000) expect(fetchSpy).toHaveBeenCalledTimes(2) - const [, options] = fetchSpy.calls.mostRecent().args + const [, options] = fetchSpy.mock.lastCall! const body = JSON.parse(options.body) expect(body.nextCursor).toBe('cursor-abc') }) @@ -461,16 +465,16 @@ describe('deliveryApi', () => { startDeliveryApiPolling(makeConfig()) await flushPromises() - expect(errorSpy).toHaveBeenCalledWith(jasmine.stringMatching(/failed with status 500/), jasmine.any(String)) + expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/failed with status 500/), expect.any(String)) }) it('should log an error when fetch throws', async () => { - fetchSpy.and.returnValue(Promise.reject(new Error('network error'))) + fetchSpy.mockReturnValue(Promise.reject(new Error('network error'))) startDeliveryApiPolling(makeConfig()) await flushPromises() - expect(errorSpy).toHaveBeenCalledWith(jasmine.stringMatching(/poll error/), jasmine.any(Error)) + expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/poll error/), expect.any(Error)) }) it('should poll at the configured interval', () => { @@ -518,7 +522,7 @@ describe('deliveryApi', () => { const POLL_INTERVAL_MS = 5000 function respondWithNetworkError() { - fetchSpy.and.returnValue(Promise.reject(new Error('network error'))) + fetchSpy.mockReturnValue(Promise.reject(new Error('network error'))) } async function tickAndFlush(ms: number) { @@ -537,9 +541,9 @@ describe('deliveryApi', () => { await tickAndFlush(POLL_INTERVAL_MS) } - const callsBefore = fetchSpy.calls.count() + const callsBefore = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS) - expect(fetchSpy.calls.count()).toBe(callsBefore + 1) + expect(fetchSpy.mock.calls.length).toBe(callsBefore + 1) }) it('should stop polling after five minutes of continuous network failures', async () => { @@ -553,9 +557,9 @@ describe('deliveryApi', () => { await tickAndFlush(POLL_INTERVAL_MS) } - const callsAtTrip = fetchSpy.calls.count() + const callsAtTrip = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS * 10) - expect(fetchSpy.calls.count()).toBe(callsAtTrip) + expect(fetchSpy.mock.calls.length).toBe(callsAtTrip) }) it('should stop polling after five minutes of continuous 5xx responses', async () => { @@ -568,9 +572,9 @@ describe('deliveryApi', () => { await tickAndFlush(POLL_INTERVAL_MS) } - const callsAtTrip = fetchSpy.calls.count() + const callsAtTrip = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS * 10) - expect(fetchSpy.calls.count()).toBe(callsAtTrip) + expect(fetchSpy.mock.calls.length).toBe(callsAtTrip) }) it('should treat 4xx responses as a config issue and trip immediately', async () => { @@ -601,9 +605,9 @@ describe('deliveryApi', () => { for (let i = 0; i < ticks; i++) { await tickAndFlush(POLL_INTERVAL_MS) } - const callsBefore = fetchSpy.calls.count() + const callsBefore = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS) - expect(fetchSpy.calls.count()).toBe(callsBefore + 1) + expect(fetchSpy.mock.calls.length).toBe(callsBefore + 1) }) it(`should eventually trip on continuous ${status} responses past the window`, async () => { @@ -616,9 +620,9 @@ describe('deliveryApi', () => { await tickAndFlush(POLL_INTERVAL_MS) } - const callsAtTrip = fetchSpy.calls.count() + const callsAtTrip = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS * 10) - expect(fetchSpy.calls.count()).toBe(callsAtTrip) + expect(fetchSpy.mock.calls.length).toBe(callsAtTrip) }) } @@ -638,9 +642,9 @@ describe('deliveryApi', () => { respondWithNetworkError() await tickAndFlush(FIVE_MINUTES_MS - POLL_INTERVAL_MS) - const callsBefore = fetchSpy.calls.count() + const callsBefore = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS) - expect(fetchSpy.calls.count()).toBe(callsBefore + 1) + expect(fetchSpy.mock.calls.length).toBe(callsBefore + 1) }) it('should clear active probes when tripping', async () => { @@ -673,15 +677,15 @@ describe('deliveryApi', () => { for (let i = 0; i < ticks; i++) { await tickAndFlush(POLL_INTERVAL_MS) } - const callsBeforeTrip = fetchSpy.calls.count() + const callsBeforeTrip = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS) - expect(fetchSpy.calls.count()).toBe(callsBeforeTrip + 1) + expect(fetchSpy.mock.calls.length).toBe(callsBeforeTrip + 1) // One more tick past the custom window should trip and stop polling. await tickAndFlush(POLL_INTERVAL_MS) - const callsAtTrip = fetchSpy.calls.count() + const callsAtTrip = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS * 10) - expect(fetchSpy.calls.count()).toBe(callsAtTrip) + expect(fetchSpy.mock.calls.length).toBe(callsAtTrip) }) it('should fall back to the default maxUnreachableDuration when option is invalid', async () => { @@ -695,9 +699,9 @@ describe('deliveryApi', () => { // Way past any "reasonable" misinterpretation - if -1 were honored, // polling would have already stopped. Confirm we're still polling at 30s. await tickAndFlush(30_000) - const callsBefore = fetchSpy.calls.count() + const callsBefore = fetchSpy.mock.calls.length await tickAndFlush(POLL_INTERVAL_MS) - expect(fetchSpy.calls.count()).toBe(callsBefore + 1) + expect(fetchSpy.mock.calls.length).toBe(callsBefore + 1) }) it('should not re-install probes from an in-flight poll that is aborted by tripping', async () => { @@ -706,7 +710,7 @@ describe('deliveryApi', () => { // would: rejecting with AbortError when the controller calls abort(). let resolveFirstPoll!: (response: unknown) => void const firstPollFetchOptions: { signal?: AbortSignal } = {} - fetchSpy.and.callFake((_url: string, options: { signal?: AbortSignal }) => { + fetchSpy.mockImplementation((_url: string, options: { signal?: AbortSignal }) => { firstPollFetchOptions.signal = options.signal return new Promise((resolve, reject) => { resolveFirstPoll = resolve @@ -732,8 +736,8 @@ describe('deliveryApi', () => { // Breaker has tripped: polling stopped, probes cleared, and the in-flight // poll's signal must have been aborted. expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toBeUndefined() - expect(firstPollFetchOptions.signal!.aborted).toBeTrue() - const callsAtTrip = fetchSpy.calls.count() + expect(firstPollFetchOptions.signal!.aborted).toBe(true) + const callsAtTrip = fetchSpy.mock.calls.length // Even if the hung response somehow still resolves with probe updates // after the abort, the poll has already rejected with AbortError and @@ -749,7 +753,7 @@ describe('deliveryApi', () => { expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toBeUndefined() // No new fetches should have been issued either. await tickAndFlush(POLL_INTERVAL_MS * 5) - expect(fetchSpy.calls.count()).toBe(callsAtTrip) + expect(fetchSpy.mock.calls.length).toBe(callsAtTrip) }) it('should warn when tripping', async () => { @@ -762,7 +766,7 @@ describe('deliveryApi', () => { await tickAndFlush(POLL_INTERVAL_MS) } - expect(warnSpy).toHaveBeenCalledWith(jasmine.stringMatching(/circuit breaker/i)) + expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/circuit breaker/i)) }) }) }) diff --git a/packages/browser-debugger/src/domain/display.spec.ts b/packages/browser-debugger/src/domain/display.spec.ts index 65feb27d60..5265ee3849 100644 --- a/packages/browser-debugger/src/domain/display.spec.ts +++ b/packages/browser-debugger/src/domain/display.spec.ts @@ -1,9 +1,10 @@ +import { describe, it, expect, vi } from 'vitest' import { createDisplay, originalConsoleMethods } from '@datadog/js-core/util' import { DEBUGGER_DISPLAY_PREFIX } from './display' describe('debugger display', () => { it('should use the debugger SDK prefix', () => { - const warnSpy = spyOn(originalConsoleMethods, 'warn') + const warnSpy = vi.spyOn(originalConsoleMethods, 'warn').mockImplementation(() => undefined) createDisplay(DEBUGGER_DISPLAY_PREFIX).warn('message') diff --git a/packages/browser-debugger/src/domain/error.spec.ts b/packages/browser-debugger/src/domain/error.spec.ts index e5c664ee58..9ed559911b 100644 --- a/packages/browser-debugger/src/domain/error.spec.ts +++ b/packages/browser-debugger/src/domain/error.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { registerCleanupTask } from '@datadog/browser-core/test' import { formatThrowable, formatUnknownError, safeReadErrorProperty } from './error' @@ -7,7 +8,7 @@ describe('error', () => { const result = formatThrowable(new Error('boom')) expect(result.message).toBe('boom') - expect(result.stacktrace).toEqual(jasmine.any(Array)) + expect(result.stacktrace).toEqual(expect.any(Array)) }) it('should format cross-realm Error values', () => { @@ -19,7 +20,7 @@ describe('error', () => { const result = formatThrowable(new iframeWindow.Error('iframe error')) expect(result.message).toBe('iframe error') - expect(result.stacktrace).toEqual(jasmine.any(Array)) + expect(result.stacktrace).toEqual(expect.any(Array)) }) it('should pass non-Error string values through unchanged', () => { diff --git a/packages/browser-debugger/src/domain/expression.spec.ts b/packages/browser-debugger/src/domain/expression.spec.ts index 8a5740738b..fcec596da3 100644 --- a/packages/browser-debugger/src/domain/expression.spec.ts +++ b/packages/browser-debugger/src/domain/expression.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it, assert, type TestContext } from 'vitest' import * as acorn from 'acorn' import { literals, @@ -55,14 +56,17 @@ describe('Expression language', () => { const baseName = generateTestCaseName(ast, vars, expected, suffix, execute) const uniqueName = makeUniqueName(baseName, testNameCounts) - it(uniqueName, () => { + it(uniqueName, (ctx: TestContext) => { if (before) { - before() + const skipReason = before() as string | undefined + if (skipReason) { + ctx.skip(true, skipReason) + } } if (execute === false) { if (expected instanceof Error) { - expect(() => compile(ast)).toThrowError(expected.constructor as new (...args: any[]) => Error) + expect(() => compile(ast)).toThrowError(expected.constructor) } else { expect(compile(ast)).toBe(expected) } @@ -86,7 +90,7 @@ describe('Expression language', () => { if (expected instanceof Error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - expect(() => fn(...args)).toThrowError(expected.constructor as new (...args: any[]) => Error) + expect(() => fn(...args)).toThrowError(expected.constructor) } else { const result = runWithDebug(fn, args) if (expected !== null && typeof expected === 'object') { @@ -163,7 +167,7 @@ describe('browser compatibility of generated code', () => { try { acorn.parse(functionCode, { ecmaVersion: OLDEST_BROWSER_ECMA_VERSION, sourceType: 'script' }) } catch (e: unknown) { - fail( + assert.fail( `Generated code is not ES${OLDEST_BROWSER_ECMA_VERSION}-compatible: ${(e as Error).message}\n\nGenerated code:\n${code}` ) } diff --git a/packages/browser-debugger/src/domain/probes.spec.ts b/packages/browser-debugger/src/domain/probes.spec.ts index 9317a1c50f..7e14a6f49e 100644 --- a/packages/browser-debugger/src/domain/probes.spec.ts +++ b/packages/browser-debugger/src/domain/probes.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { ErrorWithCause } from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import { @@ -42,7 +43,7 @@ describe('probes', () => { const retrieved = getProbes(DEFAULT_PROBE_FUNCTION_ID) expect(retrieved).toEqual([ - jasmine.objectContaining({ + expect.objectContaining({ id: probe.id, }), ]) @@ -103,7 +104,7 @@ describe('probes', () => { addProbe(createProbe({ version: 1 })) expect(() => removeProbe(staleInitializedProbe)).not.toThrow() - expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toEqual([jasmine.objectContaining({ version: 1 })]) + expect(getProbes(DEFAULT_PROBE_FUNCTION_ID)).toEqual([expect.objectContaining({ version: 1 })]) }) it('should not throw when passed a stale probe instance that is no longer registered', () => { @@ -125,9 +126,9 @@ describe('probes', () => { initializeProbe(probe) expect(probe).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ template: 'Test message', - msBetweenSampling: jasmine.any(Number), + msBetweenSampling: expect.any(Number), lastCaptureMs: -Infinity, }) ) @@ -143,9 +144,9 @@ describe('probes', () => { initializeProbe(probe) expect(probe).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ template: 'Value: {x}', - evaluateTemplate: jasmine.any(Function), + evaluateTemplate: expect.any(Function), }) ) expect(probe.segments).toBeUndefined() // Should be deleted after initialization @@ -162,8 +163,8 @@ describe('probes', () => { initializeProbe(probe) expect(probe.condition).toEqual( - jasmine.objectContaining({ - evaluate: jasmine.any(Function), + expect.objectContaining({ + evaluate: expect.any(Function), }) ) }) @@ -183,9 +184,9 @@ describe('probes', () => { error = err } - expect(error).toEqual(jasmine.any(Error)) + expect(error).toEqual(expect.any(Error)) expect((error as Error).message).toContain('Cannot compile condition') - expect((error as ErrorWithCause).cause).toEqual(jasmine.any(TypeError)) + expect((error as ErrorWithCause).cause).toEqual(expect.any(TypeError)) }) it('should calculate msBetweenSampling for snapshot probes', () => { @@ -250,14 +251,14 @@ describe('probes', () => { const compiledCaptureExpressions = probe.compiledCaptureExpressions as CaptureExpressionsWithCache expect(compiledCaptureExpressions.expressions.length).toBe(2) expect(compiledCaptureExpressions.expressions[0]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'arg', expression: 'arg', capture: { maxReferenceDepth: 3, maxCollectionSize: 4, maxFieldCount: 5, maxLength: 6 }, }) ) expect(compiledCaptureExpressions.expressions[1]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'obj.value', capture: { maxReferenceDepth: 1, maxCollectionSize: 4, maxFieldCount: 5, maxLength: 2 }, }) @@ -294,9 +295,9 @@ describe('probes', () => { error = err } - expect(error).toEqual(jasmine.any(Error)) + expect(error).toEqual(expect.any(Error)) expect((error as Error).message).toContain('Cannot compile capture expression: invalid expr') - expect((error as ErrorWithCause).cause).toEqual(jasmine.any(SyntaxError)) + expect((error as ErrorWithCause).cause).toEqual(expect.any(SyntaxError)) }) it('should not compile capture expressions when snapshot capture is enabled', () => { diff --git a/packages/browser-debugger/src/domain/stacktrace.spec.ts b/packages/browser-debugger/src/domain/stacktrace.spec.ts index 46fcdb0415..d79457f6c0 100644 --- a/packages/browser-debugger/src/domain/stacktrace.spec.ts +++ b/packages/browser-debugger/src/domain/stacktrace.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { captureStackTrace, parseStackTrace } from './stacktrace' describe('stacktrace', () => { @@ -170,11 +171,11 @@ anotherFunction@http://example.com/app.js:100:5`, expect(result.length).toBeGreaterThan(0) expect(result[0]).toEqual( - jasmine.objectContaining({ - fileName: jasmine.any(String), - function: jasmine.any(String), - lineNumber: jasmine.any(Number), - columnNumber: jasmine.any(Number), + expect.objectContaining({ + fileName: expect.any(String), + function: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), }) ) }) diff --git a/packages/browser-debugger/src/domain/template.spec.ts b/packages/browser-debugger/src/domain/template.spec.ts index 47f0300378..53b76eb0d4 100644 --- a/packages/browser-debugger/src/domain/template.spec.ts +++ b/packages/browser-debugger/src/domain/template.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it, type TestContext } from 'vitest' import { templateRequiresEvaluation, compileSegments, evaluateProbeMessage, browserInspect } from './template' import { formatUnknownError } from './error' @@ -87,10 +88,9 @@ describe('template', () => { expect(browserInspect(false)).toBe('false') }) - it('should inspect bigint', () => { + it('should inspect bigint', (ctx: TestContext) => { if (typeof BigInt === 'undefined') { - pending('BigInt is not supported in this browser') - return + ctx.skip(true, 'BigInt not supported') } expect(browserInspect(BigInt(123))).toBe('123n') }) @@ -131,7 +131,7 @@ describe('template', () => { it('should not call custom toString when object serialization fails', () => { const obj: any = { - toString: jasmine.createSpy('toString', () => 'custom').and.callThrough(), + toString: vi.fn(() => 'custom'), } obj.self = obj diff --git a/packages/browser-debugger/src/entries/main.spec.ts b/packages/browser-debugger/src/entries/main.spec.ts index d282f790e2..f796cedbfb 100644 --- a/packages/browser-debugger/src/entries/main.spec.ts +++ b/packages/browser-debugger/src/entries/main.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it, beforeEach } from 'vitest' import { Observable } from '@datadog/browser-core' import { registerCleanupTask, replaceMockableWithSpy } from '@datadog/browser-core/test' import { initDebuggerTransport } from '../domain/api' @@ -28,15 +29,15 @@ describe('datadogDebugger', () => { it('should only expose init, version, and onReady', () => { expect(datadogDebugger).toEqual({ - init: jasmine.any(Function), - version: jasmine.any(String), - onReady: jasmine.any(Function), + init: expect.any(Function), + version: expect.any(String), + onReady: expect.any(Function), }) }) it('should default the init version from build-plugin metadata', async () => { browserWindow.__DD_LIVE_DEBUGGER_BUILD__ = { version: 'build-version' } - replaceMockableWithSpy(startDebuggerBatch).and.callFake(() => ({ + replaceMockableWithSpy(startDebuggerBatch).mockImplementation(() => ({ flushObservable: new Observable(), prepareUrgentFlushObservable: new Observable(), isEmpty: false, @@ -58,10 +59,10 @@ describe('datadogDebugger', () => { await flushPromises() expect(initTransportSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ version: 'build-version' }), - jasmine.anything() + expect.objectContaining({ version: 'build-version' }), + expect.anything() ) - expect(startDeliveryApiPollingSpy).toHaveBeenCalledWith(jasmine.objectContaining({ version: 'build-version' })) + expect(startDeliveryApiPollingSpy).toHaveBeenCalledWith(expect.objectContaining({ version: 'build-version' })) expect(browserWindow.$dd_entry).toBeDefined() expect(browserWindow.$dd_return).toBeDefined() expect(browserWindow.$dd_throw).toBeDefined() @@ -70,7 +71,7 @@ describe('datadogDebugger', () => { it('should warn when the explicit init version mismatches build-plugin metadata', async () => { browserWindow.__DD_LIVE_DEBUGGER_BUILD__ = { version: 'build-version' } - replaceMockableWithSpy(startDebuggerBatch).and.callFake(() => ({ + replaceMockableWithSpy(startDebuggerBatch).mockImplementation(() => ({ flushObservable: new Observable(), prepareUrgentFlushObservable: new Observable(), isEmpty: false, @@ -82,7 +83,7 @@ describe('datadogDebugger', () => { })) replaceMockableWithSpy(initDebuggerTransport) const startDeliveryApiPollingSpy = replaceMockableWithSpy(startDeliveryApiPolling) - const warnSpy = spyOn(display, 'warn') + const warnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) datadogDebugger.init({ clientToken: 'client-token', @@ -93,8 +94,8 @@ describe('datadogDebugger', () => { await flushPromises() - expect(warnSpy).toHaveBeenCalledWith(jasmine.stringMatching(/does not match the build-plugin version/)) - expect(startDeliveryApiPollingSpy).toHaveBeenCalledWith(jasmine.objectContaining({ version: 'runtime-version' })) + expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/does not match the build-plugin version/)) + expect(startDeliveryApiPollingSpy).toHaveBeenCalledWith(expect.objectContaining({ version: 'runtime-version' })) }) }) diff --git a/packages/browser-debugger/test/expressionTestCases.ts b/packages/browser-debugger/test/expressionTestCases.ts index 0e4eab9297..3220c8b365 100644 --- a/packages/browser-debugger/test/expressionTestCases.ts +++ b/packages/browser-debugger/test/expressionTestCases.ts @@ -14,7 +14,7 @@ export interface TestCaseObject { vars?: VariableBindings expected?: unknown execute?: boolean - before?: () => void + before?: () => string | void suffix?: string } export type TestCase = TestCaseTuple | TestCaseObject @@ -763,7 +763,7 @@ export const typeAndDefinitionChecks: TestCase[] = [ expected: typeof BigInt !== 'undefined' ? true : undefined, before: () => { if (typeof BigInt === 'undefined') { - pending('BigInt is not supported in this browser') + return 'BigInt is not supported in this browser' } }, }, diff --git a/packages/browser-logs/src/boot/logsPublicApi.spec.ts b/packages/browser-logs/src/boot/logsPublicApi.spec.ts index 5b09b32c17..2d3ddc9031 100644 --- a/packages/browser-logs/src/boot/logsPublicApi.spec.ts +++ b/packages/browser-logs/src/boot/logsPublicApi.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { ContextManager } from '@datadog/browser-core' import { monitor, @@ -28,7 +29,7 @@ const getInternalContext = () => ({ session_id: mockSessionId }) describe('logs entry', () => { it('should add a `_setDebug` that works', () => { - const displaySpy = spyOn(display, 'error') + const displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const { logsPublicApi } = makeLogsPublicApiWithDefaults() const setDebug: (debug: boolean) => void = (logsPublicApi as any)._setDebug expect(!!setDebug).toEqual(true) @@ -60,7 +61,7 @@ describe('logs entry', () => { describe('common context', () => { let logsPublicApi: LogsPublicApi - let startLogsSpy: jasmine.Spy + let startLogsSpy: Mock beforeEach(async () => { ;({ logsPublicApi, startLogsSpy } = makeLogsPublicApiWithDefaults()) @@ -71,7 +72,7 @@ describe('logs entry', () => { it('should have the current date, view and global context', () => { logsPublicApi.setGlobalContextProperty('foo', 'bar') - const getCommonContext = startLogsSpy.calls.mostRecent().args[2] + const getCommonContext = startLogsSpy.mock.lastCall![2] expect(getCommonContext()).toEqual({ view: { referrer: document.referrer, @@ -98,7 +99,7 @@ describe('logs entry', () => { describe('post start API usages', () => { let logsPublicApi: LogsPublicApi let getLoggedMessage: ReturnType['getLoggedMessage'] - let startLogsSpy: jasmine.Spy + let startLogsSpy: Mock let userContext: ContextManager let accountContext: ContextManager @@ -196,25 +197,25 @@ describe('logs entry', () => { describe('user', () => { it('should call setContext', () => { - spyOn(userContext, 'setContext') + vi.spyOn(userContext, 'setContext').mockImplementation(() => undefined) logsPublicApi.setUser(2 as any) expect(userContext.setContext).toHaveBeenCalledTimes(1) }) it('should call setContextProperty', () => { - spyOn(userContext, 'setContextProperty') + vi.spyOn(userContext, 'setContextProperty').mockImplementation(() => undefined) logsPublicApi.setUserProperty('foo', 'bar') expect(userContext.setContextProperty).toHaveBeenCalledTimes(1) }) it('should call removeContextProperty', () => { - spyOn(userContext, 'removeContextProperty') + vi.spyOn(userContext, 'removeContextProperty').mockImplementation(() => undefined) logsPublicApi.removeUserProperty('foo') expect(userContext.removeContextProperty).toHaveBeenCalledTimes(1) }) it('should call clearContext', () => { - spyOn(userContext, 'clearContext') + vi.spyOn(userContext, 'clearContext').mockImplementation(() => undefined) logsPublicApi.clearUser() expect(userContext.clearContext).toHaveBeenCalledTimes(1) }) @@ -222,25 +223,25 @@ describe('logs entry', () => { describe('account', () => { it('should call setContext', () => { - spyOn(accountContext, 'setContext') + vi.spyOn(accountContext, 'setContext').mockImplementation(() => undefined) logsPublicApi.setAccount(2 as any) expect(accountContext.setContext).toHaveBeenCalledTimes(1) }) it('should call setContextProperty', () => { - spyOn(accountContext, 'setContextProperty') + vi.spyOn(accountContext, 'setContextProperty').mockImplementation(() => undefined) logsPublicApi.setAccountProperty('foo', 'bar') expect(accountContext.setContextProperty).toHaveBeenCalledTimes(1) }) it('should call removeContextProperty', () => { - spyOn(accountContext, 'removeContextProperty') + vi.spyOn(accountContext, 'removeContextProperty').mockImplementation(() => undefined) logsPublicApi.removeAccountProperty('foo') expect(accountContext.removeContextProperty).toHaveBeenCalledTimes(1) }) it('should call clearContext', () => { - spyOn(accountContext, 'clearContext') + vi.spyOn(accountContext, 'clearContext').mockImplementation(() => undefined) logsPublicApi.clearAccount() expect(accountContext.clearContext).toHaveBeenCalledTimes(1) }) @@ -253,8 +254,8 @@ function makeLogsPublicApiWithDefaults({ }: { startLogsResult?: Partial } = {}) { - const handleLogSpy = jasmine.createSpy() - const startLogsSpy = replaceMockableWithSpy(startLogs).and.callFake(() => ({ + const handleLogSpy = vi.fn() + const startLogsSpy = replaceMockableWithSpy(startLogs).mockImplementation(() => ({ handleLog: handleLogSpy, getInternalContext, accountContext: {} as any, @@ -265,7 +266,7 @@ function makeLogsPublicApiWithDefaults({ })) function getLoggedMessage(index: number) { - const [message, logger, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) + const [message, logger, savedCommonContext, savedDate] = handleLogSpy.mock.calls[index] return { message, logger, savedCommonContext, savedDate } } diff --git a/packages/browser-logs/src/boot/preStartLogs.spec.ts b/packages/browser-logs/src/boot/preStartLogs.spec.ts index 4d3901f6ac..db17c14448 100644 --- a/packages/browser-logs/src/boot/preStartLogs.spec.ts +++ b/packages/browser-logs/src/boot/preStartLogs.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { ONE_SECOND } from '@datadog/js-core/time' import { collectAsyncCalls, @@ -38,13 +39,13 @@ describe('preStartLogs', () => { }) describe('configuration validation', () => { - let displaySpy: jasmine.Spy - let doStartLogsSpy: jasmine.Spy + let displaySpy: Mock + let doStartLogsSpy: Mock let strategy: Strategy beforeEach(() => { ;({ strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults()) - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) }) it('should start when the configuration is valid', async () => { @@ -134,7 +135,7 @@ describe('preStartLogs', () => { strategy.init(DEFAULT_INIT_CONFIGURATION) await collectAsyncCalls(handleLogSpy, 1) - expect(handleLogSpy.calls.all().length).toBe(1) + expect(handleLogSpy.mock.calls.length).toBe(1) expect(getLoggedMessage(0).message.message).toBe('message') }) @@ -163,7 +164,7 @@ describe('preStartLogs', () => { it('saves the URL', async () => { const { strategy, getLoggedMessage, getCommonContextSpy, handleLogSpy } = createPreStartStrategyWithDefaults() - getCommonContextSpy.and.returnValue({ view: { url: 'url' } } as unknown as CommonContext) + getCommonContextSpy.mockReturnValue({ view: { url: 'url' } } as unknown as CommonContext) strategy.handleLog( { status: StatusType.info, @@ -206,7 +207,7 @@ describe('preStartLogs', () => { describe('tracking consent', () => { let strategy: Strategy - let doStartLogsSpy: jasmine.Spy + let doStartLogsSpy: Mock let trackingConsentState: TrackingConsentState beforeEach(() => { @@ -244,7 +245,7 @@ describe('preStartLogs', () => { it('do not call startLogs when tracking consent state is updated after init', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION) await collectAsyncCalls(doStartLogsSpy, 1) - doStartLogsSpy.calls.reset() + doStartLogsSpy.mockClear() trackingConsentState.update(TrackingConsent.GRANTED) await waitNextMicrotask() @@ -289,12 +290,12 @@ function createPreStartStrategyWithDefaults({ trackingConsentState?: TrackingConsentState startSessionManagerMock?: typeof startSessionManager } = {}) { - const handleLogSpy = jasmine.createSpy() - const doStartLogsSpy = jasmine.createSpy().and.returnValue({ + const handleLogSpy = vi.fn() + const doStartLogsSpy = vi.fn().mockReturnValue({ handleLog: handleLogSpy, } as unknown as StartLogsResult) - const getCommonContextSpy = jasmine.createSpy<() => CommonContext>() - const startTelemetrySpy = replaceMockableWithSpy(startTelemetry).and.callFake(createFakeTelemetryObject) + const getCommonContextSpy = vi.fn<() => CommonContext>() + const startTelemetrySpy = replaceMockableWithSpy(startTelemetry).mockImplementation(createFakeTelemetryObject) replaceMockable(startSessionManager, startSessionManagerMock) return { @@ -304,7 +305,7 @@ function createPreStartStrategyWithDefaults({ doStartLogsSpy, getCommonContextSpy, getLoggedMessage: (index: number) => { - const [message, logger, handlingStack, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) + const [message, logger, handlingStack, savedCommonContext, savedDate] = handleLogSpy.mock.calls[index] return { message, logger, handlingStack, savedCommonContext, savedDate } }, } diff --git a/packages/browser-logs/src/boot/startLogs.spec.ts b/packages/browser-logs/src/boot/startLogs.spec.ts index 72a6b154a5..df45d3e839 100644 --- a/packages/browser-logs/src/boot/startLogs.spec.ts +++ b/packages/browser-logs/src/boot/startLogs.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it } from 'vitest' import type { BufferedData } from '@datadog/browser-core' import { ErrorSource, display, BufferedObservable, FLUSH_DURATION_LIMIT } from '@datadog/browser-core' import type { Clock, Request } from '@datadog/browser-core/test' @@ -38,8 +39,11 @@ const COMMON_CONTEXT = { view: { referrer: 'common_referrer', url: 'common_url' }, } -function startLogsWithDefaults({ configuration }: { configuration?: Partial } = {}) { - const sessionManager = createSessionManagerMock() +function startLogsWithDefaults({ + configuration, + sessionManager: customSessionManager, +}: { configuration?: Partial; sessionManager?: ReturnType } = {}) { + const sessionManager = customSessionManager ?? createSessionManagerMock() const { handleLog, stop, globalContext, accountContext, userContext } = startLogs( { ...validateAndBuildLogsConfiguration({ clientToken: 'xxx', service: 'service', telemetrySampleRate: 0 })!, @@ -92,14 +96,14 @@ describe('logs', () => { /^https:\/\/browser-intake-datadoghq\.com\/api\/v2\/logs\?ddsource=browser&dd-api-key=xxx&dd-evp-origin-version=test&dd-evp-origin=browser&dd-request-id=/ ) expect(getLoggedMessage(requests, 0)).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), foo: 'bar', message: 'message', service: 'service', ddtags: 'sdk_version:test,service:service', - session_id: jasmine.any(String), + session_id: expect.any(String), session: { - id: jasmine.any(String), + id: expect.any(String), }, status: StatusType.warn, view: { @@ -108,10 +112,10 @@ describe('logs', () => { }, origin: ErrorSource.LOGGER, usr: { - anonymous_id: jasmine.any(String), + anonymous_id: expect.any(String), }, tab: { - id: jasmine.any(String), + id: expect.any(String), }, }) }) @@ -130,7 +134,7 @@ describe('logs', () => { }) it('should send bridge event when bridge is present', () => { - const sendSpy = spyOn(mockEventBridge(), 'send') + const sendSpy = vi.spyOn(mockEventBridge(), 'send').mockImplementation(() => undefined) const { handleLog, logger } = startLogsWithDefaults() handleLog(DEFAULT_MESSAGE, logger) @@ -138,18 +142,42 @@ describe('logs', () => { clock.tick(FLUSH_DURATION_LIMIT) expect(requests.length).toEqual(0) - const [message] = sendSpy.calls.mostRecent().args + const [message] = sendSpy.mock.lastCall! const parsedMessage = JSON.parse(message) expect(parsedMessage).toEqual({ eventType: 'log', - event: jasmine.objectContaining({ message: 'message' }), + event: expect.objectContaining({ message: 'message' }), }) }) }) + describe('sampling', () => { + it('should be applied when event bridge is present (rate 0)', () => { + const sendSpy = vi.spyOn(mockEventBridge(), 'send').mockImplementation(() => undefined) + + const { handleLog, logger } = startLogsWithDefaults({ + sessionManager: createSessionManagerMock().setNotTracked(), + }) + handleLog(DEFAULT_MESSAGE, logger) + + expect(sendSpy).not.toHaveBeenCalled() + }) + + it('should be applied when event bridge is present (rate 100)', () => { + const sendSpy = vi.spyOn(mockEventBridge(), 'send').mockImplementation(() => undefined) + + const { handleLog, logger } = startLogsWithDefaults({ + configuration: { sessionSampleRate: 100 }, + }) + handleLog(DEFAULT_MESSAGE, logger) + + expect(sendSpy).toHaveBeenCalled() + }) + }) + it('should not print the log twice when console handler is enabled', () => { - const consoleLogSpy = spyOn(console, 'log') - const displayLogSpy = spyOn(display, 'log') + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined) + const displayLogSpy = vi.spyOn(display, 'log').mockImplementation(() => undefined) startLogsWithDefaults({ configuration: { forwardConsoleLogs: ['log'] }, }) @@ -224,7 +252,7 @@ describe('logs', () => { clock.tick(FLUSH_DURATION_LIMIT) const firstRequest = getLoggedMessage(requests, 0) - expect(firstRequest.usr).toEqual(jasmine.objectContaining({ id: 'from-global-context' })) + expect(firstRequest.usr).toEqual(expect.objectContaining({ id: 'from-global-context' })) }) it('RUM context should take precedence over global context', () => { diff --git a/packages/browser-logs/src/domain/assembly.spec.ts b/packages/browser-logs/src/domain/assembly.spec.ts index 5616ea232b..4f7a5d2a42 100644 --- a/packages/browser-logs/src/domain/assembly.spec.ts +++ b/packages/browser-logs/src/domain/assembly.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime, TimeStamp } from '@datadog/js-core/time' import type { Context } from '@datadog/browser-core' import { ONE_MINUTE, toTimeStamp } from '@datadog/js-core/time' @@ -86,7 +87,7 @@ describe('startLogsAssembly', () => { describe('contexts inclusion', () => { it('should include message context', () => { - spyOn(window.DD_RUM!, 'getInternalContext').and.returnValue({ + vi.spyOn(window.DD_RUM!, 'getInternalContext').mockReturnValue({ view: { url: 'http://from-rum-context.com', id: 'view-id' }, }) @@ -102,7 +103,7 @@ describe('startLogsAssembly', () => { lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) expect(serverLogs[0]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ view: COMMON_CONTEXT.view, }) ) @@ -120,7 +121,7 @@ describe('startLogsAssembly', () => { lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE, savedCommonContext }) expect(serverLogs[0]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ view: savedCommonContext.view, }) ) @@ -167,7 +168,7 @@ describe('startLogsAssembly', () => { it('should include raw log', () => { lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) - expect(serverLogs[0]).toEqual(jasmine.objectContaining(DEFAULT_MESSAGE)) + expect(serverLogs[0]).toEqual(expect.objectContaining(DEFAULT_MESSAGE)) }) }) @@ -204,7 +205,7 @@ describe('startLogsAssembly', () => { }) expect(serverLogs[0]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ view: { referrer: 'referrer_from_defaultLogsEventAttributes', url: 'url_from_defaultLogsEventAttributes', @@ -284,7 +285,7 @@ describe('logs limitation', () => { let lifeCycle: LifeCycle let hooks: Hooks let serverLogs: Array = [] - let reportErrorSpy: jasmine.Spy + let reportErrorSpy: Mock<(...args: any[]) => any> beforeEach(() => { lifeCycle = new LifeCycle() @@ -297,7 +298,7 @@ describe('logs limitation', () => { } beforeSend = noop - reportErrorSpy = jasmine.createSpy('reportError') + reportErrorSpy = vi.fn() startLogsAssembly(configuration, lifeCycle, hooks.assemble, () => COMMON_CONTEXT, reportErrorSpy, 1) clock = mockClock() }) diff --git a/packages/browser-logs/src/domain/configuration.spec.ts b/packages/browser-logs/src/domain/configuration.spec.ts index 406d3798cc..12921de7ef 100644 --- a/packages/browser-logs/src/domain/configuration.spec.ts +++ b/packages/browser-logs/src/domain/configuration.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { InitConfiguration } from '@datadog/browser-core' import { display } from '@datadog/browser-core' import { @@ -19,40 +20,40 @@ const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx' } describe('validateAndBuildLogsConfiguration', () => { describe('forwardErrorsToLogs', () => { it('defaults to true if the option is not provided', () => { - expect(validateAndBuildLogsConfiguration(DEFAULT_INIT_CONFIGURATION)!.forwardErrorsToLogs).toBeTrue() + expect(validateAndBuildLogsConfiguration(DEFAULT_INIT_CONFIGURATION)!.forwardErrorsToLogs).toBe(true) }) it('is set to provided value', () => { expect( validateAndBuildLogsConfiguration({ ...DEFAULT_INIT_CONFIGURATION, forwardErrorsToLogs: true })! .forwardErrorsToLogs - ).toBeTrue() + ).toBe(true) expect( validateAndBuildLogsConfiguration({ ...DEFAULT_INIT_CONFIGURATION, forwardErrorsToLogs: false })! .forwardErrorsToLogs - ).toBeFalse() + ).toBe(false) }) it('the provided value is cast to boolean', () => { expect( validateAndBuildLogsConfiguration({ ...DEFAULT_INIT_CONFIGURATION, forwardErrorsToLogs: 'foo' as any })! .forwardErrorsToLogs - ).toBeTrue() + ).toBe(true) }) it('is set to true for falsy values other than `false`', () => { expect( validateAndBuildLogsConfiguration({ ...DEFAULT_INIT_CONFIGURATION, forwardErrorsToLogs: null as any })! .forwardErrorsToLogs - ).toBeTrue() + ).toBe(true) expect( validateAndBuildLogsConfiguration({ ...DEFAULT_INIT_CONFIGURATION, forwardErrorsToLogs: '' as any })! .forwardErrorsToLogs - ).toBeTrue() + ).toBe(true) expect( validateAndBuildLogsConfiguration({ ...DEFAULT_INIT_CONFIGURATION, forwardErrorsToLogs: 0 as any })! .forwardErrorsToLogs - ).toBeTrue() + ).toBe(true) }) }) @@ -82,25 +83,27 @@ describe('validateAndBuildLogsConfiguration', () => { }) describe('validateAndBuildForwardOption', () => { - let displaySpy: jasmine.Spy + let displaySpy: Mock const allowedValues = ['foo', 'bar'] const label = 'Label' const errorMessage = 'Label should be "all" or an array with allowed values "foo", "bar"' beforeEach(() => { - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) }) it('does not validate the configuration if an incorrect string is provided', () => { validateAndBuildForwardOption('foo' as any, allowedValues, label) - expect(displaySpy).toHaveBeenCalledOnceWith(errorMessage) + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith(errorMessage) }) it('does not validate the configuration if an incorrect api is provided', () => { validateAndBuildForwardOption(['dir'], allowedValues, label) - expect(displaySpy).toHaveBeenCalledOnceWith(errorMessage) + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith(errorMessage) }) it('defaults to an empty array', () => { diff --git a/packages/browser-logs/src/domain/console/consoleCollection.spec.ts b/packages/browser-logs/src/domain/console/consoleCollection.spec.ts index 999e577ef9..8429a4d83a 100644 --- a/packages/browser-logs/src/domain/console/consoleCollection.spec.ts +++ b/packages/browser-logs/src/domain/console/consoleCollection.spec.ts @@ -1,7 +1,17 @@ -import type { BufferedData, ConsoleLog, RawError } from '@datadog/browser-core' +import { vi, afterEach, beforeEach, describe, expect, it } from 'vitest' import { clocksNow } from '@datadog/js-core/time' import { ConsoleApiName } from '@datadog/js-core/util' -import { BufferedDataType, ErrorHandling, ErrorSource, Observable, noop, objectEntries } from '@datadog/browser-core' +import type { BufferedData, ConsoleLog, ErrorWithCause, RawError } from '@datadog/browser-core' +import { + BufferedDataType, + ErrorHandling, + ErrorSource, + Observable, + noop, + objectEntries, + startBufferingData, +} from '@datadog/browser-core' +import { registerCleanupTask } from '@datadog/browser-core/test' import type { RawConsoleLogsEvent } from '../../rawLogsEvent.types' import { validateAndBuildLogsConfiguration } from '../configuration' import type { RawLogsEventCollectedData } from '../lifeCycle' @@ -42,6 +52,11 @@ describe('console collection', () => { rawLogsEvents.push(rawLogsEvent as RawLogsEventCollectedData) ) stopConsoleCollection = noop + vi.spyOn(console, 'log').mockImplementation(() => true) + vi.spyOn(console, 'debug').mockImplementation(() => true) + vi.spyOn(console, 'info').mockImplementation(() => true) + vi.spyOn(console, 'warn').mockImplementation(() => true) + vi.spyOn(console, 'error').mockImplementation(() => true) }) afterEach(() => { @@ -63,16 +78,16 @@ describe('console collection', () => { error: undefined, } as ConsoleLog) - expect(rawLogsEvents[0].rawLogsEvent).toEqual({ - date: jasmine.any(Number), + expect(rawLogsEvents[0].rawLogsEvent).toStrictEqual({ + date: expect.any(Number), message: 'foo bar', status, origin: ErrorSource.CONSOLE, - error: whatever(), + error: undefined, }) - expect(rawLogsEvents[0].domainContext).toEqual({ - handlingStack: jasmine.any(String), + expect(rawLogsEvents[0].domainContext).toStrictEqual({ + handlingStack: expect.any(String), }) }) @@ -134,11 +149,53 @@ describe('console collection', () => { expect(rawLogsEvents[0].messageContext).toEqual({ foo: 'bar' }) }) -}) -function whatever() { - return { - asymmetricMatch: () => true, - jasmineToString: () => '', - } -} + it('should retrieve causes from console error', async () => { + const { observable: consoleBufferedObservable, stop: stopBuffering } = startBufferingData() + registerCleanupTask(stopBuffering) + ;({ stop: stopConsoleCollection } = startConsoleCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: ['error'] })!, + lifeCycle, + consoleBufferedObservable + )) + const error = new Error('High level error') as ErrorWithCause + error.stack = 'Error: High level error' + + const nestedError = new Error('Mid level error') as ErrorWithCause + nestedError.stack = 'Error: Mid level error' + + const deepNestedError = new TypeError('Low level error') as ErrorWithCause + deepNestedError.stack = 'TypeError: Low level error' + + nestedError.cause = deepNestedError + error.cause = nestedError + + // eslint-disable-next-line no-console + console.error(error) + + // Wait for BufferedObservable microtask to deliver the event to subscribers + await new Promise((resolve) => queueMicrotask(resolve)) + + expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({ + stack: expect.any(String), + handling: ErrorHandling.HANDLED, + causes: [ + { + source: ErrorSource.CONSOLE, + type: 'Error', + stack: expect.any(String), + message: 'Mid level error', + }, + { + source: ErrorSource.CONSOLE, + type: 'TypeError', + stack: expect.any(String), + message: 'Low level error', + }, + ], + fingerprint: undefined, + kind: 'Error', + message: undefined, + }) + }) +}) diff --git a/packages/browser-logs/src/domain/contexts/internalContext.spec.ts b/packages/browser-logs/src/domain/contexts/internalContext.spec.ts index 5080da7a31..c2a55f0130 100644 --- a/packages/browser-logs/src/domain/contexts/internalContext.spec.ts +++ b/packages/browser-logs/src/domain/contexts/internalContext.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { createSessionManagerMock } from '@datadog/browser-core/test' import { startInternalContext } from './internalContext' @@ -10,7 +11,7 @@ describe('internal context', () => { it('should return internal context corresponding to startTime', () => { const sessionManagerMock = createSessionManagerMock().setTracked() expect(startInternalContext(sessionManagerMock).get()).toEqual({ - session_id: jasmine.any(String), + session_id: expect.any(String), }) }) }) diff --git a/packages/browser-logs/src/domain/contexts/rumInternalContext.spec.ts b/packages/browser-logs/src/domain/contexts/rumInternalContext.spec.ts index a6755528fd..74228d6a01 100644 --- a/packages/browser-logs/src/domain/contexts/rumInternalContext.spec.ts +++ b/packages/browser-logs/src/domain/contexts/rumInternalContext.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { mockSyntheticsWorkerValues } from '@datadog/browser-core/test' import type { Hooks } from '../hooks' diff --git a/packages/browser-logs/src/domain/contexts/sessionContext.spec.ts b/packages/browser-logs/src/domain/contexts/sessionContext.spec.ts index 75b350e619..8c90b2e5bd 100644 --- a/packages/browser-logs/src/domain/contexts/sessionContext.spec.ts +++ b/packages/browser-logs/src/domain/contexts/sessionContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import type { SessionManager } from '@datadog/browser-core' import { createHook, DISCARDED } from '@datadog/js-core/assembly' @@ -24,7 +25,7 @@ describe('session context', () => { startTime: 0 as RelativeTime, }) as DefaultLogsEventAttributes - expect(defaultLogAttributes.service).toEqual(jasmine.any(String)) + expect(defaultLogAttributes.service).toEqual(expect.any(String)) }) it('should discard logs if session is not tracked', () => { @@ -45,9 +46,9 @@ describe('session context', () => { }) expect(defaultLogAttributes).toEqual({ - service: jasmine.any(String), - session_id: jasmine.any(String), - session: { id: jasmine.any(String) }, + service: expect.any(String), + session_id: expect.any(String), + session: { id: expect.any(String) }, }) }) @@ -61,7 +62,7 @@ describe('session context', () => { }) expect(defaultLogAttributes).toEqual({ - service: jasmine.any(String), + service: expect.any(String), session_id: undefined, session: undefined, }) diff --git a/packages/browser-logs/src/domain/contexts/trackingConsentContext.spec.ts b/packages/browser-logs/src/domain/contexts/trackingConsentContext.spec.ts index 74b48e6526..04483075b4 100644 --- a/packages/browser-logs/src/domain/contexts/trackingConsentContext.spec.ts +++ b/packages/browser-logs/src/domain/contexts/trackingConsentContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { createTrackingConsentState, TrackingConsent } from '@datadog/browser-core' import { DISCARDED } from '@datadog/js-core/assembly' diff --git a/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts b/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts index 0937e22a66..6bb6cce13c 100644 --- a/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts +++ b/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { TimeStamp, RelativeTime } from '@datadog/js-core/time' import { ErrorHandling, ErrorSource, type RawError } from '@datadog/browser-core' import { createErrorFieldFromRawError } from './createErrorFieldFromRawError' diff --git a/packages/browser-logs/src/domain/logger.spec.ts b/packages/browser-logs/src/domain/logger.spec.ts index a316f1d0a9..be9ea8d515 100644 --- a/packages/browser-logs/src/domain/logger.spec.ts +++ b/packages/browser-logs/src/domain/logger.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { ErrorWithCause } from '@datadog/browser-core' import { display, ErrorHandling, NO_ERROR_STACK_PRESENT_MESSAGE } from '@datadog/browser-core' import type { LogsMessage } from './logger' @@ -6,22 +7,22 @@ import { StatusType } from './logger/isAuthorized' describe('Logger', () => { let logger: Logger - let handleLogSpy: jasmine.Spy<(message: LogsMessage, logger: Logger, handlingStack?: string) => void> + let handleLogSpy: Mock<(message: LogsMessage, logger: Logger, handlingStack?: string) => void> function getLoggedMessage(index: number) { - return handleLogSpy.calls.argsFor(index)[0] + return handleLogSpy.mock.calls[index][0] } function getMessageLogger(index: number) { - return handleLogSpy.calls.argsFor(index)[1] + return handleLogSpy.mock.calls[index][1] } function getLoggedHandlingStack(index: number) { - return handleLogSpy.calls.argsFor(index)[2] + return handleLogSpy.mock.calls[index][2] } beforeEach(() => { - handleLogSpy = jasmine.createSpy() + handleLogSpy = vi.fn() logger = new Logger(handleLogSpy) }) @@ -49,7 +50,7 @@ describe('Logger', () => { error: { kind: 'SyntaxError', message: 'My Error', - stack: jasmine.stringMatching(/^SyntaxError: My Error/), + stack: expect.stringMatching(/^SyntaxError: My Error/), causes: undefined, handling: ErrorHandling.HANDLED, fingerprint: undefined, @@ -188,15 +189,16 @@ describe('Logger', () => { }) describe('tags', () => { - let displaySpy: jasmine.Spy + let displaySpy: Mock function expectWarning() { - expect(displaySpy).toHaveBeenCalledOnceWith( - jasmine.stringMatching("Tag .* doesn't meet tag requirements and will be sanitized") + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching("Tag .* doesn't meet tag requirements and will be sanitized") ) } beforeEach(() => { - displaySpy = spyOn(display, 'warn') + displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) }) it('should add a key:value tag', () => { @@ -325,7 +327,7 @@ describe('Logger', () => { expect(loggedError).toEqual({ message: 'Test error', - stack: jasmine.stringMatching(/^Error: Test error/), + stack: expect.stringMatching(/^Error: Test error/), kind: 'Error', causes: undefined, handling: ErrorHandling.HANDLED, diff --git a/packages/browser-logs/src/domain/logger/loggerCollection.spec.ts b/packages/browser-logs/src/domain/logger/loggerCollection.spec.ts index b6957a36db..1d28efd686 100644 --- a/packages/browser-logs/src/domain/logger/loggerCollection.spec.ts +++ b/packages/browser-logs/src/domain/logger/loggerCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { TimeStamp } from '@datadog/js-core/time' import { timeStampNow } from '@datadog/js-core/time' import { ErrorSource } from '@datadog/browser-core' @@ -26,7 +27,7 @@ describe('logger collection', () => { lifeCycle.subscribe(LifeCycleEventType.RAW_LOG_COLLECTED, (rawLogsEvent) => rawLogsEvents.push(rawLogsEvent as RawLogsEventCollectedData) ) - spyOn(console, 'error').and.callFake(() => true) + vi.spyOn(console, 'error').mockImplementation(() => true) logger = new Logger((...params) => handleLog(...params)) ;({ handleLog: handleLog } = startLoggerCollection(lifeCycle)) mockClock() @@ -35,11 +36,11 @@ describe('logger collection', () => { describe('when handle type is set to "console"', () => { beforeEach(() => { logger.setHandler(HandlerType.console) - spyOn(originalConsoleMethods, 'debug') - spyOn(originalConsoleMethods, 'info') - spyOn(originalConsoleMethods, 'warn') - spyOn(originalConsoleMethods, 'error') - spyOn(originalConsoleMethods, 'log') + vi.spyOn(originalConsoleMethods, 'debug').mockImplementation(() => undefined) + vi.spyOn(originalConsoleMethods, 'info').mockImplementation(() => undefined) + vi.spyOn(originalConsoleMethods, 'warn').mockImplementation(() => undefined) + vi.spyOn(originalConsoleMethods, 'error').mockImplementation(() => undefined) + vi.spyOn(originalConsoleMethods, 'log').mockImplementation(() => undefined) }) it('should print the log message and context to the console', () => { @@ -52,7 +53,8 @@ describe('logger collection', () => { COMMON_CONTEXT ) - expect(originalConsoleMethods.error).toHaveBeenCalledOnceWith('message', { + expect(originalConsoleMethods.error).toHaveBeenCalledTimes(1) + expect(originalConsoleMethods.error).toHaveBeenCalledExactlyOnceWith('message', { foo: 'from-logger', bar: 'from-message', }) diff --git a/packages/browser-logs/src/domain/networkError/networkErrorCollection.spec.ts b/packages/browser-logs/src/domain/networkError/networkErrorCollection.spec.ts index 846d6dfeb0..c7c8c78ba0 100644 --- a/packages/browser-logs/src/domain/networkError/networkErrorCollection.spec.ts +++ b/packages/browser-logs/src/domain/networkError/networkErrorCollection.spec.ts @@ -1,5 +1,6 @@ -import type { BufferedData, FetchResolveContext } from '@datadog/browser-core' +import { beforeEach, describe, expect, it } from 'vitest' import { clocksNow } from '@datadog/js-core/time' +import type { BufferedData, FetchResolveContext } from '@datadog/browser-core' import { BufferedDataType, ErrorSource, Observable } from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import type { RawNetworkLogsEvent } from '../../rawLogsEvent.types' @@ -67,7 +68,7 @@ describe('network error collection', () => { expect(rawLogsEvents[0].rawLogsEvent).toEqual({ message: 'Fetch error GET http://fake.com/', - date: jasmine.any(Number), + date: expect.any(Number), status: StatusType.error, origin: ErrorSource.NETWORK, error: { diff --git a/packages/browser-logs/src/domain/report/reportCollection.spec.ts b/packages/browser-logs/src/domain/report/reportCollection.spec.ts index ce68bac76c..1958ab2f1d 100644 --- a/packages/browser-logs/src/domain/report/reportCollection.spec.ts +++ b/packages/browser-logs/src/domain/report/reportCollection.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { ErrorHandling, ErrorSource, noop } from '@datadog/browser-core' import type { MockReportingObserver } from '@datadog/browser-core/test' import { mockReportingObserver } from '@datadog/browser-core/test' @@ -39,13 +40,13 @@ describe('reports', () => { expect(rawLogsEvents[0].rawLogsEvent).toEqual({ error: { kind: 'NavigatorVibrate', - stack: jasmine.any(String), + stack: expect.any(String), handling: ErrorHandling.UNHANDLED, causes: undefined, fingerprint: undefined, message: undefined, }, - date: jasmine.any(Number), + date: expect.any(Number), message: 'intervention: foo bar', status: StatusType.error, origin: ErrorSource.REPORT, @@ -71,7 +72,7 @@ describe('reports', () => { reportingObserver.raiseReport('deprecation') expect(rawLogsEvents[0].rawLogsEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), message: 'deprecation: foo bar Found in http://foo.bar/index.js:20:10', status: StatusType.warn, origin: ErrorSource.REPORT, diff --git a/packages/browser-logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts b/packages/browser-logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts index 4c28272fc8..8e52901111 100644 --- a/packages/browser-logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts +++ b/packages/browser-logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { BufferedData, RawError } from '@datadog/browser-core' import { clocksNow } from '@datadog/js-core/time' import { ErrorSource, ErrorHandling, Observable, BufferedDataType } from '@datadog/browser-core' @@ -40,12 +41,12 @@ const RAW_ERROR: RawError = { } describe('runtime error collection', () => { - let onErrorSpy: jasmine.Spy + let onErrorSpy: Mock let originalOnErrorHandler: OnErrorEventHandler beforeEach(() => { originalOnErrorHandler = window.onerror - onErrorSpy = jasmine.createSpy() + onErrorSpy = vi.fn() window.onerror = onErrorSpy }) @@ -62,10 +63,10 @@ describe('runtime error collection', () => { }) expect(rawLogsEvents[0].rawLogsEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), error: { kind: 'Error', - stack: jasmine.any(String), + stack: expect.any(String), causes: undefined, handling: ErrorHandling.UNHANDLED, fingerprint: undefined, @@ -103,22 +104,22 @@ describe('runtime error collection', () => { }) expect(rawLogsEvents[0].rawLogsEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), error: { kind: 'Error', - stack: jasmine.any(String), + stack: expect.any(String), handling: ErrorHandling.UNHANDLED, causes: [ { source: ErrorSource.SOURCE, type: 'Error', - stack: jasmine.any(String), + stack: expect.any(String), message: 'Mid level error', }, { source: ErrorSource.SOURCE, type: 'TypeError', - stack: jasmine.any(String), + stack: expect.any(String), message: 'Low level error', }, ], diff --git a/packages/browser-rum-angular/src/domain/angularPlugin.spec.ts b/packages/browser-rum-angular/src/domain/angularPlugin.spec.ts index b633b9272d..fbb966ad2d 100644 --- a/packages/browser-rum-angular/src/domain/angularPlugin.spec.ts +++ b/packages/browser-rum-angular/src/domain/angularPlugin.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' import { registerCleanupTask } from '../../../browser-core/test' import { angularPlugin, onRumInit, onRumStart, resetAngularPlugin } from './angularPlugin' @@ -15,16 +16,16 @@ describe('angularPlugin', () => { it('returns a plugin object', () => { const plugin = angularPlugin() expect(plugin).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'angular', - onInit: jasmine.any(Function), - onRumStart: jasmine.any(Function), + onInit: expect.any(Function), + onRumStart: expect.any(Function), }) ) }) it('calls callbacks registered with onRumInit during onInit', () => { - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() const pluginConfiguration = {} onRumInit(callbackSpy) @@ -36,12 +37,12 @@ describe('angularPlugin', () => { }) expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration) - expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API) + expect(callbackSpy.mock.lastCall![0]).toBe(pluginConfiguration) + expect(callbackSpy.mock.lastCall![1]).toBe(PUBLIC_API) }) it('calls callbacks immediately if onInit was already invoked', () => { - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() const pluginConfiguration = {} angularPlugin(pluginConfiguration).onInit!({ publicApi: PUBLIC_API, @@ -51,8 +52,8 @@ describe('angularPlugin', () => { onRumInit(callbackSpy) expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration) - expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API) + expect(callbackSpy.mock.lastCall![0]).toBe(pluginConfiguration) + expect(callbackSpy.mock.lastCall![1]).toBe(PUBLIC_API) }) it('enforce manual view tracking when router is enabled', () => { @@ -77,8 +78,8 @@ describe('angularPlugin', () => { }) it('calls onRumStart subscribers during onRumStart', () => { - const callbackSpy = jasmine.createSpy() - const addErrorSpy = jasmine.createSpy() + const callbackSpy = vi.fn() + const addErrorSpy = vi.fn() onRumStart(callbackSpy) angularPlugin().onRumStart!({ addError: addErrorSpy }) @@ -87,10 +88,10 @@ describe('angularPlugin', () => { }) it('calls onRumStart subscribers immediately if already started', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() angularPlugin().onRumStart!({ addError: addErrorSpy }) - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() onRumStart(callbackSpy) expect(callbackSpy).toHaveBeenCalledWith(addErrorSpy) diff --git a/packages/browser-rum-angular/src/domain/angularRouter/startAngularView.spec.ts b/packages/browser-rum-angular/src/domain/angularRouter/startAngularView.spec.ts index 3610a25d89..61f9200387 100644 --- a/packages/browser-rum-angular/src/domain/angularRouter/startAngularView.spec.ts +++ b/packages/browser-rum-angular/src/domain/angularRouter/startAngularView.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import { computeViewName } from './startAngularView' import type { RouteSnapshot } from './types' diff --git a/packages/browser-rum-angular/src/domain/error/addAngularError.spec.ts b/packages/browser-rum-angular/src/domain/error/addAngularError.spec.ts index a5a5a3879c..aac8b71082 100644 --- a/packages/browser-rum-angular/src/domain/error/addAngularError.spec.ts +++ b/packages/browser-rum-angular/src/domain/error/addAngularError.spec.ts @@ -1,9 +1,10 @@ +import { describe, it, expect, vi } from 'vitest' import { initializeAngularPlugin } from '../../../test/initializeAngularPlugin' import { addAngularError } from './addAngularError' describe('addAngularError', () => { it('delegates the error to addError', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeAngularPlugin({ addError: addErrorSpy, }) @@ -11,10 +12,11 @@ describe('addAngularError', () => { addAngularError(originalError) - expect(addErrorSpy).toHaveBeenCalledOnceWith({ + expect(addErrorSpy).toHaveBeenCalledTimes(1) + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith({ error: originalError, - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), + handlingStack: expect.any(String), + startClocks: expect.any(Object), context: { framework: 'angular', }, @@ -22,7 +24,7 @@ describe('addAngularError', () => { }) it('should merge dd_context from the original error with angular error context', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeAngularPlugin({ addError: addErrorSpy, }) @@ -31,8 +33,8 @@ describe('addAngularError', () => { addAngularError(originalError) - expect(addErrorSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ error: originalError, context: { framework: 'angular', @@ -44,18 +46,19 @@ describe('addAngularError', () => { }) it('handles non-Error values', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeAngularPlugin({ addError: addErrorSpy, }) addAngularError('string error') - expect(addErrorSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ + expect(addErrorSpy).toHaveBeenCalledTimes(1) + expect(addErrorSpy).toHaveBeenCalledWith( + expect.objectContaining({ error: 'string error', - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), + handlingStack: expect.any(String), + startClocks: expect.any(Object), context: { framework: 'angular' }, }) ) diff --git a/packages/browser-rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts b/packages/browser-rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts index 7118728b34..b03255d5c5 100644 --- a/packages/browser-rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts +++ b/packages/browser-rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, vi } from 'vitest' import type { EnvironmentInjector } from '@angular/core' import { ErrorHandler, Injector, createEnvironmentInjector } from '@angular/core' import { initializeAngularPlugin } from '../../../test/initializeAngularPlugin' @@ -10,10 +11,10 @@ function createErrorHandler(): ErrorHandler { describe('provideDatadogErrorHandler', () => { it('provides an ErrorHandler that reports errors to Datadog', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeAngularPlugin({ addError: addErrorSpy }) - spyOn(console, 'error') // Mute console.errors + vi.spyOn(console, 'error').mockImplementation(() => true) // Mute console.errors const handler = createErrorHandler() handler.handleError(new Error('test error')) @@ -23,7 +24,7 @@ describe('provideDatadogErrorHandler', () => { it('still logs the error to the console via default ErrorHandler', () => { initializeAngularPlugin() - const consoleErrorSpy = spyOn(console, 'error') + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) const handler = createErrorHandler() handler.handleError(new Error('test error')) diff --git a/packages/browser-rum-angular/tsconfig.json b/packages/browser-rum-angular/tsconfig.json index c27e50b613..684fcd0860 100644 --- a/packages/browser-rum-angular/tsconfig.json +++ b/packages/browser-rum-angular/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["jasmine"], "allowImportingTsExtensions": true, "noEmit": true }, diff --git a/packages/browser-rum-core/src/boot/preStartRum.spec.ts b/packages/browser-rum-core/src/boot/preStartRum.spec.ts index b270bd4c43..829f62091f 100644 --- a/packages/browser-rum-core/src/boot/preStartRum.spec.ts +++ b/packages/browser-rum-core/src/boot/preStartRum.spec.ts @@ -1,6 +1,7 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Duration, TimeStamp } from '@datadog/js-core/time' import type { DeflateWorker, TrackingConsentState } from '@datadog/browser-core' -import { toTimeStamp, relativeToClocks, clocksNow } from '@datadog/js-core/time' +import { relativeToClocks, clocksNow, toTimeStamp } from '@datadog/js-core/time' import { display, noop, @@ -18,10 +19,10 @@ import { mockEventBridge, mockSyntheticsWorkerValues, createFakeTelemetryObject, - registerCleanupTask, replaceMockable, replaceMockableWithSpy, createStartSessionManagerMock, + registerCleanupTask, } from '@datadog/browser-core/test' import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration' import type { ViewOptions } from '../domain/view/trackViews' @@ -43,11 +44,11 @@ const PUBLIC_API = {} as RumPublicApi describe('preStartRum', () => { describe('configuration validation', () => { let strategy: Strategy - let doStartRumSpy: jasmine.Spy - let displaySpy: jasmine.Spy + let doStartRumSpy: Mock + let displaySpy: Mock beforeEach(() => { - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) }) @@ -144,7 +145,7 @@ describe('preStartRum', () => { it('should initialize even if session cannot be handled', async () => { mockEventBridge() - spyOnProperty(document, 'cookie', 'get').and.returnValue('') + vi.spyOn(document, 'cookie', 'get').mockReturnValue('') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() @@ -152,16 +153,30 @@ describe('preStartRum', () => { }) }) - it('should not start when session manager initialization fails', async () => { - const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ - startSessionManagerMock: () => Promise.reject(new Error('Session init failed')), + describe('init', () => { + it('should not initialize if session cannot be handled and bridge is not present', async () => { + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ + startSessionManagerMock: () => { + display.warn('No storage available for session. We will not send any data.') + return Promise.resolve(null as any) + }, + }) + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(doStartRumSpy, 0) + expect(doStartRumSpy).not.toHaveBeenCalled() + expect(displaySpy).toHaveBeenCalled() + }) + + it('should not start when session manager initialization fails', async () => { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ + startSessionManagerMock: () => Promise.reject(new Error('Session init failed')), + }) + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(doStartRumSpy, 0) + expect(doStartRumSpy).not.toHaveBeenCalled() }) - strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) - await collectAsyncCalls(doStartRumSpy, 0) - expect(doStartRumSpy).not.toHaveBeenCalled() - }) - describe('init', () => { describe('skipInitIfSyntheticsWillInjectRum option', () => { it('when true, ignores init() call if Synthetics will inject its own instance of RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) @@ -202,11 +217,11 @@ describe('preStartRum', () => { describe('deflate worker', () => { let strategy: Strategy - let startDeflateWorkerSpy: jasmine.Spy - let doStartRumSpy: jasmine.Spy + let startDeflateWorkerSpy: Mock + let doStartRumSpy: Mock beforeEach(() => { - startDeflateWorkerSpy = jasmine.createSpy().and.returnValue(FAKE_WORKER) + startDeflateWorkerSpy = vi.fn().mockReturnValue(FAKE_WORKER) ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ rumPublicApiOptions: { startDeflateWorker: startDeflateWorkerSpy, @@ -221,7 +236,7 @@ describe('preStartRum', () => { await collectAsyncCalls(doStartRumSpy, 1) expect(startDeflateWorkerSpy).not.toHaveBeenCalled() - const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] + const worker: DeflateWorker | undefined = doStartRumSpy.mock.lastCall![2] expect(worker).toBeUndefined() }) }) @@ -238,12 +253,12 @@ describe('preStartRum', () => { await collectAsyncCalls(doStartRumSpy, 1) expect(startDeflateWorkerSpy).toHaveBeenCalledTimes(1) - const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] + const worker: DeflateWorker | undefined = doStartRumSpy.mock.lastCall![2] expect(worker).toBeDefined() }) it('aborts the initialization if it fails to create a deflate worker', () => { - startDeflateWorkerSpy.and.returnValue(undefined) + startDeflateWorkerSpy.mockReturnValue(undefined) strategy.init( { @@ -277,17 +292,17 @@ describe('preStartRum', () => { describe('trackViews mode', () => { let clock: Clock | undefined let strategy: Strategy - let doStartRumSpy: jasmine.Spy - let startViewSpy: jasmine.Spy - let addTimingSpy: jasmine.Spy - let setViewNameSpy: jasmine.Spy + let doStartRumSpy: Mock + let startViewSpy: Mock + let addTimingSpy: Mock + let setViewNameSpy: Mock beforeEach(() => { - startViewSpy = jasmine.createSpy('startView') - addTimingSpy = jasmine.createSpy('addTiming') - setViewNameSpy = jasmine.createSpy('setViewName') + startViewSpy = vi.fn() + addTimingSpy = vi.fn() + setViewNameSpy = vi.fn() ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) - doStartRumSpy.and.returnValue({ + doStartRumSpy.mockReturnValue({ startView: startViewSpy, addTiming: addTimingSpy, setViewName: setViewNameSpy, @@ -316,8 +331,8 @@ describe('preStartRum', () => { await collectAsyncCalls(startViewSpy, 1) expect(startViewSpy).toHaveBeenCalled() - expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo' }) - expect(startViewSpy.calls.argsFor(0)[1]).toEqual({ + expect(startViewSpy.mock.calls[0][0]).toEqual({ name: 'foo' }) + expect(startViewSpy.mock.calls[0][1]).toEqual({ relative: clock.relative(10), timeStamp: clock.timeStamp(10), }) @@ -339,7 +354,7 @@ describe('preStartRum', () => { strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() - const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] + const initialViewOptions: ViewOptions | undefined = doStartRumSpy.mock.calls[0][3] expect(initialViewOptions).toEqual({ name: 'foo' }) expect(startViewSpy).not.toHaveBeenCalled() }) @@ -370,9 +385,10 @@ describe('preStartRum', () => { await collectAsyncCalls(startViewSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() - const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] + const initialViewOptions: ViewOptions | undefined = doStartRumSpy.mock.calls[0][3] expect(initialViewOptions).toEqual({ name: 'foo' }) - expect(startViewSpy).toHaveBeenCalledOnceWith({ name: 'bar' }, relativeToClocks(clock.relative(20))) + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith({ name: 'bar' }, relativeToClocks(clock.relative(20))) }) it('calling init then startView should start rum', async () => { @@ -383,7 +399,7 @@ describe('preStartRum', () => { strategy.startView({ name: 'foo' }) await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() - const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] + const initialViewOptions: ViewOptions | undefined = doStartRumSpy.mock.calls[0][3] expect(initialViewOptions).toEqual({ name: 'foo' }) expect(startViewSpy).not.toHaveBeenCalled() }) @@ -407,11 +423,11 @@ describe('preStartRum', () => { expect(addTimingSpy).toHaveBeenCalledTimes(2) - expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('first') - expect(addTimingSpy.calls.argsFor(0)[1]).toEqual(toTimeStamp(clock.relative(10))) + expect(addTimingSpy.mock.calls[0][0]).toEqual('first') + expect(addTimingSpy.mock.calls[0][1]).toEqual(toTimeStamp(clock.relative(10))) - expect(addTimingSpy.calls.argsFor(1)[0]).toEqual('second') - expect(addTimingSpy.calls.argsFor(1)[1]).toEqual(toTimeStamp(clock.relative(30))) + expect(addTimingSpy.mock.calls[1][0]).toEqual('second') + expect(addTimingSpy.mock.calls[1][1]).toEqual(toTimeStamp(clock.relative(30))) }) }) }) @@ -439,7 +455,7 @@ describe('preStartRum', () => { PUBLIC_API ) await collectAsyncCalls(doStartRumSpy, 1) - expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(50) + expect(doStartRumSpy.mock.lastCall![0].sessionSampleRate).toEqual(50) }) it('should start with the remote configuration when remoteConfiguration.sync is true', async () => { @@ -458,7 +474,7 @@ describe('preStartRum', () => { PUBLIC_API ) await collectAsyncCalls(doStartRumSpy, 1) - expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(50) + expect(doStartRumSpy.mock.lastCall![0].sessionSampleRate).toEqual(50) }) }) @@ -495,7 +511,7 @@ describe('preStartRum', () => { ) await collectAsyncCalls(doStartRumSpy, 1) - expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toBe(25) + expect(doStartRumSpy.mock.lastCall![0].sessionSampleRate).toBe(25) }) it('should trigger a background fetch to the remote configuration endpoint', async () => { @@ -510,7 +526,7 @@ describe('preStartRum', () => { ) await interceptor.waitForAllFetchCalls() - expect(interceptor.requests.some((r) => r.url.includes(REMOTE_CONFIGURATION_ID))).toBeTrue() + expect(interceptor.requests.some((request) => request.url.includes(REMOTE_CONFIGURATION_ID))).toBe(true) }) describe('with required: true', () => { @@ -543,7 +559,7 @@ describe('preStartRum', () => { ) await interceptor.waitForAllFetchCalls() - expect(interceptor.requests.some((r) => r.url.includes(REMOTE_CONFIGURATION_ID))).toBeTrue() + expect(interceptor.requests.some((request) => request.url.includes(REMOTE_CONFIGURATION_ID))).toBe(true) }) it('should start the SDK with the cached configuration on cache hit', async () => { @@ -562,7 +578,7 @@ describe('preStartRum', () => { ) await collectAsyncCalls(doStartRumSpy, 1) - expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toBe(75) + expect(doStartRumSpy.mock.lastCall![0].sessionSampleRate).toBe(75) }) it('should not start the SDK on cache error', async () => { @@ -585,7 +601,7 @@ describe('preStartRum', () => { describe('plugins', () => { it('calls the onInit method on provided plugins', () => { - const plugin = { name: 'a', onInit: jasmine.createSpy() } + const plugin = { name: 'a', onInit: vi.fn() } const { strategy } = createPreStartStrategyWithDefaults() const initConfiguration: RumInitConfiguration = { ...DEFAULT_INIT_CONFIGURATION, plugins: [plugin] } strategy.init(initConfiguration, PUBLIC_API) @@ -614,7 +630,7 @@ describe('preStartRum', () => { await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() - expect(doStartRumSpy.calls.mostRecent().args[0].applicationId).toBe('application-id') + expect(doStartRumSpy.mock.lastCall![0].applicationId).toBe('application-id') }) }) }) @@ -636,8 +652,8 @@ describe('preStartRum', () => { describe('stopSession', () => { it('does not buffer the call before starting RUM', () => { const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() - const stopSessionSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ stopSession: stopSessionSpy } as unknown as StartRumResult) + const stopSessionSpy = vi.fn() + doStartRumSpy.mockReturnValue({ stopSession: stopSessionSpy } as unknown as StartRumResult) strategy.stopSession() strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) @@ -685,7 +701,7 @@ describe('preStartRum', () => { expect(strategy.initConfiguration).toEqual(initConfiguration) }) - it('returns the initConfiguration with the remote configuration when a remoteConfigurationId is provided (sync loading)', (done) => { + it('returns the initConfiguration with the remote configuration when a remoteConfigurationId is provided', async () => { interceptor.withFetch(() => Promise.resolve({ ok: true, @@ -693,11 +709,7 @@ describe('preStartRum', () => { }) ) const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() - doStartRumSpy.and.callFake(() => { - expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50) - done() - return {} as StartRumResult - }) + strategy.init( { ...DEFAULT_INIT_CONFIGURATION, @@ -705,9 +717,13 @@ describe('preStartRum', () => { }, PUBLIC_API ) + + await collectAsyncCalls(doStartRumSpy, 1) + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50) }) - it('exposes the user configuration when remoteConfiguration.id is provided (async loading, cache miss)', () => { + it('exposes the user configuration when remoteConfiguration.id is provided', () => { interceptor.withFetch(() => Promise.resolve({ ok: true, @@ -728,15 +744,15 @@ describe('preStartRum', () => { describe('buffers API calls before starting RUM', () => { let strategy: Strategy - let doStartRumSpy: jasmine.Spy + let doStartRumSpy: Mock beforeEach(() => { ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) }) it('addAction', async () => { - const addActionSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ addAction: addActionSpy } as unknown as StartRumResult) + const addActionSpy = vi.fn() + doStartRumSpy.mockReturnValue({ addAction: addActionSpy } as unknown as StartRumResult) const manualAction: Omit = { name: 'foo', @@ -746,12 +762,12 @@ describe('preStartRum', () => { strategy.addAction(manualAction) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(addActionSpy, 1) - expect(addActionSpy).toHaveBeenCalledOnceWith(manualAction) + expect(addActionSpy).toHaveBeenCalledExactlyOnceWith(manualAction) }) it('addError', async () => { - const addErrorSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ addError: addErrorSpy } as unknown as StartRumResult) + const addErrorSpy = vi.fn() + doStartRumSpy.mockReturnValue({ addError: addErrorSpy } as unknown as StartRumResult) const error = { error: new Error('foo'), @@ -762,48 +778,48 @@ describe('preStartRum', () => { strategy.addError(error) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(addErrorSpy, 1) - expect(addErrorSpy).toHaveBeenCalledOnceWith(error) + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith(error) }) it('startView', async () => { - const startViewSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ startView: startViewSpy } as unknown as StartRumResult) + const startViewSpy = vi.fn() + doStartRumSpy.mockReturnValue({ startView: startViewSpy } as unknown as StartRumResult) const options = { name: 'foo' } const clockState = clocksNow() strategy.startView(options, clockState) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(startViewSpy, 1) - expect(startViewSpy).toHaveBeenCalledOnceWith(options, clockState) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith(options, clockState) }) it('addTiming', async () => { - const addTimingSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ addTiming: addTimingSpy } as unknown as StartRumResult) + const addTimingSpy = vi.fn() + doStartRumSpy.mockReturnValue({ addTiming: addTimingSpy } as unknown as StartRumResult) const name = 'foo' const time = 123 as TimeStamp strategy.addTiming(name, time) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(addTimingSpy, 1) - expect(addTimingSpy).toHaveBeenCalledOnceWith(name, time) + expect(addTimingSpy).toHaveBeenCalledExactlyOnceWith(name, time) }) it('setLoadingTime', async () => { - const setLoadingTimeSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ setLoadingTime: setLoadingTimeSpy } as unknown as StartRumResult) + const setLoadingTimeSpy = vi.fn() + doStartRumSpy.mockReturnValue({ setLoadingTime: setLoadingTimeSpy } as unknown as StartRumResult) const timestamp = 123 as TimeStamp strategy.setLoadingTime(timestamp) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(setLoadingTimeSpy, 1) - expect(setLoadingTimeSpy).toHaveBeenCalledOnceWith(timestamp) + expect(setLoadingTimeSpy).toHaveBeenCalledExactlyOnceWith(timestamp) }) it('setLoadingTime should preserve call timestamp', async () => { const clock = mockClock() - const setLoadingTimeSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ setLoadingTime: setLoadingTimeSpy } as unknown as StartRumResult) + const setLoadingTimeSpy = vi.fn() + doStartRumSpy.mockReturnValue({ setLoadingTime: setLoadingTimeSpy } as unknown as StartRumResult) clock.tick(10) strategy.setLoadingTime(clock.timeStamp(10)) @@ -812,46 +828,47 @@ describe('preStartRum', () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(setLoadingTimeSpy, 1) - expect(setLoadingTimeSpy).toHaveBeenCalledOnceWith(jasmine.any(Number)) + expect(setLoadingTimeSpy).toHaveBeenCalledTimes(1) + expect(setLoadingTimeSpy).toHaveBeenCalledExactlyOnceWith(expect.any(Number)) // Verify the timestamp was captured at call time (tick 10), not at drain time (tick 30) - const capturedTimestamp = setLoadingTimeSpy.calls.argsFor(0)[0] as number + const capturedTimestamp = setLoadingTimeSpy.mock.calls[0][0] as number expect(capturedTimestamp).toBe(clock.timeStamp(10)) }) it('setViewContext', async () => { - const setViewContextSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ setViewContext: setViewContextSpy } as unknown as StartRumResult) + const setViewContextSpy = vi.fn() + doStartRumSpy.mockReturnValue({ setViewContext: setViewContextSpy } as unknown as StartRumResult) strategy.setViewContext({ foo: 'bar' }) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(setViewContextSpy, 1) - expect(setViewContextSpy).toHaveBeenCalledOnceWith({ foo: 'bar' }) + expect(setViewContextSpy).toHaveBeenCalledExactlyOnceWith({ foo: 'bar' }) }) it('setViewContextProperty', async () => { - const setViewContextPropertySpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ setViewContextProperty: setViewContextPropertySpy } as unknown as StartRumResult) + const setViewContextPropertySpy = vi.fn() + doStartRumSpy.mockReturnValue({ setViewContextProperty: setViewContextPropertySpy } as unknown as StartRumResult) strategy.setViewContextProperty('foo', 'bar') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(setViewContextPropertySpy, 1) - expect(setViewContextPropertySpy).toHaveBeenCalledOnceWith('foo', 'bar') + expect(setViewContextPropertySpy).toHaveBeenCalledExactlyOnceWith('foo', 'bar') }) it('setViewName', async () => { - const setViewNameSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ setViewName: setViewNameSpy } as unknown as StartRumResult) + const setViewNameSpy = vi.fn() + doStartRumSpy.mockReturnValue({ setViewName: setViewNameSpy } as unknown as StartRumResult) const name = 'foo' strategy.setViewName(name) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(setViewNameSpy, 1) - expect(setViewNameSpy).toHaveBeenCalledOnceWith(name) + expect(setViewNameSpy).toHaveBeenCalledExactlyOnceWith(name) }) it('addFeatureFlagEvaluation', async () => { - const addFeatureFlagEvaluationSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ + const addFeatureFlagEvaluationSpy = vi.fn() + doStartRumSpy.mockReturnValue({ addFeatureFlagEvaluation: addFeatureFlagEvaluationSpy, } as unknown as StartRumResult) @@ -860,13 +877,13 @@ describe('preStartRum', () => { strategy.addFeatureFlagEvaluation(key, value) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(addFeatureFlagEvaluationSpy, 1) - expect(addFeatureFlagEvaluationSpy).toHaveBeenCalledOnceWith(key, value) + expect(addFeatureFlagEvaluationSpy).toHaveBeenCalledExactlyOnceWith(key, value) }) it('startDurationVital', async () => { - const startDurationVitalSpy = jasmine.createSpy() - const stopDurationVitalSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ + const startDurationVitalSpy = vi.fn() + const stopDurationVitalSpy = vi.fn() + doStartRumSpy.mockReturnValue({ startDurationVital: startDurationVitalSpy, stopDurationVital: stopDurationVitalSpy, } as unknown as StartRumResult) @@ -876,13 +893,13 @@ describe('preStartRum', () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(stopDurationVitalSpy, 1) - expect(startDurationVitalSpy).toHaveBeenCalledWith('timing', undefined, jasmine.any(Object)) - expect(stopDurationVitalSpy).toHaveBeenCalledWith('timing', undefined, jasmine.any(Object)) + expect(startDurationVitalSpy).toHaveBeenCalledWith('timing', undefined, expect.any(Object)) + expect(stopDurationVitalSpy).toHaveBeenCalledWith('timing', undefined, expect.any(Object)) }) it('addDurationVital', async () => { - const addDurationVitalSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ + const addDurationVitalSpy = vi.fn() + doStartRumSpy.mockReturnValue({ addDurationVital: addDurationVitalSpy, } as unknown as StartRumResult) @@ -896,25 +913,25 @@ describe('preStartRum', () => { strategy.addDurationVital(vitalAdd) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(addDurationVitalSpy, 1) - expect(addDurationVitalSpy).toHaveBeenCalledOnceWith(vitalAdd) + expect(addDurationVitalSpy).toHaveBeenCalledExactlyOnceWith(vitalAdd) }) it('addOperationStepVital', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ + const addOperationStepVitalSpy = vi.fn() + doStartRumSpy.mockReturnValue({ addOperationStepVital: addOperationStepVitalSpy, } as unknown as StartRumResult) strategy.addOperationStepVital('foo', 'start') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(addOperationStepVitalSpy, 1) - expect(addOperationStepVitalSpy).toHaveBeenCalledOnceWith('foo', 'start', undefined, undefined) + expect(addOperationStepVitalSpy).toHaveBeenCalledExactlyOnceWith('foo', 'start', undefined, undefined) }) it('startAction / stopAction', async () => { - const startActionSpy = jasmine.createSpy() - const stopActionSpy = jasmine.createSpy() - doStartRumSpy.and.returnValue({ + const startActionSpy = vi.fn() + const stopActionSpy = vi.fn() + doStartRumSpy.mockReturnValue({ startAction: startActionSpy, stopAction: stopActionSpy, } as unknown as StartRumResult) @@ -927,20 +944,20 @@ describe('preStartRum', () => { expect(startActionSpy).toHaveBeenCalledWith( 'user_login', - jasmine.objectContaining({ + expect.objectContaining({ type: ActionType.CUSTOM, }), - jasmine.objectContaining({ - relative: jasmine.any(Number), - timeStamp: jasmine.any(Number), + expect.objectContaining({ + relative: expect.any(Number), + timeStamp: expect.any(Number), }) ) expect(stopActionSpy).toHaveBeenCalledWith( 'user_login', undefined, - jasmine.objectContaining({ - relative: jasmine.any(Number), - timeStamp: jasmine.any(Number), + expect.objectContaining({ + relative: expect.any(Number), + timeStamp: expect.any(Number), }) ) }) @@ -948,7 +965,7 @@ describe('preStartRum', () => { describe('tracking consent', () => { let strategy: Strategy - let doStartRumSpy: jasmine.Spy + let doStartRumSpy: Mock let trackingConsentState: TrackingConsentState beforeEach(() => { @@ -1006,7 +1023,7 @@ describe('preStartRum', () => { it('do not call startRum when tracking consent state is updated after init', () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) - doStartRumSpy.calls.reset() + doStartRumSpy.mockClear() trackingConsentState.update(TrackingConsent.GRANTED) @@ -1068,8 +1085,8 @@ function createPreStartStrategyWithDefaults({ trackingConsentState?: TrackingConsentState startSessionManagerMock?: typeof startSessionManager } = {}) { - const doStartRumSpy = jasmine.createSpy() - const startTelemetrySpy = replaceMockableWithSpy(startTelemetry).and.callFake(createFakeTelemetryObject) + const doStartRumSpy = vi.fn() + const startTelemetrySpy = replaceMockableWithSpy(startTelemetry).mockImplementation(createFakeTelemetryObject) replaceMockable(startSessionManager, startSessionManagerMock) return { strategy: createPreStartStrategy(rumPublicApiOptions, trackingConsentState, doStartRumSpy), diff --git a/packages/browser-rum-core/src/boot/rumPublicApi.spec.ts b/packages/browser-rum-core/src/boot/rumPublicApi.spec.ts index 1f1ef9a378..ea4e8caaa4 100644 --- a/packages/browser-rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/browser-rum-core/src/boot/rumPublicApi.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest' import { ONE_SECOND, timeStampToClocks, toTimeStamp } from '@datadog/js-core/time' import type { TimeStamp, RelativeTime } from '@datadog/js-core/time' import type { DeflateWorker } from '@datadog/browser-core' @@ -58,10 +59,10 @@ describe('rum public api', () => { describe('init', () => { describe('deflate worker', () => { let rumPublicApi: RumPublicApi - let recorderApiOnRumStartSpy: jasmine.Spy + let recorderApiOnRumStartSpy: Mock beforeEach(() => { - recorderApiOnRumStartSpy = jasmine.createSpy() + recorderApiOnRumStartSpy = vi.fn() ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ recorderApi: { onRumStart: recorderApiOnRumStartSpy, @@ -73,7 +74,7 @@ describe('rum public api', () => { }) it('passes addError to plugins on rum start', async () => { - const plugin = { name: 'test-plugin', onRumStart: jasmine.createSpy() } + const plugin = { name: 'test-plugin', onRumStart: vi.fn() } rumPublicApi.init({ ...DEFAULT_INIT_CONFIGURATION, @@ -82,8 +83,8 @@ describe('rum public api', () => { await collectAsyncCalls(plugin.onRumStart, 1) expect(plugin.onRumStart).toHaveBeenCalledWith( - jasmine.objectContaining({ - addError: jasmine.any(Function), + expect.objectContaining({ + addError: expect.any(Function), }) ) }) @@ -94,18 +95,18 @@ describe('rum public api', () => { compressIntakeRequests: true, }) await collectAsyncCalls(recorderApiOnRumStartSpy, 1) - expect(recorderApiOnRumStartSpy.calls.mostRecent().args[4]).toBe(FAKE_WORKER) + expect(recorderApiOnRumStartSpy.mock.lastCall![4]).toBe(FAKE_WORKER) }) }) }) describe('getInternalContext', () => { - let getInternalContextSpy: jasmine.Spy['getInternalContext']> + let getInternalContextSpy: Mock['getInternalContext']> let rumPublicApi: RumPublicApi - let startRumSpy: jasmine.Spy + let startRumSpy: Mock beforeEach(() => { - getInternalContextSpy = jasmine.createSpy().and.callFake(() => ({ + getInternalContextSpy = vi.fn().mockImplementation(() => ({ application_id: '123', session_id: '123', })) @@ -146,12 +147,12 @@ describe('rum public api', () => { }) describe('addAction', () => { - let addActionSpy: jasmine.Spy['addAction']> + let addActionSpy: Mock['addAction']> let rumPublicApi: RumPublicApi beforeEach(() => { mockEventBridge() - addActionSpy = jasmine.createSpy() + addActionSpy = vi.fn() ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addAction: addActionSpy, @@ -168,13 +169,13 @@ describe('rum public api', () => { await collectAsyncCalls(addActionSpy, 1) expect(addActionSpy).toHaveBeenCalledTimes(1) - expect(addActionSpy.calls.argsFor(0)).toEqual([ + expect(addActionSpy.mock.calls[0]).toEqual([ { context: { bar: 'baz' }, name: 'foo', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), type: ActionType.CUSTOM, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), }, ]) }) @@ -190,20 +191,20 @@ describe('rum public api', () => { await collectAsyncCalls(addActionSpy, 1) expect(addActionSpy).toHaveBeenCalledTimes(1) - const stacktrace = addActionSpy.calls.argsFor(0)[0].handlingStack + const stacktrace = addActionSpy.mock.calls[0][0].handlingStack expect(stacktrace).toMatch(/^HandlingStack: action\s+at triggerAction @/) }) }) describe('addError', () => { - let addErrorSpy: jasmine.Spy['addError']> + let addErrorSpy: Mock['addError']> let rumPublicApi: RumPublicApi let clock: Clock beforeEach(() => { mockEventBridge() clock = mockClock() - addErrorSpy = jasmine.createSpy() + addErrorSpy = vi.fn() ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addError: addErrorSpy, @@ -219,12 +220,12 @@ describe('rum public api', () => { await collectAsyncCalls(addErrorSpy, 1) expect(addErrorSpy).toHaveBeenCalledTimes(1) - expect(addErrorSpy.calls.argsFor(0)).toEqual([ + expect(addErrorSpy.mock.calls[0]).toEqual([ { context: undefined, error: new Error('foo'), - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), + handlingStack: expect.any(String), + startClocks: expect.any(Object), }, ]) }) @@ -239,7 +240,7 @@ describe('rum public api', () => { triggerError() await collectAsyncCalls(addErrorSpy, 1) expect(addErrorSpy).toHaveBeenCalledTimes(1) - const stacktrace = addErrorSpy.calls.argsFor(0)[0].handlingStack + const stacktrace = addErrorSpy.mock.calls[0][0].handlingStack expect(stacktrace).toMatch(/^HandlingStack: error\s+at triggerError (.|\n)*$/) }) @@ -252,19 +253,19 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) await collectAsyncCalls(addErrorSpy, 1) - expect(addErrorSpy.calls.argsFor(0)[0].startClocks.relative as number).toEqual(clock.relative(ONE_SECOND)) + expect(addErrorSpy.mock.calls[0][0].startClocks.relative as number).toEqual(clock.relative(ONE_SECOND)) }) }) }) describe('setUser', () => { - let addActionSpy: jasmine.Spy['addAction']> - let displaySpy: jasmine.Spy<() => void> + let addActionSpy: Mock['addAction']> + let displaySpy: Mock<() => void> let rumPublicApi: RumPublicApi beforeEach(() => { - addActionSpy = jasmine.createSpy() - displaySpy = spyOn(display, 'error') + addActionSpy = vi.fn() + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addAction: addActionSpy, @@ -405,13 +406,13 @@ describe('rum public api', () => { }) describe('setAccount', () => { - let addActionSpy: jasmine.Spy['addAction']> - let displaySpy: jasmine.Spy<() => void> + let addActionSpy: Mock['addAction']> + let displaySpy: Mock<() => void> let rumPublicApi: RumPublicApi beforeEach(() => { - addActionSpy = jasmine.createSpy() - displaySpy = spyOn(display, 'error') + addActionSpy = vi.fn() + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addAction: addActionSpy, @@ -548,13 +549,13 @@ describe('rum public api', () => { }) describe('addTiming', () => { - let addTimingSpy: jasmine.Spy['addTiming']> - let displaySpy: jasmine.Spy<() => void> + let addTimingSpy: Mock['addTiming']> + let displaySpy: Mock<() => void> let rumPublicApi: RumPublicApi beforeEach(() => { - addTimingSpy = jasmine.createSpy() - displaySpy = spyOn(display, 'error') + addTimingSpy = vi.fn() + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addTiming: addTimingSpy, @@ -569,8 +570,8 @@ describe('rum public api', () => { await collectAsyncCalls(addTimingSpy, 1) expect(addTimingSpy).toHaveBeenCalledTimes(1) - expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('foo') - expect(addTimingSpy.calls.argsFor(0)[1]).toBe(toTimeStamp(clock.relative(0))) + expect(addTimingSpy.mock.calls[0][0]).toEqual('foo') + expect(addTimingSpy.mock.calls[0][1]).toBe(toTimeStamp(clock.relative(0))) expect(displaySpy).not.toHaveBeenCalled() }) @@ -583,8 +584,8 @@ describe('rum public api', () => { await collectAsyncCalls(addTimingSpy, 2) - expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('before_init') - expect(addTimingSpy.calls.argsFor(1)[0]).toEqual('after_init') + expect(addTimingSpy.mock.calls[0][0]).toEqual('before_init') + expect(addTimingSpy.mock.calls[1][0]).toEqual('after_init') }) it('adds custom timing with provided time', async () => { @@ -593,18 +594,18 @@ describe('rum public api', () => { await collectAsyncCalls(addTimingSpy, 1) expect(addTimingSpy).toHaveBeenCalledTimes(1) - expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('foo') - expect(addTimingSpy.calls.argsFor(0)[1]).toBe(12 as RelativeTime) + expect(addTimingSpy.mock.calls[0][0]).toEqual('foo') + expect(addTimingSpy.mock.calls[0][1]).toBe(12 as RelativeTime) expect(displaySpy).not.toHaveBeenCalled() }) }) describe('setViewLoadingTime', () => { - let setLoadingTimeSpy: jasmine.Spy['setLoadingTime']> + let setLoadingTimeSpy: Mock['setLoadingTime']> let rumPublicApi: RumPublicApi beforeEach(() => { - setLoadingTimeSpy = jasmine.createSpy() + setLoadingTimeSpy = vi.fn() ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { setLoadingTime: setLoadingTimeSpy, @@ -618,7 +619,7 @@ describe('rum public api', () => { rumPublicApi.setViewLoadingTime() await collectAsyncCalls(setLoadingTimeSpy, 1) - expect(setLoadingTimeSpy).toHaveBeenCalledOnceWith(jasmine.any(Number)) + expect(setLoadingTimeSpy).toHaveBeenCalledExactlyOnceWith(expect.any(Number)) }) it('should not throw when called before init', () => { @@ -630,8 +631,8 @@ describe('rum public api', () => { describe('addFeatureFlagEvaluation', () => { it('should add feature flag evaluation when ff feature_flags enabled', async () => { - const addFeatureFlagEvaluationSpy = jasmine.createSpy() - const displaySpy = spyOn(display, 'error') + const addFeatureFlagEvaluationSpy = vi.fn() + const displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addFeatureFlagEvaluation: addFeatureFlagEvaluationSpy, @@ -642,14 +643,14 @@ describe('rum public api', () => { await collectAsyncCalls(addFeatureFlagEvaluationSpy, 1) expect(addFeatureFlagEvaluationSpy).toHaveBeenCalledTimes(1) - expect(addFeatureFlagEvaluationSpy.calls.argsFor(0)).toEqual(['feature', 'foo']) + expect(addFeatureFlagEvaluationSpy.mock.calls[0]).toEqual(['feature', 'foo']) expect(displaySpy).not.toHaveBeenCalled() }) }) describe('stopSession', () => { it('calls stopSession on the startRum result', async () => { - const stopSessionSpy = jasmine.createSpy() + const stopSessionSpy = vi.fn() const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ startRumResult: { stopSession: stopSessionSpy, @@ -664,7 +665,7 @@ describe('rum public api', () => { describe('startView', () => { it('should call RUM results startView with the view name', async () => { - const startViewSpy = jasmine.createSpy() + const startViewSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { startView: startViewSpy, @@ -673,11 +674,11 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView('foo') const calls = await collectAsyncCalls(startViewSpy, 1) - expect(calls.argsFor(0)[0]).toEqual({ name: 'foo', handlingStack: jasmine.any(String) }) + expect(calls.argsFor(0)[0]).toEqual({ name: 'foo', handlingStack: expect.any(String) }) }) it('should call RUM results startView with the view options', async () => { - const startViewSpy = jasmine.createSpy() + const startViewSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { startView: startViewSpy, @@ -691,7 +692,7 @@ describe('rum public api', () => { service: 'bar', version: 'baz', context: { foo: 'bar' }, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), }) }) }) @@ -700,18 +701,18 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi let startRumSpy: ReturnType['startRumSpy'] let recorderApi: { - onRumStart: jasmine.Spy - start: jasmine.Spy - stop: jasmine.Spy - getSessionReplayLink: jasmine.Spy + onRumStart: Mock + start: Mock + stop: Mock + getSessionReplayLink: Mock } beforeEach(() => { recorderApi = { - onRumStart: jasmine.createSpy(), - start: jasmine.createSpy(), - stop: jasmine.createSpy(), - getSessionReplayLink: jasmine.createSpy(), + onRumStart: vi.fn(), + start: vi.fn(), + stop: vi.fn(), + getSessionReplayLink: vi.fn(), } ;({ rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ recorderApi })) }) @@ -728,9 +729,7 @@ describe('rum public api', () => { defaultPrivacyLevel: DefaultPrivacyLevel.MASK_USER_INPUT, }) await collectAsyncCalls(startRumSpy, 1) - expect(recorderApi.onRumStart.calls.mostRecent().args[1].defaultPrivacyLevel).toBe( - DefaultPrivacyLevel.MASK_USER_INPUT - ) + expect(recorderApi.onRumStart.mock.lastCall![1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK_USER_INPUT) }) it('public api calls are forwarded to the recorder api', () => { @@ -761,7 +760,7 @@ describe('rum public api', () => { describe('startDurationVital', () => { it('should call startDurationVital on the startRum result', async () => { - const startDurationVitalSpy = jasmine.createSpy() + const startDurationVitalSpy = vi.fn() const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ startRumResult: { startDurationVital: startDurationVitalSpy, @@ -777,16 +776,16 @@ describe('rum public api', () => { vitalKey: undefined, description: 'description-value', context: { foo: 'bar' }, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), }, - jasmine.objectContaining({ relative: jasmine.any(Number), timeStamp: jasmine.any(Number) }) + expect.objectContaining({ relative: expect.any(Number), timeStamp: expect.any(Number) }) ) }) }) describe('stopDurationVital', () => { it('should call stopDurationVital with a name on the startRum result', async () => { - const stopDurationVitalSpy = jasmine.createSpy() + const stopDurationVitalSpy = vi.fn() const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ startRumResult: { stopDurationVital: stopDurationVitalSpy, @@ -804,12 +803,12 @@ describe('rum public api', () => { description: 'description-value', context: { foo: 'bar' }, }, - jasmine.objectContaining({ relative: jasmine.any(Number), timeStamp: jasmine.any(Number) }) + expect.objectContaining({ relative: expect.any(Number), timeStamp: expect.any(Number) }) ) }) it('should call stopDurationVital with a vitalKey on the startRum result', async () => { - const stopDurationVitalSpy = jasmine.createSpy() + const stopDurationVitalSpy = vi.fn() const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ startRumResult: { stopDurationVital: stopDurationVitalSpy, @@ -835,7 +834,7 @@ describe('rum public api', () => { description: 'description-value', context: { foo: 'bar' }, }, - jasmine.objectContaining({ relative: jasmine.any(Number), timeStamp: jasmine.any(Number) }) + expect.objectContaining({ relative: expect.any(Number), timeStamp: expect.any(Number) }) ) }) }) @@ -844,8 +843,8 @@ describe('rum public api', () => { it('should call startAction and stopAction on the strategy', async () => { const clock = mockClock() - const startActionSpy = jasmine.createSpy('startAction') - const stopActionSpy = jasmine.createSpy('stopAction') + const startActionSpy = vi.fn() + const stopActionSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { startAction: startActionSpy, @@ -866,7 +865,7 @@ describe('rum public api', () => { expect(startActionSpy).toHaveBeenCalledWith( 'purchase', - jasmine.objectContaining({ + expect.objectContaining({ type: ActionType.CUSTOM, context: { cart: 'abc' }, actionKey: undefined, @@ -875,7 +874,7 @@ describe('rum public api', () => { ) expect(stopActionSpy).toHaveBeenCalledWith( 'purchase', - jasmine.objectContaining({ + expect.objectContaining({ context: { total: 100 }, actionKey: undefined, }), @@ -884,7 +883,7 @@ describe('rum public api', () => { }) it('should sanitize startAction and stopAction inputs', async () => { - const startActionSpy = jasmine.createSpy() + const startActionSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { startAction: startActionSpy, @@ -900,8 +899,8 @@ describe('rum public api', () => { await collectAsyncCalls(startActionSpy, 1) - expect(startActionSpy.calls.argsFor(0)[1]).toEqual( - jasmine.objectContaining({ + expect(startActionSpy.mock.calls[0][1]).toEqual( + expect.objectContaining({ type: ActionType.CUSTOM, context: { count: 123, nested: { foo: 'bar' } }, actionKey: 'action_key', @@ -914,8 +913,8 @@ describe('rum public api', () => { it('should call startResource and stopResource on the strategy', async () => { const clock = mockClock() - const startResourceSpy = jasmine.createSpy('startResource') - const stopResourceSpy = jasmine.createSpy('stopResource') + const startResourceSpy = vi.fn() + const stopResourceSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { startResource: startResourceSpy, @@ -939,7 +938,7 @@ describe('rum public api', () => { expect(startResourceSpy).toHaveBeenCalledWith( 'https://api.example.com/data', - jasmine.objectContaining({ + expect.objectContaining({ type: ResourceType.FETCH, method: 'POST', context: { requestId: 'abc' }, @@ -949,7 +948,7 @@ describe('rum public api', () => { ) expect(stopResourceSpy).toHaveBeenCalledWith( 'https://api.example.com/data', - jasmine.objectContaining({ + expect.objectContaining({ type: ResourceType.XHR, statusCode: 200, size: 1024, @@ -961,7 +960,7 @@ describe('rum public api', () => { }) it('should sanitize startResource and stopResource inputs', async () => { - const startResourceSpy = jasmine.createSpy() + const startResourceSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { startResource: startResourceSpy, @@ -977,8 +976,8 @@ describe('rum public api', () => { }) await collectAsyncCalls(startResourceSpy, 1) - expect(startResourceSpy.calls.argsFor(0)[1]).toEqual( - jasmine.objectContaining({ + expect(startResourceSpy.mock.calls[0][1]).toEqual( + expect.objectContaining({ type: ResourceType.XHR, method: 'GET', context: { count: 123, nested: { foo: 'bar' } }, @@ -990,7 +989,7 @@ describe('rum public api', () => { describe('addDurationVital', () => { it('should call addDurationVital on the startRum result', async () => { - const addDurationVitalSpy = jasmine.createSpy() + const addDurationVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addDurationVital: addDurationVitalSpy, @@ -1007,13 +1006,13 @@ describe('rum public api', () => { await collectAsyncCalls(addDurationVitalSpy, 1) expect(addDurationVitalSpy).toHaveBeenCalledWith({ - id: jasmine.any(String), + id: expect.any(String), name: 'foo', startClocks: timeStampToClocks(startTime), duration: 100, context: { foo: 'bar' }, description: 'description-value', - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), type: VitalType.DURATION, }) }) @@ -1021,7 +1020,7 @@ describe('rum public api', () => { describe('startOperation', () => { it('should call addOperationStepVital on the startRum result with start status', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() + const addOperationStepVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addOperationStepVital: addOperationStepVitalSpy, @@ -1035,7 +1034,7 @@ describe('rum public api', () => { 'start', { operationKey: '00000000-0000-0000-0000-000000000000', - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), }, undefined ) @@ -1044,7 +1043,7 @@ describe('rum public api', () => { describe('succeedOperation', () => { it('should call addOperationStepVital on the startRum result with end status', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() + const addOperationStepVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addOperationStepVital: addOperationStepVitalSpy, @@ -1066,7 +1065,7 @@ describe('rum public api', () => { describe('failOperation', () => { it('should call addOperationStepVital on the startRum result with end status and failure reason', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() + const addOperationStepVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addOperationStepVital: addOperationStepVitalSpy, @@ -1086,7 +1085,7 @@ describe('rum public api', () => { describe('deprecated *FeatureOperation aliases', () => { it('startFeatureOperation should forward with start status and still expose a handlingStack', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() + const addOperationStepVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addOperationStepVital: addOperationStepVitalSpy }, }) @@ -1096,13 +1095,13 @@ describe('rum public api', () => { expect(addOperationStepVitalSpy).toHaveBeenCalledWith( 'foo', 'start', - { handlingStack: jasmine.any(String) }, + { handlingStack: expect.any(String) }, undefined ) }) it('succeedFeatureOperation should forward with end status', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() + const addOperationStepVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addOperationStepVital: addOperationStepVitalSpy }, }) @@ -1113,7 +1112,7 @@ describe('rum public api', () => { }) it('failFeatureOperation should forward with end status and failure reason', async () => { - const addOperationStepVitalSpy = jasmine.createSpy() + const addOperationStepVitalSpy = vi.fn() const { rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { addOperationStepVital: addOperationStepVitalSpy }, }) @@ -1130,11 +1129,11 @@ describe('rum public api', () => { }) describe('setViewName', () => { - let setViewNameSpy: jasmine.Spy['setViewName']> + let setViewNameSpy: Mock['setViewName']> let rumPublicApi: RumPublicApi beforeEach(() => { - setViewNameSpy = jasmine.createSpy() + setViewNameSpy = vi.fn() ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { setViewName: setViewNameSpy, @@ -1153,12 +1152,12 @@ describe('rum public api', () => { describe('set view specific context', () => { let rumPublicApi: RumPublicApi - let setViewContextSpy: jasmine.Spy['setViewContext']> - let setViewContextPropertySpy: jasmine.Spy['setViewContextProperty']> + let setViewContextSpy: Mock['setViewContext']> + let setViewContextPropertySpy: Mock['setViewContextProperty']> beforeEach(() => { - setViewContextSpy = jasmine.createSpy() - setViewContextPropertySpy = jasmine.createSpy() + setViewContextSpy = vi.fn() + setViewContextPropertySpy = vi.fn() ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ startRumResult: { setViewContext: setViewContextSpy, @@ -1187,12 +1186,12 @@ describe('rum public api', () => { }) describe('getViewContext', () => { - let getViewContextSpy: jasmine.Spy['getViewContext']> + let getViewContextSpy: Mock['getViewContext']> let rumPublicApi: RumPublicApi - let startRumSpy: jasmine.Spy + let startRumSpy: Mock beforeEach(() => { - getViewContextSpy = jasmine.createSpy().and.callFake(() => ({ + getViewContextSpy = vi.fn().mockImplementation(() => ({ foo: 'bar', })) ;({ rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ @@ -1243,11 +1242,11 @@ function makeRumPublicApiWithDefaults({ startRumResult?: Partial> rumPublicApiOptions?: RumPublicApiOptions } = {}) { - const startRumSpy = replaceMockableWithSpy(startRum).and.callFake(() => ({ + const startRumSpy = replaceMockableWithSpy(startRum).mockImplementation(() => ({ ...noopStartRum(), ...startRumResult, })) - replaceMockableWithSpy(startTelemetry).and.callFake(createFakeTelemetryObject) + replaceMockableWithSpy(startTelemetry).mockImplementation(createFakeTelemetryObject) replaceMockable(startSessionManager, createStartSessionManagerMock()) const rumPublicApi = makeRumPublicApi( { ...noopRecorderApi, ...recorderApi }, diff --git a/packages/browser-rum-core/src/boot/startRum.spec.ts b/packages/browser-rum-core/src/boot/startRum.spec.ts index 90163ee8fa..16fcdeab26 100644 --- a/packages/browser-rum-core/src/boot/startRum.spec.ts +++ b/packages/browser-rum-core/src/boot/startRum.spec.ts @@ -1,4 +1,5 @@ -import { ONE_SECOND, toServerDuration, relativeNow } from '@datadog/js-core/time' +import { vi, beforeEach, describe, expect, it } from 'vitest' +import { ONE_SECOND, toServerDuration } from '@datadog/js-core/time' import type { Duration } from '@datadog/js-core/time' import type { BufferedData, SessionManager } from '@datadog/browser-core' import { Observable, findLast, noop, createIdentityEncoder, BufferedObservable } from '@datadog/browser-core' @@ -80,7 +81,7 @@ describe('rum session', () => { expect(serverRumEvents.length).toEqual(2) sessionManager.setId('43') - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) expect(serverRumEvents.length).toEqual(3) @@ -143,7 +144,6 @@ describe('rum session keep alive', () => { }) describe('view events', () => { - let clock: Clock let interceptor: ReturnType let stop: () => void @@ -166,7 +166,7 @@ describe('view events', () => { } beforeEach(() => { - clock = mockClock() + mockClock() registerCleanupTask(() => { stop() @@ -182,7 +182,7 @@ describe('view events', () => { setupViewCollectionTest() - clock.tick(VIEW_DURATION - relativeNow()) + vi.setSystemTime(performance.timing.navigationStart + VIEW_DURATION) window.dispatchEvent(createNewEvent('beforeunload')) const lastRumEvents = interceptor.requests[interceptor.requests.length - 1].body @@ -198,16 +198,16 @@ describe('view events', () => { it('sends a view update on page unload when bridge is present', () => { const eventBridge = mockEventBridge() - const sendSpy = spyOn(eventBridge, 'send') + const sendSpy = vi.spyOn(eventBridge, 'send').mockImplementation(() => undefined) const VIEW_DURATION = ONE_SECOND as Duration setupViewCollectionTest() - clock.tick(VIEW_DURATION - relativeNow()) + vi.setSystemTime(performance.timing.navigationStart + VIEW_DURATION) window.dispatchEvent(createNewEvent('beforeunload')) - const lastBridgeMessage = JSON.parse(sendSpy.calls.mostRecent().args[0]) as { + const lastBridgeMessage = JSON.parse(sendSpy.mock.lastCall![0]) as { eventType: 'rum' event: RumEvent } @@ -221,7 +221,7 @@ describe('view events', () => { setupViewCollectionTest() - clock.tick(VIEW_DURATION - relativeNow()) + vi.setSystemTime(performance.timing.navigationStart + VIEW_DURATION) window.dispatchEvent(createNewEvent('beforeunload')) const lastRumEvents = interceptor.requests[interceptor.requests.length - 1].body diff --git a/packages/browser-rum-core/src/browser/cookieObservable.spec.ts b/packages/browser-rum-core/src/browser/cookieObservable.spec.ts index 2b964f1c78..5e95a0adb3 100644 --- a/packages/browser-rum-core/src/browser/cookieObservable.spec.ts +++ b/packages/browser-rum-core/src/browser/cookieObservable.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { Subscription } from '@datadog/browser-core' import { ONE_MINUTE } from '@datadog/js-core/time' import { deleteCookie, globalObject, setCookie } from '@datadog/browser-core' diff --git a/packages/browser-rum-core/src/browser/domMutationObservable.spec.ts b/packages/browser-rum-core/src/browser/domMutationObservable.spec.ts index b84800bee3..3e48c29b82 100644 --- a/packages/browser-rum-core/src/browser/domMutationObservable.spec.ts +++ b/packages/browser-rum-core/src/browser/domMutationObservable.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { MockZoneJs } from '@datadog/browser-core/test' import { registerCleanupTask, mockZoneJs } from '@datadog/browser-core/test' import { createDOMMutationObservable, getMutationObserverConstructor } from './domMutationObservable' @@ -8,27 +9,28 @@ const DOM_MUTATION_OBSERVABLE_DURATION = 16 describe('domMutationObservable', () => { function domMutationSpec(mutate: (root: HTMLElement) => void, { expectedMutations }: { expectedMutations: number }) { - return (done: DoneFn) => { - const root = document.createElement('div') - root.setAttribute('data-test', 'foo') - root.appendChild(document.createElement('span')) - root.appendChild(document.createTextNode('foo')) - document.body.appendChild(root) - - const domMutationObservable = createDOMMutationObservable() - - let counter = 0 - const domMutationSubscription = domMutationObservable.subscribe(() => (counter += 1)) - - mutate(root) - - setTimeout(() => { - expect(counter).toBe(expectedMutations) - root.parentNode!.removeChild(root) - domMutationSubscription.unsubscribe() - done() - }, DOM_MUTATION_OBSERVABLE_DURATION) - } + return () => + new Promise((resolve) => { + const root = document.createElement('div') + root.setAttribute('data-test', 'foo') + root.appendChild(document.createElement('span')) + root.appendChild(document.createTextNode('foo')) + document.body.appendChild(root) + + const domMutationObservable = createDOMMutationObservable() + + let counter = 0 + const domMutationSubscription = domMutationObservable.subscribe(() => (counter += 1)) + + mutate(root) + + setTimeout(() => { + expect(counter).toBe(expectedMutations) + root.parentNode!.removeChild(root) + domMutationSubscription.unsubscribe() + resolve() + }, DOM_MUTATION_OBSERVABLE_DURATION) + }) } it( diff --git a/packages/browser-rum-core/src/browser/htmlDomUtils.spec.ts b/packages/browser-rum-core/src/browser/htmlDomUtils.spec.ts index d60d646b42..133ea41554 100644 --- a/packages/browser-rum-core/src/browser/htmlDomUtils.spec.ts +++ b/packages/browser-rum-core/src/browser/htmlDomUtils.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { appendElement, appendText } from '../../test' import { isTextNode, @@ -151,7 +152,7 @@ describe('forEachChildNodes', () => {
toto
`) - const spy = jasmine.createSpy() + const spy = vi.fn() forEachChildNodes(container, spy) expect(spy).toHaveBeenCalledTimes(3) }) @@ -160,21 +161,21 @@ describe('forEachChildNodes', () => { const container = appendElement('
') const shadowRoot = container.attachShadow({ mode: 'open' }) - const spy = jasmine.createSpy() + const spy = vi.fn() forEachChildNodes(container, spy) expect(spy).toHaveBeenCalledTimes(1) - expect(spy.calls.argsFor(0)[0]).toBe(shadowRoot) + expect(spy.mock.calls[0][0]).toBe(shadowRoot) }) it('should iterate over the the shadow root and direct children for a node that is a host', () => { const container = appendElement('
') const shadowRoot = container.attachShadow({ mode: 'open' }) - const spy = jasmine.createSpy() + const spy = vi.fn() forEachChildNodes(container, spy) expect(spy).toHaveBeenCalledTimes(2) - expect(spy.calls.argsFor(0)[0]).toBe(container.childNodes[0]) - expect(spy.calls.argsFor(1)[0]).toBe(shadowRoot) + expect(spy.mock.calls[0][0]).toBe(container.childNodes[0]) + expect(spy.mock.calls[1][0]).toBe(shadowRoot) }) }) diff --git a/packages/browser-rum-core/src/browser/locationChangeObservable.spec.ts b/packages/browser-rum-core/src/browser/locationChangeObservable.spec.ts index 910c254308..61875a2382 100644 --- a/packages/browser-rum-core/src/browser/locationChangeObservable.spec.ts +++ b/packages/browser-rum-core/src/browser/locationChangeObservable.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { globalObject } from '@datadog/browser-core' import { buildLocation, registerCleanupTask, replaceMockable } from '@datadog/browser-core/test' import { createLocationChangeObservable } from './locationChangeObservable' @@ -8,26 +9,27 @@ describe('locationChangeObservable', () => { history.pushState({}, '', '/foo?bar=qux') - const locationChanges = observer.calls.argsFor(0)[0] + const locationChanges = observer.mock.calls[0][0] expect(locationChanges.oldLocation.href).toMatch(/\/foo$/) expect(locationChanges.newLocation.href).toMatch(/\/foo\?bar=qux$/) }) - it('should notify observers on hashchange', (done) => { - const observer = setup() + it('should notify observers on hashchange', () => + new Promise((resolve) => { + const observer = setup() - function hashChangeCallback() { - const locationChanges = observer.calls.argsFor(0)[0] - expect(locationChanges.oldLocation.href).toMatch(/\/foo$/) - expect(locationChanges.newLocation.href).toMatch(/\/foo#bar$/) + function hashChangeCallback() { + const locationChanges = observer.mock.calls[0][0] + expect(locationChanges.oldLocation.href).toMatch(/\/foo$/) + expect(locationChanges.newLocation.href).toMatch(/\/foo#bar$/) - window.removeEventListener('hashchange', hashChangeCallback) - done() - } - window.addEventListener('hashchange', hashChangeCallback) + window.removeEventListener('hashchange', hashChangeCallback) + resolve() + } + window.addEventListener('hashchange', hashChangeCallback) - window.location.hash = '#bar' - }) + window.location.hash = '#bar' + })) it('should not notify if the url has not changed', () => { const observer = setup() @@ -45,7 +47,7 @@ describe('locationChangeObservable', () => { Object.assign(fakeLocation, buildLocation('/fake-location?bar=qux')) history.pushState({}, '', '/foo?bar=qux') - const locationChanges = observer.calls.argsFor(0)[0] + const locationChanges = observer.mock.calls[0][0] expect(locationChanges.oldLocation.href).toMatch(/\/fake-location$/) expect(locationChanges.newLocation.href).toMatch(/\/fake-location\?bar=qux$/) }) @@ -56,7 +58,7 @@ describe('locationChangeObservable', () => { history.pushState({}, '', '/foo?bar=qux') - const locationChanges = observer.calls.argsFor(0)[0] + const locationChanges = observer.mock.calls[0][0] expect(locationChanges.oldLocation.href).toMatch(/\/foo$/) expect(locationChanges.newLocation.href).toMatch(/\/foo\?bar=qux$/) expect(wrapperSpy).toHaveBeenCalled() @@ -68,7 +70,7 @@ describe('locationChangeObservable', () => { history.pushState({}, '', '/foo?bar=qux') - const locationChanges = observer.calls.argsFor(0)[0] + const locationChanges = observer.mock.calls[0][0] expect(locationChanges.oldLocation.href).toMatch(/\/foo$/) expect(locationChanges.newLocation.href).toMatch(/\/foo\?bar=qux$/) expect(wrapperSpy).toHaveBeenCalled() @@ -81,7 +83,7 @@ function setup() { history.pushState({}, '', '/foo') const observable = createLocationChangeObservable() - const observer = jasmine.createSpy('obs') + const observer = vi.fn() const subscription = observable.subscribe(observer) registerCleanupTask(() => { @@ -93,7 +95,7 @@ function setup() { } function setupHistoryInstancePushStateWrapper() { - const wrapperSpy = jasmine.createSpy('wrapperSpy') + const wrapperSpy = vi.fn() const originalPushState = history.pushState.bind(history) history.pushState = (...args) => { @@ -110,7 +112,7 @@ function setupHistoryInstancePushStateWrapper() { } function setupHistoryPrototypePushStateWrapper() { - const wrapperSpy = jasmine.createSpy('wrapperSpy') + const wrapperSpy = vi.fn() const originalPushState = History.prototype.pushState.bind(history) History.prototype.pushState = (...args) => { diff --git a/packages/browser-rum-core/src/browser/performanceObservable.spec.ts b/packages/browser-rum-core/src/browser/performanceObservable.spec.ts index 68dd980ef6..e4036fed98 100644 --- a/packages/browser-rum-core/src/browser/performanceObservable.spec.ts +++ b/packages/browser-rum-core/src/browser/performanceObservable.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Subscription } from '@datadog/browser-core' import type { Duration } from '@datadog/js-core/time' import type { Clock } from '@datadog/browser-core/test' @@ -9,11 +10,11 @@ describe('performanceObservable', () => { let performanceSubscription: Subscription | undefined const forbiddenUrl = 'https://forbidden.url/abce?ddsource=browser&dd-api-key=xxxx&dd-request-id=1234567890' const allowedUrl = 'https://allowed.url' - let observableCallback: jasmine.Spy + let observableCallback: Mock let clock: Clock beforeEach(() => { - observableCallback = jasmine.createSpy() + observableCallback = vi.fn() clock = mockClock() }) @@ -29,7 +30,7 @@ describe('performanceObservable', () => { performanceSubscription = performanceResourceObservable.subscribe(observableCallback) notifyPerformanceEntries([createPerformanceEntry(RumPerformanceEntryType.RESOURCE, { name: allowedUrl })]) - expect(observableCallback).toHaveBeenCalledWith([jasmine.objectContaining({ name: allowedUrl })]) + expect(observableCallback).toHaveBeenCalledWith([expect.objectContaining({ name: allowedUrl })]) }) it('should not notify performance resources with intake url', () => { @@ -65,6 +66,6 @@ describe('performanceObservable', () => { performanceSubscription = performanceResourceObservable.subscribe(observableCallback) expect(observableCallback).not.toHaveBeenCalled() clock.tick(0) - expect(observableCallback).toHaveBeenCalledWith([jasmine.objectContaining({ name: allowedUrl })]) + expect(observableCallback).toHaveBeenCalledWith([expect.objectContaining({ name: allowedUrl })]) }) }) diff --git a/packages/browser-rum-core/src/browser/performanceUtils.spec.ts b/packages/browser-rum-core/src/browser/performanceUtils.spec.ts index d7cf09c700..26f043e38d 100644 --- a/packages/browser-rum-core/src/browser/performanceUtils.spec.ts +++ b/packages/browser-rum-core/src/browser/performanceUtils.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { type RelativeTime } from '@datadog/js-core/time' import { registerCleanupTask } from '@datadog/browser-core/test' import { createPerformanceEntry, mockGlobalPerformanceBuffer } from '../../test' @@ -9,47 +10,47 @@ describe('getNavigationEntry', () => { it('returns the navigation entry', () => { // Declare the expected value here, so TypeScript can make sure all expected fields are covered, // even though the actual value contains more fields. - const expectation: jasmine.Expected = { + const expectation: RumPerformanceNavigationTiming = { entryType: RumPerformanceEntryType.NAVIGATION, initiatorType: 'navigation', - name: jasmine.any(String), + name: expect.any(String), - domComplete: jasmine.any(Number), - domContentLoadedEventEnd: jasmine.any(Number), - domInteractive: jasmine.any(Number), - loadEventEnd: jasmine.any(Number), + domComplete: expect.any(Number), + domContentLoadedEventEnd: expect.any(Number), + domInteractive: expect.any(Number), + loadEventEnd: expect.any(Number), startTime: 0 as RelativeTime, - duration: jasmine.any(Number), - - fetchStart: jasmine.any(Number), - workerStart: jasmine.any(Number), - domainLookupStart: jasmine.any(Number), - domainLookupEnd: jasmine.any(Number), - connectStart: jasmine.any(Number), - secureConnectionStart: jasmine.any(Number), - connectEnd: jasmine.any(Number), - requestStart: jasmine.any(Number), - responseStart: jasmine.any(Number), - responseEnd: jasmine.any(Number), - redirectStart: jasmine.any(Number), - redirectEnd: jasmine.any(Number), - - toJSON: jasmine.any(Function), + duration: expect.any(Number), + + fetchStart: expect.any(Number), + workerStart: expect.any(Number), + domainLookupStart: expect.any(Number), + domainLookupEnd: expect.any(Number), + connectStart: expect.any(Number), + secureConnectionStart: expect.any(Number), + connectEnd: expect.any(Number), + requestStart: expect.any(Number), + responseStart: expect.any(Number), + responseEnd: expect.any(Number), + redirectStart: expect.any(Number), + redirectEnd: expect.any(Number), + + toJSON: expect.any(Function), } const navigationEntry = getNavigationEntry() - expect(navigationEntry).toEqual(jasmine.objectContaining(expectation)) + expect(navigationEntry).toEqual(expect.objectContaining(expectation)) if (navigationEntry.decodedBodySize) { - expect(navigationEntry.decodedBodySize).toEqual(jasmine.any(Number)) + expect(navigationEntry.decodedBodySize).toEqual(expect.any(Number)) } if (navigationEntry.encodedBodySize) { - expect(navigationEntry.encodedBodySize).toEqual(jasmine.any(Number)) + expect(navigationEntry.encodedBodySize).toEqual(expect.any(Number)) } if (navigationEntry.transferSize) { - expect(navigationEntry.transferSize).toEqual(jasmine.any(Number)) + expect(navigationEntry.transferSize).toEqual(expect.any(Number)) } }) diff --git a/packages/browser-rum-core/src/browser/scroll.spec.ts b/packages/browser-rum-core/src/browser/scroll.spec.ts index 27abc0f3c2..ac057a308a 100644 --- a/packages/browser-rum-core/src/browser/scroll.spec.ts +++ b/packages/browser-rum-core/src/browser/scroll.spec.ts @@ -1,4 +1,5 @@ -import { waitAfterNextPaint } from '../../../browser-core/test' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { waitAfterNextPaint } from '@datadog/browser-core/test' import { getScrollX, getScrollY } from './scroll' describe('scroll', () => { @@ -24,7 +25,12 @@ describe('scroll', () => { expect(getScrollY()).toBe(Math.round(window.scrollY || window.pageYOffset)) }) - it('normalized scroll updates when scrolled', () => { + it('normalized scroll updates when scrolled', (ctx) => { + ctx.skip( + navigator.userAgent.includes('Firefox'), + 'Firefox on BrowserStack returns subpixel scroll values (e.g. 99.95 instead of 100)' + ) + const SCROLL_DOWN_PX = 100 window.scrollTo(0, SCROLL_DOWN_PX) diff --git a/packages/browser-rum-core/src/browser/viewportObservable.spec.ts b/packages/browser-rum-core/src/browser/viewportObservable.spec.ts index c951bfba58..5c85fafe56 100644 --- a/packages/browser-rum-core/src/browser/viewportObservable.spec.ts +++ b/packages/browser-rum-core/src/browser/viewportObservable.spec.ts @@ -1,6 +1,7 @@ -import type { Subscription } from '@datadog/browser-core/src/tools/observable' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { Clock } from '@datadog/browser-core/test' -import { mockClock, createNewEvent, registerCleanupTask, waitAfterNextPaint } from '@datadog/browser-core/test' +import { mockClock, createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' +import type { Subscription } from '@datadog/browser-core/src/tools/observable' import type { ViewportDimension } from './viewportObservable' import { getViewportDimension, initViewportObservable } from './viewportObservable' @@ -24,7 +25,7 @@ describe('viewportObservable', () => { window.dispatchEvent(createNewEvent('resize')) clock.tick(200) - expect(viewportDimension).toEqual({ width: jasmine.any(Number), height: jasmine.any(Number) }) + expect(viewportDimension).toEqual({ width: expect.any(Number), height: expect.any(Number) }) }) describe('get layout width and height has similar native behaviour', () => { @@ -37,17 +38,16 @@ describe('viewportObservable', () => { // Add scrollbars document.body.style.setProperty('margin-bottom', '5000px') document.body.style.setProperty('margin-right', '5000px') - registerCleanupTask(async () => { + registerCleanupTask(() => { document.body.style.removeProperty('margin-bottom') document.body.style.removeProperty('margin-right') - await waitAfterNextPaint() }) expect([ // Some devices don't follow specification of including scrollbars { width: window.innerWidth, height: window.innerHeight }, { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }, - ]).toContain(getViewportDimension()) + ]).toContainEqual(getViewportDimension()) }) }) }) diff --git a/packages/browser-rum-core/src/browser/windowOpenObservable.spec.ts b/packages/browser-rum-core/src/browser/windowOpenObservable.spec.ts index 2e065dab39..7a5eab2471 100644 --- a/packages/browser-rum-core/src/browser/windowOpenObservable.spec.ts +++ b/packages/browser-rum-core/src/browser/windowOpenObservable.spec.ts @@ -1,11 +1,12 @@ +import { vi, describe, expect, it } from 'vitest' import { registerCleanupTask } from '@datadog/browser-core/test' import { createWindowOpenObservable } from './windowOpenObservable' describe('windowOpenObservable', () => { it('should notify observer on `window.open` call', () => { const original = window.open - window.open = jasmine.createSpy() - const spy = jasmine.createSpy() + window.open = vi.fn() + const spy = vi.fn() const { observable, stop } = createWindowOpenObservable() const { unsubscribe } = observable.subscribe(spy) diff --git a/packages/browser-rum-core/src/domain/action/actionCollection.spec.ts b/packages/browser-rum-core/src/domain/action/actionCollection.spec.ts index e2f8738acf..7769eadf34 100644 --- a/packages/browser-rum-core/src/domain/action/actionCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/action/actionCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime, Duration, ServerDuration, TimeStamp } from '@datadog/js-core/time' import { addDuration } from '@datadog/js-core/time' import { Observable } from '@datadog/browser-core' @@ -87,7 +88,7 @@ describe('actionCollection', () => { }, type: ActionType.CLICK, }, - date: jasmine.any(Number), + date: expect.any(Number), type: RumEventType.ACTION, _dd: { action: { @@ -121,7 +122,7 @@ describe('actionCollection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(1234 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ action: { - id: jasmine.any(String), + id: expect.any(String), target: { name: 'foo', }, @@ -130,7 +131,7 @@ describe('actionCollection', () => { type: [], }, }, - date: jasmine.any(Number), + date: expect.any(Number), type: RumEventType.ACTION, context: { foo: 'bar' }, }) @@ -175,7 +176,7 @@ describe('actionCollection', () => { ;[RumEventType.RESOURCE, RumEventType.LONG_TASK, RumEventType.ERROR].forEach((eventType) => { it(`should add action properties on ${eventType} from the context`, () => { const actionId = ['1'] - spyOn(actionContexts, 'findActionId').and.returnValue(actionId) + vi.spyOn(actionContexts, 'findActionId').mockReturnValue(actionId) const defaultRumEventAttributes = hooks.assemble.trigger({ eventType, startTime: 0 as RelativeTime, @@ -187,7 +188,7 @@ describe('actionCollection', () => { ;[RumEventType.VIEW, RumEventType.VITAL].forEach((eventType) => { it(`should not add action properties on ${eventType} from the context`, () => { const actionId = ['1'] - spyOn(actionContexts, 'findActionId').and.returnValue(actionId) + vi.spyOn(actionContexts, 'findActionId').mockReturnValue(actionId) const defaultRumEventAttributes = hooks.assemble.trigger({ eventType, startTime: 0 as RelativeTime, @@ -199,7 +200,7 @@ describe('actionCollection', () => { it('should add action properties on long task from the context when the start time is slightly before the action start time', () => { const longTaskStartTime = 100 as RelativeTime - const findActionIdSpy = spyOn(actionContexts, 'findActionId').and.returnValue([]) + const findActionIdSpy = vi.spyOn(actionContexts, 'findActionId').mockReturnValue([]) hooks.assemble.trigger({ eventType: RumEventType.LONG_TASK, @@ -207,7 +208,7 @@ describe('actionCollection', () => { duration: 50 as Duration, } as AssembleHookParams) - const [correctedStartTime] = findActionIdSpy.calls.mostRecent().args + const [correctedStartTime] = findActionIdSpy.mock.calls[findActionIdSpy.mock.calls.length - 1] expect(correctedStartTime).toEqual(addDuration(longTaskStartTime, LONG_TASK_START_TIME_CORRECTION)) }) }) @@ -215,7 +216,7 @@ describe('actionCollection', () => { describe('assemble telemetry hook', () => { it('should add action id', () => { const actionId = ['1'] - spyOn(actionContexts, 'findActionId').and.returnValue(actionId) + vi.spyOn(actionContexts, 'findActionId').mockReturnValue(actionId) const telemetryEventAttributes = hooks.assembleTelemetry.trigger({ startTime: 0 as RelativeTime, }) as DefaultTelemetryEventAttributes @@ -224,7 +225,7 @@ describe('actionCollection', () => { }) it('should not add action id if the action is not found', () => { - spyOn(actionContexts, 'findActionId').and.returnValue([]) + vi.spyOn(actionContexts, 'findActionId').mockReturnValue([]) const telemetryEventAttributes = hooks.assembleTelemetry.trigger({ startTime: 0 as RelativeTime, }) as DefaultTelemetryEventAttributes diff --git a/packages/browser-rum-core/src/domain/action/clickChain.spec.ts b/packages/browser-rum-core/src/domain/action/clickChain.spec.ts index b371f27874..f2588a9ded 100644 --- a/packages/browser-rum-core/src/domain/action/clickChain.spec.ts +++ b/packages/browser-rum-core/src/domain/action/clickChain.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' import { createFakeClick } from '../../../test' @@ -7,11 +8,11 @@ import { MAX_DISTANCE_BETWEEN_CLICKS, MAX_DURATION_BETWEEN_CLICKS, createClickCh describe('createClickChain', () => { let clickChain: ClickChain | undefined let clock: Clock - let onFinalizeSpy: jasmine.Spy + let onFinalizeSpy: Mock beforeEach(() => { clock = mockClock() - onFinalizeSpy = jasmine.createSpy('onFinalize') + onFinalizeSpy = vi.fn() }) afterEach(() => { @@ -21,8 +22,8 @@ describe('createClickChain', () => { it('creates a click chain', () => { clickChain = createClickChain(createFakeClick(), onFinalizeSpy) expect(clickChain).toEqual({ - tryAppend: jasmine.any(Function), - stop: jasmine.any(Function), + tryAppend: expect.any(Function), + stop: expect.any(Function), }) }) diff --git a/packages/browser-rum-core/src/domain/action/computeFrustration.spec.ts b/packages/browser-rum-core/src/domain/action/computeFrustration.spec.ts index a43dd813a4..e31fa5794d 100644 --- a/packages/browser-rum-core/src/domain/action/computeFrustration.spec.ts +++ b/packages/browser-rum-core/src/domain/action/computeFrustration.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { ONE_SECOND } from '@datadog/js-core/time' import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' @@ -18,8 +19,8 @@ describe('computeFrustration', () => { }) it('returns whether the clicks are considered as rage', () => { - expect(computeFrustration(clicksConsideredAsRage, rageClick).isRage).toBeTrue() - expect(computeFrustration(clicks, rageClick).isRage).toBeFalse() + expect(computeFrustration(clicksConsideredAsRage, rageClick).isRage).toBe(true) + expect(computeFrustration(clicks, rageClick).isRage).toBe(false) }) describe('if clicks are considered as rage', () => { @@ -76,7 +77,7 @@ describe('computeFrustration', () => { }) function getFrustrations(click: FakeClick) { - return click.addFrustration.calls.allArgs().map((args) => args[0]) + return click.addFrustration.mock.calls.map((args) => args[0]) } }) diff --git a/packages/browser-rum-core/src/domain/action/getActionNameFromElement.spec.ts b/packages/browser-rum-core/src/domain/action/getActionNameFromElement.spec.ts index 17e7e22626..00226d7010 100644 --- a/packages/browser-rum-core/src/domain/action/getActionNameFromElement.spec.ts +++ b/packages/browser-rum-core/src/domain/action/getActionNameFromElement.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { appendElement, mockRumConfiguration } from '../../../test' import { NodePrivacyLevel } from '../privacyConstants' import { getNodeSelfPrivacyLevel } from '../privacy' diff --git a/packages/browser-rum-core/src/domain/action/interactionSelectorCache.spec.ts b/packages/browser-rum-core/src/domain/action/interactionSelectorCache.spec.ts index ff19c3582e..20443b67be 100644 --- a/packages/browser-rum-core/src/domain/action/interactionSelectorCache.spec.ts +++ b/packages/browser-rum-core/src/domain/action/interactionSelectorCache.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { relativeNow } from '@datadog/js-core/time' import { mockClock } from '@datadog/browser-core/test' import type { Clock } from '@datadog/browser-core/test' diff --git a/packages/browser-rum-core/src/domain/action/listenActionEvents.spec.ts b/packages/browser-rum-core/src/domain/action/listenActionEvents.spec.ts index 78a7cecfe8..d63ca6b2af 100644 --- a/packages/browser-rum-core/src/domain/action/listenActionEvents.spec.ts +++ b/packages/browser-rum-core/src/domain/action/listenActionEvents.spec.ts @@ -1,18 +1,19 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent } from '@datadog/browser-core/test' import type { ActionEventsHooks } from './listenActionEvents' import { listenActionEvents } from './listenActionEvents' describe('listenActionEvents', () => { let actionEventsHooks: { - onPointerUp: jasmine.Spy['onPointerUp']> - onPointerDown: jasmine.Spy['onPointerDown']> + onPointerUp: Mock['onPointerUp']> + onPointerDown: Mock['onPointerDown']> } let stopListenEvents: () => void beforeEach(() => { actionEventsHooks = { - onPointerUp: jasmine.createSpy(), - onPointerDown: jasmine.createSpy().and.returnValue({}), + onPointerUp: vi.fn(), + onPointerDown: vi.fn().mockReturnValue({}), } ;({ stop: stopListenEvents } = listenActionEvents(actionEventsHooks)) }) @@ -23,7 +24,10 @@ describe('listenActionEvents', () => { it('listen to pointerdown events', () => { emulateClick() - expect(actionEventsHooks.onPointerDown).toHaveBeenCalledOnceWith(jasmine.objectContaining({ type: 'pointerdown' })) + expect(actionEventsHooks.onPointerDown).toHaveBeenCalledTimes(1) + expect(actionEventsHooks.onPointerDown).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ type: 'pointerdown' }) + ) }) it('ignore non-primary pointerdown events', () => { @@ -37,10 +41,11 @@ describe('listenActionEvents', () => { it('listen to pointerup events', () => { emulateClick() - expect(actionEventsHooks.onPointerUp).toHaveBeenCalledOnceWith( + expect(actionEventsHooks.onPointerUp).toHaveBeenCalledTimes(1) + expect(actionEventsHooks.onPointerUp).toHaveBeenCalledExactlyOnceWith( {}, - jasmine.objectContaining({ type: 'pointerup' }), - jasmine.any(Function) + expect.objectContaining({ type: 'pointerup' }), + expect.any(Function) ) }) @@ -50,21 +55,21 @@ describe('listenActionEvents', () => { }) it('can abort click lifecycle by returning undefined from the onPointerDown callback', () => { - actionEventsHooks.onPointerDown.and.returnValue(undefined) + actionEventsHooks.onPointerDown.mockReturnValue(undefined) emulateClick() expect(actionEventsHooks.onPointerUp).not.toHaveBeenCalled() }) it('passes the context created in onPointerDown to onPointerUp', () => { const context = {} - actionEventsHooks.onPointerDown.and.returnValue(context) + actionEventsHooks.onPointerDown.mockReturnValue(context) emulateClick() - expect(actionEventsHooks.onPointerUp.calls.mostRecent().args[0]).toBe(context) + expect(actionEventsHooks.onPointerUp.mock.lastCall![0]).toBe(context) }) it('ignore "click" events if no "pointerdown" event happened since the previous "click" event', () => { emulateClick() - actionEventsHooks.onPointerUp.calls.reset() + actionEventsHooks.onPointerUp.mockClear() window.dispatchEvent(createNewEvent('click', { target: document.body })) @@ -140,7 +145,7 @@ describe('listenActionEvents', () => { }) function hasSelectionChanged() { - return actionEventsHooks.onPointerUp.calls.mostRecent().args[2]().selection + return actionEventsHooks.onPointerUp.mock.lastCall![2]().selection } function emulateNodeSelection( @@ -200,7 +205,7 @@ describe('listenActionEvents', () => { window.dispatchEvent(createNewEvent('input')) } function hasInputUserActivity() { - return actionEventsHooks.onPointerUp.calls.mostRecent().args[2]().input + return actionEventsHooks.onPointerUp.mock.lastCall![2]().input } }) @@ -235,7 +240,7 @@ describe('listenActionEvents', () => { window.dispatchEvent(createNewEvent('scroll')) } function hasScrollUserActivity() { - return actionEventsHooks.onPointerUp.calls.mostRecent().args[2]().scroll + return actionEventsHooks.onPointerUp.mock.lastCall![2]().scroll } }) diff --git a/packages/browser-rum-core/src/domain/action/trackClickActions.spec.ts b/packages/browser-rum-core/src/domain/action/trackClickActions.spec.ts index af2294b97c..a404169623 100644 --- a/packages/browser-rum-core/src/domain/action/trackClickActions.spec.ts +++ b/packages/browser-rum-core/src/domain/action/trackClickActions.spec.ts @@ -1,3 +1,4 @@ +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' import { timeStampNow, addDuration, clocksNow, relativeNow } from '@datadog/js-core/time' import type { Duration, RelativeTime } from '@datadog/js-core/time' import { DefaultPrivacyLevel, Observable, PageExitReason } from '@datadog/browser-core' @@ -105,14 +106,14 @@ describe('trackClickActions', () => { clock.tick(EXPIRE_DELAY) const domEvent = createNewEvent('pointerup', { target: document.createElement('button') }) expect(events).toEqual([ - jasmine.objectContaining({ - counts: jasmine.objectContaining({ + expect.objectContaining({ + counts: expect.objectContaining({ errorCount: 0, longTaskCount: 0, resourceCount: 0, }), duration: BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY as Duration, - id: jasmine.any(String), + id: expect.any(String), name: 'Click me', nameSource: ActionNameSource.TEXT_CONTENT, startClocks: { @@ -126,7 +127,7 @@ describe('trackClickActions', () => { selector: '#button', width: 100, height: 100, - composedPathSelector: jasmine.any(String), + composedPathSelector: expect.any(String), }, position: { x: 50, y: 50 }, events: [domEvent], @@ -158,7 +159,7 @@ describe('trackClickActions', () => { expect(events.length).toBe(1) const clickAction = events[0] expect(clickAction.counts).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ errorCount: 2, longTaskCount: 0, resourceCount: 0, @@ -364,12 +365,9 @@ describe('trackClickActions', () => { clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toHaveLength(3) expect(events[0].frustrationTypes).toEqual( - jasmine.arrayWithExactContents([ - FrustrationType.DEAD_CLICK, - FrustrationType.ERROR_CLICK, - FrustrationType.RAGE_CLICK, - ]) + expect.arrayContaining([FrustrationType.DEAD_CLICK, FrustrationType.ERROR_CLICK, FrustrationType.RAGE_CLICK]) ) }) }) @@ -394,8 +392,9 @@ describe('trackClickActions', () => { clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toHaveLength(2) expect(events[0].frustrationTypes).toEqual( - jasmine.arrayWithExactContents([FrustrationType.ERROR_CLICK, FrustrationType.DEAD_CLICK]) + expect.arrayContaining([FrustrationType.ERROR_CLICK, FrustrationType.DEAD_CLICK]) ) }) }) @@ -597,7 +596,7 @@ describe('trackClickActions', () => { if (activity && (activity.on ?? 'click') === event) { const delay = activity.delay ?? BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY if (delay < 0) { - // Do not use `.tick()` here because negative clock tick does not work since jasmine 4: https://github.com/jasmine/jasmine/pull/1948 + // Timer advancement cannot move the clock backwards, so set the date directly. clock!.setDate(new Date(Date.now() + delay)) } else { clock!.tick(delay) diff --git a/packages/browser-rum-core/src/domain/action/trackManualActions.spec.ts b/packages/browser-rum-core/src/domain/action/trackManualActions.spec.ts index 479fe2fdf7..aa0e30f99d 100644 --- a/packages/browser-rum-core/src/domain/action/trackManualActions.spec.ts +++ b/packages/browser-rum-core/src/domain/action/trackManualActions.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { Duration, ServerDuration } from '@datadog/js-core/time' import { Observable } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' @@ -17,6 +18,7 @@ describe('trackManualActions', () => { let actionContexts: ActionContexts let startAction: ReturnType['startAction'] let stopAction: ReturnType['stopAction'] + let stopActionCollection: ReturnType['stop'] let clock: Clock beforeEach(() => { @@ -36,6 +38,7 @@ describe('trackManualActions', () => { registerCleanupTask(actionCollection.stop) startAction = actionCollection.startAction stopAction = actionCollection.stopAction + stopActionCollection = actionCollection.stop actionContexts = actionCollection.actionContexts rawRumEvents = collectAndValidateRawRumEvents(lifeCycle) @@ -47,12 +50,12 @@ describe('trackManualActions', () => { clock.tick(500) stopAction('user_login') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) expect(rawRumEvents[0].duration).toBe(500 as Duration) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ type: RumEventType.ACTION, - action: jasmine.objectContaining({ + action: expect.objectContaining({ target: { name: 'user_login' }, type: ActionType.CUSTOM, }), @@ -60,6 +63,20 @@ describe('trackManualActions', () => { ) }) + it('should not create action if stopped without starting', () => { + stopAction('never_started') + + expect(rawRumEvents).toHaveLength(0) + }) + + it('should only create action once when stopped multiple times', () => { + startAction('foo') + stopAction('foo') + stopAction('foo') + + expect(rawRumEvents).toHaveLength(1) + }) + it('should use consistent action ID from start to collected event', () => { startAction('checkout') @@ -68,7 +85,7 @@ describe('trackManualActions', () => { stopAction('checkout') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent expect(actionEvent.action.id).toEqual(actionId[0]) }) @@ -78,7 +95,7 @@ describe('trackManualActions', () => { clock.tick(500) stopAction('checkout') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent expect(actionEvent.action.loading_time).toBe((500 * 1e6) as ServerDuration) }) @@ -90,11 +107,11 @@ describe('trackManualActions', () => { startAction('test_action', { type: actionType }) stopAction('test_action') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ type: RumEventType.ACTION, - action: jasmine.objectContaining({ + action: expect.objectContaining({ type: actionType, }), }) @@ -112,20 +129,42 @@ describe('trackManualActions', () => { startAction('action3') stopAction('action3') - expect(rawRumEvents).toHaveSize(3) + expect(rawRumEvents).toHaveLength(3) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - action: jasmine.objectContaining({ type: ActionType.SCROLL }), + expect.objectContaining({ + action: expect.objectContaining({ type: ActionType.SCROLL }), }) ) expect(rawRumEvents[1].rawRumEvent).toEqual( - jasmine.objectContaining({ - action: jasmine.objectContaining({ type: ActionType.SWIPE }), + expect.objectContaining({ + action: expect.objectContaining({ type: ActionType.SWIPE }), }) ) expect(rawRumEvents[2].rawRumEvent).toEqual( - jasmine.objectContaining({ - action: jasmine.objectContaining({ type: ActionType.CUSTOM }), + expect.objectContaining({ + action: expect.objectContaining({ type: ActionType.CUSTOM }), + }) + ) + }) + }) + + describe('context merging', () => { + it('should merge contexts with stop precedence on conflicts', () => { + startAction('action1', { context: { cart: 'abc' } }) + stopAction('action1', { context: { total: 100 } }) + + startAction('action2', { context: { status: 'pending' } }) + stopAction('action2', { context: { status: 'complete' } }) + + expect(rawRumEvents).toHaveLength(2) + expect(rawRumEvents[0].rawRumEvent).toEqual( + expect.objectContaining({ + context: { cart: 'abc', total: 100 }, + }) + ) + expect(rawRumEvents[1].rawRumEvent).toEqual( + expect.objectContaining({ + context: { status: 'complete' }, }) ) }) @@ -142,7 +181,7 @@ describe('trackManualActions', () => { clock.tick(100) stopAction('click', { actionKey: 'button1' }) - expect(rawRumEvents).toHaveSize(2) + expect(rawRumEvents).toHaveLength(2) expect(rawRumEvents[0].duration).toBe(100 as Duration) expect(rawRumEvents[1].duration).toBe(200 as Duration) }) @@ -152,26 +191,50 @@ describe('trackManualActions', () => { startAction('foo', { actionKey: 'bar' }) const actionIds = actionContexts.findActionId() - expect(Array.isArray(actionIds)).toBeTrue() + expect(Array.isArray(actionIds)).toBe(true) expect(actionIds.length).toBe(2) stopAction('foo bar') stopAction('foo', { actionKey: 'bar' }) - expect(rawRumEvents).toHaveSize(2) + expect(rawRumEvents).toHaveLength(2) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - action: jasmine.objectContaining({ target: { name: 'foo bar' } }), + expect.objectContaining({ + action: expect.objectContaining({ target: { name: 'foo bar' } }), }) ) expect(rawRumEvents[1].rawRumEvent).toEqual( - jasmine.objectContaining({ - action: jasmine.objectContaining({ target: { name: 'foo' } }), + expect.objectContaining({ + action: expect.objectContaining({ target: { name: 'foo' } }), }) ) }) }) + describe('duplicate start handling', () => { + it('should clean up previous action when startAction is called twice with same key', () => { + startAction('checkout') + const firstActionId = actionContexts.findActionId() + expect(firstActionId).toBeDefined() + + clock.tick(100) + + startAction('checkout') + const secondActionId = actionContexts.findActionId() + expect(secondActionId).toBeDefined() + + expect(secondActionId).not.toEqual(firstActionId) + + clock.tick(200) + stopAction('checkout') + + expect(rawRumEvents).toHaveLength(1) + const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent + expect(actionEvent.action.id).toEqual(secondActionId[0]) + expect(rawRumEvents[0].duration).toBe(200 as Duration) + }) + }) + describe('event counting', () => { it('should include counts in the action event', () => { startAction('complex-action') @@ -197,7 +260,7 @@ describe('trackManualActions', () => { stopAction('complex-action') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent expect(actionEvent.action.error?.count).toBe(2) expect(actionEvent.action.resource?.count).toBe(1) @@ -205,6 +268,60 @@ describe('trackManualActions', () => { }) }) + describe('session renewal', () => { + it('should discard active manual actions on session renewal', () => { + startAction('cross-session-action') + + const actionIdBeforeRenewal = actionContexts.findActionId() + expect(actionIdBeforeRenewal).toBeDefined() + + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) + + expect(actionContexts.findActionId()).toHaveLength(0) + + stopAction('cross-session-action') + + expect(rawRumEvents).toHaveLength(0) + }) + + it('should stop event count subscriptions on session renewal', () => { + startAction('tracked-action') + + const actionId = actionContexts.findActionId() + + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { + type: RumEventType.ERROR, + action: { id: actionId }, + } as any) + + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) + + startAction('tracked-action') + stopAction('tracked-action') + + expect(rawRumEvents).toHaveLength(1) + const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent + expect(actionEvent.action.error?.count).toBe(0) + }) + }) + + describe('cleanup', () => { + it('should clean up active manual actions on stop()', () => { + startAction('active-when-stopped') + + const actionIdBeforeStop = actionContexts.findActionId() + expect(actionIdBeforeStop).toBeDefined() + + stopActionCollection() + + expect(actionContexts.findActionId()).toHaveLength(0) + + stopAction('active-when-stopped') + + expect(rawRumEvents).toHaveLength(0) + }) + }) + describe('frustration detection', () => { it('should include ERROR_CLICK frustration when action has errors', () => { startAction('error-action') @@ -218,7 +335,7 @@ describe('trackManualActions', () => { stopAction('error-action') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent expect(actionEvent.action.frustration?.type).toEqual([FrustrationType.ERROR_CLICK]) }) @@ -227,7 +344,7 @@ describe('trackManualActions', () => { startAction('success-action') stopAction('success-action') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent expect(actionEvent.action.frustration?.type).toEqual([]) }) @@ -248,7 +365,7 @@ describe('trackManualActions', () => { stopAction('multi-error-action') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const actionEvent = rawRumEvents[0].rawRumEvent as RawRumActionEvent expect(actionEvent.action.frustration?.type).toEqual([FrustrationType.ERROR_CLICK]) expect(actionEvent.action.error?.count).toBe(2) diff --git a/packages/browser-rum-core/src/domain/assembly.spec.ts b/packages/browser-rum-core/src/domain/assembly.spec.ts index 11db4d439c..5282afd8e8 100644 --- a/packages/browser-rum-core/src/domain/assembly.spec.ts +++ b/packages/browser-rum-core/src/domain/assembly.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { ONE_MINUTE, relativeToClocks } from '@datadog/js-core/time' import type { TimeStamp, ClocksState, RelativeTime } from '@datadog/js-core/time' import type { SessionManager } from '@datadog/browser-core' @@ -430,7 +431,7 @@ describe('rum assembly', () => { }, }) - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) notifyRawRumEvent(lifeCycle, { rawRumEvent: createRawRumEvent(RumEventType.VIEW, { view: { id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' }, @@ -558,7 +559,7 @@ describe('rum assembly', () => { rawRumEvent: createRawRumEvent(RumEventType.VIEW), }) - expect((serverRumEvents[0] as any).tab.id).toEqual(jasmine.any(String)) + expect((serverRumEvents[0] as any).tab.id).toEqual(expect.any(String)) }) }) @@ -583,7 +584,7 @@ describe('rum assembly', () => { it('should get session state from event start', () => { const sessionManager = createSessionManagerMock() - spyOn(sessionManager, 'findTrackedSession').and.callThrough() + vi.spyOn(sessionManager, 'findTrackedSession') const { lifeCycle } = setupAssemblyTestWithDefaults({ sessionManager }) notifyRawRumEvent(lifeCycle, { @@ -715,7 +716,7 @@ function setupAssemblyTestWithDefaults({ }: AssemblyTestParams = {}) { const lifeCycle = new LifeCycle() const hooks = createHooks() - const reportErrorSpy = jasmine.createSpy('reportError') + const reportErrorSpy = vi.fn() const rumSessionManager = sessionManager ?? createSessionManagerMock().setId('1234') const serverRumEvents: RumEvent[] = [] const subscription = lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (serverRumEvent) => { diff --git a/packages/browser-rum-core/src/domain/configuration/configuration.spec.ts b/packages/browser-rum-core/src/domain/configuration/configuration.spec.ts index ba3ea8ef7c..921e5537a8 100644 --- a/packages/browser-rum-core/src/domain/configuration/configuration.spec.ts +++ b/packages/browser-rum-core/src/domain/configuration/configuration.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { InitConfiguration } from '@datadog/browser-core' import { DefaultPrivacyLevel, display, TraceContextInjection } from '@datadog/browser-core' import type { @@ -17,12 +18,12 @@ import { const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx', applicationId: 'xxx' } describe('validateAndBuildRumConfiguration', () => { - let displayErrorSpy: jasmine.Spy - let displayWarnSpy: jasmine.Spy + let displayErrorSpy: Mock + let displayWarnSpy: Mock beforeEach(() => { - displayErrorSpy = spyOn(display, 'error') - displayWarnSpy = spyOn(display, 'warn') + displayErrorSpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) + displayWarnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) }) describe('applicationId', () => { @@ -30,7 +31,8 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, applicationId: undefined as any }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith( + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith( 'Application ID is not configured, no RUM data will be collected.' ) }) @@ -52,16 +54,18 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionReplaySampleRate: 'foo' as any }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith( + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith( 'Session Replay Sample Rate should be a number between 0 and 100' ) - displayErrorSpy.calls.reset() + displayErrorSpy.mockClear() expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionReplaySampleRate: 200 }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith( + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith( 'Session Replay Sample Rate should be a number between 0 and 100' ) }) @@ -82,11 +86,13 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, traceSampleRate: 'foo' as any }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Trace Sample Rate should be a number between 0 and 100') + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith('Trace Sample Rate should be a number between 0 and 100') - displayErrorSpy.calls.reset() + displayErrorSpy.mockClear() expect(validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, traceSampleRate: 200 })).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Trace Sample Rate should be a number between 0 and 100') + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith('Trace Sample Rate should be a number between 0 and 100') }) }) @@ -208,14 +214,16 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingUrls: ['foo'] }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Service needs to be configured when tracing is enabled') + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith('Service needs to be configured when tracing is enabled') }) it('does not validate the configuration if an incorrect value is provided', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingUrls: 'foo' as any }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Allowed Tracing URLs should be an array') + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith('Allowed Tracing URLs should be an array') }) }) @@ -250,55 +258,56 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, excludedActivityUrls: 'foo' as any }) ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Excluded Activity Urls should be an array') + expect(displayErrorSpy).toHaveBeenCalledTimes(1) + expect(displayErrorSpy).toHaveBeenCalledExactlyOnceWith('Excluded Activity Urls should be an array') }) }) describe('trackUserInteractions', () => { it('defaults to true', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackUserInteractions).toBeTrue() + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackUserInteractions).toBe(true) }) it('is set to provided value', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackUserInteractions: true })! .trackUserInteractions - ).toBeTrue() + ).toBe(true) expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackUserInteractions: false })! .trackUserInteractions - ).toBeFalse() + ).toBe(false) }) it('the provided value is cast to boolean', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackUserInteractions: 'foo' as any })! .trackUserInteractions - ).toBeTrue() + ).toBe(true) }) }) describe('trackViewsManually', () => { it('defaults to false', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackViewsManually).toBeFalse() + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackViewsManually).toBe(false) }) it('is set to provided value', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: true })! .trackViewsManually - ).toBeTrue() + ).toBe(true) expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: false })! .trackViewsManually - ).toBeFalse() + ).toBe(false) }) it('the provided value is cast to boolean', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: 'foo' as any })! .trackViewsManually - ).toBeTrue() + ).toBe(true) }) }) @@ -307,25 +316,25 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionReplaySampleRate: 0 })! .startSessionReplayRecordingManually - ).toBeTrue() + ).toBe(true) }) it('defaults to false if sessionReplaySampleRate is not 0', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionReplaySampleRate: 50 })! .startSessionReplayRecordingManually - ).toBeFalse() + ).toBe(false) }) it('is set to provided value', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: true })! .startSessionReplayRecordingManually - ).toBeTrue() + ).toBe(true) expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: false })! .startSessionReplayRecordingManually - ).toBeFalse() + ).toBe(false) }) it('the provided value is cast to boolean', () => { @@ -334,7 +343,7 @@ describe('validateAndBuildRumConfiguration', () => { ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: 'foo' as any, })!.startSessionReplayRecordingManually - ).toBeTrue() + ).toBe(true) }) }) @@ -377,36 +386,36 @@ describe('validateAndBuildRumConfiguration', () => { describe('enablePrivacyForActionName', () => { it('defaults to true', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.enablePrivacyForActionName).toBeTrue() + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.enablePrivacyForActionName).toBe(true) }) it('is false when the option is false', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, enablePrivacyForActionName: false })! .enablePrivacyForActionName - ).toBeFalse() + ).toBe(false) }) }) describe('trackResources', () => { it('defaults to true', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackResources).toBeTrue() + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackResources).toBe(true) }) it('is set to provided value', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResources: true })!.trackResources - ).toBeTrue() + ).toBe(true) expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResources: false })!.trackResources - ).toBeFalse() + ).toBe(false) }) it('the provided value is cast to boolean', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResources: 'foo' as any })! .trackResources - ).toBeTrue() + ).toBe(true) }) }) @@ -499,7 +508,8 @@ describe('validateAndBuildRumConfiguration', () => { validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: 42 as any })! .trackResourceHeaders ).toEqual([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( 'trackResourceHeaders should be true or an array of MatchHeader' ) }) @@ -509,7 +519,8 @@ describe('validateAndBuildRumConfiguration', () => { validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: [] })! .trackResourceHeaders ).toEqual([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( 'trackResourceHeaders is an empty array, no headers will be captured' ) }) @@ -521,7 +532,8 @@ describe('validateAndBuildRumConfiguration', () => { })!.trackResourceHeaders expect(result).toEqual([{ name: 'x-valid' }, { name: 'x-also-valid' }]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( "trackResourceHeaders[1] should be a MatchHeader object with a 'name' property" ) }) @@ -533,7 +545,8 @@ describe('validateAndBuildRumConfiguration', () => { })!.trackResourceHeaders expect(result).toEqual([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( "trackResourceHeaders[0] should be a MatchHeader object with a 'name' property" ) }) @@ -545,7 +558,8 @@ describe('validateAndBuildRumConfiguration', () => { })!.trackResourceHeaders expect(result).toEqual([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[0].url should be a MatchOption') + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith('trackResourceHeaders[0].url should be a MatchOption') }) it('warns and skips item with invalid location', () => { @@ -555,7 +569,8 @@ describe('validateAndBuildRumConfiguration', () => { })!.trackResourceHeaders expect(result).toEqual([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( "trackResourceHeaders[0].location should be 'request', 'response', or 'any'" ) }) @@ -567,30 +582,31 @@ describe('validateAndBuildRumConfiguration', () => { })!.trackResourceHeaders expect(result).toEqual([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[0].extractor should be a RegExp') + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith('trackResourceHeaders[0].extractor should be a RegExp') }) }) }) describe('trackLongTasks', () => { it('defaults to false', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackLongTasks).toBeTrue() + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackLongTasks).toBe(true) }) it('is set to provided value', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackLongTasks: true })!.trackLongTasks - ).toBeTrue() + ).toBe(true) expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackLongTasks: false })!.trackLongTasks - ).toBeFalse() + ).toBe(false) }) it('the provided value is cast to boolean', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackLongTasks: 'foo' as any })! .trackLongTasks - ).toBeTrue() + ).toBe(true) }) }) @@ -621,8 +637,9 @@ describe('validateAndBuildRumConfiguration', () => { { match: 'final', propagatorTypes: ['b3multi'] }, ], } + expect(serializeRumConfiguration(complexTracingConfig).selected_tracing_propagators).toHaveLength(4) expect(serializeRumConfiguration(complexTracingConfig).selected_tracing_propagators).toEqual( - jasmine.arrayWithExactContents(['datadog', 'b3', 'b3multi', 'tracecontext']) + expect.arrayContaining(['datadog', 'b3', 'b3multi', 'tracecontext']) ) }) @@ -728,7 +745,8 @@ describe('validateAndBuildRumConfiguration', () => { ...DEFAULT_INIT_CONFIGURATION, trackFeatureFlagsForEvents: 123 as any, })! - expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackFeatureFlagsForEvents should be an array') + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith('trackFeatureFlagsForEvents should be an array') }) }) @@ -796,7 +814,8 @@ describe('validateAndBuildRumConfiguration', () => { ...DEFAULT_INIT_CONFIGURATION, allowedGraphQlUrls: 'not-an-array' as any, }) - expect(displayWarnSpy).toHaveBeenCalledOnceWith('allowedGraphQlUrls should be an array') + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith('allowedGraphQlUrls should be an array') }) }) }) diff --git a/packages/browser-rum-core/src/domain/configuration/jsonPathParser.spec.ts b/packages/browser-rum-core/src/domain/configuration/jsonPathParser.spec.ts index 636a0803e4..9b49123349 100644 --- a/packages/browser-rum-core/src/domain/configuration/jsonPathParser.spec.ts +++ b/packages/browser-rum-core/src/domain/configuration/jsonPathParser.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { parseJsonPath } from './jsonPathParser' describe('parseJsonPath', () => { diff --git a/packages/browser-rum-core/src/domain/configuration/remoteConfiguration.spec.ts b/packages/browser-rum-core/src/domain/configuration/remoteConfiguration.spec.ts index 005602126c..25fdcf0317 100644 --- a/packages/browser-rum-core/src/domain/configuration/remoteConfiguration.spec.ts +++ b/packages/browser-rum-core/src/domain/configuration/remoteConfiguration.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { ONE_MINUTE } from '@datadog/js-core/time' import { DefaultPrivacyLevel, display, setCookie, deleteCookie, createContextManager } from '@datadog/browser-core' import { INTAKE_SITE_US1 } from '@datadog/js-core/transport' @@ -134,7 +135,7 @@ describe('remoteConfiguration', () => { const COOKIE_NAME = 'unit_rc' const root = window as any - let displaySpy: jasmine.Spy + let displaySpy: Mock let supportedContextManagers: { user: ReturnType context: ReturnType @@ -159,7 +160,7 @@ describe('remoteConfiguration', () => { } beforeEach(() => { - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) supportedContextManagers = { user: createContextManager(), context: createContextManager() } metrics = initMetrics() }) @@ -724,7 +725,7 @@ describe('remoteConfiguration', () => { {} ) expect(metrics.get()).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ cookie: { success: 2, missing: 1 }, js: { success: 1 }, }) @@ -758,7 +759,7 @@ describe('remoteConfiguration', () => { context: ReturnType } let interceptor: ReturnType - let displaySpy: jasmine.Spy + let displaySpy: Mock function withCachedEntry(config: RumRemoteConfiguration) { localStorage.setItem(CACHE_KEY, JSON.stringify({ version: 1, config, fetchedAt: 1000 })) @@ -785,7 +786,7 @@ describe('remoteConfiguration', () => { } supportedContextManagers = { user: createContextManager(), context: createContextManager() } interceptor = interceptRequests() - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) as Mock registerCleanupTask(() => { localStorage.clear() diff --git a/packages/browser-rum-core/src/domain/configuration/remoteConfigurationCache.spec.ts b/packages/browser-rum-core/src/domain/configuration/remoteConfigurationCache.spec.ts index 67bc65e507..14f543f0d4 100644 --- a/packages/browser-rum-core/src/domain/configuration/remoteConfigurationCache.spec.ts +++ b/packages/browser-rum-core/src/domain/configuration/remoteConfigurationCache.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' import { registerCleanupTask, mockClock } from '@datadog/browser-core/test' import type { Clock } from '@datadog/browser-core/test' import type { RumRemoteConfiguration } from './remoteConfiguration' @@ -93,7 +94,9 @@ describe('remoteConfigurationCache', () => { }) it('should return error when localStorage.getItem throws', () => { - spyOn(Storage.prototype, 'getItem').and.throwError('SecurityError') + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('SecurityError') + }) const cache = createConfigurationCache({ remoteConfigurationId: REMOTE_CONFIGURATION_ID }) @@ -150,7 +153,9 @@ describe('remoteConfigurationCache', () => { }) it('should silently swallow localStorage.setItem errors', () => { - spyOn(Storage.prototype, 'setItem').and.throwError('QuotaExceededError') + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('QuotaExceededError') + }) const cache = createConfigurationCache({ remoteConfigurationId: REMOTE_CONFIGURATION_ID }) @@ -172,7 +177,9 @@ describe('remoteConfigurationCache', () => { }) it('should silently swallow localStorage.removeItem errors', () => { - spyOn(Storage.prototype, 'removeItem').and.throwError('SecurityError') + vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { + throw new Error('SecurityError') + }) const cache = createConfigurationCache({ remoteConfigurationId: REMOTE_CONFIGURATION_ID }) diff --git a/packages/browser-rum-core/src/domain/contexts/ciVisibilityContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/ciVisibilityContext.spec.ts index c4947ba1eb..01c9d237b2 100644 --- a/packages/browser-rum-core/src/domain/contexts/ciVisibilityContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/ciVisibilityContext.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { display, Observable } from '@datadog/browser-core' import { createHook } from '@datadog/js-core/assembly' @@ -108,7 +109,7 @@ describe('startCiVisibilityContext', () => { }) it('should not throw and emit a warning when Cypress.env throws', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) mockCiVisibilityValues(undefined, 'globals-throws') expect(() => { @@ -122,11 +123,11 @@ describe('startCiVisibilityContext', () => { expect(defaultRumEventAttributes).toBeUndefined() expect(displaySpy).toHaveBeenCalledTimes(1) - expect(displaySpy.calls.mostRecent().args[0]).toContain('5.88.0') + expect(displaySpy.mock.calls[displaySpy.mock.calls.length - 1][0]).toContain('5.88.0') }) it('should not emit a warning when Cypress.env returns a value', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) mockCiVisibilityValues('trace_id_value') ;({ stop: stopCiVisibility } = startCiVisibilityContext(hook, cookieObservable)) @@ -134,7 +135,7 @@ describe('startCiVisibilityContext', () => { }) it('should not emit a warning when the cookie is set', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) mockCiVisibilityValues('trace_id_value', 'cookies') ;({ stop: stopCiVisibility } = startCiVisibilityContext(hook, cookieObservable)) @@ -142,7 +143,7 @@ describe('startCiVisibilityContext', () => { }) it('should not emit a warning when Cypress is not present', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) ;({ stop: stopCiVisibility } = startCiVisibilityContext(hook, cookieObservable)) expect(displaySpy).not.toHaveBeenCalled() diff --git a/packages/browser-rum-core/src/domain/contexts/connectivityContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/connectivityContext.spec.ts index 7f8ef0edbd..ba5ca566c5 100644 --- a/packages/browser-rum-core/src/domain/contexts/connectivityContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/connectivityContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { setNavigatorOnLine, setNavigatorConnection } from '@datadog/browser-core/test' import type { RelativeTime } from '@datadog/js-core/time' import { createHook } from '@datadog/js-core/assembly' diff --git a/packages/browser-rum-core/src/domain/contexts/defaultContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/defaultContext.spec.ts index 80cb60cefc..c0c1f4d7d7 100644 --- a/packages/browser-rum-core/src/domain/contexts/defaultContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/defaultContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { mockClock, mockEventBridge } from '@datadog/browser-core/test' import { timeStampNow } from '@datadog/js-core/time' import type { RelativeTime } from '@datadog/js-core/time' @@ -29,9 +30,9 @@ describe('startDefaultContext', () => { }, date: timeStampNow(), source: 'browser', - _dd: jasmine.objectContaining({ + _dd: expect.objectContaining({ format_version: 2, - drift: jasmine.any(Number), + drift: expect.any(Number), }), }) }) diff --git a/packages/browser-rum-core/src/domain/contexts/displayContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/displayContext.spec.ts index a01cff969d..1399817eb6 100644 --- a/packages/browser-rum-core/src/domain/contexts/displayContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/displayContext.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { createHook } from '@datadog/js-core/assembly' import type { AssembleHook, AssembleHookParams } from '../hooks' @@ -6,12 +7,12 @@ import { startDisplayContext } from './displayContext' describe('displayContext', () => { let displayContext: DisplayContext - let requestAnimationFrameSpy: jasmine.Spy + let requestAnimationFrameSpy: Mock let hook: AssembleHook beforeEach(() => { hook = createHook() - requestAnimationFrameSpy = spyOn(window, 'requestAnimationFrame').and.callFake((callback) => { + requestAnimationFrameSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { callback(1) return 1 }) @@ -35,8 +36,8 @@ describe('displayContext', () => { type: 'view', display: { viewport: { - width: jasmine.any(Number), - height: jasmine.any(Number), + width: expect.any(Number), + height: expect.any(Number), }, }, }) diff --git a/packages/browser-rum-core/src/domain/contexts/featureFlagContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/featureFlagContext.spec.ts index 2f13b27b71..a01dca4664 100644 --- a/packages/browser-rum-core/src/domain/contexts/featureFlagContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/featureFlagContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { relativeToClocks } from '@datadog/js-core/time' import { createHook } from '@datadog/js-core/assembly' diff --git a/packages/browser-rum-core/src/domain/contexts/internalContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/internalContext.spec.ts index 52368d6aba..f928d31b7d 100644 --- a/packages/browser-rum-core/src/domain/contexts/internalContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/internalContext.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it, type Mock } from 'vitest' import { type RelativeTime } from '@datadog/js-core/time' import { noop, type SessionManager } from '@datadog/browser-core' import { buildLocation, createSessionManagerMock } from '@datadog/browser-core/test' @@ -7,15 +8,15 @@ import type { ViewHistory } from './viewHistory' import type { UrlContexts } from './urlContexts' describe('internal context', () => { - let findUrlSpy: jasmine.Spy - let findSessionSpy: jasmine.Spy + let findUrlSpy: Mock + let findSessionSpy: Mock let fakeLocation: Location let viewHistory: ViewHistory let actionContexts: ActionContexts function setupInternalContext(sessionManager: SessionManager) { viewHistory = { - findView: jasmine.createSpy('findView').and.returnValue({ + findView: vi.fn().mockReturnValue({ id: 'abcde', name: 'foo', }), @@ -23,7 +24,7 @@ describe('internal context', () => { } actionContexts = { - findActionId: jasmine.createSpy('findActionId').and.returnValue('7890'), + findActionId: vi.fn().mockReturnValue('7890'), } fakeLocation = buildLocation('/foo') @@ -35,8 +36,8 @@ describe('internal context', () => { }), stop: noop, } - findSessionSpy = spyOn(sessionManager, 'findTrackedSession').and.callThrough() - findUrlSpy = spyOn(urlContexts, 'findUrl').and.callThrough() + findSessionSpy = vi.spyOn(sessionManager, 'findTrackedSession') + findUrlSpy = vi.spyOn(urlContexts, 'findUrl') return startInternalContext('appId', sessionManager, viewHistory, actionContexts, urlContexts) } diff --git a/packages/browser-rum-core/src/domain/contexts/pageStateHistory.spec.ts b/packages/browser-rum-core/src/domain/contexts/pageStateHistory.spec.ts index 641583404f..0c9adca9e2 100644 --- a/packages/browser-rum-core/src/domain/contexts/pageStateHistory.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/pageStateHistory.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime, Duration, ServerDuration } from '@datadog/js-core/time' import { createHook } from '@datadog/js-core/assembly' import type { Clock } from '../../../../browser-core/test' @@ -126,7 +127,7 @@ describe('pageStateHistory', () => { } as AssembleHookParams) expect(defaultRumEventAttributes).toEqual({ type: 'view', - _dd: { page_states: jasmine.any(Array) }, + _dd: { page_states: expect.any(Array) }, }) }) @@ -243,10 +244,10 @@ describe('pageStateHistory', () => { pageStateHistory = startPageStateHistory(hook) registerCleanupTask(pageStateHistory.stop) - expect(pageStateHistory.wasInPageStateDuringPeriod(PageState.ACTIVE, 5 as RelativeTime, 5 as Duration)).toBeTrue() - expect( - pageStateHistory.wasInPageStateDuringPeriod(PageState.HIDDEN, 15 as RelativeTime, 5 as Duration) - ).toBeTrue() + expect(pageStateHistory.wasInPageStateDuringPeriod(PageState.ACTIVE, 5 as RelativeTime, 5 as Duration)).toBe(true) + expect(pageStateHistory.wasInPageStateDuringPeriod(PageState.HIDDEN, 15 as RelativeTime, 5 as Duration)).toBe( + true + ) }) it('should not backfill if visibility-state is not supported', () => { @@ -257,9 +258,9 @@ describe('pageStateHistory', () => { pageStateHistory = startPageStateHistory(hook) registerCleanupTask(pageStateHistory.stop) - expect( - pageStateHistory.wasInPageStateDuringPeriod(PageState.ACTIVE, 5 as RelativeTime, 5 as Duration) - ).toBeFalse() + expect(pageStateHistory.wasInPageStateDuringPeriod(PageState.ACTIVE, 5 as RelativeTime, 5 as Duration)).toBe( + false + ) }) }) }) diff --git a/packages/browser-rum-core/src/domain/contexts/sessionContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/sessionContext.spec.ts index b23ea09c4b..880bc142c4 100644 --- a/packages/browser-rum-core/src/domain/contexts/sessionContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/sessionContext.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { clocksNow } from '@datadog/js-core/time' import { DISCARDED, createHook } from '@datadog/js-core/assembly' @@ -17,9 +18,9 @@ describe('session context', () => { startClocks: clocksNow(), sessionIsActive: false, } - let isRecordingSpy: jasmine.Spy - let getReplayStatsSpy: jasmine.Spy - let findViewSpy: jasmine.Spy + let isRecordingSpy: Mock + let getReplayStatsSpy: Mock + let findViewSpy: Mock const fakeStats = { segments_count: 4, records_count: 10, @@ -35,16 +36,16 @@ describe('session context', () => { sessionManager.setId('00000000-0000-0000-0000-000000000123') const recorderApi = noopRecorderApi - isRecordingSpy = spyOn(recorderApi, 'isRecording') - getReplayStatsSpy = spyOn(recorderApi, 'getReplayStats') - findViewSpy = spyOn(viewHistory, 'findView').and.returnValue(fakeView) + isRecordingSpy = vi.spyOn(recorderApi, 'isRecording').mockImplementation(() => undefined as never) + getReplayStatsSpy = vi.spyOn(recorderApi, 'getReplayStats').mockImplementation(() => undefined) + findViewSpy = vi.spyOn(viewHistory, 'findView').mockReturnValue(fakeView) configuration = mockRumConfiguration({ sessionReplaySampleRate: 100 }) startSessionContext(hook, configuration, sessionManager, recorderApi, viewHistory) }) it('should set id and type', () => { - isRecordingSpy.and.returnValue(true) + isRecordingSpy.mockReturnValue(true) const defaultRumEventAttributes = hook.trigger({ eventType: 'action', @@ -53,21 +54,21 @@ describe('session context', () => { expect(defaultRumEventAttributes).toEqual({ type: 'action', - session: jasmine.objectContaining({ - id: jasmine.any(String), + session: expect.objectContaining({ + id: expect.any(String), type: SessionType.USER, }), }) }) it('should set hasReplay when recording has started (isRecording) on events', () => { - isRecordingSpy.and.returnValue(true) + isRecordingSpy.mockReturnValue(true) const eventWithHasReplay = hook.trigger({ eventType: 'action', startTime: 0 as RelativeTime, } as AssembleHookParams) as DefaultRumEventAttributes - isRecordingSpy.and.returnValue(false) + isRecordingSpy.mockReturnValue(false) const eventWithoutHasReplay = hook.trigger({ eventType: 'action', startTime: 0 as RelativeTime, @@ -80,13 +81,13 @@ describe('session context', () => { }) it('should set hasReplay when there are Replay stats on view events', () => { - getReplayStatsSpy.and.returnValue(fakeStats) + getReplayStatsSpy.mockReturnValue(fakeStats) const eventWithHasReplay = hook.trigger({ eventType: 'view', startTime: 0 as RelativeTime, } as AssembleHookParams) as DefaultRumEventAttributes - getReplayStatsSpy.and.returnValue(undefined) + getReplayStatsSpy.mockReturnValue(undefined) const eventWithoutHasReplay = hook.trigger({ eventType: 'view', startTime: 0 as RelativeTime, @@ -99,12 +100,12 @@ describe('session context', () => { }) it('should set session.is_active when the session is active', () => { - findViewSpy.and.returnValue({ ...fakeView, sessionIsActive: true }) + findViewSpy.mockReturnValue({ ...fakeView, sessionIsActive: true }) const eventWithActiveSession = hook.trigger({ eventType: 'view', startTime: 0 as RelativeTime, } as AssembleHookParams) as DefaultRumEventAttributes - findViewSpy.and.returnValue({ ...fakeView, sessionIsActive: false }) + findViewSpy.mockReturnValue({ ...fakeView, sessionIsActive: false }) const eventWithoutActiveSession = hook.trigger({ eventType: 'view', startTime: 0 as RelativeTime, @@ -142,7 +143,7 @@ describe('session context', () => { }) it('should discard the event if no view', () => { - findViewSpy.and.returnValue(undefined) + findViewSpy.mockReturnValue(undefined) const defaultRumEventAttributes = hook.trigger({ eventType: 'view', startTime: 0 as RelativeTime, diff --git a/packages/browser-rum-core/src/domain/contexts/sourceCodeMfeContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/sourceCodeMfeContext.spec.ts index 46b9ebded4..80a3ef0fe5 100644 --- a/packages/browser-rum-core/src/domain/contexts/sourceCodeMfeContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/sourceCodeMfeContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { createHook } from '@datadog/js-core/assembly' import { mockSourceCodeContext } from '../../../../browser-core/test' diff --git a/packages/browser-rum-core/src/domain/contexts/syntheticsContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/syntheticsContext.spec.ts index e60a25b10c..d6260038d8 100644 --- a/packages/browser-rum-core/src/domain/contexts/syntheticsContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/syntheticsContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { createHook } from '@datadog/js-core/assembly' import { mockSyntheticsWorkerValues } from '../../../../browser-core/test' diff --git a/packages/browser-rum-core/src/domain/contexts/trackingConsentContext.spec.ts b/packages/browser-rum-core/src/domain/contexts/trackingConsentContext.spec.ts index bf5acb7863..24473e170a 100644 --- a/packages/browser-rum-core/src/domain/contexts/trackingConsentContext.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/trackingConsentContext.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { createTrackingConsentState, TrackingConsent } from '@datadog/browser-core' import { DISCARDED, createHook } from '@datadog/js-core/assembly' diff --git a/packages/browser-rum-core/src/domain/contexts/urlContexts.spec.ts b/packages/browser-rum-core/src/domain/contexts/urlContexts.spec.ts index 948d7e850f..62e533046c 100644 --- a/packages/browser-rum-core/src/domain/contexts/urlContexts.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/urlContexts.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' import type { Clock } from '@datadog/browser-core/test' import type { RelativeTime } from '@datadog/js-core/time' @@ -206,10 +207,10 @@ describe('urlContexts', () => { } as AssembleHookParams) expect(defaultRumEventAttributes).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ view: { - url: jasmine.any(String), - referrer: jasmine.any(String), + url: expect.any(String), + referrer: expect.any(String), }, }) ) diff --git a/packages/browser-rum-core/src/domain/contexts/viewHistory.spec.ts b/packages/browser-rum-core/src/domain/contexts/viewHistory.spec.ts index 5db250a895..83610bff03 100644 --- a/packages/browser-rum-core/src/domain/contexts/viewHistory.spec.ts +++ b/packages/browser-rum-core/src/domain/contexts/viewHistory.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { relativeToClocks } from '@datadog/js-core/time' import { CLEAR_OLD_VALUES_INTERVAL } from '@datadog/browser-core' @@ -137,7 +138,7 @@ describe('ViewHistory', () => { expect(viewHistory.findView(15 as RelativeTime)).toBeDefined() expect(viewHistory.findView(25 as RelativeTime)).toBeDefined() - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) expect(viewHistory.findView(15 as RelativeTime)).toBeUndefined() expect(viewHistory.findView(25 as RelativeTime)).toBeUndefined() diff --git a/packages/browser-rum-core/src/domain/error/errorCollection.spec.ts b/packages/browser-rum-core/src/domain/error/errorCollection.spec.ts index ce7aaf1d4e..a7f10e5e05 100644 --- a/packages/browser-rum-core/src/domain/error/errorCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/error/errorCollection.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { RelativeTime, TimeStamp } from '@datadog/js-core/time' import type { ErrorWithCause } from '@datadog/browser-core' import { ErrorHandling, ErrorSource, NO_ERROR_STACK_PRESENT_MESSAGE } from '@datadog/browser-core' @@ -39,14 +40,14 @@ describe('error collection', () => { error: new Error('foo'), message: 'foo', type: 'Error', - stack: jasmine.stringMatching('Error: foo'), + stack: expect.stringMatching('Error: foo'), }, { testCase: 'an error subclass via prototype', error: new (SubErrorViaPrototype as unknown as { new (message: string): Error })('bar'), message: 'bar', type: 'Error', - stack: jasmine.stringMatching('Error: bar'), + stack: expect.stringMatching('Error: bar'), }, { testCase: 'a string', @@ -76,9 +77,9 @@ describe('error collection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0]).toEqual({ rawRumEvent: { - date: jasmine.any(Number), + date: expect.any(Number), error: { - id: jasmine.any(String), + id: expect.any(String), message, source: ErrorSource.CUSTOM, stack, @@ -261,9 +262,9 @@ describe('error collection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(1234 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), error: { - id: jasmine.any(String), + id: expect.any(String), message: 'hello', source: ErrorSource.CUSTOM, stack: 'bar', diff --git a/packages/browser-rum-core/src/domain/error/trackReportError.spec.ts b/packages/browser-rum-core/src/domain/error/trackReportError.spec.ts index c352aa03f1..b64ce77b19 100644 --- a/packages/browser-rum-core/src/domain/error/trackReportError.spec.ts +++ b/packages/browser-rum-core/src/domain/error/trackReportError.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RawError, Subscription } from '@datadog/browser-core' import { clocksNow } from '@datadog/js-core/time' import { ErrorHandling, ErrorSource, Observable } from '@datadog/browser-core' @@ -15,16 +16,17 @@ import { trackReportError } from './trackReportError' describe('trackReportError', () => { let errorObservable: Observable let subscription: Subscription - let notifyLog: jasmine.Spy + let notifyLog: Mock let reportingObserver: MockReportingObserver let cspEventListener: MockCspEventListener - beforeEach(() => { + beforeEach((ctx) => { if (!window.ReportingObserver) { - pending('ReportingObserver not supported') + ctx.skip(true, 'ReportingObserver not supported') + return } errorObservable = new Observable() - notifyLog = jasmine.createSpy('notifyLog') + notifyLog = vi.fn() reportingObserver = mockReportingObserver() subscription = errorObservable.subscribe(notifyLog) mockClock() @@ -35,13 +37,13 @@ describe('trackReportError', () => { }) it('should track reports', () => { - trackReportError(errorObservable) + registerCleanupTask(trackReportError(errorObservable).stop) reportingObserver.raiseReport('intervention') expect(notifyLog).toHaveBeenCalledWith({ startClocks: clocksNow(), - message: jasmine.any(String), - stack: jasmine.any(String), + message: expect.any(String), + stack: expect.any(String), source: ErrorSource.REPORT, handling: ErrorHandling.UNHANDLED, type: 'NavigatorVibrate', @@ -50,13 +52,13 @@ describe('trackReportError', () => { }) it('should track securitypolicyviolation', () => { - trackReportError(errorObservable) + registerCleanupTask(trackReportError(errorObservable).stop) cspEventListener.dispatchEvent() expect(notifyLog).toHaveBeenCalledWith({ startClocks: clocksNow(), - message: jasmine.any(String), - stack: jasmine.any(String), + message: expect.any(String), + stack: expect.any(String), source: ErrorSource.REPORT, handling: ErrorHandling.UNHANDLED, type: FAKE_CSP_VIOLATION_EVENT.effectiveDirective, diff --git a/packages/browser-rum-core/src/domain/event/eventCollection.spec.ts b/packages/browser-rum-core/src/domain/event/eventCollection.spec.ts index 0ca20fd9f4..79416c5aa9 100644 --- a/packages/browser-rum-core/src/domain/event/eventCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/event/eventCollection.spec.ts @@ -1,4 +1,5 @@ -import type { RelativeTime, Duration, TimeStamp } from '@datadog/js-core/time' +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' +import type { Duration, RelativeTime, TimeStamp } from '@datadog/js-core/time' import type { RumEventDomainContext } from '../../domainContext.types' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' import { RumEventType, VitalType } from '../../rawRumEvent.types' @@ -6,11 +7,11 @@ import { startEventCollection } from './eventCollection' describe('eventCollection', () => { let lifeCycle: LifeCycle - let notifySpy: jasmine.Spy + let notifySpy: Mock beforeEach(() => { lifeCycle = new LifeCycle() - notifySpy = spyOn(lifeCycle, 'notify') + notifySpy = vi.spyOn(lifeCycle, 'notify').mockImplementation(() => undefined) }) it('should notify lifecycle with raw rum event when adding an event', () => { @@ -33,7 +34,7 @@ describe('eventCollection', () => { eventCollection.addEvent(startTime, event, domainContext, duration) expect(notifySpy).toHaveBeenCalledWith(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, { - startClocks: { relative: startTime, timeStamp: jasmine.any(Number) }, + startClocks: { relative: startTime, timeStamp: expect.any(Number) }, rawRumEvent: event, domainContext, duration, diff --git a/packages/browser-rum-core/src/domain/eventTracker.spec.ts b/packages/browser-rum-core/src/domain/eventTracker.spec.ts index 9852bde17f..5a911f1bf1 100644 --- a/packages/browser-rum-core/src/domain/eventTracker.spec.ts +++ b/packages/browser-rum-core/src/domain/eventTracker.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime, Duration, TimeStamp } from '@datadog/js-core/time' import { clocksNow } from '@datadog/js-core/time' import type { Clock } from '@datadog/browser-core/test' @@ -65,7 +66,7 @@ describe('eventTracker', () => { const stopped = tracker.stop('key1', clocksNow()) expect(stopped).toEqual({ - id: jasmine.any(String), + id: expect.any(String), startClocks, duration: 500 as Duration, counts: undefined, @@ -79,7 +80,7 @@ describe('eventTracker', () => { const stopped = tracker.stop('key1', clocksNow(), { extra: 'additional' }) expect(stopped).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ value: 'original', extra: 'additional', }) @@ -110,8 +111,8 @@ describe('eventTracker', () => { const result = tracker.findId() - expect(Array.isArray(result)).toBeTrue() - expect(result).toHaveSize(2) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(2) }) it('should find events within their time range', () => { @@ -149,7 +150,7 @@ describe('eventTracker', () => { tracker.stopAll() - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) expect(tracker.findId()).toEqual([]) }) diff --git a/packages/browser-rum-core/src/domain/getComposedPathSelector.spec.ts b/packages/browser-rum-core/src/domain/getComposedPathSelector.spec.ts index 30fe014f20..378a1a0556 100644 --- a/packages/browser-rum-core/src/domain/getComposedPathSelector.spec.ts +++ b/packages/browser-rum-core/src/domain/getComposedPathSelector.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { appendElement } from '../../test' import { getComposedPathSelector, CHARACTER_LIMIT } from './getComposedPathSelector' diff --git a/packages/browser-rum-core/src/domain/getSelectorFromElement.spec.ts b/packages/browser-rum-core/src/domain/getSelectorFromElement.spec.ts index e5ea417c8a..a929f5e59c 100644 --- a/packages/browser-rum-core/src/domain/getSelectorFromElement.spec.ts +++ b/packages/browser-rum-core/src/domain/getSelectorFromElement.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, describe, expect, it } from 'vitest' import { appendElement } from '../../test' import { getSelectorFromElement, isSelectorUniqueAmongSiblings, SHADOW_DOM_MARKER } from './getSelectorFromElement' @@ -163,7 +164,7 @@ describe('getSelectorFromElement', () => { describe('isSelectorUniqueAmongSiblings', () => { it('returns true when the element is alone', () => { const element = appendElement('
') - expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBeTrue() + expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBe(true) }) it('returns false when a sibling element matches the element selector', () => { @@ -171,7 +172,7 @@ describe('isSelectorUniqueAmongSiblings', () => {
`) - expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBeFalse() + expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBe(false) }) it('returns true when the element selector does not match any sibling', () => { @@ -179,7 +180,7 @@ describe('isSelectorUniqueAmongSiblings', () => {
`) - expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBeTrue() + expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', undefined)).toBe(true) }) it('returns false when the child selector matches an element in a sibling', () => { @@ -191,7 +192,7 @@ describe('isSelectorUniqueAmongSiblings', () => {
`) - expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBeFalse() + expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBe(false) }) it('returns true when the current element selector does not match the sibling', () => { @@ -203,7 +204,7 @@ describe('isSelectorUniqueAmongSiblings', () => {
`) - expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBeTrue() + expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBe(true) }) it('the selector should not consider elements deep in the tree', () => { @@ -217,7 +218,7 @@ describe('isSelectorUniqueAmongSiblings', () => { `) - expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBeTrue() + expect(isSelectorUniqueAmongSiblings(element, document, 'DIV', 'HR')).toBe(true) }) }) @@ -365,7 +366,7 @@ describe('getSelectorFromElement with shadow DOM', () => { fragment.appendChild(div2) // The function should return false because div2 matches 'DIV' selector - expect(isSelectorUniqueAmongSiblings(div1, document, 'DIV', undefined)).toBeFalse() + expect(isSelectorUniqueAmongSiblings(div1, document, 'DIV', undefined)).toBe(false) }) it('returns true when element is in DocumentFragment with no matching siblings', () => { @@ -373,6 +374,6 @@ describe('getSelectorFromElement with shadow DOM', () => { const div = document.createElement('div') fragment.appendChild(div) - expect(isSelectorUniqueAmongSiblings(div, document, 'DIV', undefined)).toBeTrue() + expect(isSelectorUniqueAmongSiblings(div, document, 'DIV', undefined)).toBe(true) }) }) diff --git a/packages/browser-rum-core/src/domain/getSessionReplayUrl.spec.ts b/packages/browser-rum-core/src/domain/getSessionReplayUrl.spec.ts index 4abc06ff09..91b45f195e 100644 --- a/packages/browser-rum-core/src/domain/getSessionReplayUrl.spec.ts +++ b/packages/browser-rum-core/src/domain/getSessionReplayUrl.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { ClocksState } from '@datadog/js-core/time' import type { SessionContext } from '@datadog/browser-core' import type { RumConfiguration } from '@datadog/browser-rum-core' diff --git a/packages/browser-rum-core/src/domain/limitModification.spec.ts b/packages/browser-rum-core/src/domain/limitModification.spec.ts index 0b21284cd0..f5829877d1 100644 --- a/packages/browser-rum-core/src/domain/limitModification.spec.ts +++ b/packages/browser-rum-core/src/domain/limitModification.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { Context } from '@datadog/browser-core' import { display, noop, objectEntries, SANITIZE_DEFAULT_MAX_CHARACTER_COUNT } from '@datadog/browser-core' import type { ModifiableFieldPaths } from './limitModification' @@ -196,7 +197,7 @@ describe('limitModification', () => { }) it("should not reset unsafe literal fields that the user didn't alter", () => { - spyOn(display, 'warn') + vi.spyOn(display, 'warn').mockImplementation(() => undefined) const wayTooLongUrl = `/${'a'.repeat(SANITIZE_DEFAULT_MAX_CHARACTER_COUNT + 1)}` const object: Context = { resource: { url: wayTooLongUrl } } const modifier = noop @@ -205,7 +206,7 @@ describe('limitModification', () => { }) it('should sanitize object fields (fieldType "object") even with a noop modifier', () => { - spyOn(display, 'warn') + vi.spyOn(display, 'warn').mockImplementation(() => undefined) const wayTooLongUrl = `/${'a'.repeat(SANITIZE_DEFAULT_MAX_CHARACTER_COUNT + 1)}` const object: Context = { context: { diff --git a/packages/browser-rum-core/src/domain/longTask/longTaskCollection.spec.ts b/packages/browser-rum-core/src/domain/longTask/longTaskCollection.spec.ts index 35c1cc292c..58910e8772 100644 --- a/packages/browser-rum-core/src/domain/longTask/longTaskCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/longTask/longTaskCollection.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { RelativeTime, ServerDuration } from '@datadog/js-core/time' import { mockSourceCodeContext, registerCleanupTask } from '@datadog/browser-core/test' import { @@ -51,9 +52,9 @@ describe('longTaskCollection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(1234 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), long_task: { - id: jasmine.any(String), + id: expect.any(String), entry_type: RumLongTaskEntryType.LONG_ANIMATION_FRAME, duration: (82 * 1e6) as ServerDuration, blocking_duration: 0 as ServerDuration, @@ -141,9 +142,9 @@ describe('longTaskCollection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(1234 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), long_task: { - id: jasmine.any(String), + id: expect.any(String), entry_type: RumLongTaskEntryType.LONG_TASK, duration: (100 * 1e6) as ServerDuration, }, @@ -158,7 +159,7 @@ describe('longTaskCollection', () => { duration: 100, entryType: 'longtask', startTime: 1234, - toJSON: jasmine.any(Function), + toJSON: expect.any(Function), }, }) }) diff --git a/packages/browser-rum-core/src/domain/plugins.spec.ts b/packages/browser-rum-core/src/domain/plugins.spec.ts index a4cf14dda1..0d115b80a7 100644 --- a/packages/browser-rum-core/src/domain/plugins.spec.ts +++ b/packages/browser-rum-core/src/domain/plugins.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import type { RumPublicApi } from '../boot/rumPublicApi' import type { RumInitConfiguration } from './configuration' import type { RumPlugin } from './plugins' @@ -5,8 +6,8 @@ import { callPluginsMethod } from './plugins' describe('callPluginsMethod', () => { it('calls the method on each plugin', () => { - const plugin1 = { name: 'a', onInit: jasmine.createSpy() } satisfies RumPlugin - const plugin2 = { name: 'b', onInit: jasmine.createSpy() } satisfies RumPlugin + const plugin1 = { name: 'a', onInit: vi.fn() } satisfies RumPlugin + const plugin2 = { name: 'b', onInit: vi.fn() } satisfies RumPlugin const parameter = { initConfiguration: {} as RumInitConfiguration, publicApi: {} as RumPublicApi } callPluginsMethod([plugin1, plugin2], 'onInit', parameter) expect(plugin1.onInit).toHaveBeenCalledWith(parameter) @@ -14,7 +15,7 @@ describe('callPluginsMethod', () => { }) it('does not call the method if the plugin does not have it', () => { - const plugin1 = { name: 'a', onInit: jasmine.createSpy() } satisfies RumPlugin + const plugin1 = { name: 'a', onInit: vi.fn() } satisfies RumPlugin const plugin2 = { name: 'b' } satisfies RumPlugin const parameter = { initConfiguration: {} as RumInitConfiguration, publicApi: {} as RumPublicApi } callPluginsMethod([plugin1, plugin2], 'onInit', parameter) diff --git a/packages/browser-rum-core/src/domain/privacy.spec.ts b/packages/browser-rum-core/src/domain/privacy.spec.ts index 7f0009c0dd..f15b28a0ca 100644 --- a/packages/browser-rum-core/src/domain/privacy.spec.ts +++ b/packages/browser-rum-core/src/domain/privacy.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { appendElement } from '../../test' import { registerCleanupTask } from '../../../browser-core/test' import { @@ -130,7 +131,13 @@ describe('getNodePrivacyLevel', () => { const node = document.createElement('div') ancestor.appendChild(node) - const parentNodeGetterSpy = spyOnProperty(node, 'parentNode').and.returnValue(ancestor) + // Use Object.defineProperty instead of vi.spyOn β€” native DOM getters + // throw "Illegal invocation" when proxied through vi.spyOn. + const parentNodeGetterSpy = vi.fn(() => ancestor) + Object.defineProperty(node, 'parentNode', { + get: parentNodeGetterSpy, + configurable: true, + }) const cache = new Map() cache.set(node, NodePrivacyLevel.MASK_USER_INPUT) @@ -407,16 +414,16 @@ describe('shouldMaskNode', () => { describe('for form elements', () => { it('returns false if the privacy level is ALLOW', () => { const element = document.createElement('input') - expect(shouldMaskNode(element, NodePrivacyLevel.ALLOW)).toBeFalse() + expect(shouldMaskNode(element, NodePrivacyLevel.ALLOW)).toBe(false) }) it('returns true if the privacy level is not ALLOW', () => { const element = document.createElement('input') - expect(shouldMaskNode(element, NodePrivacyLevel.MASK)).toBeTrue() - expect(shouldMaskNode(element, NodePrivacyLevel.MASK_USER_INPUT)).toBeTrue() - expect(shouldMaskNode(element, NodePrivacyLevel.IGNORE)).toBeTrue() - expect(shouldMaskNode(element, NodePrivacyLevel.HIDDEN)).toBeTrue() - expect(shouldMaskNode(element, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeTrue() + expect(shouldMaskNode(element, NodePrivacyLevel.MASK)).toBe(true) + expect(shouldMaskNode(element, NodePrivacyLevel.MASK_USER_INPUT)).toBe(true) + expect(shouldMaskNode(element, NodePrivacyLevel.IGNORE)).toBe(true) + expect(shouldMaskNode(element, NodePrivacyLevel.HIDDEN)).toBe(true) + expect(shouldMaskNode(element, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(true) }) }) @@ -425,24 +432,24 @@ describe('shouldMaskNode', () => { const element = document.createElement('input') const text = document.createTextNode('foo') element.appendChild(text) - expect(shouldMaskNode(text, NodePrivacyLevel.MASK)).toBeTrue() - expect(shouldMaskNode(text, NodePrivacyLevel.MASK_USER_INPUT)).toBeTrue() - expect(shouldMaskNode(text, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeTrue() + expect(shouldMaskNode(text, NodePrivacyLevel.MASK)).toBe(true) + expect(shouldMaskNode(text, NodePrivacyLevel.MASK_USER_INPUT)).toBe(true) + expect(shouldMaskNode(text, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(true) }) }) describe('for other elements', () => { it('returns false if the privacy level is ALLOW or MASK_USER_INPUT', () => { const element = document.createElement('div') - expect(shouldMaskNode(element, NodePrivacyLevel.ALLOW)).toBeFalse() - expect(shouldMaskNode(element, NodePrivacyLevel.MASK_USER_INPUT)).toBeFalse() + expect(shouldMaskNode(element, NodePrivacyLevel.ALLOW)).toBe(false) + expect(shouldMaskNode(element, NodePrivacyLevel.MASK_USER_INPUT)).toBe(false) }) it('returns true if the privacy level is not ALLOW nor MASK_USER_INPUT nor MASK_UNLESS_ALLOWLISTED', () => { const element = document.createElement('div') - expect(shouldMaskNode(element, NodePrivacyLevel.MASK)).toBeTrue() - expect(shouldMaskNode(element, NodePrivacyLevel.IGNORE)).toBeTrue() - expect(shouldMaskNode(element, NodePrivacyLevel.HIDDEN)).toBeTrue() + expect(shouldMaskNode(element, NodePrivacyLevel.MASK)).toBe(true) + expect(shouldMaskNode(element, NodePrivacyLevel.IGNORE)).toBe(true) + expect(shouldMaskNode(element, NodePrivacyLevel.HIDDEN)).toBe(true) }) describe('when privacy level is MASK_UNLESS_ALLOWLISTED', () => { @@ -457,17 +464,17 @@ describe('shouldMaskNode', () => { const allowedText = 'allowed text' textNode.textContent = allowedText defineAllowList(new Set([allowedText.toLocaleLowerCase()])) - expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeFalse() + expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(false) }) it('returns false for whitespace-only text content', () => { textNode.textContent = ' \n\t ' - expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeFalse() + expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(false) }) it('returns true for non-allowlisted, non-whitespace text content', () => { textNode.textContent = 'some text' - expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeTrue() + expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(true) }) it('returns true if text node is child of a form element regardless of allowlist', () => { @@ -475,25 +482,25 @@ describe('shouldMaskNode', () => { textNode.textContent = 'some text' input.appendChild(textNode) defineAllowList(new Set(['some text'])) - expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeTrue() + expect(shouldMaskNode(textNode, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(true) }) }) describe('for non-text nodes', () => { it('returns true for form elements', () => { const input = document.createElement('input') - expect(shouldMaskNode(input, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeTrue() + expect(shouldMaskNode(input, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(true) const textarea = document.createElement('textarea') - expect(shouldMaskNode(textarea, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeTrue() + expect(shouldMaskNode(textarea, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(true) }) it('returns false for non-form elements', () => { const div = document.createElement('div') - expect(shouldMaskNode(div, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeFalse() + expect(shouldMaskNode(div, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(false) const span = document.createElement('span') - expect(shouldMaskNode(span, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBeFalse() + expect(shouldMaskNode(span, NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED)).toBe(false) }) }) }) diff --git a/packages/browser-rum-core/src/domain/requestCollection.spec.ts b/packages/browser-rum-core/src/domain/requestCollection.spec.ts index a8c9a4f27c..ebd8a5fc30 100644 --- a/packages/browser-rum-core/src/domain/requestCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/requestCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { RequestType, startBufferingData } from '@datadog/browser-core' import { createEndpointBuilder, type EndpointPayload } from '@datadog/js-core/transport' import type { MockFetch, MockFetchManager } from '@datadog/browser-core/test' @@ -16,16 +17,16 @@ describe('collect fetch', () => { const FAKE_URL = 'http://fake-url/' let fetch: MockFetch let mockFetchManager: MockFetchManager - let startSpy: jasmine.Spy<(requestStartEvent: RequestStartEvent) => void> - let completeSpy: jasmine.Spy<(requestCompleteEvent: RequestCompleteEvent) => void> + let startSpy: Mock<(requestStartEvent: RequestStartEvent) => void> + let completeSpy: Mock<(requestCompleteEvent: RequestCompleteEvent) => void> let stopFetchTracking: () => void beforeEach(() => { mockFetchManager = mockFetch() const { observable: bufferedDataObservable, stop: stopBuffering } = startBufferingData() - startSpy = jasmine.createSpy('requestStart') - completeSpy = jasmine.createSpy('requestComplete') + startSpy = vi.fn() + completeSpy = vi.fn() const lifeCycle = new LifeCycle() lifeCycle.subscribe(LifeCycleEventType.REQUEST_STARTED, startSpy) lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, completeSpy) @@ -51,148 +52,158 @@ describe('collect fetch', () => { }) }) - it('should notify on request start', (done) => { - fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) + it('should notify on request start', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) - mockFetchManager.whenAllComplete(() => { - expect(startSpy).toHaveBeenCalledWith({ requestIndex: jasmine.any(Number) as unknown as number, url: FAKE_URL }) - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(startSpy).toHaveBeenCalledWith({ requestIndex: expect.any(Number), url: FAKE_URL }) + resolve() + }) + })) - it('should notify on request without body', (done) => { - fetch(FAKE_URL).resolveWith({ status: 200 }) + it('should notify on request without body', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 200 }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.type).toEqual(RequestType.FETCH) - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(200) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + expect(request.type).toEqual(RequestType.FETCH) + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(200) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should notify on request with body used by another instrumentation', (done) => { - fetch(FAKE_URL).resolveWith({ status: 200, bodyUsed: true }) + it('should notify on request with body used by another instrumentation', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 200, bodyUsed: true }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.type).toEqual(RequestType.FETCH) - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(200) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + expect(request.type).toEqual(RequestType.FETCH) + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(200) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should notify on request with body disturbed', (done) => { - fetch(FAKE_URL).resolveWith({ status: 200, bodyDisturbed: true }) + it('should notify on request with body disturbed', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 200, bodyDisturbed: true }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.type).toEqual(RequestType.FETCH) - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(200) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + expect(request.type).toEqual(RequestType.FETCH) + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(200) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should notify on request complete', (done) => { - fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) + it('should notify on request complete', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.type).toEqual(RequestType.FETCH) - expect(request.method).toEqual('GET') - expect(request.url).toEqual(FAKE_URL) - expect(request.status).toEqual(500) - expect(request.handlingStack).toBeDefined() - done() - }) - }) + expect(request.type).toEqual(RequestType.FETCH) + expect(request.method).toEqual('GET') + expect(request.url).toEqual(FAKE_URL) + expect(request.status).toEqual(500) + expect(request.handlingStack).toBeDefined() + resolve() + }) + })) - it('should assign a request id', (done) => { - fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) + it('should assign a request id', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 500, responseText: 'fetch error' }) - mockFetchManager.whenAllComplete(() => { - const startRequestIndex = startSpy.calls.argsFor(0)[0].requestIndex - const completeRequestIndex = completeSpy.calls.argsFor(0)[0].requestIndex + mockFetchManager.whenAllComplete(() => { + const startRequestIndex = startSpy.mock.calls[0][0].requestIndex + const completeRequestIndex = completeSpy.mock.calls[0][0].requestIndex - expect(completeRequestIndex).toBe(startRequestIndex) - done() - }) - }) + expect(completeRequestIndex).toBe(startRequestIndex) + resolve() + }) + })) - it('should ignore intake requests', (done) => { - fetch(createEndpointBuilder(mockRumConfiguration(), 'rum').build('fetch', DEFAULT_PAYLOAD)).resolveWith({ - status: 200, - responseText: 'foo', - }) + it('should ignore intake requests', () => + new Promise((resolve) => { + fetch(createEndpointBuilder(mockRumConfiguration(), 'rum').build('fetch', DEFAULT_PAYLOAD)).resolveWith({ + status: 200, + responseText: 'foo', + }) - mockFetchManager.whenAllComplete(() => { - expect(startSpy).not.toHaveBeenCalled() - expect(completeSpy).not.toHaveBeenCalled() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + expect(startSpy).not.toHaveBeenCalled() + expect(completeSpy).not.toHaveBeenCalled() + resolve() + }) + })) describe('tracing', () => { - it('should trace requests by default', (done) => { - fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'ok' }) + it('should trace requests by default', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 200, responseText: 'ok' }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.traceId).toBeDefined() - done() - }) - }) + expect(request.traceId).toBeDefined() + resolve() + }) + })) - it('should trace aborted requests', (done) => { - fetch(FAKE_URL).abort() + it('should trace aborted requests', () => + new Promise((resolve) => { + fetch(FAKE_URL).abort() - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.traceId).toBeDefined() - done() - }) - }) + expect(request.traceId).toBeDefined() + resolve() + }) + })) - it('should not trace requests ending with status 0', (done) => { - fetch(FAKE_URL).resolveWith({ status: 0, responseText: 'fetch cancelled' }) + it('should not trace requests ending with status 0', () => + new Promise((resolve) => { + fetch(FAKE_URL).resolveWith({ status: 0, responseText: 'fetch cancelled' }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] - expect(request.status).toEqual(0) - expect(request.traceId).toBeUndefined() - done() - }) - }) + expect(request.status).toEqual(0) + expect(request.traceId).toBeUndefined() + resolve() + }) + })) }) }) describe('collect xhr', () => { - let startSpy: jasmine.Spy<(requestStartEvent: RequestStartEvent) => void> - let completeSpy: jasmine.Spy<(requestCompleteEvent: RequestCompleteEvent) => void> + let startSpy: Mock<(requestStartEvent: RequestStartEvent) => void> + let completeSpy: Mock<(requestCompleteEvent: RequestCompleteEvent) => void> let stopXhrTracking: () => void beforeEach(() => { mockXhr() const { observable: bufferedDataObservable, stop: stopBuffering } = startBufferingData() - startSpy = jasmine.createSpy('requestStart') - completeSpy = jasmine.createSpy('requestComplete') + startSpy = vi.fn() + completeSpy = vi.fn() const lifeCycle = new LifeCycle() lifeCycle.subscribe(LifeCycleEventType.REQUEST_STARTED, startSpy) lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, completeSpy) @@ -211,93 +222,47 @@ describe('collect xhr', () => { }) }) - it('should notify on request start', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200) - }, - onComplete() { - expect(startSpy).toHaveBeenCalledWith({ - requestIndex: jasmine.any(Number) as unknown as number, - url: jasmine.stringMatching(/\/ok$/) as unknown as string, - }) - done() - }, - }) - }) - - it('should notify on request complete', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200, 'ok') - }, - onComplete() { - const request = completeSpy.calls.argsFor(0)[0] - - expect(request.type).toEqual(RequestType.XHR) - expect(request.method).toEqual('GET') - expect(request.url).toContain('/ok') - expect(request.status).toEqual(200) - expect(request.handlingStack).toBeDefined() - done() - }, - }) - }) - - it('should assign a request id', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(200) - }, - onComplete() { - const startRequestIndex = startSpy.calls.argsFor(0)[0].requestIndex - const completeRequestIndex = completeSpy.calls.argsFor(0)[0].requestIndex - - expect(completeRequestIndex).toBe(startRequestIndex) - done() - }, - }) - }) - - it('should ignore intake requests', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', createEndpointBuilder(mockRumConfiguration(), 'rum').build('fetch', DEFAULT_PAYLOAD)) - xhr.send() - xhr.complete(200) - }, - onComplete() { - expect(startSpy).not.toHaveBeenCalled() - expect(completeSpy).not.toHaveBeenCalled() - done() - }, - }) - }) + it('should notify on request start', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200) + }, + onComplete() { + expect(startSpy).toHaveBeenCalledWith({ + requestIndex: expect.any(Number), + url: expect.stringMatching(/\/ok$/), + }) + resolve() + }, + }) + })) - it('should not trace cancelled requests', (done) => { - withXhr({ - setup(xhr) { - xhr.open('GET', '/ok') - xhr.send() - xhr.complete(0) - }, - onComplete() { - const request = completeSpy.calls.argsFor(0)[0] - expect(request.status).toEqual(0) - expect(request.traceId).toEqual(undefined) - done() - }, - }) - }) + it('should notify on request complete', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete() { + const request = completeSpy.mock.calls[0][0] + + expect(request.type).toEqual(RequestType.XHR) + expect(request.method).toEqual('GET') + expect(request.url).toContain('/ok') + expect(request.status).toEqual(200) + expect(request.handlingStack).toBeDefined() + resolve() + }, + }) + })) - describe('tracing', () => { - it('should trace requests by default', (done) => { + it('should assign a request id', () => + new Promise((resolve) => { withXhr({ setup(xhr) { xhr.open('GET', '/ok') @@ -305,29 +270,33 @@ describe('collect xhr', () => { xhr.complete(200) }, onComplete() { - const request = completeSpy.calls.argsFor(0)[0] - expect(request.traceId).toBeDefined() - done() + const startRequestIndex = startSpy.mock.calls[0][0].requestIndex + const completeRequestIndex = completeSpy.mock.calls[0][0].requestIndex + + expect(completeRequestIndex).toBe(startRequestIndex) + resolve() }, }) - }) + })) - it('should trace aborted requests', (done) => { + it('should ignore intake requests', () => + new Promise((resolve) => { withXhr({ setup(xhr) { - xhr.open('GET', '/ok') + xhr.open('GET', createEndpointBuilder(mockRumConfiguration(), 'rum').build('fetch', DEFAULT_PAYLOAD)) xhr.send() - xhr.abort() + xhr.complete(200) }, onComplete() { - const request = completeSpy.calls.argsFor(0)[0] - expect(request.traceId).toBeDefined() - done() + expect(startSpy).not.toHaveBeenCalled() + expect(completeSpy).not.toHaveBeenCalled() + resolve() }, }) - }) + })) - it('should not trace requests ending with status 0', (done) => { + it('should not trace cancelled requests', () => + new Promise((resolve) => { withXhr({ setup(xhr) { xhr.open('GET', '/ok') @@ -335,13 +304,63 @@ describe('collect xhr', () => { xhr.complete(0) }, onComplete() { - const request = completeSpy.calls.argsFor(0)[0] + const request = completeSpy.mock.calls[0][0] expect(request.status).toEqual(0) - expect(request.traceId).toBeUndefined() - done() + expect(request.traceId).toEqual(undefined) + resolve() }, }) - }) + })) + + describe('tracing', () => { + it('should trace requests by default', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(200) + }, + onComplete() { + const request = completeSpy.mock.calls[0][0] + expect(request.traceId).toBeDefined() + resolve() + }, + }) + })) + + it('should trace aborted requests', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.abort() + }, + onComplete() { + const request = completeSpy.mock.calls[0][0] + expect(request.traceId).toBeDefined() + resolve() + }, + }) + })) + + it('should not trace requests ending with status 0', () => + new Promise((resolve) => { + withXhr({ + setup(xhr) { + xhr.open('GET', '/ok') + xhr.send() + xhr.complete(0) + }, + onComplete() { + const request = completeSpy.mock.calls[0][0] + expect(request.status).toEqual(0) + expect(request.traceId).toBeUndefined() + resolve() + }, + }) + })) }) }) @@ -350,7 +369,7 @@ describe('GraphQL response text collection', () => { function setupGraphQlFetchTest(trackResponseErrors: boolean) { const mockFetchManager = mockFetch() - const completeSpy = jasmine.createSpy('requestComplete') + const completeSpy = vi.fn() const lifeCycle = new LifeCycle() lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, completeSpy) @@ -358,7 +377,7 @@ describe('GraphQL response text collection', () => { allowedGraphQlUrls: [{ match: /\/graphql$/, trackResponseErrors }], }) const { observable: bufferedDataObservable, stop: stopBuffering } = startBufferingData() - const tracerStub: Partial = { clearTracingIfNeeded, traceFetch: jasmine.createSpy() } + const tracerStub: Partial = { clearTracingIfNeeded, traceFetch: vi.fn() } const { stop } = trackFetch(lifeCycle, configuration, tracerStub as Tracer, bufferedDataObservable) registerCleanupTask(() => { stop() @@ -368,49 +387,51 @@ describe('GraphQL response text collection', () => { return { mockFetchManager, completeSpy, fetch: window.fetch as MockFetch } } - it('should collect responseBody when trackResponseErrors is enabled', (done) => { - const { mockFetchManager, completeSpy, fetch } = setupGraphQlFetchTest(true) + it('should collect responseBody when trackResponseErrors is enabled', () => + new Promise((resolve) => { + const { mockFetchManager, completeSpy, fetch } = setupGraphQlFetchTest(true) - const responseBody = JSON.stringify({ - data: null, - errors: [{ message: 'Not found' }, { message: 'Unauthorized' }], - }) + const responseBody = JSON.stringify({ + data: null, + errors: [{ message: 'Not found' }, { message: 'Unauthorized' }], + }) - fetch(FAKE_GRAPHQL_URL, { - method: 'POST', - body: JSON.stringify({ query: 'query Test { test }' }), - }).resolveWith({ - status: 200, - responseText: responseBody, - }) + fetch(FAKE_GRAPHQL_URL, { + method: 'POST', + body: JSON.stringify({ query: 'query Test { test }' }), + }).resolveWith({ + status: 200, + responseText: responseBody, + }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] - expect(request.responseBody).toBe(responseBody) - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] + expect(request.responseBody).toBe(responseBody) + resolve() + }) + })) - it('should not collect responseBody when trackResponseErrors is disabled', (done) => { - const { mockFetchManager, completeSpy, fetch } = setupGraphQlFetchTest(false) + it('should not collect responseBody when trackResponseErrors is disabled', () => + new Promise((resolve) => { + const { mockFetchManager, completeSpy, fetch } = setupGraphQlFetchTest(false) - const responseBody = JSON.stringify({ - data: null, - errors: [{ message: 'Not found' }], - }) + const responseBody = JSON.stringify({ + data: null, + errors: [{ message: 'Not found' }], + }) - fetch(FAKE_GRAPHQL_URL, { - method: 'POST', - body: JSON.stringify({ query: 'query Test { test }' }), - }).resolveWith({ - status: 200, - responseText: responseBody, - }) + fetch(FAKE_GRAPHQL_URL, { + method: 'POST', + body: JSON.stringify({ query: 'query Test { test }' }), + }).resolveWith({ + status: 200, + responseText: responseBody, + }) - mockFetchManager.whenAllComplete(() => { - const request = completeSpy.calls.argsFor(0)[0] - expect(request.responseBody).toBeUndefined() - done() - }) - }) + mockFetchManager.whenAllComplete(() => { + const request = completeSpy.mock.calls[0][0] + expect(request.responseBody).toBeUndefined() + resolve() + }) + })) }) diff --git a/packages/browser-rum-core/src/domain/resource/graphql.spec.ts b/packages/browser-rum-core/src/domain/resource/graphql.spec.ts index 343b727ac5..e718dd749c 100644 --- a/packages/browser-rum-core/src/domain/resource/graphql.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/graphql.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mockRumConfiguration } from '../../../test' import type { RequestCompleteEvent } from '../requestCollection' import { diff --git a/packages/browser-rum-core/src/domain/resource/requestRegistry.spec.ts b/packages/browser-rum-core/src/domain/resource/requestRegistry.spec.ts index dee133fd74..d767212a5e 100644 --- a/packages/browser-rum-core/src/domain/resource/requestRegistry.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/requestRegistry.spec.ts @@ -1,4 +1,5 @@ -import type { RelativeTime, Duration, TimeStamp } from '@datadog/js-core/time' +import { describe, expect, it } from 'vitest' +import type { Duration, RelativeTime, TimeStamp } from '@datadog/js-core/time' import { RequestType } from '@datadog/browser-core' import { createPerformanceEntry } from '../../../test' import { RumPerformanceEntryType } from '../../browser/performanceObservable' diff --git a/packages/browser-rum-core/src/domain/resource/resourceCollection.spec.ts b/packages/browser-rum-core/src/domain/resource/resourceCollection.spec.ts index af9f660c02..9162169ab5 100644 --- a/packages/browser-rum-core/src/domain/resource/resourceCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/resourceCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime, Duration, ServerDuration, TimeStamp } from '@datadog/js-core/time' import type { MatchOption, TaskQueue } from '@datadog/browser-core' import { elapsed, relativeToClocks, toServerDuration } from '@datadog/js-core/time' @@ -38,7 +39,7 @@ describe('resourceCollection', () => { let lifeCycle: LifeCycle let notifyPerformanceEntries: (entries: RumPerformanceEntry[]) => void let rawRumEvents: Array> = [] - let taskQueuePushSpy: jasmine.Spy + let taskQueuePushSpy: Mock let clock: Clock function setupResourceCollection(partialConfig: Partial = { trackResources: true }) { @@ -46,7 +47,7 @@ describe('resourceCollection', () => { lifeCycle = new LifeCycle() const taskQueue = createTaskQueue() replaceMockable(createTaskQueue, () => taskQueue) - taskQueuePushSpy = spyOn(taskQueue, 'push') + taskQueuePushSpy = vi.spyOn(taskQueue, 'push').mockImplementation(() => undefined) const startResult = startResourceCollection(lifeCycle, { ...baseConfiguration, ...partialConfig }) rawRumEvents = collectAndValidateRawRumEvents(lifeCycle) @@ -79,9 +80,9 @@ describe('resourceCollection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(200 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number) as unknown as TimeStamp, + date: expect.any(Number), resource: { - id: jasmine.any(String), + id: expect.any(String), duration: (100 * 1e6) as ServerDuration, size: 51, encoded_body_size: 42, @@ -89,8 +90,8 @@ describe('resourceCollection', () => { transfer_size: 63, type: ResourceType.IMAGE, url: 'https://resource.com/valid', - download: jasmine.any(Object), - first_byte: jasmine.any(Object), + download: expect.any(Object), + first_byte: expect.any(Object), status_code: 200, protocol: 'HTTP/1.0', delivery_type: 'cache', @@ -166,9 +167,9 @@ describe('resourceCollection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(200 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), resource: { - id: jasmine.any(String), + id: expect.any(String), duration: (100 * 1e6) as ServerDuration, method: 'GET', status_code: 200, @@ -192,9 +193,9 @@ describe('resourceCollection', () => { }) expect(rawRumEvents[0].domainContext).toEqual({ xhr, - performanceEntry: jasmine.any(Object), + performanceEntry: expect.any(Object), isAborted: false, - handlingStack: jasmine.stringMatching(HANDLING_STACK_REGEX), + handlingStack: expect.stringMatching(HANDLING_STACK_REGEX), isManual: false, isWebSocket: false, requestInit: undefined, @@ -265,8 +266,8 @@ describe('resourceCollection', () => { }) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - resource: jasmine.objectContaining({ + expect.objectContaining({ + resource: expect.objectContaining({ graphql: { operationType: 'query', operationName: 'GetUser', @@ -313,8 +314,8 @@ describe('resourceCollection', () => { }) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - resource: jasmine.objectContaining({ + expect.objectContaining({ + resource: expect.objectContaining({ graphql: { operationType: 'mutation', operationName: 'CreateUser', @@ -341,8 +342,8 @@ describe('resourceCollection', () => { runTasks() expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - resource: jasmine.objectContaining({ + expect.objectContaining({ + resource: expect.objectContaining({ response: { headers: { 'content-type': 'image/png', @@ -377,9 +378,9 @@ describe('resourceCollection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0].startClocks.relative).toBe(200 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), resource: { - id: jasmine.any(String), + id: expect.any(String), duration: (100 * 1e6) as ServerDuration, method: undefined, status_code: 200, @@ -402,7 +403,7 @@ describe('resourceCollection', () => { }, }) expect(rawRumEvents[0].domainContext).toEqual({ - performanceEntry: jasmine.any(Object), + performanceEntry: expect.any(Object), isManual: false, isWebSocket: false, isAborted: false, @@ -439,7 +440,12 @@ describe('resourceCollection', () => { }) describe('and resource is traced', () => { - it('should collect the initial document navigation entry', () => { + it('should collect the initial document navigation entry', (ctx) => { + // Safari on BrowserStack may emit extra navigation performance entries in the iframe context + if (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')) { + ctx.skip(true, 'Safari emits extra navigation entries in iframe context') + } + replaceMockable(getDocumentTraceId, () => '1234') const { triggerOnDomLoaded } = setupResourceCollection({ trackResources: false }) @@ -447,7 +453,7 @@ describe('resourceCollection', () => { runTasks() expect(rawRumEvents.length).toBe(1) - expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd.discarded).toBeTrue() + expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd.discarded).toBe(true) }) it('should collect a resource from a completed XHR request', () => { @@ -462,7 +468,7 @@ describe('resourceCollection', () => { }) expect(rawRumEvents.length).toBe(1) - expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd.discarded).toBeTrue() + expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd.discarded).toBe(true) }) }) }) @@ -487,9 +493,9 @@ describe('resourceCollection', () => { expect(rawRumEvents[0].startClocks.relative).toBe(200 as RelativeTime) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), resource: { - id: jasmine.any(String), + id: expect.any(String), duration: (100 * 1e6) as ServerDuration, method: 'GET', status_code: 200, @@ -513,13 +519,13 @@ describe('resourceCollection', () => { }, }) expect(rawRumEvents[0].domainContext).toEqual({ - performanceEntry: jasmine.any(Object), + performanceEntry: expect.any(Object), response, requestInput: 'https://resource.com/valid', requestInit: { headers: { foo: 'bar' } }, error: undefined, isAborted: false, - handlingStack: jasmine.stringMatching(HANDLING_STACK_REGEX), + handlingStack: expect.stringMatching(HANDLING_STACK_REGEX), isManual: false, isWebSocket: false, xhr: undefined, @@ -547,7 +553,7 @@ describe('resourceCollection', () => { }) expect(rawRumEvents[0].domainContext).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ error, }) ) @@ -628,7 +634,7 @@ describe('resourceCollection', () => { }) const xhr = new XMLHttpRequest() - spyOn(xhr, 'getAllResponseHeaders').and.returnValue( + vi.spyOn(xhr, 'getAllResponseHeaders').mockReturnValue( 'Content-Type: application/json\r\nCache-Control: max-age=300\r\nX-Other: ignored\r\n' ) @@ -650,7 +656,7 @@ describe('resourceCollection', () => { setupResourceCollection({ trackResourceHeaders: buildMatchHeadersForAllUrls(['content-type']) }) const xhr = new XMLHttpRequest() - spyOn(xhr, 'getAllResponseHeaders').and.returnValue('Content-Type: text/html\r\n') + vi.spyOn(xhr, 'getAllResponseHeaders').mockReturnValue('Content-Type: text/html\r\n') notifyRequest({ request: { type: RequestType.XHR, xhr }, @@ -799,7 +805,7 @@ describe('resourceCollection', () => { describe('limit headers size', () => { it('should truncate header values exceeding the max length', () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) setupResourceCollection({ trackResourceHeaders: buildMatchHeadersForAllUrls(['x-long']) }) const longValue = 'a'.repeat(200) @@ -812,11 +818,14 @@ describe('resourceCollection', () => { const event = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(event.resource.response!.headers!['x-long']).toBe('a'.repeat(128)) - expect(displaySpy).toHaveBeenCalledOnceWith('Header "x-long" value was truncated from 200 to 128 characters.') + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( + 'Header "x-long" value was truncated from 200 to 128 characters.' + ) }) it('should limit the number of collected headers', () => { - spyOn(display, 'warn') + vi.spyOn(display, 'warn').mockImplementation(() => undefined) const headerNames = Array.from({ length: 101 }, (_, i) => `x-header-${i}`) setupResourceCollection({ trackResourceHeaders: buildMatchHeadersForAllUrls(headerNames) }) @@ -837,7 +846,7 @@ describe('resourceCollection', () => { }) it('should only count headers that pass filtering toward the limit', () => { - spyOn(display, 'warn') + vi.spyOn(display, 'warn').mockImplementation(() => undefined) const allowedHeaders = Array.from({ length: 100 }, (_, i) => `x-header-${i}`) // Include a forbidden header name in the matchers - it won't be counted const allMatchers = [...allowedHeaders, 'authorization', 'x-extra'] @@ -865,7 +874,7 @@ describe('resourceCollection', () => { }) it('should not emit telemetry when the max number of headers has not been reached', async () => { - spyOn(display, 'warn') + vi.spyOn(display, 'warn').mockImplementation(() => undefined) const telemetry: MockTelemetry = startMockTelemetry() setupResourceCollection({ trackResourceHeaders: buildMatchHeadersForAllUrls(['x-header-0', 'x-header-1']) }) @@ -876,13 +885,13 @@ describe('resourceCollection', () => { }, }) - expect(await telemetry.getEvents()).not.toContain( - jasmine.objectContaining({ message: 'Maximum number of resource headers reached' }) + expect(await telemetry.getEvents()).not.toContainEqual( + expect.objectContaining({ message: 'Maximum number of resource headers reached' }) ) }) it('should warn and emit telemetry when the max number of headers is reached', async () => { - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) const telemetry: MockTelemetry = startMockTelemetry() const headerNames = Array.from({ length: 110 }, (_, i) => `x-header-${i}`) setupResourceCollection({ trackResourceHeaders: buildMatchHeadersForAllUrls(headerNames) }) @@ -900,11 +909,12 @@ describe('resourceCollection', () => { }, }) - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( 'Maximum number of headers (100) has been reached. Further headers are dropped.' ) - expect(await telemetry.getEvents()).toContain( - jasmine.objectContaining({ + expect(await telemetry.getEvents()).toContainEqual( + expect.objectContaining({ message: 'Maximum number of resource headers reached', collectedHeaderCount: 100, // Account for automatically added content-type header @@ -1168,7 +1178,7 @@ describe('resourceCollection', () => { const privateFields = (rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd expect(privateFields).toBeDefined() expect(privateFields.trace_id).toBe('1234') - expect(privateFields.span_id).toEqual(jasmine.any(String)) + expect(privateFields.span_id).toEqual(expect.any(String)) }) it('should be processed from sampled completed request', () => { @@ -1310,7 +1320,7 @@ describe('resourceCollection', () => { expect(rawRumEvents.length).toBe(0) - taskQueuePushSpy.calls.allArgs().forEach(([task], index) => { + taskQueuePushSpy.mock.calls.forEach(([task], index) => { task() expect(rawRumEvents.length).toBe(index + 1) }) @@ -1320,10 +1330,10 @@ describe('resourceCollection', () => { // Request-type entries are queued through a `setTimeout(…, REQUEST_MATCHING_DELAY)` before // they reach the task queue β€” advance past it so they get pushed. clock.tick(REQUEST_MATCHING_DELAY) - taskQueuePushSpy.calls.allArgs().forEach(([task]) => { + taskQueuePushSpy.mock.calls.forEach(([task]) => { task() }) - taskQueuePushSpy.calls.reset() + taskQueuePushSpy.mockClear() } function notifyRequest({ diff --git a/packages/browser-rum-core/src/domain/resource/resourceUtils.spec.ts b/packages/browser-rum-core/src/domain/resource/resourceUtils.spec.ts index e30b9e05ea..1ba316f930 100644 --- a/packages/browser-rum-core/src/domain/resource/resourceUtils.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/resourceUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { Duration, ServerDuration, RelativeTime } from '@datadog/js-core/time' import { addExperimentalFeatures, ExperimentalFeature } from '@datadog/browser-core' import { RumPerformanceEntryType } from '../../browser/performanceObservable' diff --git a/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts b/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts index 8771b50ab9..a942396dfb 100644 --- a/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { Duration, ServerDuration } from '@datadog/js-core/time' import { ResourceType } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' @@ -35,12 +36,12 @@ describe('trackManualResources', () => { clock.tick(500) stopResource('https://api.example.com/data') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) expect(rawRumEvents[0].duration).toBe(500 as Duration) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ type: RumEventType.RESOURCE, - resource: jasmine.objectContaining({ + resource: expect.objectContaining({ url: 'https://api.example.com/data', type: ResourceType.OTHER, }), @@ -53,7 +54,7 @@ describe('trackManualResources', () => { clock.tick(500) stopResource('https://api.example.com/data') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.duration).toBe((500 * 1e6) as ServerDuration) }) @@ -64,7 +65,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data') stopResource('https://api.example.com/data') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.type).toBe(ResourceType.OTHER) }) @@ -74,7 +75,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data', { type: resourceType }) stopResource('https://api.example.com/data') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.type).toBe(resourceType) }) @@ -85,7 +86,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data', { type: ResourceType.XHR }) stopResource('https://api.example.com/data', { type: ResourceType.FETCH }) - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.type).toBe(ResourceType.FETCH) }) @@ -94,7 +95,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data', { type: ResourceType.IMAGE }) stopResource('https://api.example.com/data') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.type).toBe(ResourceType.IMAGE) }) @@ -105,7 +106,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data', { method: 'POST' }) stopResource('https://api.example.com/data', { statusCode: 200 }) - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.method).toBe('POST') expect(resourceEvent.resource.status_code).toBe(200) @@ -117,7 +118,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data') stopResource('https://api.example.com/data', { size: 1234 }) - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.size).toBe(1234) }) @@ -126,7 +127,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data') stopResource('https://api.example.com/data') - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.resource.size).toBeUndefined() }) @@ -143,7 +144,7 @@ describe('trackManualResources', () => { clock.tick(100) stopResource('https://api.example.com/data', { resourceKey: 'request1' }) - expect(rawRumEvents).toHaveSize(2) + expect(rawRumEvents).toHaveLength(2) expect(rawRumEvents[0].duration).toBe(100 as Duration) expect(rawRumEvents[1].duration).toBe(200 as Duration) }) @@ -155,7 +156,7 @@ describe('trackManualResources', () => { stopResource('https://api.example.com/foo/bar') stopResource('https://api.example.com/foo', { resourceKey: 'bar' }) - expect(rawRumEvents).toHaveSize(2) + expect(rawRumEvents).toHaveLength(2) expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent).resource.url).toBe('https://api.example.com/foo/bar') expect((rawRumEvents[1].rawRumEvent as RawRumResourceEvent).resource.url).toBe('https://api.example.com/foo') }) @@ -166,7 +167,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data', { context: { startKey: 'value1' } }) stopResource('https://api.example.com/data', { context: { stopKey: 'value2' } }) - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) const resourceEvent = rawRumEvents[0].rawRumEvent as RawRumResourceEvent expect(resourceEvent.context).toEqual({ startKey: 'value1', @@ -180,7 +181,7 @@ describe('trackManualResources', () => { startResource('https://api.example.com/data', { resourceKey: 'key1' }) stopResource(undefined as any, { resourceKey: 'key1' }) - expect(rawRumEvents).toHaveSize(1) + expect(rawRumEvents).toHaveLength(1) expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent).resource.url).toBe('https://api.example.com/data') }) }) diff --git a/packages/browser-rum-core/src/domain/resource/webSocketCollection.spec.ts b/packages/browser-rum-core/src/domain/resource/webSocketCollection.spec.ts index 08d59ef268..80fc804972 100644 --- a/packages/browser-rum-core/src/domain/resource/webSocketCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/webSocketCollection.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { WebSocketContext } from '@datadog/browser-core' import { initWebSocketObservable, Observable } from '@datadog/browser-core' import { mockClock, registerCleanupTask, type Clock } from '@datadog/browser-core/test' @@ -29,10 +30,7 @@ describe('webSocketCollection', () => { }) }) - function startTracking( - viewHistory = mockViewHistory(), - addDurationVital: (vital: DurationVital) => void = jasmine.createSpy() - ) { + function startTracking(viewHistory = mockViewHistory(), addDurationVital: (vital: DurationVital) => void = vi.fn()) { return trackWebSocket(lifeCycle, wsObservable, viewHistory, addDurationVital) } @@ -92,23 +90,23 @@ describe('webSocketCollection', () => { notifyConnecting() notifyOpen(10) notifyClosed(40, 1000, 'bye', true) - expect(completed[0].handshakeSucceeded).toBeTrue() + expect(completed[0].handshakeSucceeded).toBe(true) notifyConnecting() notifyOpen(10) tracker.flushOpenConnections('session_end') - expect(completed[1].handshakeSucceeded).toBeTrue() + expect(completed[1].handshakeSucceeded).toBe(true) }) it('is false when the open event never fired before completion', () => { const tracker = startTracking() notifyConnecting() notifyClosed(25, 1006, 'abnormal', false) - expect(completed[0].handshakeSucceeded).toBeFalse() + expect(completed[0].handshakeSucceeded).toBe(false) notifyConnecting() tracker.flushOpenConnections('session_end') - expect(completed[1].handshakeSucceeded).toBeFalse() + expect(completed[1].handshakeSucceeded).toBe(false) }) }) @@ -134,7 +132,7 @@ describe('webSocketCollection', () => { expect(webSocket.trackingEndReason).toBe('close_event') expect(webSocket.closeCode).toBe(closeCode) expect(webSocket.closeReason).toBe(closeReason) - expect(webSocket.wasClean).toBeTrue() + expect(webSocket.wasClean).toBe(true) expect(webSocket.url).toBe(url) expect(webSocket.protocol).toBe(protocol) expect(webSocket.messagesIn).toEqual({ count: 1, size: messageInSize }) @@ -329,7 +327,7 @@ describe('webSocketCollection', () => { [relativeStartViewB]: { id: 'view-B', startClocks: relativeToClocks(relativeStartViewB) }, } const viewHistory = mockViewHistory() - spyOn(viewHistory, 'findView').and.callFake((startTime?: RelativeTime) => + vi.spyOn(viewHistory, 'findView').mockImplementation((startTime?: RelativeTime) => startTime !== undefined ? viewByRelative[startTime as number] : undefined ) @@ -352,7 +350,7 @@ describe('webSocketCollection', () => { expect(completed.length).toBe(1) expect(completed[0].trackingEndReason).toBe('session_end') - expect(completed[0].handshakeSucceeded).toBeTrue() + expect(completed[0].handshakeSucceeded).toBe(true) expect(completed[0].closeCode).toBeUndefined() expect(completed[0].closeReason).toBeUndefined() expect(completed[0].wasClean).toBeUndefined() @@ -380,12 +378,13 @@ describe('webSocketCollection', () => { describe('websocket-connecting vital', () => { it('emits a duration-0 vital on connecting', () => { - const addDurationVital = jasmine.createSpy<(vital: DurationVital) => void>() + const addDurationVital = vi.fn<(vital: DurationVital) => void>() startTracking(mockViewHistory(), addDurationVital) notifyConnecting() - expect(addDurationVital).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ + expect(addDurationVital).toHaveBeenCalledTimes(1) + expect(addDurationVital).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ name: WEBSOCKET_CONNECTING_VITAL_NAME, type: VitalType.DURATION, duration: 0, @@ -394,12 +393,15 @@ describe('webSocketCollection', () => { }) it('uses the same id as the subsequent WEBSOCKET_COMPLETED connectionId', () => { - const addDurationVital = jasmine.createSpy<(vital: DurationVital) => void>() + const addDurationVital = vi.fn<(vital: DurationVital) => void>() startTracking(mockViewHistory(), addDurationVital) notifyConnecting() notifyClosed(1, 1000, 'bye', true) - expect(addDurationVital).toHaveBeenCalledOnceWith(jasmine.objectContaining({ id: completed[0].connectionId })) + expect(addDurationVital).toHaveBeenCalledTimes(1) + expect(addDurationVital).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ id: completed[0].connectionId }) + ) }) it('includes url, protocols, and startViewId in the vital context', () => { @@ -409,18 +411,19 @@ describe('webSocketCollection', () => { [relativeStartView]: { id: 'view-start', startClocks: relativeToClocks(relativeStartView) }, } const viewHistory = mockViewHistory() - spyOn(viewHistory, 'findView').and.callFake((startTime?: RelativeTime) => + vi.spyOn(viewHistory, 'findView').mockImplementation((startTime?: RelativeTime) => startTime !== undefined ? viewByRelative[startTime as number] : undefined ) - const addDurationVital = jasmine.createSpy<(vital: DurationVital) => void>() + const addDurationVital = vi.fn<(vital: DurationVital) => void>() const url = 'wss://example.com/socket' const protocols = ['chat.v1', 'json'] startTracking(viewHistory, addDurationVital) notifyConnecting(startView, url, undefined, protocols) - expect(addDurationVital).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ + expect(addDurationVital).toHaveBeenCalledTimes(1) + expect(addDurationVital).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ context: { url, protocols, @@ -437,7 +440,7 @@ describe('webSocketCollection', () => { const singletonObservable = () => initWebSocketObservable() function startCollection() { - const collection = startWebSocketCollection(lifeCycle, mockViewHistory(), jasmine.createSpy()) + const collection = startWebSocketCollection(lifeCycle, mockViewHistory(), vi.fn()) registerCleanupTask(() => collection.stop()) return collection } @@ -489,7 +492,7 @@ describe('webSocketCollection', () => { expect(completed.length).toBe(1) expect(completed[0].trackingEndReason).toBe('session_end') - expect(completed[0].handshakeSucceeded).toBeFalse() + expect(completed[0].handshakeSucceeded).toBe(false) expect(completed[0].closeCode).toBeUndefined() expect(completed[0].closeReason).toBeUndefined() expect(completed[0].wasClean).toBeUndefined() diff --git a/packages/browser-rum-core/src/domain/sessionReplayState.spec.ts b/packages/browser-rum-core/src/domain/sessionReplayState.spec.ts index 9ef48df152..ad79c081c3 100644 --- a/packages/browser-rum-core/src/domain/sessionReplayState.spec.ts +++ b/packages/browser-rum-core/src/domain/sessionReplayState.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { SessionContext } from '@datadog/browser-core' import { timeStampNow } from '@datadog/js-core/time' import { BridgeCapability } from '@datadog/browser-core' diff --git a/packages/browser-rum-core/src/domain/startCustomerDataTelemetry.spec.ts b/packages/browser-rum-core/src/domain/startCustomerDataTelemetry.spec.ts index 5c35c3019e..b9304d98b5 100644 --- a/packages/browser-rum-core/src/domain/startCustomerDataTelemetry.spec.ts +++ b/packages/browser-rum-core/src/domain/startCustomerDataTelemetry.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { FlushEvent, Context, Telemetry } from '@datadog/browser-core' import { Observable } from '@datadog/browser-core' import type { Clock, MockTelemetry } from '@datadog/browser-core/test' @@ -60,7 +61,7 @@ describe('customerDataTelemetry', () => { clock.tick(MEASURES_PERIOD_DURATION) expect(await telemetry.getEvents()).toEqual([ - jasmine.objectContaining({ + expect.objectContaining({ type: 'log', status: 'debug', message: 'Customer data measures', @@ -85,7 +86,7 @@ describe('customerDataTelemetry', () => { generateBatch({ eventNumber: 10, contextBytesCount: 10, batchBytesCount: 10 }) clock.tick(MEASURES_PERIOD_DURATION) expect(await telemetry.getEvents()).toEqual([ - jasmine.objectContaining({ + expect.objectContaining({ type: 'log', status: 'debug', message: 'Customer data measures', @@ -112,8 +113,8 @@ describe('customerDataTelemetry', () => { clock.tick(MEASURES_PERIOD_DURATION) expect(await telemetry.getEvents()).toEqual([ - jasmine.objectContaining({ - batchMessagesCount: jasmine.objectContaining({ sum: 1 }), + expect.objectContaining({ + batchMessagesCount: expect.objectContaining({ sum: 1 }), }), ]) }) diff --git a/packages/browser-rum-core/src/domain/tracing/getDocumentTraceId.spec.ts b/packages/browser-rum-core/src/domain/tracing/getDocumentTraceId.spec.ts index 2292771491..82595680d8 100644 --- a/packages/browser-rum-core/src/domain/tracing/getDocumentTraceId.spec.ts +++ b/packages/browser-rum-core/src/domain/tracing/getDocumentTraceId.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import type { TimeStamp } from '@datadog/js-core/time' import { getTimeOrigin } from '@datadog/js-core/time' import { diff --git a/packages/browser-rum-core/src/domain/tracing/identifier.spec.ts b/packages/browser-rum-core/src/domain/tracing/identifier.spec.ts index 6835e2cbc3..e82623645e 100644 --- a/packages/browser-rum-core/src/domain/tracing/identifier.spec.ts +++ b/packages/browser-rum-core/src/domain/tracing/identifier.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { createSpanIdentifier, createTraceIdentifier, toPaddedHexadecimalString } from './identifier' describe('identifier', () => { @@ -38,7 +39,7 @@ describe('toPaddedHexadecimalString', () => { }) function mockRandomValues(cb: (buffer: Uint8Array) => void) { - spyOn(window.crypto, 'getRandomValues').and.callFake((bufferView) => { + vi.spyOn(window.crypto, 'getRandomValues').mockImplementation((bufferView) => { cb(new Uint8Array(bufferView.buffer)) return bufferView }) diff --git a/packages/browser-rum-core/src/domain/tracing/tracer.spec.ts b/packages/browser-rum-core/src/domain/tracing/tracer.spec.ts index c957727f72..07a7f75ccf 100644 --- a/packages/browser-rum-core/src/domain/tracing/tracer.spec.ts +++ b/packages/browser-rum-core/src/domain/tracing/tracer.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { ContextManager, ContextValue } from '@datadog/browser-core' import { display, objectEntries, TraceContextInjection } from '@datadog/browser-core' import type { SessionManagerMock } from '@datadog/browser-core/test' @@ -135,9 +136,9 @@ describe('tracer', () => { tracer.traceXhr(context, xhr as unknown as XMLHttpRequest) expect(xhr.headers).toEqual( - jasmine.objectContaining({ - b3: jasmine.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-0$/), - traceparent: jasmine.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-00$/), + expect.objectContaining({ + b3: expect.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-0$/), + traceparent: expect.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-00$/), tracestate: 'dd=s:0;o:rum', 'X-B3-Sampled': '0', }) @@ -182,9 +183,9 @@ describe('tracer', () => { tracer.traceXhr(context, xhr as unknown as XMLHttpRequest) expect(xhr.headers).toEqual( - jasmine.objectContaining({ - 'X-B3-TraceId': jasmine.stringMatching(/^[0-9a-f]{16}$/), - 'X-B3-SpanId': jasmine.stringMatching(/^[0-9a-f]{16}$/), + expect.objectContaining({ + 'X-B3-TraceId': expect.stringMatching(/^[0-9a-f]{16}$/), + 'X-B3-SpanId': expect.stringMatching(/^[0-9a-f]{16}$/), 'X-B3-Sampled': '1', }) ) @@ -205,9 +206,9 @@ describe('tracer', () => { tracer.traceXhr(context, xhr as unknown as XMLHttpRequest) expect(xhr.headers).toEqual( - jasmine.objectContaining({ - b3: jasmine.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-1$/), - traceparent: jasmine.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-01$/), + expect.objectContaining({ + b3: expect.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-1$/), + traceparent: expect.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-01$/), tracestate: 'dd=s:1;o:rum', }) ) @@ -357,7 +358,7 @@ describe('tracer', () => { }) it('should display an error when a matching function throws', () => { - const displaySpy = spyOn(display, 'error') + const displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) const tracer = startTracerWithDefaults({ initConfiguration: { allowedTracingUrls: [ @@ -598,22 +599,22 @@ describe('tracer', () => { const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } tracer.traceFetch(context) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['X-B3-TraceId'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['X-B3-SpanId'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['X-B3-Sampled'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['X-B3-TraceId'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['X-B3-SpanId'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['X-B3-Sampled'])) expect(context.init!.headers).toEqual( - jasmine.arrayContaining([ - ['X-B3-TraceId', jasmine.stringMatching(/^[0-9a-f]{16}$/)], - ['X-B3-SpanId', jasmine.stringMatching(/^[0-9a-f]{16}$/)], + expect.arrayContaining([ + ['X-B3-TraceId', expect.stringMatching(/^[0-9a-f]{16}$/)], + ['X-B3-SpanId', expect.stringMatching(/^[0-9a-f]{16}$/)], ['X-B3-Sampled', '1'], ]) ) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-origin'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-parent-id'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-trace-id'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-sampling-priority'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['x-datadog-origin'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['x-datadog-parent-id'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['x-datadog-trace-id'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['x-datadog-sampling-priority'])) }) it('should add headers for b3 (single) and tracecontext propagators', () => { @@ -627,9 +628,9 @@ describe('tracer', () => { tracer.traceFetch(context) expect(context.init!.headers).toEqual( - jasmine.arrayContaining([ - ['b3', jasmine.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-1$/)], - ['traceparent', jasmine.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-01$/)], + expect.arrayContaining([ + ['b3', expect.stringMatching(/^[0-9a-f]{16}-[0-9a-f]{16}-1$/)], + ['traceparent', expect.stringMatching(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-01$/)], ['tracestate', 'dd=s:1;o:rum'], ]) ) @@ -645,11 +646,11 @@ describe('tracer', () => { const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } tracer.traceFetch(context) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['b3'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['traceparent'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['tracestate'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['x-datadog-trace-id'])) - expect(context.init!.headers).not.toContain(jasmine.arrayContaining(['X-B3-TraceId'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['b3'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['traceparent'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['tracestate'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['x-datadog-trace-id'])) + expect(context.init!.headers).not.toContainEqual(expect.arrayContaining(['X-B3-TraceId'])) }) it('should not add headers when trace not sampled and config set to sampled', () => { const tracer = startTracerWithDefaults({ @@ -676,10 +677,10 @@ describe('tracer', () => { const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } tracer.traceFetch(context) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-origin'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-parent-id'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-trace-id'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-sampling-priority'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-origin'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-parent-id'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-trace-id'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-sampling-priority'])) }) it('should add headers when trace not sampled and config set to all', () => { @@ -693,10 +694,10 @@ describe('tracer', () => { const context: Partial = { ...ALLOWED_DOMAIN_CONTEXT } tracer.traceFetch(context) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-origin'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-parent-id'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-trace-id'])) - expect(context.init!.headers).toContain(jasmine.arrayContaining(['x-datadog-sampling-priority'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-origin'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-parent-id'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-trace-id'])) + expect(context.init!.headers).toContainEqual(expect.arrayContaining(['x-datadog-sampling-priority'])) }) }) diff --git a/packages/browser-rum-core/src/domain/trackEventCounts.spec.ts b/packages/browser-rum-core/src/domain/trackEventCounts.spec.ts index 4b707bdb12..b5475a5bc3 100644 --- a/packages/browser-rum-core/src/domain/trackEventCounts.spec.ts +++ b/packages/browser-rum-core/src/domain/trackEventCounts.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { objectValues } from '@datadog/browser-core' import type { AssembledRumEvent } from '../rawRumEvent.types' import { FrustrationType, RumEventType } from '../rawRumEvent.types' @@ -76,7 +77,7 @@ describe('trackEventCounts', () => { }) it('invokes a potential callback when a count is increased', () => { - const spy = jasmine.createSpy<() => void>() + const spy = vi.fn<() => void>() trackEventCounts({ lifeCycle, isChildEvent: () => true, onChange: spy }) notifyCollectedRawRumEvent({ type: RumEventType.RESOURCE }) diff --git a/packages/browser-rum-core/src/domain/view/bfCacheSupport.spec.ts b/packages/browser-rum-core/src/domain/view/bfCacheSupport.spec.ts index 5d05f892a4..656f9d9694 100644 --- a/packages/browser-rum-core/src/domain/view/bfCacheSupport.spec.ts +++ b/packages/browser-rum-core/src/domain/view/bfCacheSupport.spec.ts @@ -1,9 +1,10 @@ +import { vi, describe, expect, it } from 'vitest' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { onBFCacheRestore } from './bfCacheSupport' describe('onBFCacheRestore', () => { it('should invoke the callback only for BFCache restoration and stop listening when stopped', () => { - const callback = jasmine.createSpy('callback') + const callback = vi.fn() const stop = onBFCacheRestore(callback) registerCleanupTask(stop) diff --git a/packages/browser-rum-core/src/domain/view/trackViewEventCounts.spec.ts b/packages/browser-rum-core/src/domain/view/trackViewEventCounts.spec.ts index 5d4fb0d542..f151981a2d 100644 --- a/packages/browser-rum-core/src/domain/view/trackViewEventCounts.spec.ts +++ b/packages/browser-rum-core/src/domain/view/trackViewEventCounts.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import { registerCleanupTask } from '@datadog/browser-core/test' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' import type { AssembledRumEvent } from '../../rawRumEvent.types' @@ -9,7 +10,7 @@ describe('trackViewEventCounts', () => { let onChange: () => void beforeEach(() => { - onChange = jasmine.createSpy('onChange') + onChange = vi.fn() const viewEventCountsTracking = trackViewEventCounts(lifeCycle, 'view-id', onChange) registerCleanupTask(viewEventCountsTracking.stop) diff --git a/packages/browser-rum-core/src/domain/view/trackViews.spec.ts b/packages/browser-rum-core/src/domain/view/trackViews.spec.ts index 1f44be90fc..d4811bb2dd 100644 --- a/packages/browser-rum-core/src/domain/view/trackViews.spec.ts +++ b/packages/browser-rum-core/src/domain/view/trackViews.spec.ts @@ -1,7 +1,7 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime, Duration } from '@datadog/js-core/time' import { timeStampNow, relativeToClocks, relativeNow } from '@datadog/js-core/time' import { PageExitReason, display } from '@datadog/browser-core' - import type { Clock } from '@datadog/browser-core/test' import { mockClock, registerCleanupTask, createNewEvent } from '@datadog/browser-core/test' import { createPerformanceEntry, mockPerformanceObserver } from '../../../test' @@ -66,7 +66,7 @@ describe('track views automatically', () => { function mockGetElementById() { const fakeGetElementById = (elementId: string) => (elementId === 'testHashValue') as any as HTMLElement - return spyOn(document, 'getElementById').and.callFake(fakeGetElementById) + return vi.spyOn(document, 'getElementById').mockImplementation(fakeGetElementById) } it('should not create a new view when it is an Anchor navigation', () => { @@ -137,13 +137,13 @@ describe('view lifecycle', () => { let lifeCycle: LifeCycle let viewTest: ViewTest let clock: Clock - let notifySpy: jasmine.Spy + let notifySpy: Mock let changeLocation: (to: string) => void beforeEach(() => { clock = mockClock() lifeCycle = new LifeCycle() - notifySpy = spyOn(lifeCycle, 'notify').and.callThrough() + notifySpy = vi.spyOn(lifeCycle, 'notify') viewTest = setupViewTest( { lifeCycle, initialLocation: '/foo' }, @@ -218,7 +218,7 @@ describe('view lifecycle', () => { expect(getViewCreateCount()).toBe(1) lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) expect(getViewCreateCount()).toBe(2) }) @@ -227,7 +227,7 @@ describe('view lifecycle', () => { const { getViewUpdate, getViewUpdateCount } = viewTest lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) expect(getViewUpdate(getViewUpdateCount() - 1).loadingType).toBe(ViewLoadingType.SESSION_RENEWAL) }) @@ -235,71 +235,71 @@ describe('view lifecycle', () => { it('should use the current view name, service and version for the new view', () => { const { getViewCreateCount, getViewCreate, startView } = viewTest lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) startView({ name: 'view 1', service: 'service 1', version: 'version 1' }) startView({ name: 'view 2', service: 'service 2', version: 'version 2' }) lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) startView({ name: 'view 3', service: 'service 3', version: 'version 3' }) changeLocation('/bar') lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED, undefined) expect(getViewCreateCount()).toBe(8) expect(getViewCreate(0)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'initial view name', service: 'initial service', version: 'initial version', }) ) expect(getViewCreate(1)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'initial view name', service: 'initial service', version: 'initial version', }) ) expect(getViewCreate(2)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'view 1', service: 'service 1', version: 'version 1', }) ) expect(getViewCreate(3)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'view 2', service: 'service 2', version: 'version 2', }) ) expect(getViewCreate(4)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'view 2', service: 'service 2', version: 'version 2', }) ) expect(getViewCreate(5)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'view 3', service: 'service 3', version: 'version 3', }) ) expect(getViewCreate(6)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: undefined, service: undefined, version: undefined, }) ) expect(getViewCreate(7)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: undefined, service: undefined, version: undefined, @@ -381,17 +381,17 @@ describe('view lifecycle', () => { }) it('should notify BEFORE_VIEW_CREATED before VIEW_CREATED', () => { - expect(notifySpy.calls.argsFor(0)[0]).toEqual(LifeCycleEventType.BEFORE_VIEW_CREATED) - expect(notifySpy.calls.argsFor(1)[0]).toEqual(LifeCycleEventType.VIEW_CREATED) + expect(notifySpy.mock.calls[0][0]).toEqual(LifeCycleEventType.BEFORE_VIEW_CREATED) + expect(notifySpy.mock.calls[1][0]).toEqual(LifeCycleEventType.VIEW_CREATED) }) it('should notify AFTER_VIEW_ENDED after VIEW_ENDED', () => { - const callsCount = notifySpy.calls.count() + const callsCount = notifySpy.mock.calls.length viewTest.stop() - expect(notifySpy.calls.argsFor(callsCount)[0]).toEqual(LifeCycleEventType.VIEW_ENDED) - expect(notifySpy.calls.argsFor(callsCount + 1)[0]).toEqual(LifeCycleEventType.AFTER_VIEW_ENDED) + expect(notifySpy.mock.calls[callsCount][0]).toEqual(LifeCycleEventType.VIEW_ENDED) + expect(notifySpy.mock.calls[callsCount + 1][0]).toEqual(LifeCycleEventType.AFTER_VIEW_ENDED) }) }) @@ -442,9 +442,10 @@ describe('view metrics', () => { }) describe('common view metrics', () => { - it('should be updated when notified with a PERFORMANCE_ENTRY_COLLECTED event (throttled)', () => { + it('should be updated when notified with a PERFORMANCE_ENTRY_COLLECTED event (throttled)', (ctx) => { if (!isLayoutShiftSupported()) { - pending('CLS web vital not supported') + ctx.skip(true, 'Layout Shift API not supported') + return } const { getViewUpdateCount, getViewUpdate } = viewTest @@ -464,13 +465,14 @@ describe('view metrics', () => { time: clock.relative(0), previousRect: undefined, currentRect: undefined, - devicePixelRatio: jasmine.any(Number), + devicePixelRatio: expect.any(Number), }) }) - it('should not be updated after view end', () => { + it('should not be updated after view end', (ctx) => { if (!isLayoutShiftSupported()) { - pending('CLS web vital not supported') + ctx.skip(true, 'Layout Shift API not supported') + return } const { getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = viewTest startView() @@ -501,7 +503,7 @@ describe('view metrics', () => { clock.tick(1) expect(getViewUpdateCount()).toEqual(2) - expect(getViewUpdate(1).initialViewMetrics.navigationTimings).toEqual(jasmine.any(Object)) + expect(getViewUpdate(1).initialViewMetrics.navigationTimings).toEqual(expect.any(Object)) }) it('should be updated for 5 min after view end', () => { @@ -588,9 +590,9 @@ describe('view metrics', () => { it('should be added only on the initial view', () => { expect(initialView.last.initialViewMetrics).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ firstContentfulPaint: 123 as Duration, - navigationTimings: jasmine.any(Object), + navigationTimings: expect.any(Object), largestContentfulPaint: { value: 789 as Duration, targetSelector: undefined, @@ -611,7 +613,7 @@ describe('view metrics', () => { }) it('should update the initial view loadingTime following the loadEventEnd value', () => { - expect(initialView.last.commonViewMetrics.loadingTime).toEqual(jasmine.any(Number)) + expect(initialView.last.commonViewMetrics.loadingTime).toEqual(expect.any(Number)) }) }) }) @@ -752,7 +754,7 @@ describe('view custom timings', () => { it('should sanitized timing name', () => { const { getViewUpdate, addTiming } = viewTest - const displaySpy = spyOn(display, 'warn') + const displaySpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) clock.tick(1234) addTiming('foo bar-qux.@zip_21%$*β‚¬πŸ‘‹', timeStampNow()) @@ -1036,19 +1038,19 @@ describe('start view', () => { startView({ service: 'service 2', version: 'version 2' }) expect(getViewUpdate(2)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ service: undefined, version: undefined, }) ) expect(getViewUpdate(4)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ service: 'service 1', version: 'version 1', }) ) expect(getViewUpdate(6)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ service: 'service 2', version: 'version 2', }) @@ -1060,7 +1062,7 @@ describe('start view', () => { startView({ service: null, version: null }) expect(getViewUpdate(2)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ service: undefined, version: undefined, }) diff --git a/packages/browser-rum-core/src/domain/view/viewCollection.spec.ts b/packages/browser-rum-core/src/domain/view/viewCollection.spec.ts index 25aa28edab..675e5234df 100644 --- a/packages/browser-rum-core/src/domain/view/viewCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it, type Mock } from 'vitest' import { Observable } from '@datadog/browser-core' import { DISCARDED } from '@datadog/js-core/assembly' import type { Duration, ServerDuration, TimeStamp, RelativeTime } from '@datadog/js-core/time' @@ -65,7 +66,7 @@ const VIEW: ViewEvent = { describe('viewCollection', () => { const lifeCycle = new LifeCycle() let hooks: Hooks - let getReplayStatsSpy: jasmine.Spy + let getReplayStatsSpy: Mock let rawRumEvents: Array> = [] function setupViewCollection( partialConfiguration: Partial = {}, @@ -73,7 +74,7 @@ describe('viewCollection', () => { ) { hooks = createHooks() const viewHistory = mockViewHistory(viewHistoryEntry) - getReplayStatsSpy = jasmine.createSpy() + getReplayStatsSpy = vi.fn() const domMutationObservable = new Observable() const windowOpenObservable = new Observable() const locationChangeObservable = new Observable() @@ -112,12 +113,12 @@ describe('viewCollection', () => { document_version: 3, replay_stats: undefined, configuration: { - start_session_replay_recording_manually: jasmine.any(Boolean), + start_session_replay_recording_manually: expect.any(Boolean), remote_configuration_id: undefined, }, cls: undefined, }, - date: jasmine.any(Number), + date: expect.any(Number), type: RumEventType.VIEW, view: { action: { @@ -193,9 +194,9 @@ describe('viewCollection', () => { }, privacy: { replay_level: 'mask-user-input' }, device: { - locale: jasmine.any(String), - locales: jasmine.any(Array), - time_zone: jasmine.any(String), + locale: expect.any(String), + locales: expect.any(Array), + time_zone: expect.any(String), }, }) }) @@ -271,7 +272,7 @@ describe('viewCollection', () => { } as AssembleHookParams) expect(defaultRumEventAttributes).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ service: VIEW.service, version: VIEW.version, context: VIEW.context, diff --git a/packages/browser-rum-core/src/domain/view/viewDiff.spec.ts b/packages/browser-rum-core/src/domain/view/viewDiff.spec.ts index 412081908b..695c4ddd15 100644 --- a/packages/browser-rum-core/src/domain/view/viewDiff.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewDiff.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import { isEqual, diffMerge } from './viewDiff' describe('isEqual', () => { diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/getClsAttributionImpactedArea.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/getClsAttributionImpactedArea.spec.ts index 8be1244bc9..6eb747c83e 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/getClsAttributionImpactedArea.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/getClsAttributionImpactedArea.spec.ts @@ -1,11 +1,13 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RumLayoutShiftAttribution } from '../../../browser/performanceObservable' import { getClsAttributionImpactedArea } from './getClsAttributionImpactedArea' import { isLayoutShiftSupported } from './trackCumulativeLayoutShift' describe('getClsAttributionImpactedArea', () => { - beforeEach(() => { + beforeEach((ctx) => { if (!isLayoutShiftSupported()) { - pending('No LayoutShift API support') + ctx.skip(true, 'Layout Shift API not supported') + return } }) it('should calculate the impacted area when rectangles do not overlap', () => { diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackBfcacheMetrics.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackBfcacheMetrics.spec.ts index 26c93d9a07..dcdb22957d 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackBfcacheMetrics.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackBfcacheMetrics.spec.ts @@ -1,4 +1,5 @@ -import type { RelativeTime, Duration, TimeStamp } from '@datadog/js-core/time' +import { vi, beforeEach, describe, expect, it } from 'vitest' +import type { Duration, RelativeTime, TimeStamp } from '@datadog/js-core/time' import type { Clock } from '@datadog/browser-core/test' import { createNewEvent, mockClock } from '@datadog/browser-core/test' import { trackBfcacheMetrics } from './trackBfcacheMetrics' @@ -10,7 +11,7 @@ describe('trackBfcacheMetrics', () => { beforeEach(() => { clock = mockClock() - spyOn(window, 'requestAnimationFrame').and.callFake((cb: FrameRequestCallback): number => { + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: FrameRequestCallback): number => { cb(performance.now()) return 0 }) @@ -24,7 +25,7 @@ describe('trackBfcacheMetrics', () => { const pageshow = createPageshowEvent() as PageTransitionEvent const metrics: InitialViewMetrics = {} - const scheduleSpy = jasmine.createSpy('schedule') + const scheduleSpy = vi.fn() clock.tick(50) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.spec.ts index 8abe990004..58aa83fa57 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { Duration } from '@datadog/js-core/time' import { clocksOrigin } from '@datadog/js-core/time' import { Observable } from '@datadog/browser-core' @@ -19,14 +20,14 @@ describe('trackCommonViewMetrics', () => { let clock: Clock let domMutationObservable: Observable let windowOpenObservable: Observable - let scheduleViewUpdateSpy: jasmine.Spy + let scheduleViewUpdateSpy: ReturnType beforeEach(() => { mockGlobalPerformanceBuffer() clock = mockClock() domMutationObservable = new Observable() windowOpenObservable = new Observable() - scheduleViewUpdateSpy = jasmine.createSpy('scheduleViewUpdate') + scheduleViewUpdateSpy = vi.fn() }) describe('manual loading time suppresses auto-detected loading time callback', () => { @@ -36,7 +37,7 @@ describe('trackCommonViewMetrics', () => { domMutationObservable, windowOpenObservable, mockRumConfiguration(), - scheduleViewUpdateSpy, + scheduleViewUpdateSpy as unknown as () => void, ViewLoadingType.INITIAL_LOAD, clocksOrigin() ) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index 9aa6663414..a0331db7f4 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -1,5 +1,6 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' +import { elapsed, ONE_SECOND } from '@datadog/js-core/time' import type { RelativeTime } from '@datadog/js-core/time' -import { ONE_SECOND, elapsed } from '@datadog/js-core/time' import { registerCleanupTask } from '@datadog/browser-core/test' import { appendElement, @@ -20,7 +21,7 @@ interface StartCLSTrackingArgs { describe('trackCumulativeLayoutShift', () => { let originalSupportedEntryTypes: PropertyDescriptor | undefined - let clsCallback: jasmine.Spy<(csl: CumulativeLayoutShift) => void> + let clsCallback: Mock<(csl: CumulativeLayoutShift) => void> let notifyPerformanceEntries: (entries: RumPerformanceEntry[]) => void function startCLSTracking({ @@ -29,7 +30,7 @@ describe('trackCumulativeLayoutShift', () => { }: Partial = {}) { ;({ notifyPerformanceEntries } = mockPerformanceObserver()) - clsCallback = jasmine.createSpy() + clsCallback = vi.fn() originalSupportedEntryTypes = Object.getOwnPropertyDescriptor(PerformanceObserver, 'supportedEntryTypes') Object.defineProperty(PerformanceObserver, 'supportedEntryTypes', { get: () => (isLayoutShiftSupported ? ['layout-shift'] : []), @@ -45,15 +46,17 @@ describe('trackCumulativeLayoutShift', () => { }) } - beforeEach(() => { + beforeEach((ctx) => { if (!isLayoutShiftSupported()) { - pending('No LayoutShift API support') + ctx.skip(true, 'Layout Shift API not supported') + return } }) it('should be initialized to 0', () => { startCLSTracking() - expect(clsCallback).toHaveBeenCalledOnceWith({ value: 0 }) + expect(clsCallback).toHaveBeenCalledTimes(1) + expect(clsCallback).toHaveBeenCalledExactlyOnceWith({ value: 0 }) }) it('should be initialized to undefined if layout-shift is not supported', () => { @@ -73,13 +76,13 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(3) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0.3, time: 2 as RelativeTime, targetSelector: undefined, previousRect: undefined, currentRect: undefined, - devicePixelRatio: jasmine.any(Number), + devicePixelRatio: expect.any(Number), }) }) @@ -90,7 +93,7 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(1) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0, }) }) @@ -102,7 +105,7 @@ describe('trackCumulativeLayoutShift', () => { notifyPerformanceEntries([createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 1.11111111111 })]) expect(clsCallback).toHaveBeenCalledTimes(3) - expect(clsCallback.calls.mostRecent().args[0].value).toEqual(2.3457) + expect(clsCallback.mock.lastCall![0].value).toEqual(2.3457) }) it('should ignore entries with recent input', () => { @@ -115,7 +118,7 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(1) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0, }) }) @@ -138,13 +141,13 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(3) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0.3, time: 1 as RelativeTime, targetSelector: undefined, previousRect: undefined, currentRect: undefined, - devicePixelRatio: jasmine.any(Number), + devicePixelRatio: expect.any(Number), }) }) @@ -160,13 +163,13 @@ describe('trackCumulativeLayoutShift', () => { } // window 1: { value: 0.6, time: 999 } | window 2: { value: 0.1, time: 5994(6*999) } expect(clsCallback).toHaveBeenCalledTimes(7) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0.6, time: 999 as RelativeTime, targetSelector: undefined, previousRect: undefined, currentRect: undefined, - devicePixelRatio: jasmine.any(Number), + devicePixelRatio: expect.any(Number), }) }) @@ -215,13 +218,13 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(4) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0.5, time: 5002 as RelativeTime, targetSelector: undefined, previousRect: undefined, currentRect: undefined, - devicePixelRatio: jasmine.any(Number), + devicePixelRatio: expect.any(Number), }) }) @@ -234,7 +237,7 @@ describe('trackCumulativeLayoutShift', () => { createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1, startTime: shiftStart }), ]) - expect(clsCallback.calls.mostRecent().args[0].time).toEqual(elapsed(viewStart, shiftStart)) + expect(clsCallback.mock.lastCall![0].time).toEqual(elapsed(viewStart, shiftStart)) }) describe('cls target element', () => { @@ -266,7 +269,7 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(2) - expect(clsCallback.calls.mostRecent().args[0].targetSelector).toEqual('#div-element') + expect(clsCallback.mock.lastCall![0].targetSelector).toEqual('#div-element') }) it('should not return the target element when the element is detached from the DOM before the performance entry event is triggered', () => { @@ -279,7 +282,7 @@ describe('trackCumulativeLayoutShift', () => { }), ]) - expect(clsCallback.calls.mostRecent().args[0].value).toEqual(0.2) + expect(clsCallback.mock.lastCall![0].value).toEqual(0.2) // second session window // first shift with an element const divElement = appendElement('
') @@ -299,8 +302,8 @@ describe('trackCumulativeLayoutShift', () => { }), ]) - expect(clsCallback.calls.mostRecent().args[0].value).toEqual(0.2) - expect(clsCallback.calls.mostRecent().args[0].targetSelector).toEqual(undefined) + expect(clsCallback.mock.lastCall![0].value).toEqual(0.2) + expect(clsCallback.mock.lastCall![0].targetSelector).toEqual(undefined) }) it('should get the target element, time, and rects of the largest layout shift', () => { @@ -343,13 +346,13 @@ describe('trackCumulativeLayoutShift', () => { ]) expect(clsCallback).toHaveBeenCalledTimes(4) - expect(clsCallback.calls.mostRecent().args[0]).toEqual({ + expect(clsCallback.mock.lastCall![0]).toEqual({ value: 0.5, time: 1 as RelativeTime, targetSelector: '#div-element', previousRect: { x: 0, y: 0, width: 10, height: 10 }, currentRect: { x: 50, y: 50, width: 10, height: 10 }, - devicePixelRatio: jasmine.any(Number), + devicePixelRatio: expect.any(Number), }) }) }) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts index 3e2609a762..f79590fe3f 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { clocksOrigin } from '@datadog/js-core/time' import { registerCleanupTask, restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' @@ -8,13 +9,13 @@ import { FCP_MAXIMUM_DELAY, trackFirstContentfulPaint } from './trackFirstConten import { trackFirstHidden } from './trackFirstHidden' describe('trackFirstContentfulPaint', () => { - let fcpCallback: jasmine.Spy<(value: RelativeTime) => void> + let fcpCallback: Mock<(value: RelativeTime) => void> let notifyPerformanceEntries: (entries: RumPerformanceEntry[]) => void function startTrackingFCP() { ;({ notifyPerformanceEntries } = mockPerformanceObserver()) - fcpCallback = jasmine.createSpy() + fcpCallback = vi.fn() const firstHidden = trackFirstHidden(clocksOrigin()) const firstContentfulPaint = trackFirstContentfulPaint(firstHidden, fcpCallback) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstHidden.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstHidden.spec.ts index de300a73bc..cf91c16b2e 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstHidden.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackFirstHidden.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime, TimeStamp } from '@datadog/js-core/time' import { clocksOrigin } from '@datadog/js-core/time' import { DOM_EVENT } from '@datadog/browser-core' diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts index 523073f815..be8bc99475 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Duration } from '@datadog/js-core/time' import { clocksOrigin } from '@datadog/js-core/time' import type { Clock } from '@datadog/browser-core/test' @@ -9,17 +10,17 @@ import { trackInitialViewMetrics } from './trackInitialViewMetrics' describe('trackInitialViewMetrics', () => { let clock: Clock - let scheduleViewUpdateSpy: jasmine.Spy<() => void> + let scheduleViewUpdateSpy: Mock<() => void> let trackInitialViewMetricsResult: ReturnType - let setLoadEventSpy: jasmine.Spy<(loadEvent: Duration) => void> + let setLoadEventSpy: Mock<(loadEvent: Duration) => void> let notifyPerformanceEntries: (entries: RumPerformanceEntry[]) => void beforeEach(() => { ;({ notifyPerformanceEntries } = mockPerformanceObserver()) const configuration = mockRumConfiguration() - scheduleViewUpdateSpy = jasmine.createSpy() - setLoadEventSpy = jasmine.createSpy() + scheduleViewUpdateSpy = vi.fn() + setLoadEventSpy = vi.fn() clock = mockClock() trackInitialViewMetricsResult = trackInitialViewMetrics( @@ -42,7 +43,7 @@ describe('trackInitialViewMetrics', () => { expect(scheduleViewUpdateSpy).toHaveBeenCalledTimes(2) expect(trackInitialViewMetricsResult.initialViewMetrics).toEqual({ - navigationTimings: jasmine.any(Object), + navigationTimings: expect.any(Object), firstContentfulPaint: 123 as Duration, }) }) @@ -52,6 +53,7 @@ describe('trackInitialViewMetrics', () => { clock.tick(0) - expect(setLoadEventSpy).toHaveBeenCalledOnceWith(jasmine.any(Number)) + expect(setLoadEventSpy).toHaveBeenCalledTimes(1) + expect(setLoadEventSpy).toHaveBeenCalledExactlyOnceWith(expect.any(Number)) }) }) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 66ba4776ca..f984c38a00 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import type { RelativeTime, Duration } from '@datadog/js-core/time' import { elapsed, relativeNow } from '@datadog/js-core/time' import { registerCleanupTask } from '@datadog/browser-core/test' @@ -57,9 +58,10 @@ describe('trackInteractionToNextPaint', () => { }) } - beforeEach(() => { + beforeEach((ctx) => { if (!isInteractionToNextPaintSupported()) { - pending('No INP support') + ctx.skip(true, 'Interaction to Next Paint not supported') + return } }) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts index eccd9ff594..b27834ec0c 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime } from '@datadog/js-core/time' import { clocksOrigin } from '@datadog/js-core/time' import { DOM_EVENT } from '@datadog/browser-core' @@ -28,7 +29,7 @@ interface StartLCPTrackingOptions { } describe('trackLargestContentfulPaint', () => { - let lcpCallback: jasmine.Spy<(lcp: LargestContentfulPaint) => void> + let lcpCallback: Mock<(lcp: LargestContentfulPaint) => void> let eventTarget: Window let notifyPerformanceEntries: (entries: RumPerformanceEntry[]) => void let clock: Clock @@ -80,7 +81,7 @@ describe('trackLargestContentfulPaint', () => { } beforeEach(() => { - lcpCallback = jasmine.createSpy() + lcpCallback = vi.fn() eventTarget = document.createElement('div') as unknown as Window // Mock clock and advance time so that responseStart: 789 passes the sanitizeFirstByte check // which requires responseStart <= relativeNow() @@ -92,7 +93,8 @@ describe('trackLargestContentfulPaint', () => { startLCPTracking() notifyPerformanceEntries([createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT)]) - expect(lcpCallback).toHaveBeenCalledOnceWith({ + expect(lcpCallback).toHaveBeenCalledTimes(1) + expect(lcpCallback).toHaveBeenCalledExactlyOnceWith({ value: 789 as RelativeTime, targetSelector: undefined, resourceUrl: undefined, @@ -108,7 +110,8 @@ describe('trackLargestContentfulPaint', () => { }), ]) - expect(lcpCallback).toHaveBeenCalledOnceWith({ + expect(lcpCallback).toHaveBeenCalledTimes(1) + expect(lcpCallback).toHaveBeenCalledExactlyOnceWith({ value: 789 as RelativeTime, targetSelector: '#lcp-target-element', resourceUrl: undefined, @@ -124,7 +127,8 @@ describe('trackLargestContentfulPaint', () => { }), ]) - expect(lcpCallback).toHaveBeenCalledOnceWith({ + expect(lcpCallback).toHaveBeenCalledTimes(1) + expect(lcpCallback).toHaveBeenCalledExactlyOnceWith({ value: 789 as RelativeTime, targetSelector: undefined, resourceUrl: 'https://example.com/lcp-resource', @@ -177,7 +181,8 @@ describe('trackLargestContentfulPaint', () => { }), ]) - expect(lcpCallback).toHaveBeenCalledOnceWith(jasmine.objectContaining({ value: 1 })) + expect(lcpCallback).toHaveBeenCalledTimes(1) + expect(lcpCallback).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ value: 1 })) }) it('should notify multiple times when the size is bigger than the previous entry', () => { @@ -196,8 +201,8 @@ describe('trackLargestContentfulPaint', () => { }), ]) expect(lcpCallback).toHaveBeenCalledTimes(2) - expect(lcpCallback.calls.first().args[0]).toEqual(jasmine.objectContaining({ value: 1 })) - expect(lcpCallback.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ value: 2 })) + expect(lcpCallback.mock.calls[0][0]).toEqual(expect.objectContaining({ value: 1 })) + expect(lcpCallback.mock.lastCall![0]).toEqual(expect.objectContaining({ value: 2 })) }) it('should return undefined when LCP entry has an empty string as url', () => { @@ -208,7 +213,8 @@ describe('trackLargestContentfulPaint', () => { }), ]) - expect(lcpCallback).toHaveBeenCalledOnceWith({ + expect(lcpCallback).toHaveBeenCalledTimes(1) + expect(lcpCallback).toHaveBeenCalledExactlyOnceWith({ value: 789 as RelativeTime, targetSelector: undefined, resourceUrl: undefined, @@ -223,7 +229,8 @@ describe('trackLargestContentfulPaint', () => { notifyPerformanceEntries([createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT)]) - expect(lcpCallback).toHaveBeenCalledOnceWith({ + expect(lcpCallback).toHaveBeenCalledTimes(1) + expect(lcpCallback).toHaveBeenCalledExactlyOnceWith({ value: 789 as RelativeTime, targetSelector: undefined, resourceUrl: undefined, @@ -363,7 +370,7 @@ describe('trackLargestContentfulPaint', () => { }), ]) - const result = lcpCallback.calls.mostRecent().args[0] + const result = lcpCallback.mock.lastCall![0] expect(result.subParts).toEqual( expectedSubParts as { diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts index 69d9ada1a2..330a9e244f 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime, Duration } from '@datadog/js-core/time' import { clocksNow, clocksOrigin } from '@datadog/js-core/time' import { noop, Observable } from '@datadog/browser-core' @@ -32,7 +33,7 @@ describe('trackLoadingTime', () => { let clock: Clock let domMutationObservable: Observable let windowOpenObservable: Observable - let loadingTimeCallback: jasmine.Spy<(loadingTime: Duration) => void> + let loadingTimeCallback: Mock<(loadingTime: Duration) => void> let setLoadEvent: (loadEvent: Duration) => void let stopLoadingTimeTracking = noop let performanceBufferMock: GlobalPerformanceBufferMock @@ -68,7 +69,7 @@ describe('trackLoadingTime', () => { clock = mockClock() domMutationObservable = new Observable() windowOpenObservable = new Observable() - loadingTimeCallback = jasmine.createSpy<(loadingTime: Duration) => void>() + loadingTimeCallback = vi.fn<(loadingTime: Duration) => void>() }) afterEach(() => { @@ -86,7 +87,8 @@ describe('trackLoadingTime', () => { emulatePageActivityDuringViewLoading() - expect(loadingTimeCallback).toHaveBeenCalledOnceWith(clock.relative(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)) + expect(loadingTimeCallback).toHaveBeenCalledTimes(1) + expect(loadingTimeCallback).toHaveBeenCalledExactlyOnceWith(clock.relative(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)) }) it('should use loadEventEnd for initial view when having no activity', () => { @@ -98,7 +100,8 @@ describe('trackLoadingTime', () => { setLoadEvent(entry.loadEventEnd) clock.tick(PAGE_ACTIVITY_END_DELAY) - expect(loadingTimeCallback).toHaveBeenCalledOnceWith(entry.loadEventEnd) + expect(loadingTimeCallback).toHaveBeenCalledTimes(1) + expect(loadingTimeCallback).toHaveBeenCalledExactlyOnceWith(entry.loadEventEnd) }) it('should use loadEventEnd for initial view when load event is bigger than computed loading time', () => { @@ -111,7 +114,8 @@ describe('trackLoadingTime', () => { domMutationObservable.notify([createMutationRecord()]) clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY) - expect(loadingTimeCallback).toHaveBeenCalledOnceWith(clock.relative(LOAD_EVENT_AFTER_ACTIVITY_TIMING)) + expect(loadingTimeCallback).toHaveBeenCalledTimes(1) + expect(loadingTimeCallback).toHaveBeenCalledExactlyOnceWith(clock.relative(LOAD_EVENT_AFTER_ACTIVITY_TIMING)) }) it('should use computed loading time for initial view when load event is smaller than computed loading time', () => { @@ -125,7 +129,8 @@ describe('trackLoadingTime', () => { domMutationObservable.notify([createMutationRecord()]) clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY) - expect(loadingTimeCallback).toHaveBeenCalledOnceWith(clock.relative(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)) + expect(loadingTimeCallback).toHaveBeenCalledTimes(1) + expect(loadingTimeCallback).toHaveBeenCalledExactlyOnceWith(clock.relative(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)) }) it('should use computed loading time from time origin for initial view', () => { @@ -147,7 +152,8 @@ describe('trackLoadingTime', () => { domMutationObservable.notify([createMutationRecord()]) clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY) - expect(loadingTimeCallback).toHaveBeenCalledOnceWith( + expect(loadingTimeCallback).toHaveBeenCalledTimes(1) + expect(loadingTimeCallback).toHaveBeenCalledExactlyOnceWith( clock.relative(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY + CLOCK_GAP) ) }) @@ -161,9 +167,10 @@ describe('trackLoadingTime', () => { expect(loadingTimeCallback).not.toHaveBeenCalled() }) - it('should not discard loading time if page was hidden before the view start', () => { + it('should not discard loading time if page was hidden before the view start', (ctx) => { if (!supportPerformanceTimingEvent(RumPerformanceEntryType.VISIBILITY_STATE)) { - pending('Performance Timing Event is not supported') + ctx.skip(true, 'VisibilityState performance entry not supported') + return } performanceBufferMock.addPerformanceEntry({ @@ -181,9 +188,10 @@ describe('trackLoadingTime', () => { expect(loadingTimeCallback).toHaveBeenCalled() }) - it('should discard loading time if page was hidden during the loading time', () => { + it('should discard loading time if page was hidden during the loading time', (ctx) => { if (!supportPerformanceTimingEvent(RumPerformanceEntryType.VISIBILITY_STATE)) { - pending('Performance Timing Event is not supported') + ctx.skip(true, 'VisibilityState performance entry not supported') + return } clock.tick(RANDOM_VIEW_START) diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts index 1fa8f1aee9..86224d7968 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { type Duration, relativeNow, type RelativeTime } from '@datadog/js-core/time' import type { Clock } from '@datadog/browser-core/test' import { mockClock, registerCleanupTask, replaceMockable } from '@datadog/browser-core/test' @@ -23,12 +24,12 @@ const FAKE_INCOMPLETE_NAVIGATION_ENTRY: RelevantNavigationTiming = { } describe('trackNavigationTimings', () => { - let navigationTimingsCallback: jasmine.Spy<(timings: NavigationTimings) => void> + let navigationTimingsCallback: Mock<(timings: NavigationTimings) => void> let stop: () => void let clock: Clock beforeEach(() => { - navigationTimingsCallback = jasmine.createSpy() + navigationTimingsCallback = vi.fn() clock = mockClock() registerCleanupTask(() => { @@ -42,7 +43,8 @@ describe('trackNavigationTimings', () => { clock.tick(0) - expect(navigationTimingsCallback).toHaveBeenCalledOnceWith({ + expect(navigationTimingsCallback).toHaveBeenCalledTimes(1) + expect(navigationTimingsCallback).toHaveBeenCalledExactlyOnceWith({ firstByte: 123 as Duration, domComplete: 456 as Duration, domContentLoaded: 345 as Duration, @@ -60,7 +62,7 @@ describe('trackNavigationTimings', () => { clock.tick(0) - expect(navigationTimingsCallback.calls.mostRecent().args[0].firstByte).toBeUndefined() + expect(navigationTimingsCallback.mock.lastCall![0].firstByte).toBeUndefined() }) it('does not report "firstByte" if "responseStart" is in the future', () => { @@ -72,7 +74,7 @@ describe('trackNavigationTimings', () => { clock.tick(0) - expect(navigationTimingsCallback.calls.mostRecent().args[0].firstByte).toBeUndefined() + expect(navigationTimingsCallback.mock.lastCall![0].firstByte).toBeUndefined() }) it('wait for the load event to provide navigation timing', () => { diff --git a/packages/browser-rum-core/src/domain/view/viewMetrics/trackScrollMetrics.spec.ts b/packages/browser-rum-core/src/domain/view/viewMetrics/trackScrollMetrics.spec.ts index 06e22b1443..3703bfbb86 100644 --- a/packages/browser-rum-core/src/domain/view/viewMetrics/trackScrollMetrics.spec.ts +++ b/packages/browser-rum-core/src/domain/view/viewMetrics/trackScrollMetrics.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { RelativeTime, TimeStamp } from '@datadog/js-core/time' import type { Subscription } from '@datadog/browser-core' import { DOM_EVENT, Observable } from '@datadog/browser-core' @@ -45,12 +46,12 @@ describe('createScrollValuesObserver', () => { describe('trackScrollMetrics', () => { let clock: Clock - let scrollMetricsCallback: jasmine.Spy<(metrics: ScrollMetrics) => void> + let scrollMetricsCallback: Mock<(metrics: ScrollMetrics) => void> const scrollObservable = new Observable() beforeEach(() => { - scrollMetricsCallback = jasmine.createSpy() + scrollMetricsCallback = vi.fn() clock = mockClock() trackScrollMetrics( { relative: 0 as RelativeTime, timeStamp: 0 as TimeStamp }, @@ -70,7 +71,8 @@ describe('trackScrollMetrics', () => { it('should update scroll height and scroll depth', () => { updateScrollValues({ scrollDepth: 700, scrollHeight: 2000, scrollTop: 100 }) - expect(scrollMetricsCallback).toHaveBeenCalledOnceWith({ + expect(scrollMetricsCallback).toHaveBeenCalledTimes(1) + expect(scrollMetricsCallback).toHaveBeenCalledExactlyOnceWith({ maxDepth: 700, maxScrollHeight: 2000, maxScrollHeightTime: clock.relative(100), @@ -80,7 +82,8 @@ describe('trackScrollMetrics', () => { it('should update time and scroll height only if it has increased', () => { updateScrollValues({ scrollDepth: 700, scrollHeight: 2000, scrollTop: 100 }) updateScrollValues({ scrollDepth: 700, scrollHeight: 1900, scrollTop: 100 }) - expect(scrollMetricsCallback).toHaveBeenCalledOnceWith({ + expect(scrollMetricsCallback).toHaveBeenCalledTimes(1) + expect(scrollMetricsCallback).toHaveBeenCalledExactlyOnceWith({ maxDepth: 700, maxScrollHeight: 2000, maxScrollHeightTime: clock.relative(100), @@ -91,7 +94,8 @@ describe('trackScrollMetrics', () => { it('should update max depth only if it has increased', () => { updateScrollValues({ scrollDepth: 700, scrollHeight: 2000, scrollTop: 100 }) updateScrollValues({ scrollDepth: 600, scrollHeight: 2000, scrollTop: 0 }) - expect(scrollMetricsCallback).toHaveBeenCalledOnceWith({ + expect(scrollMetricsCallback).toHaveBeenCalledTimes(1) + expect(scrollMetricsCallback).toHaveBeenCalledExactlyOnceWith({ maxDepth: 700, maxScrollHeight: 2000, maxScrollHeightTime: clock.relative(100), diff --git a/packages/browser-rum-core/src/domain/vital/vitalCollection.spec.ts b/packages/browser-rum-core/src/domain/vital/vitalCollection.spec.ts index 4db30c90a9..c2e9c256bf 100644 --- a/packages/browser-rum-core/src/domain/vital/vitalCollection.spec.ts +++ b/packages/browser-rum-core/src/domain/vital/vitalCollection.spec.ts @@ -1,6 +1,7 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' +import { clocksNow } from '@datadog/js-core/time' import type { Duration } from '@datadog/js-core/time' import { mockClock, type Clock } from '@datadog/browser-core/test' -import { clocksNow } from '@datadog/js-core/time' import { generateUUID } from '@datadog/browser-core' import { collectAndValidateRawRumEvents, mockPageStateHistory } from '../../../test' import type { RawRumEvent, RawRumVitalEvent } from '../../rawRumEvent.types' @@ -16,11 +17,13 @@ describe('vitalCollection', () => { let rawRumEvents: Array> = [] let clock: Clock let vitalCollection: ReturnType - let wasInPageStateDuringPeriodSpy: jasmine.Spy + let wasInPageStateDuringPeriodSpy: Mock<(...args: any[]) => any> beforeEach(() => { clock = mockClock() - wasInPageStateDuringPeriodSpy = spyOn(pageStateHistory, 'wasInPageStateDuringPeriod') + wasInPageStateDuringPeriodSpy = vi + .spyOn(pageStateHistory, 'wasInPageStateDuringPeriod') + .mockImplementation(() => undefined as never) vitalCollection = startVitalCollection(lifeCycle, pageStateHistory) rawRumEvents = collectAndValidateRawRumEvents(lifeCycle) @@ -39,8 +42,8 @@ describe('vitalCollection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - vital: jasmine.objectContaining({ duration: 100000000, description: 'baz' }), + expect.objectContaining({ + vital: expect.objectContaining({ duration: 100000000, description: 'baz' }), context: { foo: 'bar' }, }) ) @@ -59,8 +62,8 @@ describe('vitalCollection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - vital: jasmine.objectContaining({ duration: 100000000, description: 'baz' }), + expect.objectContaining({ + vital: expect.objectContaining({ duration: 100000000, description: 'baz' }), context: { foo: 'bar' }, }) ) @@ -75,7 +78,7 @@ describe('vitalCollection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ vital: jasmine.objectContaining({ duration: 100000000 }) }) + expect.objectContaining({ vital: expect.objectContaining({ duration: 100000000 }) }) ) }) @@ -90,13 +93,13 @@ describe('vitalCollection', () => { expect(rawRumEvents.length).toBe(2) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - vital: jasmine.objectContaining({ description: 'component 2', duration: 100000000 }), + expect.objectContaining({ + vital: expect.objectContaining({ description: 'component 2', duration: 100000000 }), }) ) expect(rawRumEvents[1].rawRumEvent).toEqual( - jasmine.objectContaining({ - vital: jasmine.objectContaining({ description: 'component 1', duration: 300000000 }), + expect.objectContaining({ + vital: expect.objectContaining({ description: 'component 1', duration: 300000000 }), }) ) }) @@ -146,7 +149,7 @@ describe('vitalCollection', () => { }) it('should discard a vital for which a frozen state happened', () => { - wasInPageStateDuringPeriodSpy.and.returnValue(true) + wasInPageStateDuringPeriodSpy.mockReturnValue(true) vitalCollection.addDurationVital({ id: generateUUID(), @@ -163,11 +166,11 @@ describe('vitalCollection', () => { vitalCollection.startDurationVital('foo') vitalCollection.stopDurationVital('foo') - expect(rawRumEvents[0].startClocks.relative).toEqual(jasmine.any(Number)) + expect(rawRumEvents[0].startClocks.relative).toEqual(expect.any(Number)) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), vital: { - id: jasmine.any(String), + id: expect.any(String), type: VitalType.DURATION, name: 'foo', duration: 0, @@ -203,20 +206,21 @@ describe('vitalCollection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - vital: jasmine.objectContaining({ duration: 100000000, description: 'baz' }), + expect.objectContaining({ + vital: expect.objectContaining({ duration: 100000000, description: 'baz' }), context: { foo: 'bar' }, }) ) }) it('should notify lifecycle with vital started event when starting a duration vital', () => { - const subscriberSpy = jasmine.createSpy() + const subscriberSpy = vi.fn() lifeCycle.subscribe(LifeCycleEventType.DURATION_VITAL_STARTED, subscriberSpy) vitalCollection.startDurationVital('foo') - expect(subscriberSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ name: 'foo' })) + expect(subscriberSpy).toHaveBeenCalledTimes(1) + expect(subscriberSpy).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ name: 'foo' })) }) }) @@ -224,11 +228,11 @@ describe('vitalCollection', () => { it('should collect raw rum event from operation step vital', () => { vitalCollection.addOperationStepVital('foo', 'start') - expect(rawRumEvents[0].startClocks.relative).toEqual(jasmine.any(Number)) + expect(rawRumEvents[0].startClocks.relative).toEqual(expect.any(Number)) expect(rawRumEvents[0].rawRumEvent).toEqual({ - date: jasmine.any(Number), + date: expect.any(Number), vital: { - id: jasmine.any(String), + id: expect.any(String), type: VitalType.OPERATION_STEP, name: 'foo', step_type: 'start', @@ -266,8 +270,8 @@ describe('vitalCollection', () => { expect(rawRumEvents.length).toBe(1) expect(rawRumEvents[0].rawRumEvent).toEqual( - jasmine.objectContaining({ - vital: jasmine.objectContaining({ + expect.objectContaining({ + vital: expect.objectContaining({ step_type: 'end', operation_key: '00000000-0000-0000-0000-000000000000', failure_reason: 'error', diff --git a/packages/browser-rum-core/src/domain/waitPageActivityEnd.spec.ts b/packages/browser-rum-core/src/domain/waitPageActivityEnd.spec.ts index e548643eed..a5eaeab8bc 100644 --- a/packages/browser-rum-core/src/domain/waitPageActivityEnd.spec.ts +++ b/packages/browser-rum-core/src/domain/waitPageActivityEnd.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Subscription } from '@datadog/browser-core' import { ONE_SECOND } from '@datadog/js-core/time' import { Observable } from '@datadog/browser-core' @@ -239,11 +240,11 @@ describe('createPageActivityObservable', () => { describe('waitPageActivityEnd', () => { let clock: Clock - let idlPageActivityCallbackSpy: jasmine.Spy<(event: PageActivityEndEvent) => void> + let idlPageActivityCallbackSpy: Mock<(event: PageActivityEndEvent) => void> let activityObservable: Observable beforeEach(() => { - idlPageActivityCallbackSpy = jasmine.createSpy() + idlPageActivityCallbackSpy = vi.fn() clock = mockClock() activityObservable = new Observable() replaceMockable(createPageActivityObservable, () => activityObservable) @@ -260,7 +261,8 @@ describe('waitPageActivityEnd', () => { clock.tick(EXPIRE_DELAY) - expect(idlPageActivityCallbackSpy).toHaveBeenCalledOnceWith({ + expect(idlPageActivityCallbackSpy).toHaveBeenCalledTimes(1) + expect(idlPageActivityCallbackSpy).toHaveBeenCalledExactlyOnceWith({ hadActivity: false, }) }) @@ -279,7 +281,8 @@ describe('waitPageActivityEnd', () => { clock.tick(EXPIRE_DELAY) - expect(idlPageActivityCallbackSpy).toHaveBeenCalledOnceWith({ + expect(idlPageActivityCallbackSpy).toHaveBeenCalledTimes(1) + expect(idlPageActivityCallbackSpy).toHaveBeenCalledExactlyOnceWith({ hadActivity: true, end: clock.timeStamp(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY), }) @@ -305,7 +308,8 @@ describe('waitPageActivityEnd', () => { clock.tick(EXPIRE_DELAY) - expect(idlPageActivityCallbackSpy).toHaveBeenCalledOnceWith({ + expect(idlPageActivityCallbackSpy).toHaveBeenCalledTimes(1) + expect(idlPageActivityCallbackSpy).toHaveBeenCalledExactlyOnceWith({ hadActivity: true, end: clock.timeStamp(extendCount * BEFORE_PAGE_ACTIVITY_END_DELAY), }) @@ -317,7 +321,7 @@ describe('waitPageActivityEnd', () => { // Extend the action until it's more than MAX_DURATION const extendCount = Math.ceil(MAX_DURATION / BEFORE_PAGE_ACTIVITY_END_DELAY + 1) - idlPageActivityCallbackSpy.and.callFake(() => { + idlPageActivityCallbackSpy.mockImplementation(() => { stop = true }) waitPageActivityEnd( @@ -336,7 +340,8 @@ describe('waitPageActivityEnd', () => { clock.tick(EXPIRE_DELAY) - expect(idlPageActivityCallbackSpy).toHaveBeenCalledOnceWith({ + expect(idlPageActivityCallbackSpy).toHaveBeenCalledTimes(1) + expect(idlPageActivityCallbackSpy).toHaveBeenCalledExactlyOnceWith({ hadActivity: true, end: clock.timeStamp(MAX_DURATION), }) @@ -361,7 +366,8 @@ describe('waitPageActivityEnd', () => { clock.tick(EXPIRE_DELAY) - expect(idlPageActivityCallbackSpy).toHaveBeenCalledOnceWith({ + expect(idlPageActivityCallbackSpy).toHaveBeenCalledTimes(1) + expect(idlPageActivityCallbackSpy).toHaveBeenCalledExactlyOnceWith({ hadActivity: true, end: clock.timeStamp(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY + PAGE_ACTIVITY_END_DELAY * 2), }) @@ -382,7 +388,8 @@ describe('waitPageActivityEnd', () => { clock.tick(EXPIRE_DELAY) - expect(idlPageActivityCallbackSpy).toHaveBeenCalledOnceWith({ + expect(idlPageActivityCallbackSpy).toHaveBeenCalledTimes(1) + expect(idlPageActivityCallbackSpy).toHaveBeenCalledExactlyOnceWith({ hadActivity: true, end: clock.timeStamp(MAX_DURATION), }) diff --git a/packages/browser-rum-core/src/transport/formDataTransport.spec.ts b/packages/browser-rum-core/src/transport/formDataTransport.spec.ts index 3707965030..f26b3203d7 100644 --- a/packages/browser-rum-core/src/transport/formDataTransport.spec.ts +++ b/packages/browser-rum-core/src/transport/formDataTransport.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { createIdentityEncoder, DeflateEncoderStreamId as CoreDeflateEncoderStreamId } from '@datadog/browser-core' import { interceptRequests, readFormDataRequest } from '@datadog/browser-core/test' import { LifeCycle } from '../domain/lifeCycle' @@ -28,7 +29,7 @@ describe('createFormDataTransport', () => { await transport.send(payload) - expect(interceptor.requests).toHaveSize(1) + expect(interceptor.requests).toHaveLength(1) expect(interceptor.requests[0].body).toBeInstanceOf(FormData) expect(await readFormDataRequest(interceptor.requests[0])).toEqual(payload) }) diff --git a/packages/browser-rum-core/src/transport/startRumBatch.spec.ts b/packages/browser-rum-core/src/transport/startRumBatch.spec.ts index 13389e6b38..ed493dc8ed 100644 --- a/packages/browser-rum-core/src/transport/startRumBatch.spec.ts +++ b/packages/browser-rum-core/src/transport/startRumBatch.spec.ts @@ -1,6 +1,7 @@ +import { describe, it, expect, vi } from 'vitest' +import type { Mock } from 'vitest' import { Observable } from '@datadog/browser-core' import type { FlushEvent } from '@datadog/browser-core/src/transport/flushController' - import type { AssembledRumEvent } from '../rawRumEvent.types' import { RumEventType } from '../rawRumEvent.types' import type { RumViewEvent } from '../rumEvent.types' @@ -227,8 +228,8 @@ function createMockBatch() { const flushObservable = new Observable() const upsertedKeys = new Set() - const addSpy = jasmine.createSpy<(message: object) => void>('add') - const upsertSpy = jasmine.createSpy<(message: object, key: string) => void>('upsert') + const addSpy: Mock<(message: object) => void> = vi.fn() + const upsertSpy: Mock<(message: object, key: string) => void> = vi.fn() const batch = { flushObservable, @@ -292,9 +293,9 @@ describe('createBatchDispatcher', () => { dispatch(v1) dispatch(v2) - expect(upsertSpy.calls.count()).toBe(2) - expect((upsertSpy.calls.argsFor(0)[0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) - expect((upsertSpy.calls.argsFor(1)[0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) + expect(upsertSpy.mock.calls.length).toBe(2) + expect((upsertSpy.mock.calls[0][0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) + expect((upsertSpy.mock.calls[1][0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) }) }) @@ -306,8 +307,8 @@ describe('createBatchDispatcher', () => { const action = { type: RumEventType.ACTION } as unknown as AssembledRumEvent dispatch(action) - expect(addSpy.calls.count()).toBe(1) - expect(upsertSpy.calls.count()).toBe(0) + expect(addSpy.mock.calls.length).toBe(1) + expect(upsertSpy.mock.calls.length).toBe(0) }) }) @@ -334,12 +335,12 @@ describe('createBatchDispatcher', () => { dispatch(v2) // intermediate β€” opt 1 // Both calls must be upsert with full VIEW type - expect(upsertSpy.calls.count()).toBe(2) - expect((upsertSpy.calls.argsFor(0)[0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) - expect((upsertSpy.calls.argsFor(1)[0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) + expect(upsertSpy.mock.calls.length).toBe(2) + expect((upsertSpy.mock.calls[0][0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) + expect((upsertSpy.mock.calls[1][0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) // Both use the same key (view ID) - expect(upsertSpy.calls.argsFor(0)[1]).toBe('view-1') - expect(upsertSpy.calls.argsFor(1)[1]).toBe('view-1') + expect(upsertSpy.mock.calls[0][1]).toBe('view-1') + expect(upsertSpy.mock.calls[1][1]).toBe('view-1') }) it('should not emit any view_update events while the VIEW is in the batch', () => { @@ -350,8 +351,8 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 2)) dispatch(makeView('view-1', 3)) - const emittedTypes = upsertSpy.calls.allArgs().map(([event]) => (event as AssembledRumEvent).type) - expect(emittedTypes.every((t) => t === RumEventType.VIEW)).toBeTrue() + const emittedTypes = upsertSpy.mock.calls.map(([event]) => (event as AssembledRumEvent).type) + expect(emittedTypes.every((t) => t === RumEventType.VIEW)).toBe(true) }) }) @@ -363,7 +364,7 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 1)) flush() // batchHasFullView resets; batchBase = v1 - upsertSpy.calls.reset() + upsertSpy.mockClear() const v2 = makeView('view-1', 2, { view: { @@ -380,10 +381,10 @@ describe('createBatchDispatcher', () => { }) dispatch(v2) - expect(upsertSpy.calls.count()).toBe(1) - const emitted = upsertSpy.calls.argsFor(0)[0] as AssembledRumEvent + expect(upsertSpy.mock.calls.length).toBe(1) + const emitted = upsertSpy.mock.calls[0][0] as AssembledRumEvent expect(emitted.type).toBe(RumEventType.VIEW_UPDATE) - expect(upsertSpy.calls.argsFor(0)[1]).toBe('view-1') + expect(upsertSpy.mock.calls[0][1]).toBe('view-1') }) it('should aggregate multiple updates into a single view_update per batch', () => { @@ -393,7 +394,7 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 1)) flush() - upsertSpy.calls.reset() + upsertSpy.mockClear() // Three intermediate updates, each with a distinct action.count so each produces a diff. const makeUpdate = (docVersion: number, actionCount: number) => @@ -416,11 +417,11 @@ describe('createBatchDispatcher', () => { // Each call upserts with the same key β€” the last one is what gets sent. // Assert all three called upsert (not add), all under the same key. - expect(upsertSpy.calls.count()).toBe(3) - upsertSpy.calls.allArgs().forEach(([, key]) => expect(key).toBe('view-1')) + expect(upsertSpy.mock.calls.length).toBe(3) + upsertSpy.mock.calls.forEach(([, key]) => expect(key).toBe('view-1')) // All emitted events are view_update type (aggregate, not full VIEW) - const emittedTypes = upsertSpy.calls.allArgs().map(([e]) => (e as { type: string }).type) - expect(emittedTypes.every((t) => t === RumEventType.VIEW_UPDATE)).toBeTrue() + const emittedTypes = upsertSpy.mock.calls.map(([e]) => (e as { type: string }).type) + expect(emittedTypes.every((t) => t === RumEventType.VIEW_UPDATE)).toBe(true) }) it('should compute the diff from batchBase, not from the previous intermediate update', () => { @@ -431,7 +432,7 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 1)) flush() // batchBase = v1 (action.count: 0) - upsertSpy.calls.reset() + upsertSpy.mockClear() // Two intermediate updates: action count goes 0 β†’ 1 β†’ 2 dispatch( @@ -466,7 +467,7 @@ describe('createBatchDispatcher', () => { ) // The second upsert's aggregate diff must reflect action.count: 2 (diff from base, not from v2) - const lastEmitted = upsertSpy.calls.mostRecent().args[0] as AssembledViewDiff + const lastEmitted = upsertSpy.mock.lastCall![0] as AssembledViewDiff expect(lastEmitted.view.action?.count).toBe(2) }) @@ -478,13 +479,13 @@ describe('createBatchDispatcher', () => { dispatch(v1) flush() - upsertSpy.calls.reset() + upsertSpy.mockClear() // Same content, only document_version differs (always-required, ignored in diff) const v2 = makeView('view-1', 2) dispatch(v2) - expect(upsertSpy.calls.count()).toBe(0) + expect(upsertSpy.mock.calls.length).toBe(0) }) }) @@ -496,7 +497,7 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 1)) flush() - upsertSpy.calls.reset() + upsertSpy.mockClear() // Trigger exactly PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL updates for (let i = 2; i <= PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL + 1; i++) { @@ -518,7 +519,7 @@ describe('createBatchDispatcher', () => { } // The last upserted event must be a full VIEW (checkpoint) - const lastEmitted = upsertSpy.calls.mostRecent().args[0] as AssembledRumEvent + const lastEmitted = upsertSpy.mock.lastCall![0] as AssembledRumEvent expect(lastEmitted.type).toBe(RumEventType.VIEW) }) }) @@ -531,10 +532,10 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 1)) dispatch(makeView('view-2', 1)) // new view - const keys = upsertSpy.calls.allArgs().map(([, key]) => key) + const keys = upsertSpy.mock.calls.map(([, key]) => key) expect(keys).toEqual(['view-1', 'view-2']) - const types = upsertSpy.calls.allArgs().map(([e]) => (e as AssembledRumEvent).type) + const types = upsertSpy.mock.calls.map(([e]) => (e as AssembledRumEvent).type) expect(types).toEqual([RumEventType.VIEW, RumEventType.VIEW]) }) @@ -545,7 +546,7 @@ describe('createBatchDispatcher', () => { dispatch(makeView('view-1', 1)) flush() - upsertSpy.calls.reset() + upsertSpy.mockClear() const endView = makeView('view-1', 2, { view: { @@ -562,9 +563,9 @@ describe('createBatchDispatcher', () => { }) dispatch(endView) - expect(upsertSpy.calls.count()).toBe(1) - expect((upsertSpy.calls.argsFor(0)[0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) - expect((upsertSpy.calls.argsFor(0)[0] as RumViewEvent).view.is_active).toBe(false) + expect(upsertSpy.mock.calls.length).toBe(1) + expect((upsertSpy.mock.calls[0][0] as AssembledRumEvent).type).toBe(RumEventType.VIEW) + expect((upsertSpy.mock.calls[0][0] as RumViewEvent).view.is_active).toBe(false) }) it('should upsert a stale view-end without resetting state for the current view', () => { @@ -577,7 +578,7 @@ describe('createBatchDispatcher', () => { // view-2 starts dispatch(makeView('view-2', 1)) - upsertSpy.calls.reset() + upsertSpy.mockClear() // stale view-1 end arrives after view-2 has started const staleEnd = makeView('view-1', 2, { @@ -596,8 +597,8 @@ describe('createBatchDispatcher', () => { dispatch(staleEnd) // stale end is upserted under its own key - expect(upsertSpy.calls.count()).toBe(1) - expect(upsertSpy.calls.argsFor(0)[1]).toBe('view-1') + expect(upsertSpy.mock.calls.length).toBe(1) + expect(upsertSpy.mock.calls[0][1]).toBe('view-1') // subsequent update for view-2 still uses opt-1 (state for view-2 was not reset) dispatch( @@ -615,7 +616,7 @@ describe('createBatchDispatcher', () => { }, }) ) - const view2Type = (upsertSpy.calls.mostRecent().args[0] as AssembledRumEvent).type + const view2Type = (upsertSpy.mock.lastCall![0] as AssembledRumEvent).type expect(view2Type).toBe(RumEventType.VIEW) }) @@ -645,9 +646,9 @@ describe('createBatchDispatcher', () => { }) ) - const view2Calls = upsertSpy.calls.allArgs().filter(([, k]) => k === 'view-2') + const view2Calls = upsertSpy.mock.calls.filter(([, k]) => k === 'view-2') const emittedTypes = view2Calls.map(([e]) => (e as AssembledRumEvent).type) - expect(emittedTypes.every((t) => t === RumEventType.VIEW)).toBeTrue() + expect(emittedTypes.every((t) => t === RumEventType.VIEW)).toBe(true) }) }) }) diff --git a/packages/browser-rum-core/test/allJsonSchemas.d.ts b/packages/browser-rum-core/test/allJsonSchemas.d.ts deleted file mode 100644 index 70993f1bf6..0000000000 --- a/packages/browser-rum-core/test/allJsonSchemas.d.ts +++ /dev/null @@ -1 +0,0 @@ -export const allJsonSchemas: object[] diff --git a/packages/browser-rum-core/test/allJsonSchemas.js b/packages/browser-rum-core/test/allJsonSchemas.js deleted file mode 100644 index 6ca01bbe04..0000000000 --- a/packages/browser-rum-core/test/allJsonSchemas.js +++ /dev/null @@ -1,19 +0,0 @@ -/* global require */ - -// This file is not in TypeScript because it uses 'require.context', and we cannot type it -// correctly because: -// -// * We don't want to declare this type as a global (via TS `declare global`), because it would be -// declared all across the project. -// -// * We can't use something like `(globalThis as Something).require.context` because Webpack is -// looking for `require.context` as a standalone statement at build time. - -const requireSchema = require.context( - '@datadog/rum-events-format/schemas', - true /* use sub directories */, - /\.json*$/, - 'sync' -) - -export const allJsonSchemas = requireSchema.keys().map(requireSchema) diff --git a/packages/browser-rum-core/test/allJsonSchemas.ts b/packages/browser-rum-core/test/allJsonSchemas.ts new file mode 100644 index 0000000000..be57bbffc9 --- /dev/null +++ b/packages/browser-rum-core/test/allJsonSchemas.ts @@ -0,0 +1,13 @@ +/// +// Load all JSON schema files from @datadog/rum-events-format dependency. +// Uses Vite's import.meta.glob (replaces webpack's require.context). +const schemaModules = import.meta.glob('../../../node_modules/@datadog/rum-events-format/schemas/**/*.json', { + eager: true, +}) + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +export const allJsonSchemas = Object.values(schemaModules).map((mod: any) => mod.default || mod) + +if (allJsonSchemas.length === 0) { + throw new Error('No JSON schemas loaded from @datadog/rum-events-format. Check the glob path in allJsonSchemas.ts.') +} diff --git a/packages/browser-rum-core/test/createFakeClick.ts b/packages/browser-rum-core/test/createFakeClick.ts index 0a82cf9a2d..8c2c19c72e 100644 --- a/packages/browser-rum-core/test/createFakeClick.ts +++ b/packages/browser-rum-core/test/createFakeClick.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import { clocksNow, timeStampNow } from '@datadog/js-core/time' import { Observable } from '@datadog/browser-core' import { createNewEvent } from '@datadog/browser-core/test' @@ -31,8 +32,8 @@ export function createFakeClick({ isStopped = true stopObservable.notify() }, - discard: jasmine.createSpy(), - validate: jasmine.createSpy(), + discard: vi.fn(), + validate: vi.fn(), startClocks: clocksNow(), hasError, hasPageActivity, @@ -42,8 +43,8 @@ export function createFakeClick({ scroll: false, ...userActivity, }), - addFrustration: jasmine.createSpy(), - clone: jasmine.createSpy().and.callFake(clone), + addFrustration: vi.fn(), + clone: vi.fn().mockImplementation(clone), event: createNewEvent('pointerup', { clientX: 100, diff --git a/packages/browser-rum-core/test/emulate/mockDocumentReadyState.ts b/packages/browser-rum-core/test/emulate/mockDocumentReadyState.ts index ffea9e0fad..721a7b6a5c 100644 --- a/packages/browser-rum-core/test/emulate/mockDocumentReadyState.ts +++ b/packages/browser-rum-core/test/emulate/mockDocumentReadyState.ts @@ -1,9 +1,10 @@ +import { vi } from 'vitest' import { DOM_EVENT } from '@datadog/browser-core' import { createNewEvent } from '../../../browser-core/test' export function mockDocumentReadyState() { let readyState: DocumentReadyState = 'loading' - spyOnProperty(Document.prototype, 'readyState', 'get').and.callFake(() => readyState) + vi.spyOn(Document.prototype, 'readyState', 'get').mockImplementation(() => readyState) return { triggerOnDomLoaded: () => { readyState = 'interactive' diff --git a/packages/browser-rum-core/test/emulate/mockGlobalPerformanceBuffer.ts b/packages/browser-rum-core/test/emulate/mockGlobalPerformanceBuffer.ts index ccb654e935..77918ae7ac 100644 --- a/packages/browser-rum-core/test/emulate/mockGlobalPerformanceBuffer.ts +++ b/packages/browser-rum-core/test/emulate/mockGlobalPerformanceBuffer.ts @@ -1,3 +1,5 @@ +import { vi } from 'vitest' + export interface GlobalPerformanceBufferMock { addPerformanceEntry: (entry: PerformanceEntry) => void } @@ -5,11 +7,11 @@ export interface GlobalPerformanceBufferMock { export function mockGlobalPerformanceBuffer(initialEntries: PerformanceEntry[] = []): GlobalPerformanceBufferMock { const performanceEntries: PerformanceEntry[] = initialEntries - spyOn(performance, 'getEntries').and.callFake(() => performanceEntries.slice()) - spyOn(performance, 'getEntriesByName').and.callFake((name) => + vi.spyOn(performance, 'getEntries').mockImplementation(() => performanceEntries.slice()) + vi.spyOn(performance, 'getEntriesByName').mockImplementation((name) => performanceEntries.filter((entry) => entry.name === name) ) - spyOn(performance, 'getEntriesByType').and.callFake((type) => + vi.spyOn(performance, 'getEntriesByType').mockImplementation((type) => performanceEntries.filter((entry) => entry.entryType === type) ) diff --git a/packages/browser-rum-core/test/formatValidation.ts b/packages/browser-rum-core/test/formatValidation.ts index b52439feac..e6f2f9d85e 100644 --- a/packages/browser-rum-core/test/formatValidation.ts +++ b/packages/browser-rum-core/test/formatValidation.ts @@ -94,7 +94,7 @@ function validateRumFormat(rumEvent: Context) { return ` event${error.instancePath || ''} ${message}` }) .join('\n') - fail(`Invalid RUM event format:\n${errors}`) + throw new Error(`Invalid RUM event format:\n${errors}`) } } diff --git a/packages/browser-rum-nextjs/src/domain/error/addNextjsError.spec.ts b/packages/browser-rum-nextjs/src/domain/error/addNextjsError.spec.ts index 14b89fd776..97b5a07333 100644 --- a/packages/browser-rum-nextjs/src/domain/error/addNextjsError.spec.ts +++ b/packages/browser-rum-nextjs/src/domain/error/addNextjsError.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, it, expect } from 'vitest' import { registerCleanupTask } from '@datadog/browser-core/test' import { resetNextjsPlugin } from '../nextjsPlugin' import { initializeNextjsPlugin } from '../../../test/initializeNextjsPlugin' @@ -12,23 +13,24 @@ describe('addNextjsError', () => { }) it('delegates the error to addError', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const originalError = new Error('test error') addNextjsError(originalError, { componentStack: 'at ComponentSpy toto.js' }) - expect(addErrorSpy).toHaveBeenCalledOnceWith({ + expect(addErrorSpy).toHaveBeenCalledTimes(1) + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith({ error: originalError, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), componentStack: 'at ComponentSpy toto.js', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), context: { framework: 'nextjs' }, }) }) it('merges dd_context from the original error with nextjs error context', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const originalError = new Error('error message') ;(originalError as any).dd_context = { component: 'Menu', param: 123 } @@ -36,7 +38,7 @@ describe('addNextjsError', () => { addNextjsError(originalError, {}) expect(addErrorSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ + expect.objectContaining({ error: originalError, context: { framework: 'nextjs', @@ -48,57 +50,57 @@ describe('addNextjsError', () => { }) it('adds nextjs.digest context when error.digest is present', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const error = Object.assign(new Error('server error'), { digest: 'abc123' }) addNextjsError(error, {}) - expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( - jasmine.objectContaining({ - context: jasmine.objectContaining({ framework: 'nextjs', nextjs: { digest: 'abc123' } }), + expect(addErrorSpy.mock.lastCall![0]).toEqual( + expect.objectContaining({ + context: expect.objectContaining({ framework: 'nextjs', nextjs: { digest: 'abc123' } }), }) ) }) it('omits nextjs key when digest is undefined', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const error = new Error('client error') addNextjsError(error) - expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( - jasmine.objectContaining({ + expect(addErrorSpy.mock.lastCall![0]).toEqual( + expect.objectContaining({ context: { framework: 'nextjs' }, }) ) }) it('omits componentStack when errorInfo is missing', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const error = new Error('client error') addNextjsError(error) - expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( - jasmine.objectContaining({ + expect(addErrorSpy.mock.lastCall![0]).toEqual( + expect.objectContaining({ componentStack: undefined, }) ) }) it('does not let error.dd_context overwrite framework', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const error = Object.assign(new Error('test error'), { dd_context: { framework: 'from-dd-context' } }) addNextjsError(error, {}) - expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( - jasmine.objectContaining({ - context: jasmine.objectContaining({ framework: 'nextjs' }), + expect(addErrorSpy.mock.lastCall![0]).toEqual( + expect.objectContaining({ + context: expect.objectContaining({ framework: 'nextjs' }), }) ) }) diff --git a/packages/browser-rum-nextjs/src/domain/error/errorBoundary.spec.tsx b/packages/browser-rum-nextjs/src/domain/error/errorBoundary.spec.tsx index 456b0fce00..a9cbb013e9 100644 --- a/packages/browser-rum-nextjs/src/domain/error/errorBoundary.spec.tsx +++ b/packages/browser-rum-nextjs/src/domain/error/errorBoundary.spec.tsx @@ -1,6 +1,7 @@ +import { vi, describe, it, expect } from 'vitest' import React from 'react' -import { disableJasmineUncaughtExceptionTracking, ignoreConsoleLogs } from '@datadog/browser-core/test' +import { disableUncaughtExceptionTracking, ignoreConsoleLogs } from '@datadog/browser-core/test' import { appendComponent } from '../../../../browser-rum-react/test/appendComponent' import { initReactOldBrowsersSupport } from '../../../../browser-rum-react/test/reactOldBrowsersSupport' import { initializeNextjsPlugin } from '../../../test/initializeNextjsPlugin' @@ -12,13 +13,15 @@ import { ErrorBoundary } from './errorBoundary' describe('NextjsErrorBoundary', () => { it('reports the error through addNextjsError', () => { ignoreConsoleLogs('error', 'Error: error') - disableJasmineUncaughtExceptionTracking() + disableUncaughtExceptionTracking() initReactOldBrowsersSupport() - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNextjsPlugin({ addError: addErrorSpy }) const originalError = new Error('error') - const ComponentSpy = jasmine.createSpy().and.throwError(originalError) + const ComponentSpy = vi.fn().mockImplementation(() => { + throw originalError + }) ;(ComponentSpy as any).displayName = 'ComponentSpy' appendComponent( @@ -27,15 +30,16 @@ describe('NextjsErrorBoundary', () => { ) - expect(addErrorSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ + expect(addErrorSpy).toHaveBeenCalledTimes(1) + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ error: originalError, - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), - context: jasmine.objectContaining({ + handlingStack: expect.any(String), + startClocks: expect.any(Object), + context: expect.objectContaining({ framework: 'nextjs', }), - componentStack: jasmine.stringContaining('ComponentSpy'), + componentStack: expect.stringContaining('ComponentSpy'), }) ) }) diff --git a/packages/browser-rum-nextjs/src/domain/nextJSRouter/computeViewNameFromParams.spec.ts b/packages/browser-rum-nextjs/src/domain/nextJSRouter/computeViewNameFromParams.spec.ts index 8280097978..75fe3918c9 100644 --- a/packages/browser-rum-nextjs/src/domain/nextJSRouter/computeViewNameFromParams.spec.ts +++ b/packages/browser-rum-nextjs/src/domain/nextJSRouter/computeViewNameFromParams.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import type { useParams } from 'next/navigation' import { computeViewNameFromParams } from './computeViewNameFromParams' diff --git a/packages/browser-rum-nextjs/src/domain/nextjsPlugin.spec.ts b/packages/browser-rum-nextjs/src/domain/nextjsPlugin.spec.ts index 83cce6c565..e39405f465 100644 --- a/packages/browser-rum-nextjs/src/domain/nextjsPlugin.spec.ts +++ b/packages/browser-rum-nextjs/src/domain/nextjsPlugin.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest' import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' import { registerCleanupTask } from '../../../browser-core/test' import { @@ -12,7 +13,7 @@ import { const INIT_CONFIGURATION = {} as RumInitConfiguration function createPublicApi() { - const startViewSpy = jasmine.createSpy('startView') + const startViewSpy = vi.fn() return { publicApi: { startView: startViewSpy } as unknown as RumPublicApi, startViewSpy } } @@ -34,10 +35,10 @@ describe('nextjsPlugin', () => { const plugin = nextjsPlugin() expect(plugin).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'nextjs', - onInit: jasmine.any(Function), - onRumStart: jasmine.any(Function), + onInit: expect.any(Function), + onRumStart: expect.any(Function), }) ) }) @@ -62,7 +63,8 @@ describe('nextjsPlugin', () => { startNextjsView('/about') - expect(startViewSpy).toHaveBeenCalledOnceWith({ name: '/about', url: undefined }) + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith({ name: '/about', url: undefined }) }) it('uses onRouterTransitionStart URL when available', () => { @@ -71,7 +73,8 @@ describe('nextjsPlugin', () => { onRouterTransitionStart('/about?foo=bar') startNextjsView('/about') - expect(startViewSpy).toHaveBeenCalledOnceWith({ + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith({ name: '/about', url: `${window.location.origin}/about?foo=bar`, }) @@ -84,12 +87,12 @@ describe('nextjsPlugin', () => { startNextjsView('/about') startNextjsView('/other') - expect(startViewSpy.calls.mostRecent().args[0]).toEqual({ name: '/other', url: undefined }) + expect(startViewSpy.mock.lastCall![0]).toEqual({ name: '/other', url: undefined }) }) describe('lifecycle subscribers', () => { it('calls onRumInit subscribers during onInit', () => { - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() const { publicApi } = createPublicApi() onRumInit(callbackSpy) @@ -101,11 +104,11 @@ describe('nextjsPlugin', () => { }) expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(publicApi) + expect(callbackSpy.mock.lastCall![0]).toBe(publicApi) }) it('calls onRumInit subscriber immediately if already initialized', () => { - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() const { publicApi } = createPublicApi() nextjsPlugin().onInit({ @@ -116,12 +119,12 @@ describe('nextjsPlugin', () => { onRumInit(callbackSpy) expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(publicApi) + expect(callbackSpy.mock.lastCall![0]).toBe(publicApi) }) it('calls onRumStart subscribers during onRumStart', () => { - const callbackSpy = jasmine.createSpy() - const mockAddError = jasmine.createSpy() + const callbackSpy = vi.fn() + const mockAddError = vi.fn() onRumStart(callbackSpy) const { plugin } = initPlugin() @@ -131,11 +134,11 @@ describe('nextjsPlugin', () => { }) it('calls onRumStart subscriber immediately if already started', () => { - const mockAddError = jasmine.createSpy() + const mockAddError = vi.fn() const { plugin } = initPlugin() plugin.onRumStart({ addError: mockAddError }) - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() onRumStart(callbackSpy) expect(callbackSpy).toHaveBeenCalledWith(mockAddError) diff --git a/packages/browser-rum-nextjs/tsconfig.json b/packages/browser-rum-nextjs/tsconfig.json index c27e50b613..684fcd0860 100644 --- a/packages/browser-rum-nextjs/tsconfig.json +++ b/packages/browser-rum-nextjs/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["jasmine"], "allowImportingTsExtensions": true, "noEmit": true }, diff --git a/packages/browser-rum-nuxt/src/domain/error/addNuxtError.spec.ts b/packages/browser-rum-nuxt/src/domain/error/addNuxtError.spec.ts index 1812d12c96..5524eb7061 100644 --- a/packages/browser-rum-nuxt/src/domain/error/addNuxtError.spec.ts +++ b/packages/browser-rum-nuxt/src/domain/error/addNuxtError.spec.ts @@ -1,28 +1,30 @@ +import { describe, it, expect, vi } from 'vitest' import type { ComponentInternalInstance, ComponentPublicInstance } from 'vue' import { initializeNuxtPlugin } from '../../../test/initializeNuxtPlugin' import { addNuxtError } from './addNuxtError' describe('addNuxtError', () => { it('reports the error to the SDK with info as first line of component_stack', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNuxtPlugin({ addError: addErrorSpy }) const error = new Error('something broke') addNuxtError(error, null, 'mounted hook') - expect(addErrorSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ + expect(addErrorSpy).toHaveBeenCalledTimes(1) + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ error, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), componentStack: 'mounted hook', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), context: { framework: 'nuxt' }, }) ) }) it('includes component hierarchy in component_stack when instance is provided', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNuxtPlugin({ addError: addErrorSpy }) const parentInternal = { type: { name: 'ParentComponent' }, parent: null } as unknown as ComponentInternalInstance @@ -34,29 +36,29 @@ describe('addNuxtError', () => { addNuxtError(new Error('oops'), mockInstance, 'mounted hook') - const componentStack = addErrorSpy.calls.mostRecent().args[0].componentStack as string + const componentStack = addErrorSpy.mock.lastCall![0].componentStack as string expect(componentStack).toContain('mounted hook') expect(componentStack).toContain('at ') expect(componentStack).toContain('at ') }) it('handles empty info gracefully', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNuxtPlugin({ addError: addErrorSpy }) addNuxtError(new Error('oops'), null, '') expect(addErrorSpy).toHaveBeenCalledTimes(1) - expect(addErrorSpy.calls.mostRecent().args[0].componentStack).toBeUndefined() + expect(addErrorSpy.mock.lastCall![0].componentStack).toBeUndefined() }) it('should merge dd_context from the original error with nuxt error context', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeNuxtPlugin({ addError: addErrorSpy }) const originalError = new Error('error message') ;(originalError as any).dd_context = { component: 'Menu', param: 123 } addNuxtError(originalError, null, 'mounted hook') - expect(addErrorSpy.calls.mostRecent().args[0].context).toEqual({ + expect(addErrorSpy.mock.lastCall![0].context).toEqual({ framework: 'nuxt', component: 'Menu', param: 123, diff --git a/packages/browser-rum-nuxt/src/domain/error/setupNuxtErrorHandling.spec.ts b/packages/browser-rum-nuxt/src/domain/error/setupNuxtErrorHandling.spec.ts index e3b9b86a6f..7597b92591 100644 --- a/packages/browser-rum-nuxt/src/domain/error/setupNuxtErrorHandling.spec.ts +++ b/packages/browser-rum-nuxt/src/domain/error/setupNuxtErrorHandling.spec.ts @@ -1,12 +1,13 @@ +import { describe, it, expect, vi } from 'vitest' import type { App } from 'vue' import type { NuxtApp } from './setupNuxtErrorHandling' import { setupNuxtErrorHandling } from './setupNuxtErrorHandling' describe('setupNuxtErrorHandling', () => { it('reports Vue errors and preserves the original error handler', () => { - const reportErrorSpy = jasmine.createSpy() - const originalErrorHandlerSpy = jasmine.createSpy() - const hookSpy = jasmine.createSpy() + const reportErrorSpy = vi.fn() + const originalErrorHandlerSpy = vi.fn() + const hookSpy = vi.fn() const nuxtApp = { vueApp: { config: { @@ -22,19 +23,21 @@ describe('setupNuxtErrorHandling', () => { const errorHandler = nuxtApp.vueApp.config.errorHandler as NonNullable errorHandler(error, null, 'mounted hook') - expect(reportErrorSpy).toHaveBeenCalledOnceWith(error, null, 'mounted hook') - expect(originalErrorHandlerSpy).toHaveBeenCalledOnceWith(error, null, 'mounted hook') - expect(hookSpy).toHaveBeenCalledWith('app:error', jasmine.any(Function)) + expect(reportErrorSpy).toHaveBeenCalledTimes(1) + expect(reportErrorSpy).toHaveBeenCalledExactlyOnceWith(error, null, 'mounted hook') + expect(originalErrorHandlerSpy).toHaveBeenCalledTimes(1) + expect(originalErrorHandlerSpy).toHaveBeenCalledExactlyOnceWith(error, null, 'mounted hook') + expect(hookSpy).toHaveBeenCalledExactlyOnceWith('app:error', expect.any(Function)) }) it('deduplicates the same error between Vue and app:error hooks', () => { - const reportErrorSpy = jasmine.createSpy() + const reportErrorSpy = vi.fn() let appErrorCallback!: (err: unknown) => void const nuxtApp = { vueApp: { config: {}, }, - hook: jasmine.createSpy().and.callFake((_name: string, callback: (err: unknown) => void) => { + hook: vi.fn().mockImplementation((_name: string, callback: (err: unknown) => void) => { appErrorCallback = callback }), } as unknown as NuxtApp diff --git a/packages/browser-rum-nuxt/src/domain/nuxtPlugin.spec.ts b/packages/browser-rum-nuxt/src/domain/nuxtPlugin.spec.ts index fa2a1bf820..b3b4ced55b 100644 --- a/packages/browser-rum-nuxt/src/domain/nuxtPlugin.spec.ts +++ b/packages/browser-rum-nuxt/src/domain/nuxtPlugin.spec.ts @@ -1,10 +1,11 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' import type { Router } from 'vue-router' import { createRouter, createMemoryHistory } from 'vue-router' import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' import { registerCleanupTask } from '../../../browser-core/test' import { nuxtRumPlugin, resetNuxtPlugin } from './nuxtPlugin' -const PUBLIC_API = { startView: jasmine.createSpy() } as unknown as RumPublicApi +const PUBLIC_API = { startView: vi.fn() } as unknown as RumPublicApi const INIT_CONFIGURATION = {} as RumInitConfiguration function makeRouter(): Router { @@ -17,7 +18,7 @@ describe('nuxtRumPlugin', () => { }) it('returns a plugin object with name "nuxt"', () => { - expect(nuxtRumPlugin({ router: makeRouter() })).toEqual(jasmine.objectContaining({ name: 'nuxt' })) + expect(nuxtRumPlugin({ router: makeRouter() })).toEqual(expect.objectContaining({ name: 'nuxt' })) }) it('sets trackViewsManually to true', () => { diff --git a/packages/browser-rum-nuxt/src/domain/router/nuxtRouter.spec.ts b/packages/browser-rum-nuxt/src/domain/router/nuxtRouter.spec.ts index d38da0773f..0c6cb91913 100644 --- a/packages/browser-rum-nuxt/src/domain/router/nuxtRouter.spec.ts +++ b/packages/browser-rum-nuxt/src/domain/router/nuxtRouter.spec.ts @@ -1,15 +1,17 @@ +import { describe, it, expect, vi } from 'vitest' +import type { Mock } from 'vitest' import { createRouter, createMemoryHistory } from 'vue-router' import type { RouteLocationMatched } from 'vue-router' import type { RumPublicApi } from '@datadog/browser-rum-core' import { startTrackingNuxtViews, computeNuxtViewName } from './nuxtRouter' -function makePublicApi(startViewSpy: jasmine.Spy) { +function makePublicApi(startViewSpy: Mock) { return { startView: startViewSpy } as unknown as RumPublicApi } describe('startTrackingNuxtViews', () => { - it('tracks the initial view via afterEach when router is not yet ready', (done) => { - const startViewSpy = jasmine.createSpy() + it('tracks the initial view via afterEach when router is not yet ready', async () => { + const startViewSpy = vi.fn() const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: {} }], @@ -18,34 +20,26 @@ describe('startTrackingNuxtViews', () => { startTrackingNuxtViews(makePublicApi(startViewSpy), router) expect(startViewSpy).not.toHaveBeenCalled() - router - .push('/') - .then(() => { - expect(startViewSpy).toHaveBeenCalledOnceWith('/') - done() - }) - .catch(done.fail) + await router.push('/') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/') }) - it('tracks the initial view immediately when router is already ready', (done) => { - const startViewSpy = jasmine.createSpy() + it('tracks the initial view immediately when router is already ready', async () => { + const startViewSpy = vi.fn() const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: {} }], }) - router - .push('/') - .then(() => { - startTrackingNuxtViews(makePublicApi(startViewSpy), router) - expect(startViewSpy).toHaveBeenCalledOnceWith('/') - done() - }) - .catch(done.fail) + await router.push('/') + startTrackingNuxtViews(makePublicApi(startViewSpy), router) + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/') }) - it('tracks subsequent navigations', (done) => { - const startViewSpy = jasmine.createSpy() + it('tracks subsequent navigations', async () => { + const startViewSpy = vi.fn() const router = createRouter({ history: createMemoryHistory(), routes: [ @@ -54,22 +48,16 @@ describe('startTrackingNuxtViews', () => { ], }) - router - .push('/') - .then(() => { - startTrackingNuxtViews(makePublicApi(startViewSpy), router) - startViewSpy.calls.reset() - return router.push('/about') - }) - .then(() => { - expect(startViewSpy).toHaveBeenCalledOnceWith('/about') - done() - }) - .catch(done.fail) + await router.push('/') + startTrackingNuxtViews(makePublicApi(startViewSpy), router) + startViewSpy.mockClear() + await router.push('/about') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/about') }) - it('does not track a new view when navigation fails', (done) => { - const startViewSpy = jasmine.createSpy() + it('does not track a new view when navigation fails', async () => { + const startViewSpy = vi.fn() const router = createRouter({ history: createMemoryHistory(), routes: [ @@ -84,60 +72,40 @@ describe('startTrackingNuxtViews', () => { } }) - router - .push('/') - .then(() => { - startTrackingNuxtViews(makePublicApi(startViewSpy), router) - startViewSpy.calls.reset() - return router.push('/protected') - }) - .then(() => { - expect(startViewSpy).not.toHaveBeenCalled() - done() - }) - .catch(done.fail) + await router.push('/') + startTrackingNuxtViews(makePublicApi(startViewSpy), router) + startViewSpy.mockClear() + await router.push('/protected') + expect(startViewSpy).not.toHaveBeenCalled() }) - it('does not track a new view when only query params change', (done) => { - const startViewSpy = jasmine.createSpy() + it('does not track a new view when only query params change', async () => { + const startViewSpy = vi.fn() const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/products', component: {} }], }) - router - .push('/products?page=1') - .then(() => { - startTrackingNuxtViews(makePublicApi(startViewSpy), router) - startViewSpy.calls.reset() - return router.push('/products?page=2') - }) - .then(() => { - expect(startViewSpy).not.toHaveBeenCalled() - done() - }) - .catch(done.fail) + await router.push('/products?page=1') + startTrackingNuxtViews(makePublicApi(startViewSpy), router) + startViewSpy.mockClear() + await router.push('/products?page=2') + expect(startViewSpy).not.toHaveBeenCalled() }) - it('tracks a new view when the hash changes', (done) => { - const startViewSpy = jasmine.createSpy() + it('tracks a new view when the hash changes', async () => { + const startViewSpy = vi.fn() const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/products', component: {} }], }) - router - .push('/products?page=1') - .then(() => { - startTrackingNuxtViews(makePublicApi(startViewSpy), router) - startViewSpy.calls.reset() - return router.push('/products#details') - }) - .then(() => { - expect(startViewSpy).toHaveBeenCalledOnceWith('/products') - done() - }) - .catch(done.fail) + await router.push('/products?page=1') + startTrackingNuxtViews(makePublicApi(startViewSpy), router) + startViewSpy.mockClear() + await router.push('/products#details') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/products') }) }) diff --git a/packages/browser-rum-react/src/domain/error/addReactError.spec.ts b/packages/browser-rum-react/src/domain/error/addReactError.spec.ts index 117365f50a..fe88d2c958 100644 --- a/packages/browser-rum-react/src/domain/error/addReactError.spec.ts +++ b/packages/browser-rum-react/src/domain/error/addReactError.spec.ts @@ -1,21 +1,23 @@ +import { vi, describe, expect, it } from 'vitest' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' import { addReactError } from './addReactError' describe('addReactError', () => { - it('delegates the error to addError', () => { - const addErrorSpy = jasmine.createSpy() + it('reports the error to the SDK', () => { + const addEventSpy = vi.fn() initializeReactPlugin({ - addError: addErrorSpy, + addError: addEventSpy, }) const originalError = new Error('error message') addReactError(originalError, { componentStack: 'at ComponentSpy toto.js' }) - expect(addErrorSpy).toHaveBeenCalledOnceWith({ + expect(addEventSpy).toHaveBeenCalledTimes(1) + expect(addEventSpy).toHaveBeenCalledExactlyOnceWith({ error: originalError, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), componentStack: 'at ComponentSpy toto.js', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), context: { framework: 'react', }, @@ -23,18 +25,17 @@ describe('addReactError', () => { }) it('should merge dd_context from the original error with react error context', () => { - const addErrorSpy = jasmine.createSpy() + const addEventSpy = vi.fn() initializeReactPlugin({ - addError: addErrorSpy, + addError: addEventSpy, }) const originalError = new Error('error message') ;(originalError as any).dd_context = { component: 'Menu', param: 123 } addReactError(originalError, {}) - expect(addErrorSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ - error: originalError, + expect(addEventSpy.mock.lastCall![0]).toEqual( + expect.objectContaining({ context: { framework: 'react', component: 'Menu', diff --git a/packages/browser-rum-react/src/domain/error/createErrorBoundary.spec.tsx b/packages/browser-rum-react/src/domain/error/createErrorBoundary.spec.tsx index 023a09560e..5dfeddde76 100644 --- a/packages/browser-rum-react/src/domain/error/createErrorBoundary.spec.tsx +++ b/packages/browser-rum-react/src/domain/error/createErrorBoundary.spec.tsx @@ -1,6 +1,7 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' import React, { act } from 'react' -import { disableJasmineUncaughtExceptionTracking, ignoreConsoleLogs } from '../../../../browser-core/test' +import { disableUncaughtExceptionTracking, ignoreConsoleLogs } from '../../../../browser-core/test' import { appendComponent } from '../../../test/appendComponent' import { initReactOldBrowsersSupport } from '../../../test/reactOldBrowsersSupport' import type { ErrorBoundaryFallback } from './errorBoundary' @@ -11,21 +12,23 @@ type FallbackFunctionComponent = Extract { beforeEach(() => { ignoreConsoleLogs('error', 'Error: error') - disableJasmineUncaughtExceptionTracking() + disableUncaughtExceptionTracking() initReactOldBrowsersSupport() }) it('renders children', () => { - const TestErrorBoundary = createErrorBoundary(jasmine.createSpy(), 'TestErrorBoundary') + const TestErrorBoundary = createErrorBoundary(vi.fn(), 'TestErrorBoundary') const container = appendComponent( null}>bar) expect(container.innerHTML).toBe('bar') }) it('renders the fallback when an error occurs', () => { - const TestErrorBoundary = createErrorBoundary(jasmine.createSpy(), 'TestErrorBoundary') - const fallbackSpy = jasmine.createSpy().and.returnValue('fallback') - const ComponentSpy = jasmine.createSpy().and.throwError(new Error('error')) + const TestErrorBoundary = createErrorBoundary(vi.fn(), 'TestErrorBoundary') + const fallbackSpy = vi.fn().mockReturnValue('fallback' as any) + const ComponentSpy = vi.fn().mockImplementation(() => { + throw new Error('error') + }) const container = appendComponent( @@ -33,28 +36,30 @@ describe('createErrorBoundary', () => { ) expect(fallbackSpy).toHaveBeenCalled() - fallbackSpy.calls.all().forEach(({ args }) => { - expect(args[0]).toEqual({ + fallbackSpy.mock.calls.forEach(([arg]) => { + expect(arg).toEqual({ error: new Error('error'), - resetError: jasmine.any(Function), + resetError: expect.any(Function), }) }) expect(container.innerHTML).toBe('fallback') }) it('resets the error when resetError is called', () => { - const TestErrorBoundary = createErrorBoundary(jasmine.createSpy(), 'TestErrorBoundary') - const fallbackSpy = jasmine.createSpy().and.returnValue('fallback') - const ComponentSpy = jasmine.createSpy().and.throwError(new Error('error')) + const TestErrorBoundary = createErrorBoundary(vi.fn(), 'TestErrorBoundary') + const fallbackSpy = vi.fn().mockReturnValue('fallback' as any) + const ComponentSpy = vi.fn().mockImplementation(() => { + throw new Error('error') + }) const container = appendComponent( ) - ComponentSpy.and.returnValue('bar') + ComponentSpy.mockReturnValue('bar' as any) - const { resetError } = fallbackSpy.calls.mostRecent().args[0] + const { resetError } = fallbackSpy.mock.lastCall![0] act(() => { resetError() }) @@ -63,10 +68,12 @@ describe('createErrorBoundary', () => { }) it('passes error and errorInfo to the report callback', () => { - const reportErrorSpy = jasmine.createSpy() + const reportErrorSpy = vi.fn() const TestErrorBoundary = createErrorBoundary(reportErrorSpy, 'TestErrorBoundary') const originalError = new Error('error') - const ComponentSpy = jasmine.createSpy().and.throwError(originalError) + const ComponentSpy = vi.fn().mockImplementation(() => { + throw originalError + }) ;(ComponentSpy as any).displayName = 'ComponentSpy' appendComponent( @@ -75,10 +82,11 @@ describe('createErrorBoundary', () => { ) - expect(reportErrorSpy).toHaveBeenCalledOnceWith( + expect(reportErrorSpy).toHaveBeenCalledTimes(1) + expect(reportErrorSpy).toHaveBeenCalledExactlyOnceWith( originalError, - jasmine.objectContaining({ - componentStack: jasmine.stringContaining('ComponentSpy'), + expect.objectContaining({ + componentStack: expect.stringContaining('ComponentSpy'), }) ) }) diff --git a/packages/browser-rum-react/src/domain/error/errorBoundary.spec.tsx b/packages/browser-rum-react/src/domain/error/errorBoundary.spec.tsx index 46cb9baa24..fbb8e08142 100644 --- a/packages/browser-rum-react/src/domain/error/errorBoundary.spec.tsx +++ b/packages/browser-rum-react/src/domain/error/errorBoundary.spec.tsx @@ -1,6 +1,6 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import React, { act } from 'react' - -import { disableJasmineUncaughtExceptionTracking, ignoreConsoleLogs } from '../../../../browser-core/test' +import { disableUncaughtExceptionTracking, ignoreConsoleLogs } from '@datadog/browser-core/test' import { appendComponent } from '../../../test/appendComponent' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' import { initReactOldBrowsersSupport } from '../../../test/reactOldBrowsersSupport' @@ -14,7 +14,7 @@ describe('ErrorBoundary', () => { // Prevent React from displaying the error in the console ignoreConsoleLogs('error', 'Error: error') - disableJasmineUncaughtExceptionTracking() + disableUncaughtExceptionTracking() initReactOldBrowsersSupport() }) @@ -24,8 +24,10 @@ describe('ErrorBoundary', () => { }) it('renders the fallback function component when an error occurs', () => { - const fallbackSpy = jasmine.createSpy().and.returnValue('fallback') - const ComponentSpy = jasmine.createSpy().and.throwError(new Error('error')) + const fallbackSpy = vi.fn().mockReturnValue('fallback') + const ComponentSpy = vi.fn().mockImplementation(() => { + throw new Error('error') + }) const container = appendComponent( @@ -33,10 +35,10 @@ describe('ErrorBoundary', () => { ) expect(fallbackSpy).toHaveBeenCalled() // React calls the component multiple times while rendering - fallbackSpy.calls.all().forEach(({ args }) => { + fallbackSpy.mock.calls.forEach((args) => { expect(args[0]).toEqual({ error: new Error('error'), - resetError: jasmine.any(Function), + resetError: expect.any(Function), }) }) expect(container.innerHTML).toBe('fallback') @@ -46,7 +48,7 @@ describe('ErrorBoundary', () => { class FallbackComponent extends React.Component<{ error: Error; resetError: () => void }> { constructor(props: { error: Error; resetError: () => void }) { super(props) - expect(props).toEqual({ error: new Error('error'), resetError: jasmine.any(Function) }) + expect(props).toEqual({ error: new Error('error'), resetError: expect.any(Function) }) } render() { @@ -54,7 +56,9 @@ describe('ErrorBoundary', () => { } } - const ComponentSpy = jasmine.createSpy().and.throwError(new Error('error')) + const ComponentSpy = vi.fn().mockImplementation(() => { + throw new Error('error') + }) const container = appendComponent( @@ -64,8 +68,10 @@ describe('ErrorBoundary', () => { }) it('resets the error when resetError is called', () => { - const fallbackSpy = jasmine.createSpy().and.returnValue('fallback') - const ComponentSpy = jasmine.createSpy().and.throwError(new Error('error')) + const fallbackSpy = vi.fn().mockReturnValue('fallback') + const ComponentSpy = vi.fn().mockImplementation(() => { + throw new Error('error') + }) const container = appendComponent( @@ -73,9 +79,9 @@ describe('ErrorBoundary', () => { ) // Don't throw the second time - ComponentSpy.and.returnValue('bar') + ComponentSpy.mockReturnValue('bar') - const { resetError } = fallbackSpy.calls.mostRecent().args[0] + const { resetError } = fallbackSpy.mock.lastCall![0] act(() => { resetError() }) @@ -83,13 +89,15 @@ describe('ErrorBoundary', () => { expect(container.innerHTML).toBe('bar') }) - it('reports the error through addReactError', () => { - const addErrorSpy = jasmine.createSpy() + it('reports the error to the SDK', () => { + const addEventSpy = vi.fn() initializeReactPlugin({ - addError: addErrorSpy, + addError: addEventSpy, }) const originalError = new Error('error') - const ComponentSpy = jasmine.createSpy().and.throwError(originalError) + const ComponentSpy = vi.fn().mockImplementation(() => { + throw originalError + }) ;(ComponentSpy as any).displayName = 'ComponentSpy' appendComponent( @@ -98,16 +106,15 @@ describe('ErrorBoundary', () => { ) - expect(addErrorSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ - error: originalError, - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), - context: { - framework: 'react', - }, - componentStack: jasmine.stringContaining('ComponentSpy'), - }) - ) + expect(addEventSpy).toHaveBeenCalledTimes(1) + expect(addEventSpy).toHaveBeenCalledExactlyOnceWith({ + error: originalError, + handlingStack: expect.any(String), + componentStack: expect.stringContaining('ComponentSpy'), + startClocks: expect.any(Object), + context: { + framework: 'react', + }, + }) }) }) diff --git a/packages/browser-rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/browser-rum-react/src/domain/performance/reactComponentTracker.spec.tsx index cdb35769e2..0cb6120b6a 100644 --- a/packages/browser-rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/browser-rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import React, { useEffect, useLayoutEffect, act } from 'react' import { appendComponent } from '../../../test/appendComponent' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' @@ -27,7 +28,7 @@ describe('UNSTABLE_ReactComponentTracker', () => { }) it('should call addDurationVital after the component rendering', () => { - const addDurationVitalSpy = jasmine.createSpy() + const addDurationVitalSpy = vi.fn() initializeReactPlugin({ publicApi: { addDurationVital: addDurationVitalSpy, @@ -41,7 +42,7 @@ describe('UNSTABLE_ReactComponentTracker', () => { ) expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) - const [name, options] = addDurationVitalSpy.calls.mostRecent().args + const [name, options] = addDurationVitalSpy.mock.lastCall! expect(name).toBe('reactComponentRender') expect(options).toEqual({ description: 'ChildComponent', @@ -58,7 +59,7 @@ describe('UNSTABLE_ReactComponentTracker', () => { }) it('should call addDurationVital on rerender', () => { - const addDurationVitalSpy = jasmine.createSpy() + const addDurationVitalSpy = vi.fn() initializeReactPlugin({ publicApi: { addDurationVital: addDurationVitalSpy, @@ -88,7 +89,7 @@ describe('UNSTABLE_ReactComponentTracker', () => { }) expect(addDurationVitalSpy).toHaveBeenCalledTimes(2) - const options = addDurationVitalSpy.calls.mostRecent().args[1] + const options = addDurationVitalSpy.mock.lastCall![1] expect(options).toEqual({ description: 'ChildComponent', startTime: clock.timeStamp(TOTAL_DURATION + 1), diff --git a/packages/browser-rum-react/src/domain/performance/timer.spec.ts b/packages/browser-rum-react/src/domain/performance/timer.spec.ts index 2f86369e3d..7051d8c9b7 100644 --- a/packages/browser-rum-react/src/domain/performance/timer.spec.ts +++ b/packages/browser-rum-react/src/domain/performance/timer.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { mockClock } from '@datadog/browser-core/test' import type { Duration } from '@datadog/js-core/time' import { createTimer } from './timer' diff --git a/packages/browser-rum-react/src/domain/reactPlugin.spec.ts b/packages/browser-rum-react/src/domain/reactPlugin.spec.ts index ced4dd1952..a3cae2a4dc 100644 --- a/packages/browser-rum-react/src/domain/reactPlugin.spec.ts +++ b/packages/browser-rum-react/src/domain/reactPlugin.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, describe, expect, it } from 'vitest' import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' import { onRumInit, onRumStart, reactPlugin, resetReactPlugin } from './reactPlugin' @@ -12,16 +13,16 @@ describe('reactPlugin', () => { it('returns a plugin object', () => { const plugin = reactPlugin() expect(plugin).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name: 'react', - onInit: jasmine.any(Function), - onRumStart: jasmine.any(Function), + onInit: expect.any(Function), + onRumStart: expect.any(Function), }) ) }) it('calls callbacks registered with onReactPluginInit during onInit', () => { - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() const pluginConfiguration = {} onRumInit(callbackSpy) @@ -33,12 +34,12 @@ describe('reactPlugin', () => { }) expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration) - expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API) + expect(callbackSpy.mock.lastCall![0]).toBe(pluginConfiguration) + expect(callbackSpy.mock.lastCall![1]).toBe(PUBLIC_API) }) it('calls callbacks immediately if onInit was already invoked', () => { - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() const pluginConfiguration = {} reactPlugin(pluginConfiguration).onInit({ publicApi: PUBLIC_API, @@ -48,8 +49,8 @@ describe('reactPlugin', () => { onRumInit(callbackSpy) expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration) - expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API) + expect(callbackSpy.mock.lastCall![0]).toBe(pluginConfiguration) + expect(callbackSpy.mock.lastCall![1]).toBe(PUBLIC_API) }) it('enforce manual view tracking when router is enabled', () => { @@ -74,8 +75,8 @@ describe('reactPlugin', () => { }) it('calls onRumStart subscribers during onRumStart', () => { - const callbackSpy = jasmine.createSpy() - const addErrorSpy = jasmine.createSpy() + const callbackSpy = vi.fn() + const addErrorSpy = vi.fn() onRumStart(callbackSpy) reactPlugin().onRumStart({ addError: addErrorSpy }) @@ -84,10 +85,10 @@ describe('reactPlugin', () => { }) it('calls onRumStart subscribers immediately if already started', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() reactPlugin().onRumStart({ addError: addErrorSpy }) - const callbackSpy = jasmine.createSpy() + const callbackSpy = vi.fn() onRumStart(callbackSpy) expect(callbackSpy).toHaveBeenCalledWith(addErrorSpy) diff --git a/packages/browser-rum-react/src/domain/reactRouter/createRouter.spec.ts b/packages/browser-rum-react/src/domain/reactRouter/createRouter.spec.ts index b465ae5867..7baf144a8f 100644 --- a/packages/browser-rum-react/src/domain/reactRouter/createRouter.spec.ts +++ b/packages/browser-rum-react/src/domain/reactRouter/createRouter.spec.ts @@ -1,8 +1,7 @@ -import { registerCleanupTask } from '@datadog/browser-core/test' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createMemoryRouter as createMemoryRouterV7 } from 'react-router-dom' import { createMemoryRouter as createMemoryRouterV6 } from 'react-router-dom-6' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' -import { resetReactPlugin } from '../reactPlugin' import { wrapCreateRouter } from './createRouter' import type { AnyRouter, AnyRouterSubscriber } from './types' @@ -14,17 +13,17 @@ describe('createRouter', () => { for (const { label, createMemoryRouter } of versions) { describe(label, () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let startViewSpy: ReturnType let router: ReturnType beforeEach(() => { - startViewSpy = jasmine.createSpy() + startViewSpy = vi.fn() initializeReactPlugin({ configuration: { router: true, }, publicApi: { - startView: startViewSpy, + startView: startViewSpy as never, }, }) @@ -45,34 +44,34 @@ describe('createRouter', () => { }) it('creates a new view when the router navigates', async () => { - startViewSpy.calls.reset() + startViewSpy.mockClear() await router.navigate('/bar') expect(startViewSpy).toHaveBeenCalledWith('/bar') }) it('creates a new view when the router navigates to a nested route', async () => { await router.navigate('/bar') - startViewSpy.calls.reset() + startViewSpy.mockClear() await router.navigate('/bar/nested') expect(startViewSpy).toHaveBeenCalledWith('/bar/nested') }) it('creates a new view with the fallback route', async () => { - startViewSpy.calls.reset() + startViewSpy.mockClear() await router.navigate('/non-existent') expect(startViewSpy).toHaveBeenCalledWith('/non-existent') }) it('does not create a new view when navigating to the same URL', async () => { await router.navigate('/bar') - startViewSpy.calls.reset() + startViewSpy.mockClear() await router.navigate('/bar') expect(startViewSpy).not.toHaveBeenCalled() }) it('does not create a new view when just changing query parameters', async () => { await router.navigate('/bar') - startViewSpy.calls.reset() + startViewSpy.mockClear() await router.navigate('/bar?baz=1') expect(startViewSpy).not.toHaveBeenCalled() }) @@ -86,11 +85,6 @@ describe('wrapCreateRouter β€” initial error forwarding', () => { const normalOpts = { newErrors: null, deletedFetchers: [] } const noop = () => undefined - beforeEach(() => { - resetReactPlugin() - registerCleanupTask(resetReactPlugin) - }) - function buildFakeRouter(syncReplay?: typeof errorOpts) { let ourCallback: AnyRouterSubscriber | undefined const fakeRouter: AnyRouter = { @@ -111,23 +105,25 @@ describe('wrapCreateRouter β€” initial error forwarding', () => { it('forwards a synchronous buffer replay (react-router β‰₯7.15.1)', () => { const { fakeRouter } = buildFakeRouter(errorOpts) - const spy = jasmine.createSpy() + const spy = vi.fn() fakeRouter.subscribe(spy) - expect(spy).toHaveBeenCalledOnceWith(fakeRouter.state, errorOpts) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(fakeRouter.state, errorOpts) }) it('forwards an async newErrors notification that fires before the next subscriber attaches', () => { const { fakeRouter, notify } = buildFakeRouter() notify(errorOpts) - const spy = jasmine.createSpy() + const spy = vi.fn() fakeRouter.subscribe(spy) - expect(spy).toHaveBeenCalledOnceWith(fakeRouter.state, errorOpts) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(fakeRouter.state, errorOpts) }) it('does not forward when there is no error', () => { const { fakeRouter, notify } = buildFakeRouter() notify(normalOpts) - const spy = jasmine.createSpy() + const spy = vi.fn() fakeRouter.subscribe(spy) expect(spy).not.toHaveBeenCalled() }) @@ -148,7 +144,7 @@ describe('wrapCreateRouter β€” initial error forwarding', () => { } wrapCreateRouter((_r: unknown[], _o?: unknown) => fakeRouter)([], {}) - const routerProvider = jasmine.createSpy() + const routerProvider = vi.fn() fakeRouter.subscribe(routerProvider) // Broadcast as react-router would for a post-mount navigation error @@ -157,7 +153,7 @@ describe('wrapCreateRouter β€” initial error forwarding', () => { } expect(routerProvider).toHaveBeenCalledTimes(1) - const resubscribed = jasmine.createSpy() + const resubscribed = vi.fn() fakeRouter.subscribe(resubscribed) expect(resubscribed).not.toHaveBeenCalled() }) diff --git a/packages/browser-rum-react/src/domain/reactRouter/routesComponent.spec.tsx b/packages/browser-rum-react/src/domain/reactRouter/routesComponent.spec.tsx index e26a2800e3..c3a722b7f7 100644 --- a/packages/browser-rum-react/src/domain/reactRouter/routesComponent.spec.tsx +++ b/packages/browser-rum-react/src/domain/reactRouter/routesComponent.spec.tsx @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import React, { act } from 'react' import * as rrdom6 from 'react-router-dom-6' import * as rrdom7 from 'react-router-dom' @@ -41,12 +42,12 @@ import { wrapUseRoutes } from './useRoutes' type NavigateFunction = ReturnType describe(`Routes component (${version})`, () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let startViewSpy: Mock<(name?: string | object) => void> beforeEach(() => { ignoreReactRouterDeprecationWarnings() initReactOldBrowsersSupport() - startViewSpy = jasmine.createSpy() + startViewSpy = vi.fn() initializeReactPlugin({ configuration: { router: true, @@ -66,7 +67,8 @@ import { wrapUseRoutes } from './useRoutes' ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/foo') }) it('renders the matching route', () => { @@ -125,11 +127,12 @@ import { wrapUseRoutes } from './useRoutes' ) - startViewSpy.calls.reset() + startViewSpy.mockClear() await act(async () => { await navigate!('/bar') }) - expect(startViewSpy).toHaveBeenCalledOnceWith('/bar') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/bar') }) it('does not start a new view if the URL is the same', async () => { @@ -149,7 +152,7 @@ import { wrapUseRoutes } from './useRoutes' ) - startViewSpy.calls.reset() + startViewSpy.mockClear() await act(async () => { await navigate!('/foo') }) @@ -174,7 +177,7 @@ import { wrapUseRoutes } from './useRoutes' ) - startViewSpy.calls.reset() + startViewSpy.mockClear() await act(async () => { await navigate!('/foo?bar=baz') }) @@ -206,7 +209,8 @@ import { wrapUseRoutes } from './useRoutes' ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/foo') }) it('allows passing a location string', () => { @@ -218,7 +222,8 @@ import { wrapUseRoutes } from './useRoutes' ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/foo') }) }) }) diff --git a/packages/browser-rum-react/src/domain/reactRouter/startReactRouterView.spec.ts b/packages/browser-rum-react/src/domain/reactRouter/startReactRouterView.spec.ts index 7c4b708b25..ca4a0395b9 100644 --- a/packages/browser-rum-react/src/domain/reactRouter/startReactRouterView.spec.ts +++ b/packages/browser-rum-react/src/domain/reactRouter/startReactRouterView.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { display } from '@datadog/browser-core' import { createMemoryRouter as createMemoryRouterV6, @@ -29,7 +30,7 @@ routerVersions.forEach(({ version, createMemoryRouter }) => { describe(`startReactRouterView (${version})`, () => { describe('startReactRouterView', () => { it('creates a new view with the computed view name', () => { - const startViewSpy = jasmine.createSpy() + const startViewSpy = vi.fn() initializeReactPlugin({ configuration: { router: true, @@ -45,17 +46,19 @@ routerVersions.forEach(({ version, createMemoryRouter }) => { { route: { path: ':id' } }, ] as unknown as AnyRouteMatch[]) - expect(startViewSpy).toHaveBeenCalledOnceWith('/user/:id') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/user/:id') }) it('displays a warning if the router integration is not enabled', () => { - const displayWarnSpy = spyOn(display, 'warn') + const displayWarnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) initializeReactPlugin({ configuration: {}, }) startReactRouterView([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( '`router: true` is missing from the react plugin configuration, the view will not be tracked.' ) }) diff --git a/packages/browser-rum-react/src/domain/reactRouter/useRoutes.spec.tsx b/packages/browser-rum-react/src/domain/reactRouter/useRoutes.spec.tsx index 13477264d2..1da7f77dfc 100644 --- a/packages/browser-rum-react/src/domain/reactRouter/useRoutes.spec.tsx +++ b/packages/browser-rum-react/src/domain/reactRouter/useRoutes.spec.tsx @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import React, { act } from 'react' import * as rrdom6 from 'react-router-dom-6' import * as rrdom7 from 'react-router-dom' @@ -46,12 +47,12 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { } describe(`useRoutes (${version})`, () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let startViewSpy: Mock<(name?: string | object) => void> beforeEach(() => { ignoreReactRouterDeprecationWarnings() initReactOldBrowsersSupport() - startViewSpy = jasmine.createSpy() + startViewSpy = vi.fn() initializeReactPlugin({ configuration: { router: true, @@ -76,7 +77,8 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/foo') }) it('renders the matching route', () => { @@ -147,13 +149,14 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { ) - startViewSpy.calls.reset() + startViewSpy.mockClear() await act(async () => { await navigate!('/bar') }) - expect(startViewSpy).toHaveBeenCalledOnceWith('/bar') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/bar') }) it('does not start a new view if the URL is the same', async () => { @@ -171,7 +174,7 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { ) - startViewSpy.calls.reset() + startViewSpy.mockClear() await act(async () => { await navigate!('/foo') @@ -195,7 +198,7 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { ) - startViewSpy.calls.reset() + startViewSpy.mockClear() await act(async () => { await navigate!('/foo?bar=baz') @@ -224,7 +227,8 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/foo') }) it('allows passing a location string', () => { @@ -242,7 +246,8 @@ versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/foo') }) }) }) diff --git a/packages/browser-rum-react/src/domain/tanstackRouter/startTanStackRouterView.spec.ts b/packages/browser-rum-react/src/domain/tanstackRouter/startTanStackRouterView.spec.ts index 6f5bbe11fd..b64b7b5a81 100644 --- a/packages/browser-rum-react/src/domain/tanstackRouter/startTanStackRouterView.spec.ts +++ b/packages/browser-rum-react/src/domain/tanstackRouter/startTanStackRouterView.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, vi } from 'vitest' import { display } from '@datadog/browser-core' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' import { startTanStackRouterView, computeViewName } from './startTanStackRouterView' @@ -5,7 +6,7 @@ import type { AnyTanStackRouteMatch } from './types' describe('startTanStackRouterView', () => { it('creates a new view with the computed view name', () => { - const startViewSpy = jasmine.createSpy() + const startViewSpy = vi.fn() initializeReactPlugin({ configuration: { router: true, @@ -20,17 +21,19 @@ describe('startTanStackRouterView', () => { { fullPath: '/users/$userId', pathname: '/users/1', params: { userId: '1' } }, ]) - expect(startViewSpy).toHaveBeenCalledOnceWith('/users/$userId') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/users/$userId') }) it('displays a warning if the router integration is not enabled', () => { - const displayWarnSpy = spyOn(display, 'warn') + const displayWarnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) initializeReactPlugin({ configuration: {}, }) startTanStackRouterView([]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( + expect(displayWarnSpy).toHaveBeenCalledTimes(1) + expect(displayWarnSpy).toHaveBeenCalledExactlyOnceWith( '`router: true` is missing from the react plugin configuration, the view will not be tracked.' ) }) diff --git a/packages/browser-rum-react/src/domain/tanstackRouter/wrapCreateRouter.spec.ts b/packages/browser-rum-react/src/domain/tanstackRouter/wrapCreateRouter.spec.ts index fa12391a96..a972238621 100644 --- a/packages/browser-rum-react/src/domain/tanstackRouter/wrapCreateRouter.spec.ts +++ b/packages/browser-rum-react/src/domain/tanstackRouter/wrapCreateRouter.spec.ts @@ -1,12 +1,14 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { Mock } from 'vitest' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' import { wrapCreateRouter } from './wrapCreateRouter' import type { AnyTanStackCreateRouter, AnyTanStackNavigationEvent, AnyTanStackRouterInstance } from './types' describe('wrapCreateRouter', () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let startViewSpy: Mock<(name?: string | object) => void> beforeEach(() => { - startViewSpy = jasmine.createSpy() + startViewSpy = vi.fn() initializeReactPlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -27,7 +29,7 @@ describe('wrapCreateRouter', () => { it('should not start a new view when only query params change', () => { const { triggerOnLoad } = createFakeRouter([{ fullPath: '/', pathname: '/', params: {} }]) - startViewSpy.calls.reset() + startViewSpy.mockClear() triggerOnLoad({ type: 'onLoad', pathChanged: false, toLocation: { pathname: '/' } }) expect(startViewSpy).not.toHaveBeenCalled() diff --git a/packages/browser-rum-slim/src/domain/getSessionReplayLink.spec.ts b/packages/browser-rum-slim/src/domain/getSessionReplayLink.spec.ts index 7cafb6911d..9ddb747de8 100644 --- a/packages/browser-rum-slim/src/domain/getSessionReplayLink.spec.ts +++ b/packages/browser-rum-slim/src/domain/getSessionReplayLink.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { RumConfiguration } from '@datadog/browser-rum-core' import { getSessionReplayLink } from './getSessionReplayLink' diff --git a/packages/browser-rum-vue/src/domain/error/addVueError.spec.ts b/packages/browser-rum-vue/src/domain/error/addVueError.spec.ts index f02030a4c7..4f46fae98e 100644 --- a/packages/browser-rum-vue/src/domain/error/addVueError.spec.ts +++ b/packages/browser-rum-vue/src/domain/error/addVueError.spec.ts @@ -1,28 +1,30 @@ +import { describe, it, expect, vi } from 'vitest' import type { ComponentInternalInstance, ComponentPublicInstance } from 'vue' import { initializeVuePlugin } from '../../../test/initializeVuePlugin' import { addVueError } from './addVueError' describe('addVueError', () => { it('reports the error to the SDK with info as first line of component_stack', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeVuePlugin({ addError: addErrorSpy }) const error = new Error('something broke') addVueError(error, null, 'mounted hook') - expect(addErrorSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ + expect(addErrorSpy).toHaveBeenCalledTimes(1) + expect(addErrorSpy).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ error, - handlingStack: jasmine.any(String), + handlingStack: expect.any(String), componentStack: 'mounted hook', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), context: { framework: 'vue' }, }) ) }) it('includes component hierarchy in component_stack when instance is provided', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeVuePlugin({ addError: addErrorSpy }) // Build a mock instance chain without @vue/test-utils to avoid @@ -36,29 +38,29 @@ describe('addVueError', () => { addVueError(new Error('oops'), mockInstance, 'mounted hook') - const componentStack = addErrorSpy.calls.mostRecent().args[0].componentStack as string + const componentStack = addErrorSpy.mock.lastCall![0].componentStack as string expect(componentStack).toContain('mounted hook') expect(componentStack).toContain('at ') expect(componentStack).toContain('at ') }) it('handles empty info gracefully', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeVuePlugin({ addError: addErrorSpy }) addVueError(new Error('oops'), null, '') expect(addErrorSpy).toHaveBeenCalledTimes(1) - expect(addErrorSpy.calls.mostRecent().args[0].componentStack).toBeUndefined() + expect(addErrorSpy.mock.lastCall![0].componentStack).toBeUndefined() }) it('should merge dd_context from the original error with vue error context', () => { - const addErrorSpy = jasmine.createSpy() + const addErrorSpy = vi.fn() initializeVuePlugin({ addError: addErrorSpy }) const originalError = new Error('error message') ;(originalError as any).dd_context = { component: 'Menu', param: 123 } addVueError(originalError, null, 'mounted hook') - expect(addErrorSpy.calls.mostRecent().args[0].context).toEqual({ + expect(addErrorSpy.mock.lastCall![0].context).toEqual({ framework: 'vue', component: 'Menu', param: 123, diff --git a/packages/browser-rum-vue/src/domain/router/startVueRouterView.spec.ts b/packages/browser-rum-vue/src/domain/router/startVueRouterView.spec.ts index e1f3fe9041..d6ff957b13 100644 --- a/packages/browser-rum-vue/src/domain/router/startVueRouterView.spec.ts +++ b/packages/browser-rum-vue/src/domain/router/startVueRouterView.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, vi } from 'vitest' import { display } from '@datadog/browser-core' import type { RouteLocationMatched } from 'vue-router' import { initializeVuePlugin } from '../../../test/initializeVuePlugin' @@ -5,7 +6,7 @@ import { startVueRouterView, computeViewName } from './startVueRouterView' describe('startVueRouterView', () => { it('starts a new view with the computed view name', () => { - const startViewSpy = jasmine.createSpy() + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -16,14 +17,16 @@ describe('startVueRouterView', () => { '/user/1' ) - expect(startViewSpy).toHaveBeenCalledOnceWith('/user/:id') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/user/:id') }) it('warns if router: true is missing from plugin config', () => { - const warnSpy = spyOn(display, 'warn') + const warnSpy = vi.spyOn(display, 'warn').mockImplementation(() => undefined) initializeVuePlugin({ configuration: {} }) startVueRouterView([], '/') - expect(warnSpy).toHaveBeenCalledOnceWith( + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy).toHaveBeenCalledExactlyOnceWith( '`router: true` is missing from the vue plugin configuration, the view will not be tracked.' ) }) diff --git a/packages/browser-rum-vue/src/domain/router/vueRouter.spec.ts b/packages/browser-rum-vue/src/domain/router/vueRouter.spec.ts index 3c0949f37a..4262645d90 100644 --- a/packages/browser-rum-vue/src/domain/router/vueRouter.spec.ts +++ b/packages/browser-rum-vue/src/domain/router/vueRouter.spec.ts @@ -1,10 +1,11 @@ +import { describe, it, expect, vi } from 'vitest' import { createMemoryHistory } from 'vue-router' import { initializeVuePlugin } from '../../../test/initializeVuePlugin' import { createRouter } from './vueRouter' describe('createRouter (wrapped)', () => { - it('calls startView on navigation', (done) => { - const startViewSpy = jasmine.createSpy() + it('calls startView on navigation', async () => { + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -18,21 +19,14 @@ describe('createRouter (wrapped)', () => { ], }) - router - .push('/') - .then(() => { - expect(startViewSpy).toHaveBeenCalledWith('/') - return router.push('/about') - }) - .then(() => { - expect(startViewSpy).toHaveBeenCalledWith('/about') - done() - }) - .catch(done.fail) + await router.push('/') + expect(startViewSpy).toHaveBeenCalledExactlyOnceWith('/') + await router.push('/about') + expect(startViewSpy).toHaveBeenCalledWith('/about') }) - it('does not call startView when navigation is duplicated', (done) => { - const startViewSpy = jasmine.createSpy() + it('does not call startView when navigation is duplicated', async () => { + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -43,21 +37,14 @@ describe('createRouter (wrapped)', () => { routes: [{ path: '/', component: {} }], }) - router - .push('/') - .then(() => { - startViewSpy.calls.reset() - return router.push('/') - }) - .then(() => { - expect(startViewSpy).not.toHaveBeenCalled() - done() - }) - .catch(done.fail) + await router.push('/') + startViewSpy.mockClear() + await router.push('/') + expect(startViewSpy).not.toHaveBeenCalled() }) - it('does not call startView when only query params change', (done) => { - const startViewSpy = jasmine.createSpy() + it('does not call startView when only query params change', async () => { + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -68,21 +55,14 @@ describe('createRouter (wrapped)', () => { routes: [{ path: '/products', component: {} }], }) - router - .push('/products?page=1') - .then(() => { - startViewSpy.calls.reset() - return router.push('/products?page=2') - }) - .then(() => { - expect(startViewSpy).not.toHaveBeenCalled() - done() - }) - .catch(done.fail) + await router.push('/products?page=1') + startViewSpy.mockClear() + await router.push('/products?page=2') + expect(startViewSpy).not.toHaveBeenCalled() }) - it('calls startView on initial navigation to /', (done) => { - const startViewSpy = jasmine.createSpy() + it('calls startView on initial navigation to /', async () => { + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -93,17 +73,13 @@ describe('createRouter (wrapped)', () => { routes: [{ path: '/', component: {} }], }) - router - .push('/') - .then(() => { - expect(startViewSpy).toHaveBeenCalledOnceWith('/') - done() - }) - .catch(done.fail) + await router.push('/') + expect(startViewSpy).toHaveBeenCalledTimes(1) + expect(startViewSpy).toHaveBeenCalledWith('/') }) - it('substitutes catch-all pattern with the actual path', (done) => { - const startViewSpy = jasmine.createSpy() + it('substitutes catch-all pattern with the actual path', async () => { + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -117,17 +93,12 @@ describe('createRouter (wrapped)', () => { ], }) - router - .push('/unknown/page') - .then(() => { - expect(startViewSpy).toHaveBeenCalledWith('/unknown/page') - done() - }) - .catch(done.fail) + await router.push('/unknown/page') + expect(startViewSpy).toHaveBeenCalledWith('/unknown/page') }) - it('does not call startView when navigation is blocked', (done) => { - const startViewSpy = jasmine.createSpy() + it('does not call startView when navigation is blocked', async () => { + const startViewSpy = vi.fn() initializeVuePlugin({ configuration: { router: true }, publicApi: { startView: startViewSpy }, @@ -148,16 +119,9 @@ describe('createRouter (wrapped)', () => { } }) - router - .push('/') - .then(() => { - startViewSpy.calls.reset() - return router.push('/protected') - }) - .then(() => { - expect(startViewSpy).not.toHaveBeenCalled() - done() - }) - .catch(done.fail) + await router.push('/') + startViewSpy.mockClear() + await router.push('/protected') + expect(startViewSpy).not.toHaveBeenCalled() }) }) diff --git a/packages/browser-rum-vue/src/domain/vuePlugin.spec.ts b/packages/browser-rum-vue/src/domain/vuePlugin.spec.ts index 2337989a12..191ddfb98c 100644 --- a/packages/browser-rum-vue/src/domain/vuePlugin.spec.ts +++ b/packages/browser-rum-vue/src/domain/vuePlugin.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' import { registerCleanupTask } from '../../../browser-core/test' import { onRumInit, vuePlugin, resetVuePlugin } from './vuePlugin' @@ -11,23 +12,25 @@ describe('vuePlugin', () => { }) it('returns a plugin object with name "vue"', () => { - expect(vuePlugin()).toEqual(jasmine.objectContaining({ name: 'vue' })) + expect(vuePlugin()).toEqual(expect.objectContaining({ name: 'vue' })) }) it('calls callbacks registered with onRumInit during onInit', () => { - const spy = jasmine.createSpy() + const spy = vi.fn() const config = {} onRumInit(spy) vuePlugin(config).onInit({ publicApi: PUBLIC_API, initConfiguration: INIT_CONFIGURATION }) - expect(spy).toHaveBeenCalledOnceWith(config, PUBLIC_API) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(config, PUBLIC_API) }) it('calls callbacks immediately if onInit was already invoked', () => { - const spy = jasmine.createSpy() + const spy = vi.fn() const config = {} vuePlugin(config).onInit({ publicApi: PUBLIC_API, initConfiguration: INIT_CONFIGURATION }) onRumInit(spy) - expect(spy).toHaveBeenCalledOnceWith(config, PUBLIC_API) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledExactlyOnceWith(config, PUBLIC_API) }) it('sets trackViewsManually when router is true', () => { diff --git a/packages/browser-rum/src/boot/datadogRecorder.spec.ts b/packages/browser-rum/src/boot/datadogRecorder.spec.ts index e8c690c47c..7ea779e5da 100644 --- a/packages/browser-rum/src/boot/datadogRecorder.spec.ts +++ b/packages/browser-rum/src/boot/datadogRecorder.spec.ts @@ -1,5 +1,6 @@ -import type { HttpRequest, HttpRequestEvent, Telemetry } from '@datadog/browser-core' +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { TimeStamp } from '@datadog/js-core/time' +import type { HttpRequest, HttpRequestEvent, Telemetry } from '@datadog/browser-core' import { PageExitReason, DefaultPrivacyLevel, noop, DeflateEncoderStreamId, Observable } from '@datadog/browser-core' import type { ViewCreatedEvent } from '@datadog/browser-rum-core' import { LifeCycle, LifeCycleEventType, startViewHistory } from '@datadog/browser-rum-core' @@ -30,14 +31,14 @@ describe('startRecording', () => { let sessionManager: SessionManagerMock let viewId: string let textField: HTMLInputElement - let requestSendSpy: jasmine.Spy + let requestSendSpy: Mock let stopRecording: () => void function setupStartRecording() { const configuration = mockRumConfiguration({ defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW }) const worker = startDeflateWorker(configuration, 'Session Replay', noop) - requestSendSpy = jasmine.createSpy() + requestSendSpy = vi.fn() const httpRequest = { observable: new Observable>(), send: requestSendSpy, @@ -81,27 +82,28 @@ describe('startRecording', () => { flushSegment(lifeCycle) const requests = await readSentRequests(1) - expect(requests[0].segment).toEqual(jasmine.any(Object)) + expect(requests[0].segment).toEqual(expect.any(Object)) expect(requests[0].event).toEqual({ application: { id: 'appId', }, creation_reason: 'init', - end: jasmine.stringMatching(/^\d{13}$/), + end: expect.any(Number), has_full_snapshot: true, records_count: recordsPerFullSnapshot(), session: { id: MOCK_SESSION_ID, }, - start: jasmine.any(Number), - raw_segment_size: jasmine.any(Number), - compressed_segment_size: jasmine.any(Number), + start: expect.any(Number), + raw_segment_size: expect.any(Number), + compressed_segment_size: expect.any(Number), view: { id: 'view-id', }, index_in_view: 0, source: 'browser', }) + expect(String(requests[0].event.end)).toMatch(/^\d{13}$/) }) it('flushes the segment when its compressed data reaches the segment bytes limit', async () => { @@ -219,7 +221,7 @@ describe('startRecording', () => { it('should send records through the bridge when it is present', () => { const eventBridge = mockEventBridge() setupStartRecording() - const sendSpy = spyOn(eventBridge, 'send') + const sendSpy = vi.spyOn(eventBridge, 'send').mockImplementation(() => undefined) // send click record document.body.dispatchEvent(createNewEvent('click', { clientX: 1, clientY: 2 })) @@ -227,25 +229,25 @@ describe('startRecording', () => { // send view end record and meta record changeView(lifeCycle) - const record1 = JSON.parse(sendSpy.calls.argsFor(0)[0]) - const record2 = JSON.parse(sendSpy.calls.argsFor(1)[0]) - const record3 = JSON.parse(sendSpy.calls.argsFor(2)[0]) + const record1 = JSON.parse(sendSpy.mock.calls[0][0]) + const record2 = JSON.parse(sendSpy.mock.calls[1][0]) + const record3 = JSON.parse(sendSpy.mock.calls[2][0]) expect(record1).toEqual({ eventType: 'record', - event: jasmine.objectContaining({ type: RecordType.IncrementalSnapshot }), + event: expect.objectContaining({ type: RecordType.IncrementalSnapshot }), view: { id: 'view-id' }, }) expect(record2).toEqual({ eventType: 'record', - event: jasmine.objectContaining({ type: RecordType.ViewEnd }), + event: expect.objectContaining({ type: RecordType.ViewEnd }), view: { id: 'view-id' }, }) expect(record3).toEqual({ eventType: 'record', - event: jasmine.objectContaining({ type: RecordType.Meta }), + event: expect.objectContaining({ type: RecordType.Meta }), view: { id: 'view-id-2' }, }) }) diff --git a/packages/browser-rum/src/boot/lazyLoadRecorder.spec.ts b/packages/browser-rum/src/boot/lazyLoadRecorder.spec.ts index 0413fb40d7..02819d4113 100644 --- a/packages/browser-rum/src/boot/lazyLoadRecorder.spec.ts +++ b/packages/browser-rum/src/boot/lazyLoadRecorder.spec.ts @@ -1,15 +1,16 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { display } from '@datadog/browser-core' import type { MockTelemetry } from '@datadog/browser-core/test' import { replaceMockable, startMockTelemetry } from '@datadog/browser-core/test' import { lazyLoadRecorder, importRecorder } from './lazyLoadRecorder' describe('lazyLoadRecorder', () => { - let displaySpy: jasmine.Spy + let displaySpy: Mock let telemetry: MockTelemetry beforeEach(() => { telemetry = startMockTelemetry() - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) }) it('should report a console error and metrics but no telemetry error if CSP blocks the module', async () => { @@ -17,8 +18,8 @@ describe('lazyLoadRecorder', () => { replaceMockable(importRecorder, () => Promise.reject(loadRecorderError)) await lazyLoadRecorder() - expect(displaySpy).toHaveBeenCalledWith(jasmine.stringContaining('Recorder failed to start'), loadRecorderError) - expect(displaySpy).toHaveBeenCalledWith(jasmine.stringContaining('Please make sure CSP is correctly configured')) + expect(displaySpy).toHaveBeenCalledWith(expect.stringContaining('Recorder failed to start'), loadRecorderError) + expect(displaySpy).toHaveBeenCalledWith(expect.stringContaining('Please make sure CSP is correctly configured')) expect(await telemetry.getEvents()).toEqual([]) }) @@ -27,7 +28,7 @@ describe('lazyLoadRecorder', () => { replaceMockable(importRecorder, () => Promise.reject(loadRecorderError)) await lazyLoadRecorder() - expect(displaySpy).toHaveBeenCalledWith(jasmine.stringContaining('Recorder failed to start'), loadRecorderError) + expect(displaySpy).toHaveBeenCalledWith(expect.stringContaining('Recorder failed to start'), loadRecorderError) expect(await telemetry.getEvents()).toEqual([]) }) }) diff --git a/packages/browser-rum/src/boot/profilerApi.spec.ts b/packages/browser-rum/src/boot/profilerApi.spec.ts index 194afbd506..dd692548d2 100644 --- a/packages/browser-rum/src/boot/profilerApi.spec.ts +++ b/packages/browser-rum/src/boot/profilerApi.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it, beforeEach, type Mock } from 'vitest' import { MID_HASH_UUID, replaceMockableWithSpy, @@ -36,12 +37,10 @@ describe('profilerApi', () => { }) describe('bridge mode', () => { - let createRumProfilerSpy: jasmine.Spy + let createRumProfilerSpy: Mock beforeEach(() => { - createRumProfilerSpy = jasmine - .createSpy('createRumProfiler') - .and.returnValue({ start: jasmine.createSpy(), stop: jasmine.createSpy() }) + createRumProfilerSpy = vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) replaceMockable(isProfilingSupported, () => true) replaceMockable(lazyLoadProfiler, () => Promise.resolve(createRumProfilerSpy)) }) diff --git a/packages/browser-rum/src/boot/recorderApi.spec.ts b/packages/browser-rum/src/boot/recorderApi.spec.ts index ec64e276a7..02e6866af6 100644 --- a/packages/browser-rum/src/boot/recorderApi.spec.ts +++ b/packages/browser-rum/src/boot/recorderApi.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { DeflateEncoder, DeflateWorker, @@ -33,11 +34,11 @@ import { importRecorder } from './lazyLoadRecorder' describe('makeRecorderApi', () => { let lifeCycle: LifeCycle let recorderApi: RecorderApi - let startRecordingSpy: jasmine.Spy - let importRecorderSpy: jasmine.Spy<() => Promise> - let stopRecordingSpy: jasmine.Spy<() => void> + let startRecordingSpy: Mock + let importRecorderSpy: Mock<() => Promise> + let stopRecordingSpy: Mock<() => void> let mockWorker: MockWorker - let createDeflateWorkerSpy: jasmine.Spy + let createDeflateWorkerSpy: Mock let rumInit: (options?: { worker?: DeflateWorker }) => void let telemetry: MockTelemetry @@ -54,19 +55,19 @@ describe('makeRecorderApi', () => { } = {}) { telemetry = startMockTelemetry() mockWorker = new MockWorker() - createDeflateWorkerSpy = replaceMockableWithSpy(createDeflateWorker).and.callFake(() => mockWorker) - spyOn(display, 'error') + createDeflateWorkerSpy = replaceMockableWithSpy(createDeflateWorker).mockImplementation(() => mockWorker) + vi.spyOn(display, 'error').mockImplementation(() => undefined) lifeCycle = new LifeCycle() - stopRecordingSpy = jasmine.createSpy('stopRecording') - startRecordingSpy = jasmine.createSpy('startRecording') + stopRecordingSpy = vi.fn() + startRecordingSpy = vi.fn() importRecorderSpy = replaceMockableWithSpy(importRecorder) if (loadRecorderError) { - importRecorderSpy.and.resolveTo(undefined) + importRecorderSpy.mockResolvedValue(undefined as unknown as StartRecording) } else { // Workaround because using resolveTo(startRecordingSpy) was not working - importRecorderSpy.and.resolveTo((...args: any) => { + importRecorderSpy.mockResolvedValue((...args: any) => { startRecordingSpy(...args) return { stop: stopRecordingSpy, @@ -205,7 +206,7 @@ describe('makeRecorderApi', () => { }) it('should start recording if session is tracked without session replay when forced', async () => { - const updateSessionStateSpy = jasmine.createSpy() + const updateSessionStateSpy = vi.fn() setupRecorderApi({ sessionManager: { @@ -251,7 +252,9 @@ describe('makeRecorderApi', () => { setupRecorderApi({ startSessionReplayRecordingManually: true }) rumInit() - createDeflateWorkerSpy.and.throwError('Crash') + createDeflateWorkerSpy.mockImplementation(() => { + throw new Error('Crash') + }) recorderApi.start() expect(importRecorderSpy).toHaveBeenCalled() @@ -260,8 +263,8 @@ describe('makeRecorderApi', () => { expect(startRecordingSpy).not.toHaveBeenCalled() const events = await telemetry.getEvents() expect(events).toEqual([ - jasmine.objectContaining({ - error: jasmine.anything(), + expect.objectContaining({ + error: expect.anything(), }), expectedRecorderInitTelemetry({ result: 'deflate-encoder-load-failed' }), ]) @@ -286,14 +289,14 @@ describe('makeRecorderApi', () => { await collectAsyncCalls(startRecordingSpy, 1) - const firstCallDeflateEncoder: DeflateEncoder = startRecordingSpy.calls.mostRecent().args[4] + const firstCallDeflateEncoder: DeflateEncoder = startRecordingSpy.mock.lastCall![4] firstCallDeflateEncoder.write('foo') recorderApi.stop() recorderApi.start() await collectAsyncCalls(startRecordingSpy, 2) - const secondCallDeflateEncoder: DeflateEncoder = startRecordingSpy.calls.mostRecent().args[4] + const secondCallDeflateEncoder: DeflateEncoder = startRecordingSpy.mock.lastCall![4] secondCallDeflateEncoder.write('foo') const writeMessages = mockWorker.pendingMessages.filter( @@ -576,7 +579,7 @@ describe('makeRecorderApi', () => { mockWorker.processAllMessages() - expect(recorderApi.isRecording()).toBeFalse() + expect(recorderApi.isRecording()).toBe(false) }) it('is false when the worker is not yet initialized', async () => { @@ -584,7 +587,7 @@ describe('makeRecorderApi', () => { rumInit() await collectAsyncCalls(startRecordingSpy, 1) - expect(recorderApi.isRecording()).toBeFalse() + expect(recorderApi.isRecording()).toBe(false) }) it('is false when the worker failed to initialize', async () => { @@ -594,7 +597,7 @@ describe('makeRecorderApi', () => { mockWorker.dispatchErrorEvent() - expect(recorderApi.isRecording()).toBeFalse() + expect(recorderApi.isRecording()).toBe(false) }) it('is true when recording is started and the worker is initialized', async () => { @@ -604,7 +607,7 @@ describe('makeRecorderApi', () => { mockWorker.processAllMessages() - expect(recorderApi.isRecording()).toBeTrue() + expect(recorderApi.isRecording()).toBe(true) }) it('is false before the DOM is loaded', async () => { @@ -616,14 +619,14 @@ describe('makeRecorderApi', () => { mockWorker.processAllMessages() - expect(recorderApi.isRecording()).toBeFalse() + expect(recorderApi.isRecording()).toBe(false) triggerOnDomLoaded() await collectAsyncCalls(startRecordingSpy, 1) mockWorker.processAllMessages() - expect(recorderApi.isRecording()).toBeTrue() + expect(recorderApi.isRecording()).toBe(true) }) }) @@ -684,10 +687,10 @@ function expectedRecorderInitTelemetry(overrides: Partial = message: 'Recorder init metrics', metrics: { forced: false, - loadRecorderModuleDuration: jasmine.any(Number), - recorderInitDuration: jasmine.any(Number), + loadRecorderModuleDuration: expect.any(Number), + recorderInitDuration: expect.any(Number), result: 'succeeded', - waitForDocReadyDuration: jasmine.any(Number), + waitForDocReadyDuration: expect.any(Number), ...overrides, }, } diff --git a/packages/browser-rum/src/domain/deflate/deflateEncoder.spec.ts b/packages/browser-rum/src/domain/deflate/deflateEncoder.spec.ts index ec61c9fe13..bef6cc730e 100644 --- a/packages/browser-rum/src/domain/deflate/deflateEncoder.spec.ts +++ b/packages/browser-rum/src/domain/deflate/deflateEncoder.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { EncoderResult, Uint8ArrayBuffer } from '@datadog/browser-core' import { noop, DeflateEncoderStreamId } from '@datadog/browser-core' import { MockWorker } from '../../../test' @@ -19,7 +20,7 @@ describe('createDeflateEncoder', () => { describe('write()', () => { it('invokes write callbacks', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const writeCallbackSpy = jasmine.createSpy() + const writeCallbackSpy = vi.fn() encoder.write('foo', writeCallbackSpy) encoder.write('bar', writeCallbackSpy) @@ -28,8 +29,8 @@ describe('createDeflateEncoder', () => { worker.processAllMessages() expect(writeCallbackSpy).toHaveBeenCalledTimes(2) - expect(writeCallbackSpy.calls.argsFor(0)).toEqual([3]) - expect(writeCallbackSpy.calls.argsFor(1)).toEqual([3]) + expect(writeCallbackSpy.mock.calls[0]).toEqual([3]) + expect(writeCallbackSpy.mock.calls[1]).toEqual([3]) }) it('marks the encoder as not empty', () => { @@ -42,14 +43,14 @@ describe('createDeflateEncoder', () => { describe('finish()', () => { it('invokes the callback with the encoded data', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const finishCallbackSpy = jasmine.createSpy<(result: EncoderResult) => void>() + const finishCallbackSpy = vi.fn<(result: EncoderResult) => void>() encoder.write('foo') encoder.write('bar') encoder.finish(finishCallbackSpy) worker.processAllMessages() - expect(finishCallbackSpy).toHaveBeenCalledOnceWith({ + expect(finishCallbackSpy).toHaveBeenCalledExactlyOnceWith({ output: new Uint8Array([...ENCODED_FOO, ...ENCODED_BAR, ...TRAILER]), outputBytesCount: 7, rawBytesCount: 6, @@ -59,10 +60,10 @@ describe('createDeflateEncoder', () => { it('invokes the callback even if nothing has been written', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const finishCallbackSpy = jasmine.createSpy<(result: EncoderResult) => void>() + const finishCallbackSpy = vi.fn<(result: EncoderResult) => void>() encoder.finish(finishCallbackSpy) - expect(finishCallbackSpy).toHaveBeenCalledOnceWith({ + expect(finishCallbackSpy).toHaveBeenCalledExactlyOnceWith({ output: new Uint8Array(0), outputBytesCount: 0, rawBytesCount: 0, @@ -72,7 +73,7 @@ describe('createDeflateEncoder', () => { it('cancels pending write callbacks', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const writeCallbackSpy = jasmine.createSpy() + const writeCallbackSpy = vi.fn() encoder.write('foo', writeCallbackSpy) encoder.write('bar', writeCallbackSpy) encoder.finish(noop) @@ -91,7 +92,7 @@ describe('createDeflateEncoder', () => { it('supports calling finish() while another finish() call is pending', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const finishCallbackSpy = jasmine.createSpy<(result: EncoderResult) => void>() + const finishCallbackSpy = vi.fn<(result: EncoderResult) => void>() encoder.write('foo') encoder.finish(finishCallbackSpy) encoder.write('bar') @@ -100,7 +101,7 @@ describe('createDeflateEncoder', () => { worker.processAllMessages() expect(finishCallbackSpy).toHaveBeenCalledTimes(2) - expect(finishCallbackSpy.calls.allArgs()).toEqual([ + expect(finishCallbackSpy.mock.calls).toEqual([ [ { output: new Uint8Array([...ENCODED_FOO, ...TRAILER]), @@ -140,7 +141,7 @@ describe('createDeflateEncoder', () => { it('cancels pending write callbacks', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const writeCallbackSpy = jasmine.createSpy() + const writeCallbackSpy = vi.fn() encoder.write('foo', writeCallbackSpy) encoder.write('bar', writeCallbackSpy) encoder.finishSync() @@ -159,7 +160,7 @@ describe('createDeflateEncoder', () => { it('supports calling finishSync() while another finish() call is pending', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const finishCallbackSpy = jasmine.createSpy<(result: EncoderResult) => void>() + const finishCallbackSpy = vi.fn<(result: EncoderResult) => void>() encoder.write('foo') encoder.finish(finishCallbackSpy) encoder.write('bar') @@ -182,7 +183,7 @@ describe('createDeflateEncoder', () => { createDeflateEncoder(worker, OTHER_STREAM_ID).write('foo', noop) const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const writeCallbackSpy = jasmine.createSpy() + const writeCallbackSpy = vi.fn() encoder.write('foo', writeCallbackSpy) // Process the first write action only @@ -204,7 +205,7 @@ describe('createDeflateEncoder', () => { it('do not notify data twice when calling finishSync() then finish()', () => { const encoder = createDeflateEncoder(worker, DeflateEncoderStreamId.REPLAY) - const finishCallbackSpy = jasmine.createSpy<(result: EncoderResult) => void>() + const finishCallbackSpy = vi.fn<(result: EncoderResult) => void>() encoder.write('foo') encoder.finishSync() @@ -214,7 +215,8 @@ describe('createDeflateEncoder', () => { worker.processAllMessages() - expect(finishCallbackSpy).toHaveBeenCalledOnceWith({ + expect(finishCallbackSpy).toHaveBeenCalledTimes(1) + expect(finishCallbackSpy).toHaveBeenCalledExactlyOnceWith({ rawBytesCount: 3, output: new Uint8Array([...ENCODED_BAR, ...TRAILER]), outputBytesCount: 4, diff --git a/packages/browser-rum/src/domain/deflate/deflateWorker.spec.ts b/packages/browser-rum/src/domain/deflate/deflateWorker.spec.ts index 0f1ea2d931..88dfe6be02 100644 --- a/packages/browser-rum/src/domain/deflate/deflateWorker.spec.ts +++ b/packages/browser-rum/src/domain/deflate/deflateWorker.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { display } from '@datadog/browser-core' import type { RumConfiguration } from '@datadog/browser-rum-core' import type { Clock, MockTelemetry } from '@datadog/browser-core/test' @@ -16,8 +17,8 @@ const TEST_STREAM_ID = 5 describe('startDeflateWorker', () => { let mockWorker: MockWorker - let createDeflateWorkerSpy: jasmine.Spy - let onInitializationFailureSpy: jasmine.Spy<() => void> + let createDeflateWorkerSpy: Mock + let onInitializationFailureSpy: Mock<() => void> function startDeflateWorkerWithDefaults({ configuration = {}, @@ -31,8 +32,8 @@ describe('startDeflateWorker', () => { beforeEach(() => { mockWorker = new MockWorker() - onInitializationFailureSpy = jasmine.createSpy('onInitializationFailureSpy') - createDeflateWorkerSpy = replaceMockableWithSpy(createDeflateWorker).and.callFake(() => mockWorker) + onInitializationFailureSpy = vi.fn() + createDeflateWorkerSpy = replaceMockableWithSpy(createDeflateWorker).mockImplementation(() => mockWorker) }) afterEach(() => { @@ -71,10 +72,10 @@ describe('startDeflateWorker', () => { let telemetry: MockTelemetry // mimic Chrome behavior let CSP_ERROR: DOMException - let displaySpy: jasmine.Spy + let displaySpy: Mock beforeEach(() => { - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) telemetry = startMockTelemetry() CSP_ERROR = new DOMException( "Failed to construct 'Worker': Access to the script at 'blob:https://example.org/9aadbb61-effe-41ee-aa76-fc607053d642' is denied by the document's Content Security Policy." @@ -83,29 +84,35 @@ describe('startDeflateWorker', () => { describe('Chrome and Safari behavior: exception during worker creation', () => { it('returns undefined when the worker creation throws an exception', () => { - createDeflateWorkerSpy.and.throwError(CSP_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw CSP_ERROR + }) const worker = startDeflateWorkerWithDefaults() expect(worker).toBeUndefined() }) it('displays CSP instructions when the worker creation throws a CSP error', () => { - createDeflateWorkerSpy.and.throwError(CSP_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw CSP_ERROR + }) startDeflateWorkerWithDefaults() - expect(displaySpy).toHaveBeenCalledWith( - jasmine.stringContaining('Please make sure CSP is correctly configured') - ) + expect(displaySpy).toHaveBeenCalledWith(expect.stringContaining('Please make sure CSP is correctly configured')) }) it('does not report CSP errors to telemetry', async () => { - createDeflateWorkerSpy.and.throwError(CSP_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw CSP_ERROR + }) startDeflateWorkerWithDefaults() expect(await telemetry.hasEvents()).toBe(false) }) it('does not try to create a worker again after the creation failed', () => { - createDeflateWorkerSpy.and.throwError(CSP_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw CSP_ERROR + }) startDeflateWorkerWithDefaults() - createDeflateWorkerSpy.calls.reset() + createDeflateWorkerSpy.mockClear() startDeflateWorkerWithDefaults() expect(createDeflateWorkerSpy).not.toHaveBeenCalled() }) @@ -115,9 +122,7 @@ describe('startDeflateWorker', () => { it('displays ErrorEvent as CSP error', () => { startDeflateWorkerWithDefaults() mockWorker.dispatchErrorEvent() - expect(displaySpy).toHaveBeenCalledWith( - jasmine.stringContaining('Please make sure CSP is correctly configured') - ) + expect(displaySpy).toHaveBeenCalledWith(expect.stringContaining('Please make sure CSP is correctly configured')) }) it('calls the initialization failure callback when of an error occurs during loading', () => { @@ -129,7 +134,7 @@ describe('startDeflateWorker', () => { it('returns undefined if an error occurred in a previous loading', () => { startDeflateWorkerWithDefaults() mockWorker.dispatchErrorEvent() - onInitializationFailureSpy.calls.reset() + onInitializationFailureSpy.mockClear() const worker = startDeflateWorkerWithDefaults() @@ -145,7 +150,7 @@ describe('startDeflateWorker', () => { }) mockWorker.dispatchErrorEvent() expect(displaySpy).toHaveBeenCalledWith( - jasmine.stringContaining( + expect.stringContaining( 'Please make sure the worker URL /worker.js is correct and CSP is correctly configured.' ) ) @@ -161,12 +166,12 @@ describe('startDeflateWorker', () => { }) describe('initialization timeout', () => { - let displaySpy: jasmine.Spy + let displaySpy: Mock let clock: Clock beforeEach(() => { - displaySpy = spyOn(display, 'error') - createDeflateWorkerSpy.and.callFake( + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) + createDeflateWorkerSpy.mockImplementation( () => // Creates a worker that does nothing new Worker(URL.createObjectURL(new Blob(['']))) @@ -177,7 +182,8 @@ describe('startDeflateWorker', () => { it('displays an error message when the worker does not respond to the init action', () => { startDeflateWorkerWithDefaults() clock.tick(INITIALIZATION_TIME_OUT_DELAY) - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( 'Session Replay failed to start: a timeout occurred while initializing the Worker' ) }) @@ -185,7 +191,8 @@ describe('startDeflateWorker', () => { it('displays a customized error message', () => { startDeflateWorkerWithDefaults({ source: 'Foo' }) clock.tick(INITIALIZATION_TIME_OUT_DELAY) - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( 'Foo failed to start: a timeout occurred while initializing the Worker' ) }) @@ -194,40 +201,48 @@ describe('startDeflateWorker', () => { describe('worker unknown error', () => { let telemetry: MockTelemetry const UNKNOWN_ERROR = new Error('boom') - let displaySpy: jasmine.Spy + let displaySpy: Mock beforeEach(() => { - displaySpy = spyOn(display, 'error') + displaySpy = vi.spyOn(display, 'error').mockImplementation(() => undefined) telemetry = startMockTelemetry() }) it('displays an error message when the worker creation throws an unknown error', () => { - createDeflateWorkerSpy.and.throwError(UNKNOWN_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw UNKNOWN_ERROR + }) startDeflateWorkerWithDefaults() - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( 'Session Replay failed to start: an error occurred while initializing the worker:', UNKNOWN_ERROR ) }) it('displays a customized error message', () => { - createDeflateWorkerSpy.and.throwError(UNKNOWN_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw UNKNOWN_ERROR + }) startDeflateWorkerWithDefaults({ source: 'Foo' }) - expect(displaySpy).toHaveBeenCalledOnceWith( + expect(displaySpy).toHaveBeenCalledTimes(1) + expect(displaySpy).toHaveBeenCalledExactlyOnceWith( 'Foo failed to start: an error occurred while initializing the worker:', UNKNOWN_ERROR ) }) it('reports unknown errors to telemetry', async () => { - createDeflateWorkerSpy.and.throwError(UNKNOWN_ERROR) + createDeflateWorkerSpy.mockImplementation(() => { + throw UNKNOWN_ERROR + }) startDeflateWorkerWithDefaults() expect(await telemetry.getEvents()).toEqual([ { type: 'log', status: 'error', message: 'boom', - error: { kind: 'Error', stack: jasmine.any(String) }, + error: { kind: 'Error', stack: expect.any(String) }, }, ]) }) @@ -235,7 +250,7 @@ describe('startDeflateWorker', () => { it('does not display error messages as CSP error', () => { startDeflateWorkerWithDefaults() mockWorker.dispatchErrorMessage('foo') - expect(displaySpy).not.toHaveBeenCalledWith(jasmine.stringContaining('CSP')) + expect(displaySpy).not.toHaveBeenCalledWith(expect.stringContaining('CSP')) }) it('reports errors occurring after loading to telemetry', async () => { @@ -248,7 +263,7 @@ describe('startDeflateWorker', () => { type: 'log', status: 'error', message: 'Uncaught "boom"', - error: { stack: jasmine.any(String) }, + error: { stack: expect.any(String) }, worker_version: 'dev', stream_id: TEST_STREAM_ID, }, diff --git a/packages/browser-rum/src/domain/getSessionReplayLink.spec.ts b/packages/browser-rum/src/domain/getSessionReplayLink.spec.ts index ebe1a6e48c..185f593315 100644 --- a/packages/browser-rum/src/domain/getSessionReplayLink.spec.ts +++ b/packages/browser-rum/src/domain/getSessionReplayLink.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { ViewHistory } from '@datadog/browser-rum-core' import { createSessionManagerMock } from '@datadog/browser-core/test' import { mockRumConfiguration } from '../../../browser-rum-core/test' diff --git a/packages/browser-rum/src/domain/profiling/actionHistory.spec.ts b/packages/browser-rum/src/domain/profiling/actionHistory.spec.ts index ea0d6d376f..fb5c6f7734 100644 --- a/packages/browser-rum/src/domain/profiling/actionHistory.spec.ts +++ b/packages/browser-rum/src/domain/profiling/actionHistory.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach } from 'vitest' import { type Duration, relativeToClocks, type RelativeTime } from '@datadog/js-core/time' import { noop } from '@datadog/browser-core' import { LifeCycle, LifeCycleEventType, RumEventType } from '@datadog/browser-rum-core' diff --git a/packages/browser-rum/src/domain/profiling/datadogProfiler.spec.ts b/packages/browser-rum/src/domain/profiling/datadogProfiler.spec.ts index a1fcea0866..48ebff1900 100644 --- a/packages/browser-rum/src/domain/profiling/datadogProfiler.spec.ts +++ b/packages/browser-rum/src/domain/profiling/datadogProfiler.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import { elapsed, timeStampNow, @@ -36,6 +37,7 @@ import { mockRumConfiguration, mockViewHistory } from '../../../../browser-rum-c import { mockProfiler } from '../../../test' import type { BrowserProfilerTrace } from '../../types' import { checkProfilingQuota } from './quotaCheck' +import type { QuotaResult } from './quotaCheck' import { mockedTrace } from './test-utils/mockedTrace' import { createRumProfiler } from './datadogProfiler' import { createFormDataEmitter } from './transport/formDataEmitter' @@ -52,13 +54,13 @@ import { createVitalHistory } from './vitalHistory' describe('profiler', () => { // Store the original pathname const originalPathname = document.location.pathname - let emitPayloadSpy: jasmine.Spy - let checkProfilingQuotaSpy: jasmine.Spy + let checkProfilingQuotaSpy: ReturnType> + let emitPayloadSpy: Mock beforeEach(() => { - // Default: quota always ok. Individual quota-check tests can reconfigure via spy.and.callFake(...) + // Default: quota always ok. Individual quota-check tests can reconfigure via spy.mockImplementation(...) checkProfilingQuotaSpy = replaceMockableWithSpy(checkProfilingQuota) - checkProfilingQuotaSpy.and.returnValue(Promise.resolve({ decision: 'quota_ok', reason: 'quota_ok' })) + checkProfilingQuotaSpy.mockReturnValue(Promise.resolve({ decision: 'quota_ok', reason: 'quota_ok' })) }) afterEach(() => { @@ -123,7 +125,7 @@ describe('profiler', () => { }) replaceMockable(createVitalHistory, () => vitalHistory) - emitPayloadSpy = jasmine.createSpy('emitPayload') + emitPayloadSpy = vi.fn() replaceMockable(createFormDataEmitter, () => emitPayloadSpy) // Start collection of profile. @@ -145,8 +147,8 @@ describe('profiler', () => { return { profiler, profilingContextManager, - sessionManager, mockedRumProfilerTrace, + sessionManager, addLongTask: (longTask: LongTaskContext) => { longTaskHistory.add(longTask, relativeNow()).close(addDuration(relativeNow(), longTask.duration)) }, @@ -190,10 +192,10 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('stopped') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) - expect(emitPayloadSpy.calls.count()).toBe(1) - const payload = emitPayloadSpy.calls.argsFor(0)[0] + expect(emitPayloadSpy.mock.calls.length).toBe(1) + const payload = emitPayloadSpy.mock.calls[0][0] expect(payload.profile.session).toEqual({ id: 'session-id-1' }) expect(payload.trace.stacks).toEqual(mockedRumProfilerTrace.stacks) expect(payload.trace.samples).toEqual(mockedRumProfilerTrace.samples) @@ -218,10 +220,10 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('running') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) // Assert that the profiler has collected data on pause. - expect(emitPayloadSpy.calls.count()).toBe(1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) // Change back to visible setVisibilityState('visible') @@ -238,12 +240,12 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('stopped') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 2) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 2) - expect(emitPayloadSpy.calls.count()).toBe(2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) // Check the emitPayloadSpy was called with the mocked trace - const payload = emitPayloadSpy.calls.argsFor(1)[0] + const payload = emitPayloadSpy.mock.calls[1][0] expect(payload.profile.session).toEqual({ id: 'session-id-1' }) expect(payload.trace.stacks).toEqual(mockedRumProfilerTrace.stacks) expect(payload.trace.samples).toEqual(mockedRumProfilerTrace.samples) @@ -305,22 +307,22 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) - const payloadOne = emitPayloadSpy.calls.argsFor(0)[0] - const payloadTwo = emitPayloadSpy.calls.argsFor(1)[0] + const payloadOne = emitPayloadSpy.mock.calls[0][0] + const payloadTwo = emitPayloadSpy.mock.calls[1][0] expect(payloadOne.profile.long_task?.id.length).toBe(2) expect(payloadOne.trace.longTasks).toEqual([ { id: 'long-task-id-2', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 100 as Duration, entryType: RumPerformanceEntryType.LONG_ANIMATION_FRAME, }, { id: 'long-task-id-1', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 50 as Duration, entryType: RumPerformanceEntryType.LONG_ANIMATION_FRAME, }, @@ -330,7 +332,7 @@ describe('profiler', () => { expect(payloadTwo.trace.longTasks).toEqual([ { id: 'long-task-id-3', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 100 as Duration, entryType: RumPerformanceEntryType.LONG_ANIMATION_FRAME, }, @@ -393,22 +395,22 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) - const payloadOne = emitPayloadSpy.calls.argsFor(0)[0] - const payloadTwo = emitPayloadSpy.calls.argsFor(1)[0] + const payloadOne = emitPayloadSpy.mock.calls[0][0] + const payloadTwo = emitPayloadSpy.mock.calls[1][0] expect(payloadOne.profile.action?.id.length).toBe(2) expect(payloadOne.trace.actions).toEqual([ { id: 'action-id-2', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 100 as Duration, label: 'action-label-2', }, { id: 'action-id-1', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 50 as Duration, label: 'action-label-1', }, @@ -418,7 +420,7 @@ describe('profiler', () => { expect(payloadTwo.trace.actions).toEqual([ { id: 'action-id-3', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 100 as Duration, label: 'action-label-3', }, @@ -484,22 +486,22 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) - const payloadOne = emitPayloadSpy.calls.argsFor(0)[0] - const payloadTwo = emitPayloadSpy.calls.argsFor(1)[0] + const payloadOne = emitPayloadSpy.mock.calls[0][0] + const payloadTwo = emitPayloadSpy.mock.calls[1][0] expect(payloadOne.profile.vital?.id.length).toBe(2) expect(payloadOne.trace.vitals).toEqual([ { id: 'vital-id-2', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 100 as Duration, label: 'vital-label-2', }, { id: 'vital-id-1', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 50 as Duration, label: 'vital-label-1', }, @@ -509,7 +511,7 @@ describe('profiler', () => { expect(payloadTwo.trace.vitals).toEqual([ { id: 'vital-id-3', - startClocks: jasmine.any(Object), + startClocks: expect.any(Object), duration: 100 as Duration, label: 'vital-label-3', }, @@ -559,21 +561,21 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(3) + expect(emitPayloadSpy.mock.calls.length).toBe(3) - const vitals1 = emitPayloadSpy.calls.argsFor(0)[0].trace.vitals as BrowserProfilerTrace['vitals'] - const vitals2 = emitPayloadSpy.calls.argsFor(1)[0].trace.vitals as BrowserProfilerTrace['vitals'] - const vitals3 = emitPayloadSpy.calls.argsFor(2)[0].trace.vitals as BrowserProfilerTrace['vitals'] + const vitals1 = emitPayloadSpy.mock.calls[0][0].trace.vitals as BrowserProfilerTrace['vitals'] + const vitals2 = emitPayloadSpy.mock.calls[1][0].trace.vitals as BrowserProfilerTrace['vitals'] + const vitals3 = emitPayloadSpy.mock.calls[2][0].trace.vitals as BrowserProfilerTrace['vitals'] // Profile 1: all three operations present, only op1 has a duration - expect(vitals1?.map((v) => v.id)).toEqual(jasmine.arrayContaining(['op-id-1', 'op-id-2', 'op-id-3'])) + expect(vitals1?.map((v) => v.id)).toEqual(expect.arrayContaining(['op-id-1', 'op-id-2', 'op-id-3'])) expect(vitals1?.find((v) => v.id === 'op-id-1')?.duration).toBe(30 as Duration) expect(vitals1?.find((v) => v.id === 'op-id-2')?.duration).toBeUndefined() expect(vitals1?.find((v) => v.id === 'op-id-3')?.duration).toBeUndefined() // Profile 2: op1 is gone (ended before profile 2 started), op2 and op3 present, only op2 has a duration expect(vitals2?.map((v) => v.id)).not.toContain('op-id-1') - expect(vitals2?.map((v) => v.id)).toEqual(jasmine.arrayContaining(['op-id-2', 'op-id-3'])) + expect(vitals2?.map((v) => v.id)).toEqual(expect.arrayContaining(['op-id-2', 'op-id-3'])) expect(vitals2?.find((v) => v.id === 'op-id-2')?.duration).toBe(140 as Duration) expect(vitals2?.find((v) => v.id === 'op-id-3')?.duration).toBeUndefined() @@ -625,9 +627,9 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('stopped') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) - const views = emitPayloadSpy.calls.argsFor(0)[0].trace.views + const views = emitPayloadSpy.mock.calls[0][0].trace.views expect(views.length).toBe(2) expect(views[0].viewId).toBe('view-user') @@ -678,12 +680,12 @@ describe('profiler', () => { await waitForBoolean(() => profiler.isPaused()) // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) // Assert that the profiler has collected data on pause. - expect(emitPayloadSpy.calls.count()).toBe(1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) - const payload = emitPayloadSpy.calls.argsFor(0)[0] + const payload = emitPayloadSpy.mock.calls[0][0] expect(payload.profile.session).toEqual({ id: 'session-id-1' }) expect(payload.trace.views).toEqual([ { @@ -713,11 +715,11 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('stopped') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 2) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 2) - expect(emitPayloadSpy.calls.count()).toBe(2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) - const payload2 = emitPayloadSpy.calls.argsFor(1)[0] + const payload2 = emitPayloadSpy.mock.calls[1][0] expect(payload2.profile.session).toEqual({ id: 'session-id-1' }) expect(payload2.trace.stacks).toEqual(mockedRumProfilerTrace.stacks) expect(payload2.trace.samples).toEqual(mockedRumProfilerTrace.samples) @@ -740,10 +742,10 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('stopped') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) // Verify that profiler collected data before stopping - expect(emitPayloadSpy.calls.count()).toBe(1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) }) it('should not restart profiling after session expiration when visibility changes', async () => { @@ -792,10 +794,10 @@ describe('profiler', () => { expect(profilingContextManager.get()?.status).toBe('stopped') // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) // Verify that profiler collected data before stopping - expect(emitPayloadSpy.calls.count()).toBe(1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) // Notify that the session has been renewed lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) @@ -811,10 +813,10 @@ describe('profiler', () => { expect(profiler.isStopped()).toBe(true) // Wait for data collection to complete (async fire-and-forget) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 2) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 2) // Should have collected data from both sessions (before expiration and after renewal) - expect(emitPayloadSpy.calls.count()).toBe(2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) }) it('should handle multiple session expiration and renewal cycles', async () => { @@ -832,8 +834,8 @@ describe('profiler', () => { expect(profiler.isStopped()).toBe(true) expect(profilingContextManager.get()?.status).toBe('stopped') - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) - expect(emitPayloadSpy.calls.count()).toBe(1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) await waitForBoolean(() => profiler.isRunning()) @@ -844,8 +846,8 @@ describe('profiler', () => { expect(profiler.isStopped()).toBe(true) expect(profilingContextManager.get()?.status).toBe('stopped') - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 2) - expect(emitPayloadSpy.calls.count()).toBe(2) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 2) + expect(emitPayloadSpy.mock.calls.length).toBe(2) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) await waitForBoolean(() => profiler.isRunning()) @@ -856,8 +858,8 @@ describe('profiler', () => { expect(profiler.isStopped()).toBe(true) // Should have collected data from: initial session + first renewal + second renewal = 3 profiles - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 3) - expect(emitPayloadSpy.calls.count()).toBe(3) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 3) + expect(emitPayloadSpy.mock.calls.length).toBe(3) }) it('should not restart profiling on session renewal if profiler was manually stopped', async () => { @@ -975,7 +977,7 @@ describe('profiler', () => { // Session expires testLifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - expect(profiler.isStopped()).toBeTrue() + expect(profiler.isStopped()).toBe(true) // Session renews with HIGH_HASH_UUID, which is not sampled at profilingSampleRate: 50 sessionManager.setId(HIGH_HASH_UUID) @@ -983,7 +985,7 @@ describe('profiler', () => { await new Promise((resolve) => setTimeout(resolve, 100)) - expect(profiler.isStopped()).toBeTrue() + expect(profiler.isStopped()).toBe(true) profiler.stop() }) @@ -1019,7 +1021,7 @@ describe('profiler', () => { // Simulate clock drift: Date.now() drifted 1000ms ahead of performance.now() // This mimics NTP sync or system clock adjustments in production - ;(performance.now as jasmine.Spy).and.callFake(() => Date.now() - timeOrigin - 1000) + vi.spyOn(performance, 'now').mockImplementation(() => Date.now() - timeOrigin - 1000) // Stop profiler β€” state changes synchronously, data collection is async via Promise profiler.stop() @@ -1029,8 +1031,8 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(1) - const trace = emitPayloadSpy.calls.argsFor(0)[0].trace + expect(emitPayloadSpy.mock.calls.length).toBe(1) + const trace = emitPayloadSpy.mock.calls[0][0].trace // Should only include the long task that occurred during the actual profiling window. // Without the fix (using timeStamp for duration), both long tasks would be included @@ -1050,9 +1052,9 @@ describe('profiler', () => { profiler.stop() - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) - expect(emitPayloadSpy.calls.argsFor(0)[0].profile.session).toEqual({ id: 'session-id-1' }) + expect(emitPayloadSpy.mock.calls[0][0].profile.session).toEqual({ id: 'session-id-1' }) }) describe('discard logic', () => { @@ -1070,7 +1072,7 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(0) + expect(emitPayloadSpy.mock.calls.length).toBe(0) }) it('should send profile when below duration threshold if a long task is present', async () => { @@ -1094,7 +1096,7 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) }) it('should send profile when duration threshold is met', async () => { @@ -1111,7 +1113,7 @@ describe('profiler', () => { await waitNextMicrotask() await waitNextMicrotask() - expect(emitPayloadSpy.calls.count()).toBe(1) + expect(emitPayloadSpy.mock.calls.length).toBe(1) }) }) @@ -1151,7 +1153,7 @@ describe('profiler', () => { describe('quota check', () => { it('should stop profiler and set quota_exceeded context when quota check returns quota_exceeded', async () => { - checkProfilingQuotaSpy.and.returnValue(Promise.resolve({ decision: 'quota_ko', reason: 'quota_exceeded' })) + checkProfilingQuotaSpy.mockReturnValue(Promise.resolve({ decision: 'quota_ko', reason: 'quota_exceeded' })) const { profiler, profilingContextManager } = setupProfiler() profiler.start() @@ -1162,11 +1164,11 @@ describe('profiler', () => { error_reason: undefined, quota_reason: 'quota_exceeded', } as any) - expect(emitPayloadSpy.calls.count()).toBe(0) // no data sent + expect(emitPayloadSpy.mock.calls.length).toBe(0) // no data sent }) it('should stop profiler and set org_disabled context when quota check returns org_disabled', async () => { - checkProfilingQuotaSpy.and.returnValue(Promise.resolve({ decision: 'quota_ko', reason: 'org_disabled' })) + checkProfilingQuotaSpy.mockReturnValue(Promise.resolve({ decision: 'quota_ko', reason: 'org_disabled' })) const { profiler, profilingContextManager } = setupProfiler() profiler.start() @@ -1177,11 +1179,13 @@ describe('profiler', () => { error_reason: undefined, quota_reason: 'org_disabled', } as any) - expect(emitPayloadSpy.calls.count()).toBe(0) // no data sent + expect(emitPayloadSpy.mock.calls.length).toBe(0) // no data sent }) it('should stop profiler and set unknown_reason context when quota check returns unknown_reason', async () => { - checkProfilingQuotaSpy.and.returnValue(Promise.resolve({ decision: 'quota_ko', reason: 'unknown_reason' })) + checkProfilingQuotaSpy.mockReturnValue( + Promise.resolve({ decision: 'quota_ko', reason: 'unknown_reason' } as unknown as QuotaResult) + ) const { profiler, profilingContextManager } = setupProfiler() profiler.start() @@ -1192,7 +1196,7 @@ describe('profiler', () => { error_reason: undefined, quota_reason: 'unknown_reason', } as any) - expect(emitPayloadSpy.calls.count()).toBe(0) // no data sent + expect(emitPayloadSpy.mock.calls.length).toBe(0) // no data sent }) it('should keep profiler running when quota check returns quota-ok', async () => { @@ -1214,7 +1218,7 @@ describe('profiler', () => { const hooks = createHooks() const profilingContextManager = startProfilingContext(hooks) const noSessionManager = createSessionManagerMock() - spyOn(noSessionManager, 'findTrackedSession').and.returnValue(undefined) + vi.spyOn(noSessionManager, 'findTrackedSession').mockReturnValue(undefined) const profilerNoSession = createRumProfiler( mockRumConfiguration({ profilingSampleRate: 100 }), new LifeCycle(), @@ -1235,8 +1239,8 @@ describe('profiler', () => { }) it('should discard quota-exceeded result when profiler was already stopped by user', async () => { - let resolveQuota!: (result: { decision: string; reason: string }) => void - checkProfilingQuotaSpy.and.callFake( + let resolveQuota!: (result: QuotaResult) => void + checkProfilingQuotaSpy.mockImplementation( () => new Promise((resolve) => { resolveQuota = resolve @@ -1259,8 +1263,8 @@ describe('profiler', () => { }) it('should discard quota-exceeded result when SESSION_EXPIRED fired before quota resolved', async () => { - let resolveQuota!: (result: { decision: string; reason: string }) => void - checkProfilingQuotaSpy.and.callFake( + let resolveQuota!: (result: QuotaResult) => void + checkProfilingQuotaSpy.mockImplementation( () => new Promise((resolve) => { resolveQuota = resolve @@ -1280,13 +1284,13 @@ describe('profiler', () => { expect(profilingContextManager.get()?.error_reason).toBeUndefined() // data IS sent (normal session-expired collection happens) - await waitForBoolean(() => emitPayloadSpy.calls.count() >= 1) - expect(emitPayloadSpy.calls.count()).toBeGreaterThanOrEqual(1) + await waitForBoolean(() => emitPayloadSpy.mock.calls.length >= 1) + expect(emitPayloadSpy.mock.calls.length).toBeGreaterThanOrEqual(1) }) it('should stop profiler and not resume when quota-exceeded resolves while paused', async () => { - let resolveQuota!: (result: { decision: string; reason: string }) => void - checkProfilingQuotaSpy.and.callFake( + let resolveQuota!: (result: QuotaResult) => void + checkProfilingQuotaSpy.mockImplementation( () => new Promise((resolve) => { resolveQuota = resolve @@ -1317,9 +1321,9 @@ describe('profiler', () => { }) it('should discard stale quota result when SESSION_RENEWED restarts the profiler', async () => { - let resolveOldQuota!: (result: { decision: string; reason: string }) => void + let resolveOldQuota!: (result: QuotaResult) => void let callCount = 0 - checkProfilingQuotaSpy.and.callFake(() => { + checkProfilingQuotaSpy.mockImplementation(() => { callCount++ if (callCount === 1) { return new Promise((resolve) => { @@ -1348,7 +1352,7 @@ describe('profiler', () => { it('should restart profiler and re-check quota on SESSION_RENEWED after quota_exceeded or org_disabled', async () => { let callCount = 0 - checkProfilingQuotaSpy.and.callFake(() => { + checkProfilingQuotaSpy.mockImplementation(() => { callCount++ return Promise.resolve( callCount === 1 diff --git a/packages/browser-rum/src/domain/profiling/longTaskHistory.spec.ts b/packages/browser-rum/src/domain/profiling/longTaskHistory.spec.ts index cc270868d8..a203a9787d 100644 --- a/packages/browser-rum/src/domain/profiling/longTaskHistory.spec.ts +++ b/packages/browser-rum/src/domain/profiling/longTaskHistory.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { type Duration, relativeToClocks, type RelativeTime } from '@datadog/js-core/time' import { LifeCycle, LifeCycleEventType, RumEventType, RumPerformanceEntryType } from '@datadog/browser-rum-core' import { createRawRumEvent } from '@datadog/browser-rum-core/test' diff --git a/packages/browser-rum/src/domain/profiling/profilingContext.spec.ts b/packages/browser-rum/src/domain/profiling/profilingContext.spec.ts index 22c988a2f5..133c3eadf5 100644 --- a/packages/browser-rum/src/domain/profiling/profilingContext.spec.ts +++ b/packages/browser-rum/src/domain/profiling/profilingContext.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { RumEventType, createHooks } from '@datadog/browser-rum-core' import type { RelativeTime } from '@datadog/js-core/time' import type { AssembleHookParams } from '@datadog/browser-rum-core/src/domain/hooks' @@ -19,7 +20,7 @@ describe('Profiling Context', () => { } as AssembleHookParams) expect(eventAttributes).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ _dd: { profiling: { status: 'running' }, }, diff --git a/packages/browser-rum/src/domain/profiling/quotaCheck.spec.ts b/packages/browser-rum/src/domain/profiling/quotaCheck.spec.ts index 0086bb6b0c..1562e93091 100644 --- a/packages/browser-rum/src/domain/profiling/quotaCheck.spec.ts +++ b/packages/browser-rum/src/domain/profiling/quotaCheck.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach } from 'vitest' import { mockClock, interceptRequests, NETWORK_ERROR_FETCH_MOCK } from '@datadog/browser-core/test' import { mockRumConfiguration } from '../../../../browser-rum-core/test' import { checkProfilingQuota } from './quotaCheck' diff --git a/packages/browser-rum/src/domain/profiling/transport/buildProfileEvent.spec.ts b/packages/browser-rum/src/domain/profiling/transport/buildProfileEvent.spec.ts index 128351c1e5..bdee865e9f 100644 --- a/packages/browser-rum/src/domain/profiling/transport/buildProfileEvent.spec.ts +++ b/packages/browser-rum/src/domain/profiling/transport/buildProfileEvent.spec.ts @@ -1,4 +1,6 @@ +import { describe, expect, it } from 'vitest' import { clocksOrigin } from '@datadog/js-core/time' + import { RumPerformanceEntryType } from '@datadog/browser-rum-core' import type { BrowserProfilerTrace, RumViewEntry } from '../../../types' import type { LongTaskContext } from '../longTaskHistory' diff --git a/packages/browser-rum/src/domain/profiling/transport/formDataEmitter.spec.ts b/packages/browser-rum/src/domain/profiling/transport/formDataEmitter.spec.ts index 15c0b5b508..3b63f69f00 100644 --- a/packages/browser-rum/src/domain/profiling/transport/formDataEmitter.spec.ts +++ b/packages/browser-rum/src/domain/profiling/transport/formDataEmitter.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it, beforeEach } from 'vitest' import { relativeNow, timeStampNow } from '@datadog/js-core/time' import { createIdentityEncoder } from '@datadog/browser-core' import { interceptRequests, DEFAULT_FETCH_MOCK, readFormDataRequest } from '@datadog/browser-core/test' diff --git a/packages/browser-rum/src/domain/profiling/transport/profilingBridge.spec.ts b/packages/browser-rum/src/domain/profiling/transport/profilingBridge.spec.ts index 8fb64bb444..2d457d9ad8 100644 --- a/packages/browser-rum/src/domain/profiling/transport/profilingBridge.spec.ts +++ b/packages/browser-rum/src/domain/profiling/transport/profilingBridge.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { BridgeCapability } from '@datadog/browser-core' import { mockEventBridge } from '@datadog/browser-core/test' import { mockedTrace } from '../test-utils/mockedTrace' @@ -7,7 +8,7 @@ import { createBridgeEmitter } from './profilingBridge' describe('createBridgeEmitter', () => { it('sends the payload through the bridge as a profile event', () => { const bridge = mockEventBridge({ capabilities: [BridgeCapability.RECORDS, BridgeCapability.PROFILES] }) - const sendSpy = spyOn(bridge, 'send') + const sendSpy = vi.spyOn(bridge, 'send').mockImplementation(() => undefined) const emit = createBridgeEmitter() const payload: ProfilingPayload = { @@ -28,8 +29,8 @@ describe('createBridgeEmitter', () => { emit(payload) - expect(sendSpy).toHaveBeenCalledOnceWith(jasmine.stringContaining('"eventType":"profile"')) - const sent = JSON.parse(sendSpy.calls.first().args[0]) + expect(sendSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('"eventType":"profile"')) + const sent = JSON.parse(sendSpy.mock.calls[0][0]) expect(sent.eventType).toBe('profile') expect(sent.event.profile).toEqual(payload.profile) expect(sent.event.trace).toEqual(payload.trace) diff --git a/packages/browser-rum/src/domain/profiling/utils/getCustomOrDefaultViewName.spec.ts b/packages/browser-rum/src/domain/profiling/utils/getCustomOrDefaultViewName.spec.ts index 0db13fa55c..d53906ee09 100644 --- a/packages/browser-rum/src/domain/profiling/utils/getCustomOrDefaultViewName.spec.ts +++ b/packages/browser-rum/src/domain/profiling/utils/getCustomOrDefaultViewName.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { getCustomOrDefaultViewName } from './getCustomOrDefaultViewName' describe('getCustomOrDefaultViewName', () => { diff --git a/packages/browser-rum/src/domain/profiling/utils/getDefaultViewName.spec.ts b/packages/browser-rum/src/domain/profiling/utils/getDefaultViewName.spec.ts index f8e312d352..b1c820c4fb 100644 --- a/packages/browser-rum/src/domain/profiling/utils/getDefaultViewName.spec.ts +++ b/packages/browser-rum/src/domain/profiling/utils/getDefaultViewName.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { getDefaultViewName } from './getDefaultViewName' // Replicating the tests from SimpleUrlGroupingProcessorTest.java diff --git a/packages/browser-rum/src/domain/profiling/vitalHistory.spec.ts b/packages/browser-rum/src/domain/profiling/vitalHistory.spec.ts index c6f58c77a7..e01bffe82c 100644 --- a/packages/browser-rum/src/domain/profiling/vitalHistory.spec.ts +++ b/packages/browser-rum/src/domain/profiling/vitalHistory.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach } from 'vitest' import { type Duration, relativeToClocks, type RelativeTime } from '@datadog/js-core/time' import { LifeCycle, LifeCycleEventType, RumEventType, VitalType } from '@datadog/browser-rum-core' import { createRawRumEvent } from '@datadog/browser-rum-core/test' diff --git a/packages/browser-rum/src/domain/record/internalApi.spec.ts b/packages/browser-rum/src/domain/record/internalApi.spec.ts index b07c813cfc..a6b3d4922f 100644 --- a/packages/browser-rum/src/domain/record/internalApi.spec.ts +++ b/packages/browser-rum/src/domain/record/internalApi.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { BrowserChangeRecord, BrowserFullSnapshotChangeRecord, BrowserRecord } from '../../types' import { ChangeType, RecordType, SnapshotFormat } from '../../types' import { appendElement } from '../../../../browser-rum-core/test' @@ -7,44 +8,45 @@ import { createChangeDecoder } from './serialization' describe('takeFullSnapshot', () => { it('should produce Meta, Focus, and FullSnapshot records', () => { expect(takeFullSnapshot()).toEqual( - jasmine.arrayContaining([ + expect.arrayContaining([ { data: { - height: jasmine.any(Number), + height: expect.any(Number), href: window.location.href, - width: jasmine.any(Number), + width: expect.any(Number), }, type: RecordType.Meta, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, { data: { has_focus: document.hasFocus(), }, type: RecordType.Focus, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, { - data: jasmine.any(Object), + data: expect.any(Object), format: SnapshotFormat.Change, type: RecordType.FullSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, ]) ) }) - it('should produce VisualViewport records when supported', () => { + it('should produce VisualViewport records when supported', (ctx) => { if (!window.visualViewport) { - pending('visualViewport not supported') + ctx.skip(true, 'VisualViewport API not supported') + return } expect(takeFullSnapshot()).toEqual( - jasmine.arrayContaining([ + expect.arrayContaining([ { - data: jasmine.any(Object), + data: expect.any(Object), type: RecordType.VisualViewport, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, ]) ) @@ -75,7 +77,7 @@ describe('takeNodeSnapshot', () => { type: RecordType.FullSnapshot, format: SnapshotFormat.Change, data: [[ChangeType.AddNode, [null, 'DIV'], [1, '#text', 'Hello '], [0, 'B'], [1, '#text', 'world']]], - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }) }) @@ -87,7 +89,7 @@ describe('takeNodeSnapshot', () => { type: RecordType.FullSnapshot, format: SnapshotFormat.Change, data: [[ChangeType.AddNode, [null, 'DIV'], [1, '#text', 'Hello'], [0, '#shadow-root'], [1, '#text', 'world']]], - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }) }) }) diff --git a/packages/browser-rum/src/domain/record/itemIds.spec.ts b/packages/browser-rum/src/domain/record/itemIds.spec.ts index 5303c903ad..515d916a92 100644 --- a/packages/browser-rum/src/domain/record/itemIds.spec.ts +++ b/packages/browser-rum/src/domain/record/itemIds.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { EventId, ItemIds, NodeId, StringId, StyleSheetId } from './itemIds' import { createEventIds, diff --git a/packages/browser-rum/src/domain/record/mutationBatch.spec.ts b/packages/browser-rum/src/domain/record/mutationBatch.spec.ts index d6489decef..0cc4c30059 100644 --- a/packages/browser-rum/src/domain/record/mutationBatch.spec.ts +++ b/packages/browser-rum/src/domain/record/mutationBatch.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { Clock, RequestIdleCallbackMock } from '@datadog/browser-core/test' import { mockClock, mockRequestIdleCallback } from '@datadog/browser-core/test' import type { RumMutationRecord } from '@datadog/browser-rum-core' @@ -5,14 +6,14 @@ import { MUTATION_PROCESS_MIN_DELAY, createMutationBatch } from './mutationBatch describe('createMutationBatch', () => { let mutationBatch: ReturnType - let processMutationBatchSpy: jasmine.Spy<(mutations: RumMutationRecord[]) => void> + let processMutationBatchSpy: Mock<(mutations: RumMutationRecord[]) => void> let clock: Clock let requestIdleCallbackMock: RequestIdleCallbackMock beforeEach(() => { clock = mockClock() requestIdleCallbackMock = mockRequestIdleCallback() - processMutationBatchSpy = jasmine.createSpy() + processMutationBatchSpy = vi.fn() mutationBatch = createMutationBatch(processMutationBatchSpy) }) @@ -28,7 +29,7 @@ describe('createMutationBatch', () => { expect(processMutationBatchSpy).not.toHaveBeenCalled() requestIdleCallbackMock.idle() clock.tick(MUTATION_PROCESS_MIN_DELAY) - expect(processMutationBatchSpy).toHaveBeenCalledWith([mutation]) + expect(processMutationBatchSpy).toHaveBeenCalledExactlyOnceWith([mutation]) }) it('calls the callback synchronously on flush', () => { @@ -36,7 +37,8 @@ describe('createMutationBatch', () => { mutationBatch.addMutations([mutation]) mutationBatch.flush() - expect(processMutationBatchSpy).toHaveBeenCalledOnceWith([mutation]) + expect(processMutationBatchSpy).toHaveBeenCalledTimes(1) + expect(processMutationBatchSpy).toHaveBeenCalledWith([mutation]) }) it('appends mutations to the batch when adding more mutations', () => { @@ -47,12 +49,14 @@ describe('createMutationBatch', () => { mutationBatch.addMutations([mutation2, mutation3]) mutationBatch.flush() - expect(processMutationBatchSpy).toHaveBeenCalledOnceWith([mutation1, mutation2, mutation3]) + expect(processMutationBatchSpy).toHaveBeenCalledTimes(1) + expect(processMutationBatchSpy).toHaveBeenCalledExactlyOnceWith([mutation1, mutation2, mutation3]) }) it('calls the callback on flush even if there is no pending mutation', () => { mutationBatch.flush() - expect(processMutationBatchSpy).toHaveBeenCalledOnceWith([]) + expect(processMutationBatchSpy).toHaveBeenCalledTimes(1) + expect(processMutationBatchSpy).toHaveBeenCalledExactlyOnceWith([]) }) }) diff --git a/packages/browser-rum/src/domain/record/record.spec.ts b/packages/browser-rum/src/domain/record/record.spec.ts index 3c30e044ba..81746ca16c 100644 --- a/packages/browser-rum/src/domain/record/record.spec.ts +++ b/packages/browser-rum/src/domain/record/record.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { DefaultPrivacyLevel, findLast, noop } from '@datadog/browser-core' import type { RumConfiguration, ViewCreatedEvent } from '@datadog/browser-rum-core' import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' @@ -23,11 +24,11 @@ import { createChangeDecoder } from './serialization' describe('record', () => { let recordApi: RecordAPI let lifeCycle: LifeCycle - let emitSpy: jasmine.Spy + let emitSpy: Mock const FAKE_VIEW_ID = '123' beforeEach(() => { - emitSpy = jasmine.createSpy() + emitSpy = vi.fn() registerCleanupTask(() => { recordApi?.stop() @@ -64,12 +65,12 @@ describe('record', () => { .map((record) => record.data) expect(styleSheetRuleData).toEqual([ - jasmine.objectContaining({ adds: [{ rule: 'body { background: #000; }', index: undefined }] }), - jasmine.objectContaining({ adds: [{ rule: 'body { background: #111; }', index: undefined }] }), - jasmine.objectContaining({ removes: [{ index: 0 }] }), - jasmine.objectContaining({ adds: [{ rule: 'body { color: #fff; }', index: undefined }] }), - jasmine.objectContaining({ removes: [{ index: 0 }] }), - jasmine.objectContaining({ adds: [{ rule: 'body { color: #ccc; }', index: undefined }] }), + expect.objectContaining({ adds: [{ rule: 'body { background: #000; }', index: undefined }] }), + expect.objectContaining({ adds: [{ rule: 'body { background: #111; }', index: undefined }] }), + expect.objectContaining({ removes: [{ index: 0 }] }), + expect.objectContaining({ adds: [{ rule: 'body { color: #fff; }', index: undefined }] }), + expect.objectContaining({ removes: [{ index: 0 }] }), + expect.objectContaining({ adds: [{ rule: 'body { color: #ccc; }', index: undefined }] }), ]) }) @@ -235,7 +236,7 @@ describe('record', () => { const shadowRoot = createShadow() appendElement('
', shadowRoot) startRecording() - spyOn(recordApi.shadowRootsController, 'removeShadowRoot') + vi.spyOn(recordApi.shadowRootsController, 'removeShadowRoot').mockImplementation(() => undefined) expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot()) expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(0) @@ -263,8 +264,8 @@ describe('record', () => { appendElement('
', host.shadowRoot!) startRecording() - spyOn(recordApi.shadowRootsController, 'removeShadowRoot') - expect(getEmittedRecordCount()).toBe(recordsPerFullSnapshot()) + vi.spyOn(recordApi.shadowRootsController, 'removeShadowRoot').mockImplementation(() => undefined) + expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot()) expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(0) parent.remove() @@ -303,7 +304,7 @@ describe('record', () => { input = appendElement('') as HTMLInputElement audio = appendElement('') as HTMLAudioElement startRecording() - emitSpy.calls.reset() + emitSpy.mockClear() }) it('move', () => { @@ -358,9 +359,10 @@ describe('record', () => { expect(getEmittedRecords()[0].type).toBe(RecordType.Focus) }) - it('visual viewport resize', () => { + it('visual viewport resize', (ctx) => { if (!window.visualViewport) { - pending('visualViewport not supported') + ctx.skip(true, 'VisualViewport API not supported') + return } visualViewport!.dispatchEvent(createNewEvent('resize')) @@ -394,14 +396,14 @@ describe('record', () => { } function getEmittedRecordCount(): number { - return emitSpy.calls.allArgs().length + return getEmittedRecords().length } function getEmittedRecords(): BrowserRecord[] { const changeDecoder = createChangeDecoder() const decodedRecords: BrowserRecord[] = [] - for (const [record] of emitSpy.calls.allArgs()) { + for (const [record] of emitSpy.mock.calls) { if ( record.type === RecordType.Change || (record.type === RecordType.FullSnapshot && record.format === SnapshotFormat.Change) @@ -425,7 +427,7 @@ export function getLastIncrementalSnapshotData record.type === RecordType.IncrementalSnapshot && record.data.source === source ) - expect(record).toBeTruthy(`Could not find IncrementalSnapshot/${source} in ${records.length} records`) + expect(record).toBeTruthy() return record!.data } diff --git a/packages/browser-rum/src/domain/record/serialization/changeEncoder.spec.ts b/packages/browser-rum/src/domain/record/serialization/changeEncoder.spec.ts index e3a4114856..dd4b2ea337 100644 --- a/packages/browser-rum/src/domain/record/serialization/changeEncoder.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/changeEncoder.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { ChangeType } from '../../../types' import type { StringId } from '../itemIds' import { createStringIds, StringIdConstants } from '../itemIds' @@ -40,7 +41,7 @@ describe('ChangeEncoder', () => { expect(changes).toEqual([ [ChangeType.AddString, 'Hello World'], - [ChangeType.Text, [0, 0 as StringId]], + [ChangeType.Text, [0, 0]], ]) }) @@ -52,7 +53,7 @@ describe('ChangeEncoder', () => { expect(changes).toEqual([ [ChangeType.AddString, 'foo', 'bar'], - [ChangeType.Text, [0, 0 as StringId], [1, 1 as StringId], [2, 0 as StringId]], + [ChangeType.Text, [0, 0], [1, 1], [2, 0]], ]) }) @@ -111,7 +112,7 @@ describe('ChangeEncoder', () => { expect(changes).toEqual([ [ChangeType.AddString, ''], - [ChangeType.Text, [0, 0 as StringId]], + [ChangeType.Text, [0, 0]], ]) }) @@ -125,7 +126,7 @@ describe('ChangeEncoder', () => { expect(changes).toEqual([ [ChangeType.AddString, 'new-string'], // Only the new string is added. - [ChangeType.Text, [0, preExistingId], [1, 1 as StringId]], + [ChangeType.Text, [0, preExistingId], [1, 1]], ]) }) @@ -137,7 +138,7 @@ describe('ChangeEncoder', () => { const changes = encoder.flush() // The second flush should not have an AddString change for 'persistent'. - expect(changes).toEqual([[ChangeType.Text, [1, 0 as StringId]]]) + expect(changes).toEqual([[ChangeType.Text, [1, 0]]]) }) }) diff --git a/packages/browser-rum/src/domain/record/serialization/insertionCursor.spec.ts b/packages/browser-rum/src/domain/record/serialization/insertionCursor.spec.ts index 8469dc5bbb..699d5e8383 100644 --- a/packages/browser-rum/src/domain/record/serialization/insertionCursor.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/insertionCursor.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { InsertionPoint } from '../../../types' import type { NodeId, NodeIds } from '../itemIds' import { createNodeIds } from '../itemIds' diff --git a/packages/browser-rum/src/domain/record/serialization/serializationStats.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializationStats.spec.ts index 9fdc1d5d9e..dab2173898 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializationStats.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializationStats.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { aggregateSerializationStats, createSerializationStats, updateSerializationStats } from './serializationStats' describe('serializationStats', () => { diff --git a/packages/browser-rum/src/domain/record/serialization/serializationUtils.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializationUtils.spec.ts index 46c2a86be9..6faaa48f51 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializationUtils.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializationUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { NodePrivacyLevel } from '@datadog/browser-rum-core' import { getElementInputValue, switchToAbsoluteUrl } from './serializationUtils' diff --git a/packages/browser-rum/src/domain/record/serialization/serializeAttribute.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializeAttribute.spec.ts index 0caaa2a799..13c6ffc307 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializeAttribute.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializeAttribute.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import type { RumConfiguration } from '@datadog/browser-rum-core' import { STABLE_ATTRIBUTES, diff --git a/packages/browser-rum/src/domain/record/serialization/serializeAttributes.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializeAttributes.spec.ts index ec339111fd..3fb996b69d 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializeAttributes.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializeAttributes.spec.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it } from 'vitest' import { isSafari } from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import { @@ -241,7 +242,7 @@ describe('serializeDOMAttributes', () => { const attributes = serializeDOMAttributes(element, privacyLevel, transaction) const actual = attributes[attribute.name] as boolean | string | undefined const expected = expectedValueForPrivacyLevel(testCase, element, attribute, privacyLevel) - expect(actual).withContext(`${testCase.html} for ${privacyLevel}`).toEqual(expected) + expect(actual, `${testCase.html} for ${privacyLevel}`).toEqual(expected) } } @@ -345,7 +346,7 @@ describe('serializeVirtualAttributes', () => { for (const privacyLevel of PRIVACY_LEVELS) { const actual = serializeVirtualAttributes(element, privacyLevel, transaction) const expected = privacyLevel === NodePrivacyLevel.HIDDEN ? {} : expectedWhenNotHidden - expect(actual).withContext(`${element.tagName} ${privacyLevel}`).toEqual(expected) + expect(actual, `${element.tagName} ${privacyLevel}`).toEqual(expected) after?.(privacyLevel) } } @@ -531,9 +532,14 @@ describe('getCssRulesString', () => { styleNode.sheet!.insertRule(`@import url("${CSS_FILE_URL}");`) // Simulates an accessible external stylesheet - spyOnProperty(styleNode.sheet!.cssRules[0] as CSSImportRule, 'styleSheet').and.returnValue({ - cssRules: [{ cssText: 'p { margin: 0; }' } as CSSRule] as unknown as CSSRuleList, - } as CSSStyleSheet) + // Use Object.defineProperty instead of vi.spyOn β€” native CSSImportRule getters + // throw "Illegal invocation" when proxied through vi.spyOn. + Object.defineProperty(styleNode.sheet!.cssRules[0], 'styleSheet', { + get: () => ({ + cssRules: [{ cssText: 'p { margin: 0; }' } as CSSRule] as unknown as CSSRuleList, + }), + configurable: true, + }) expect(getCssRulesString(styleNode.sheet)).toBe('p { margin: 0; }') }) @@ -542,11 +548,16 @@ describe('getCssRulesString', () => { styleNode.sheet!.insertRule(`@import url("${CSS_FILE_URL}");`) // Simulates an inaccessible external stylesheet - spyOnProperty(styleNode.sheet!.cssRules[0] as CSSImportRule, 'styleSheet').and.returnValue({ - get cssRules(): CSSRuleList { - throw new Error('Cannot access rules') - }, - } as CSSStyleSheet) + // Use Object.defineProperty instead of vi.spyOn β€” native CSSImportRule getters + // throw "Illegal invocation" when proxied through vi.spyOn. + Object.defineProperty(styleNode.sheet!.cssRules[0], 'styleSheet', { + get: () => ({ + get cssRules(): CSSRuleList { + throw new Error('Cannot access rules') + }, + }), + configurable: true, + }) expect(getCssRulesString(styleNode.sheet)).toBe(`@import url("${CSS_FILE_URL}");`) }) diff --git a/packages/browser-rum/src/domain/record/serialization/serializeNode.form.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializeNode.form.spec.ts index a2b83adbc9..48c1987c47 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializeNode.form.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializeNode.form.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import type { BrowserWindow } from '@datadog/browser-rum-core' import { PRIVACY_ATTR_NAME, @@ -11,17 +12,6 @@ import { ChangeType } from '../../../types' import { serializeHtml } from '../test/serializeHtml.specHelper' describe('serializeNode for form elements', () => { - let originalTimeout: number - - beforeAll(() => { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 - }) - - afterAll(() => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - }) - describe('', () => { it('serializes the element', async () => { const record = await serializeHtml('') @@ -689,15 +679,15 @@ describe('serializeNode for form elements', () => { select.options.selectedIndex = 1 expect(await serializeHtmlNode(select, NodePrivacyLevel.ALLOW, transaction)).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ attributes: { value: 'bar' }, childNodes: [ - jasmine.objectContaining({ + expect.objectContaining({ attributes: { value: 'foo', }, }), - jasmine.objectContaining({ + expect.objectContaining({ attributes: { value: 'bar', selected: '', diff --git a/packages/browser-rum/src/domain/record/serialization/serializeNode.node.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializeNode.node.spec.ts index 009aa497ea..0ca715a59b 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializeNode.node.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializeNode.node.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import { PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN, PRIVACY_ATTR_VALUE_MASK } from '@datadog/browser-rum-core' import { DefaultPrivacyLevel } from '@datadog/browser-core' import type { BrowserChangeRecord, BrowserFullSnapshotChangeRecord } from '../../../types' @@ -8,17 +9,6 @@ import { serializeHtml } from '../test/serializeHtml.specHelper' import { SerializationKind } from './serializationTransaction' describe('serializeNode for DOM nodes', () => { - let originalTimeout: number - - beforeAll(() => { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 - }) - - afterAll(() => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - }) - describe('for #cdata-section nodes', () => { it('serializes the node', async () => { const record = await serializeHtml('
', { @@ -301,7 +291,7 @@ describe('serializeNode for DOM nodes', () => { `) expect(record?.data).toEqual([ [ChangeType.AddNode, [null, 'DIV', ['data-dd-privacy', 'hidden']]], - [ChangeType.Size, [0, jasmine.any(Number), jasmine.any(Number)]], + [ChangeType.Size, [0, expect.any(Number), expect.any(Number)]], ]) }) }) diff --git a/packages/browser-rum/src/domain/record/serialization/serializeNode.snapshot.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializeNode.snapshot.spec.ts index beabf53ec4..58cb03787d 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializeNode.snapshot.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializeNode.snapshot.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import type { BrowserWindow } from '@datadog/browser-rum-core' import { PRIVACY_ATTR_NAME, @@ -13,17 +14,6 @@ import { ChangeType, PlaybackState } from '../../../types' import { serializeHtml } from '../test/serializeHtml.specHelper' describe('serializeNode for snapshotted documents', () => { - let originalTimeout: number - - beforeAll(() => { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 - }) - - afterAll(() => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - }) - describe('for a simple document', () => { const GREEN_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEUA/wA0XsCoAAAADElEQVR4nGNgIA0AAAAwAAEWiZrRAAAAAElFTkSuQmCC' @@ -162,7 +152,7 @@ describe('serializeNode for snapshotted documents', () => { [1, '#doctype', 'html', '', ''], [0, 'HTML', [PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN]], ], - [ChangeType.Size, [2, jasmine.any(Number), jasmine.any(Number)]], + [ChangeType.Size, [2, expect.any(Number), expect.any(Number)]], [ChangeType.ScrollPosition, [0, 0, 0]], ]) }) diff --git a/packages/browser-rum/src/domain/record/serialization/serializeNode.stylesheet.spec.ts b/packages/browser-rum/src/domain/record/serialization/serializeNode.stylesheet.spec.ts index 20aefc7225..053b7da99a 100644 --- a/packages/browser-rum/src/domain/record/serialization/serializeNode.stylesheet.spec.ts +++ b/packages/browser-rum/src/domain/record/serialization/serializeNode.stylesheet.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, type TestContext } from 'vitest' import { isAdoptedStyleSheetsSupported } from '@datadog/browser-core/test' import { ChangeType } from '../../../types' import type { RecordingScope } from '../recordingScope' @@ -5,17 +6,6 @@ import { serializeHtml } from '../test/serializeHtml.specHelper' import type { SerializationStats } from './serializationStats' describe('serializeNode for stylesheets', () => { - let originalTimeout: number - - beforeAll(() => { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 - }) - - afterAll(() => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - }) - const css = 'div { color: green; }' const dynamicCss = 'span { color: red; }' @@ -25,7 +15,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 0, max: 0, sum: 0 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -44,7 +34,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 2, max: 21, sum: 42 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, } @@ -61,7 +51,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 1, max: 21, sum: 21 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -80,7 +70,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 1, max: 20, sum: 20 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -99,7 +89,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 1, max: 41, sum: 41 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -121,7 +111,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 1, max: 20, sum: 20 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -163,7 +153,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 1, max: 21, sum: 21 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -179,7 +169,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 0, max: 0, sum: 0 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -209,7 +199,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 0, max: 0, sum: 0 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, }) @@ -220,9 +210,10 @@ describe('serializeNode for stylesheets', () => { }) describe('for documents with adopted stylesheets', () => { - it('serializes the stylesheet', async () => { + it('serializes the stylesheet', async (ctx: TestContext) => { if (!isAdoptedStyleSheetsSupported()) { - pending('No adoptedStyleSheets support.') + ctx.skip(true, 'No adoptedStyleSheets support.') + return } const record = await serializeHtml( @@ -242,7 +233,7 @@ describe('serializeNode for stylesheets', () => { after(_target: Node, _scope: RecordingScope, stats: SerializationStats): void { expect(stats).toEqual({ cssText: { count: 1, max: 21, sum: 21 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }, } diff --git a/packages/browser-rum/src/domain/record/startFullSnapshots.spec.ts b/packages/browser-rum/src/domain/record/startFullSnapshots.spec.ts index a863accff2..4e513c614a 100644 --- a/packages/browser-rum/src/domain/record/startFullSnapshots.spec.ts +++ b/packages/browser-rum/src/domain/record/startFullSnapshots.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { ViewCreatedEvent } from '@datadog/browser-rum-core' import type { TimeStamp } from '@datadog/js-core/time' import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' @@ -11,13 +12,13 @@ import { createRecordingScopeForTesting } from './test/recordingScope.specHelper describe('startFullSnapshots', () => { const viewStartClock = { relative: 1, timeStamp: 1 as TimeStamp } let lifeCycle: LifeCycle - let emitRecordCallback: jasmine.Spy - let emitStatsCallback: jasmine.Spy + let emitRecordCallback: Mock + let emitStatsCallback: Mock beforeEach(() => { lifeCycle = new LifeCycle() - emitRecordCallback = jasmine.createSpy() - emitStatsCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() + emitStatsCallback = vi.fn() appendElement('', document.head) @@ -30,7 +31,7 @@ describe('startFullSnapshots', () => { }) it('takes a full snapshot when the view changes', () => { - emitRecordCallback.calls.reset() + emitRecordCallback.mockClear() lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, { startClocks: viewStartClock, @@ -40,66 +41,67 @@ describe('startFullSnapshots', () => { }) it('full snapshot related records should have the view change date', () => { - emitRecordCallback.calls.reset() + emitRecordCallback.mockClear() lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, { startClocks: viewStartClock, } as Partial as any) - const records = emitRecordCallback.calls.allArgs().map((args) => args[0]) + const records = emitRecordCallback.mock.calls.map((args) => args[0]) expect(records[0].timestamp).toEqual(1) expect(records[1].timestamp).toEqual(1) expect(records[2].timestamp).toEqual(1) }) it('full snapshot records should contain Meta, Focus, FullSnapshot', () => { - const records = emitRecordCallback.calls.allArgs().map((args) => args[0]) + const records = emitRecordCallback.mock.calls.map((args) => args[0]) expect(records).toEqual( - jasmine.arrayContaining([ + expect.arrayContaining([ { data: { - height: jasmine.any(Number), + height: expect.any(Number), href: window.location.href, - width: jasmine.any(Number), + width: expect.any(Number), }, type: RecordType.Meta, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, { data: { has_focus: document.hasFocus(), }, type: RecordType.Focus, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, { - data: jasmine.any(Array), + data: expect.any(Array), type: RecordType.FullSnapshot, format: SnapshotFormat.Change, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }, ]) ) }) - it('full snapshot records should contain visualViewport when supported', () => { + it('full snapshot records should contain visualViewport when supported', (ctx) => { if (!window.visualViewport) { - pending('visualViewport not supported') + ctx.skip(true, 'VisualViewport API not supported') + return } - const record = emitRecordCallback.calls.mostRecent().args[0] + const record = emitRecordCallback.mock.lastCall![0] expect(record).toEqual({ - data: jasmine.any(Object), + data: expect.any(Object), type: RecordType.VisualViewport, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }) }) it('full snapshot records should be emitted with serialization stats', () => { - expect(emitStatsCallback.calls.mostRecent().args[0]).toEqual({ + expect(emitStatsCallback.mock.lastCall![0]).toEqual({ cssText: { count: 1, max: 21, sum: 21 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackFocus.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackFocus.spec.ts index 381c30bfc7..1f05570179 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackFocus.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackFocus.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { RecordType } from '../../../types' import type { EmitRecordCallback } from '../record.types' @@ -7,10 +8,10 @@ import type { Tracker } from './tracker.types' describe('trackFocus', () => { let focusTracker: Tracker - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock beforeEach(() => { - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() focusTracker = trackFocus(emitRecordCallback) registerCleanupTask(() => { focusTracker.stop() @@ -18,24 +19,24 @@ describe('trackFocus', () => { }) it('collects focus', () => { - spyOn(document, 'hasFocus').and.returnValue(true) + vi.spyOn(document, 'hasFocus').mockReturnValue(true) window.dispatchEvent(createNewEvent('focus')) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ data: { has_focus: true }, type: RecordType.Focus, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }) }) it('collects blur', () => { - spyOn(document, 'hasFocus').and.returnValue(false) + vi.spyOn(document, 'hasFocus').mockReturnValue(false) window.dispatchEvent(createNewEvent('blur')) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ data: { has_focus: false }, type: RecordType.Focus, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), }) }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackInput.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackInput.spec.ts index 23dbb925e2..7e5d6641fa 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackInput.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackInput.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { DefaultPrivacyLevel } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { createNewEvent, mockClock, registerCleanupTask } from '@datadog/browser-core/test' @@ -14,7 +15,7 @@ import type { Tracker } from './tracker.types' describe('trackInput', () => { let inputTracker: Tracker - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock let input: HTMLInputElement let clock: Clock | undefined let scope: RecordingScope @@ -22,7 +23,7 @@ describe('trackInput', () => { beforeEach(() => { input = appendElement('
') as HTMLInputElement - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) @@ -32,7 +33,7 @@ describe('trackInput', () => { }) function getLatestInputPayload(): InputData & { text?: string } { - const latestRecord = emitRecordCallback.calls.mostRecent()?.args[0] as BrowserIncrementalSnapshotRecord + const latestRecord = emitRecordCallback.mock.lastCall?.[0] as BrowserIncrementalSnapshotRecord return latestRecord.data as InputData } @@ -40,13 +41,14 @@ describe('trackInput', () => { inputTracker = trackInput(document, emitRecordCallback, scope) dispatchInputEvent('foo') - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.Input, text: 'foo', - id: jasmine.any(Number) as unknown as number, + id: expect.any(Number), }, }) }) @@ -58,13 +60,14 @@ describe('trackInput', () => { clock.tick(0) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.Input, text: 'foo', - id: jasmine.any(Number) as unknown as number, + id: expect.any(Number), }, }) }) @@ -97,13 +100,14 @@ describe('trackInput', () => { inputTracker = trackInput(document, emitRecordCallback, scope) dispatchInputEventWithInShadowDom('foo') - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.Input, text: 'foo', - id: jasmine.any(Number) as unknown as number, + id: expect.any(Number), }, }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackMediaInteraction.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackMediaInteraction.spec.ts index 8fc6507630..b11a0736cf 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackMediaInteraction.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackMediaInteraction.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { appendElement } from '../../../../../browser-rum-core/test' import { IncrementalSource, MediaInteractionType, RecordType } from '../../../types' @@ -9,7 +10,7 @@ import type { Tracker } from './tracker.types' describe('trackMediaInteraction', () => { let mediaInteractionTracker: Tracker - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock let audio: HTMLAudioElement beforeEach(() => { @@ -18,7 +19,7 @@ describe('trackMediaInteraction', () => { const scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() mediaInteractionTracker = trackMediaInteraction(emitRecordCallback, scope) registerCleanupTask(() => { mediaInteractionTracker.stop() @@ -28,12 +29,13 @@ describe('trackMediaInteraction', () => { it('collects play interactions', () => { audio.dispatchEvent(createNewEvent('play', { target: audio })) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.MediaInteraction, - id: jasmine.any(Number) as unknown as number, + id: expect.any(Number), type: MediaInteractionType.Play, }, }) @@ -42,12 +44,13 @@ describe('trackMediaInteraction', () => { it('collects pause interactions', () => { audio.dispatchEvent(createNewEvent('pause', { target: audio })) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.MediaInteraction, - id: jasmine.any(Number) as unknown as number, + id: expect.any(Number), type: MediaInteractionType.Pause, }, }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackMouseInteraction.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackMouseInteraction.spec.ts index 02a58d4c60..84c319cf79 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackMouseInteraction.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackMouseInteraction.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { DOM_EVENT } from '@datadog/browser-core' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { appendElement } from '../../../../../browser-rum-core/test' @@ -10,7 +11,7 @@ import { trackMouseInteraction } from './trackMouseInteraction' import type { Tracker } from './tracker.types' describe('trackMouseInteraction', () => { - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock let mouseInteractionTracker: Tracker let scope: RecordingScope let a: HTMLAnchorElement @@ -22,7 +23,7 @@ describe('trackMouseInteraction', () => { scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() mouseInteractionTracker = trackMouseInteraction(emitRecordCallback, scope) registerCleanupTask(() => { mouseInteractionTracker.stop() @@ -33,15 +34,15 @@ describe('trackMouseInteraction', () => { a.dispatchEvent(createNewEvent(DOM_EVENT.CLICK, { clientX: 0, clientY: 0 })) expect(emitRecordCallback).toHaveBeenCalledWith({ - id: jasmine.any(Number), + id: expect.any(Number), type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.MouseInteraction, type: MouseInteractionType.Click, - id: jasmine.any(Number), - x: jasmine.any(Number), - y: jasmine.any(Number), + id: expect.any(Number), + x: expect.any(Number), + y: expect.any(Number), }, }) }) @@ -53,13 +54,13 @@ describe('trackMouseInteraction', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ id: scope.eventIds.getOrInsert(pointerupEvent), type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.MouseInteraction, type: MouseInteractionType.MouseUp, - id: jasmine.any(Number), - x: jasmine.any(Number), - y: jasmine.any(Number), + id: expect.any(Number), + x: expect.any(Number), + y: expect.any(Number), }, }) }) @@ -75,13 +76,13 @@ describe('trackMouseInteraction', () => { a.dispatchEvent(createNewEvent(DOM_EVENT.BLUR)) expect(emitRecordCallback).toHaveBeenCalledWith({ - id: jasmine.any(Number), + id: expect.any(Number), type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.MouseInteraction, type: MouseInteractionType.Blur, - id: jasmine.any(Number), + id: expect.any(Number), }, }) }) @@ -90,9 +91,10 @@ describe('trackMouseInteraction', () => { describe('forced layout issue', () => { let coordinatesComputed: boolean - beforeEach(() => { + beforeEach((ctx) => { if (!window.visualViewport) { - pending('no visualViewport') + ctx.skip(true, 'VisualViewport API not supported') + return } coordinatesComputed = false @@ -115,12 +117,12 @@ describe('trackMouseInteraction', () => { it('should compute x/y coordinates for click record', () => { a.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) - expect(coordinatesComputed).toBeTrue() + expect(coordinatesComputed).toBe(true) }) it('should not compute x/y coordinates for blur record', () => { a.dispatchEvent(createNewEvent(DOM_EVENT.BLUR)) - expect(coordinatesComputed).toBeFalse() + expect(coordinatesComputed).toBe(false) }) }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackMove.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackMove.spec.ts index bbb626330d..855b1e1bae 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackMove.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackMove.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { IncrementalSource, RecordType } from '../../../types' import type { EmitRecordCallback } from '../record.types' @@ -7,14 +8,14 @@ import { trackMove } from './trackMove' import type { Tracker } from './tracker.types' describe('trackMove', () => { - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock let moveTracker: Tracker beforeEach(() => { const scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() moveTracker = trackMove(emitRecordCallback, scope) registerCleanupTask(() => { moveTracker.stop() @@ -27,14 +28,14 @@ describe('trackMove', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.MouseMove, positions: [ { x: 1, y: 2, - id: jasmine.any(Number), + id: expect.any(Number), timeOffset: 0, }, ], @@ -48,14 +49,14 @@ describe('trackMove', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.TouchMove, positions: [ { x: 1, y: 2, - id: jasmine.any(Number), + id: expect.any(Number), timeOffset: 0, }, ], diff --git a/packages/browser-rum/src/domain/record/trackers/trackMutation.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackMutation.spec.ts index f458ab7a5a..d408124706 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackMutation.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackMutation.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { DefaultPrivacyLevel } from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import type { RumConfiguration } from '@datadog/browser-rum-core' @@ -138,7 +139,7 @@ describe('trackMutation', () => { ]) expect(stats).toEqual({ cssText: { count: 1, max: 21, sum: 21 }, - serializationDuration: jasmine.anything(), + serializationDuration: expect.anything(), }) }) @@ -335,13 +336,13 @@ describe('trackMutation', () => { }) describe('for shadow DOM', () => { - let addShadowRootSpy: jasmine.Spy - let removeShadowRootSpy: jasmine.Spy + let addShadowRootSpy: Mock + let removeShadowRootSpy: Mock let scope: RecordingScope beforeEach(() => { - addShadowRootSpy = jasmine.createSpy() - removeShadowRootSpy = jasmine.createSpy() + addShadowRootSpy = vi.fn() + removeShadowRootSpy = vi.fn() scope = createRecordingScopeForTesting({ addShadowRoot: addShadowRootSpy, removeShadowRoot: removeShadowRootSpy, @@ -363,7 +364,8 @@ describe('trackMutation', () => { ) expect(mutation?.data).toEqual([[ChangeType.AddNode, [1, 'DIV'], [1, '#shadow-root'], [1, 'SPAN']]]) - expect(addShadowRootSpy).toHaveBeenCalledOnceWith(shadowRoot, jasmine.anything()) + expect(addShadowRootSpy).toHaveBeenCalledTimes(1) + expect(addShadowRootSpy).toHaveBeenCalledExactlyOnceWith(shadowRoot, expect.anything()) expect(removeShadowRootSpy).not.toHaveBeenCalled() }) @@ -386,7 +388,8 @@ describe('trackMutation', () => { expect(mutation?.data).toEqual([[ChangeType.RemoveNode, 1]]) expect(addShadowRootSpy).toHaveBeenCalledTimes(1) - expect(removeShadowRootSpy).toHaveBeenCalledOnceWith(shadowRoot) + expect(removeShadowRootSpy).toHaveBeenCalledTimes(1) + expect(removeShadowRootSpy).toHaveBeenCalledExactlyOnceWith(shadowRoot) }) it('should call removeShadowRoot when parent of host is removed', async () => { @@ -408,7 +411,8 @@ describe('trackMutation', () => { expect(mutation?.data).toEqual([[ChangeType.RemoveNode, 1]]) expect(addShadowRootSpy).toHaveBeenCalledTimes(1) - expect(removeShadowRootSpy).toHaveBeenCalledOnceWith(shadowRoot) + expect(removeShadowRootSpy).toHaveBeenCalledTimes(1) + expect(removeShadowRootSpy).toHaveBeenCalledExactlyOnceWith(shadowRoot) }) it('should call removeShadowRoot when removing a host containing other hosts in its children', async () => { @@ -435,8 +439,11 @@ describe('trackMutation', () => { expect(mutation?.data).toEqual([[ChangeType.RemoveNode, 1]]) expect(addShadowRootSpy).toHaveBeenCalledTimes(2) expect(removeShadowRootSpy).toHaveBeenCalledTimes(2) - expect(removeShadowRootSpy.calls.argsFor(0)[0]).toBe(parentShadowRoot) - expect(removeShadowRootSpy.calls.argsFor(1)[0]).toBe(childShadowRoot) + // Note: `toHaveBeenCalledWith` does not assert strict equality, we need to actually + // retrieve the argument and using `toBe` to make sure the spy has been called with both + // shadow roots. + expect(removeShadowRootSpy.mock.calls[0][0]).toBe(parentShadowRoot) + expect(removeShadowRootSpy.mock.calls[1][0]).toBe(childShadowRoot) }) }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackScroll.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackScroll.spec.ts index 04c2b7fa81..b34d1ed38b 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackScroll.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackScroll.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { appendElement } from '../../../../../browser-rum-core/test' import { IncrementalSource, RecordType } from '../../../types' @@ -9,7 +10,7 @@ import type { Tracker } from './tracker.types' describe('trackScroll', () => { let scrollTracker: Tracker - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock let div: HTMLDivElement beforeEach(() => { @@ -18,7 +19,7 @@ describe('trackScroll', () => { const scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() scrollTracker = trackScroll(document, emitRecordCallback, scope) registerCleanupTask(() => { scrollTracker.stop() @@ -28,14 +29,15 @@ describe('trackScroll', () => { it('collects scrolls', () => { div.dispatchEvent(createNewEvent('scroll', { target: div })) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { source: IncrementalSource.Scroll, - id: jasmine.any(Number), - x: jasmine.any(Number), - y: jasmine.any(Number), + id: expect.any(Number), + x: expect.any(Number), + y: expect.any(Number), }, }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackStyleSheet.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackStyleSheet.spec.ts index aeb0a7ea07..5ada889251 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackStyleSheet.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackStyleSheet.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { isFirefox, registerCleanupTask } from '@datadog/browser-core/test' import { IncrementalSource, RecordType } from '../../../types' import type { EmitRecordCallback } from '../record.types' @@ -10,7 +11,7 @@ import type { Tracker } from './tracker.types' describe('trackStyleSheet', () => { let scope: RecordingScope let styleSheetTracker: Tracker - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock let styleElement: HTMLStyleElement let styleSheet: CSSStyleSheet const styleRule = '.selector-1 { color: #fff }' @@ -20,7 +21,7 @@ describe('trackStyleSheet', () => { document.head.appendChild(styleElement) styleSheet = styleElement.sheet! - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) @@ -38,11 +39,11 @@ describe('trackStyleSheet', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - id: jasmine.any(Number), + id: expect.any(Number), source: IncrementalSource.StyleSheetRule, - adds: [jasmine.objectContaining({ index: undefined })], + adds: [expect.objectContaining({ index: undefined })], }, }) }) @@ -55,11 +56,11 @@ describe('trackStyleSheet', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - id: jasmine.any(Number), + id: expect.any(Number), source: IncrementalSource.StyleSheetRule, - adds: [jasmine.objectContaining({ index })], + adds: [expect.objectContaining({ index })], }, }) }) @@ -75,11 +76,11 @@ describe('trackStyleSheet', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - id: jasmine.any(Number), + id: expect.any(Number), source: IncrementalSource.StyleSheetRule, - removes: [jasmine.objectContaining({ index })], + removes: [expect.objectContaining({ index })], }, }) }) @@ -98,18 +99,19 @@ describe('trackStyleSheet', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - id: jasmine.any(Number), + id: expect.any(Number), source: IncrementalSource.StyleSheetRule, - adds: [jasmine.objectContaining({ index: [1, 0, 1] })], + adds: [expect.objectContaining({ index: [1, 0, 1] })], }, }) }) - it('should not create record when inserting into a detached CSSGroupingRule', () => { + it('should not create record when inserting into a detached CSSGroupingRule', (ctx) => { if (isFirefox()) { - pending('Firefox does not support inserting rules in detached group') + ctx.skip(true, 'not supported in Firefox') + return } styleSheet.insertRule('@media cond-2 { @media cond-1 { .nest-1 { color: #ccc } } }') @@ -136,18 +138,19 @@ describe('trackStyleSheet', () => { expect(emitRecordCallback).toHaveBeenCalledWith({ type: RecordType.IncrementalSnapshot, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - id: jasmine.any(Number), + id: expect.any(Number), source: IncrementalSource.StyleSheetRule, - removes: [jasmine.objectContaining({ index: [1, 0, 0] })], + removes: [expect.objectContaining({ index: [1, 0, 0] })], }, }) }) - it('should not create record when removing from a detached CSSGroupingRule', () => { + it('should not create record when removing from a detached CSSGroupingRule', (ctx) => { if (isFirefox()) { - pending('Firefox does not support inserting rules in detached group') + ctx.skip(true, 'not supported in Firefox') + return } styleSheet.insertRule('@media cond-2 { @media cond-1 { .nest-1 { color: #ccc } } }') diff --git a/packages/browser-rum/src/domain/record/trackers/trackViewEnd.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackViewEnd.spec.ts index 9af40caa06..b4ab9c57c8 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackViewEnd.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackViewEnd.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' import { RecordType } from '../../../types' import type { EmitRecordCallback } from '../record.types' @@ -6,14 +7,14 @@ import type { Tracker } from './tracker.types' describe('trackViewEnd', () => { let lifeCycle: LifeCycle - let emitRecordCallback: jasmine.Spy - let flushMutationsCallback: jasmine.Spy<() => void> + let emitRecordCallback: Mock + let flushMutationsCallback: Mock<() => void> let viewEndTracker: Tracker beforeEach(() => { lifeCycle = new LifeCycle() - emitRecordCallback = jasmine.createSpy() - flushMutationsCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() + flushMutationsCallback = vi.fn() viewEndTracker = trackViewEnd(lifeCycle, emitRecordCallback, flushMutationsCallback) }) @@ -26,7 +27,7 @@ describe('trackViewEnd', () => { expect(flushMutationsCallback).toHaveBeenCalledWith() expect(emitRecordCallback).toHaveBeenCalledWith({ - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), type: RecordType.ViewEnd, }) }) diff --git a/packages/browser-rum/src/domain/record/trackers/trackViewportResize.spec.ts b/packages/browser-rum/src/domain/record/trackers/trackViewportResize.spec.ts index 8ff97b2208..976eb01872 100644 --- a/packages/browser-rum/src/domain/record/trackers/trackViewportResize.spec.ts +++ b/packages/browser-rum/src/domain/record/trackers/trackViewportResize.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import { createNewEvent, registerCleanupTask } from '@datadog/browser-core/test' import { RecordType } from '../../../types' import type { EmitRecordCallback } from '../record.types' @@ -8,14 +9,15 @@ import type { Tracker } from './tracker.types' describe('trackViewportResize', () => { let viewportResizeTracker: Tracker - let emitRecordCallback: jasmine.Spy + let emitRecordCallback: Mock - beforeEach(() => { + beforeEach((ctx) => { if (!window.visualViewport) { - pending('visualViewport not supported') + ctx.skip(true, 'VisualViewport API not supported') + return } - emitRecordCallback = jasmine.createSpy() + emitRecordCallback = vi.fn() const scope = createRecordingScopeForTesting() takeFullSnapshotForTesting(scope) @@ -28,17 +30,18 @@ describe('trackViewportResize', () => { it('collects visual viewport on resize', () => { visualViewport!.dispatchEvent(createNewEvent('resize')) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.VisualViewport, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - scale: jasmine.any(Number), - offsetLeft: jasmine.any(Number), - offsetTop: jasmine.any(Number), - pageLeft: jasmine.any(Number), - pageTop: jasmine.any(Number), - height: jasmine.any(Number), - width: jasmine.any(Number), + scale: expect.any(Number), + offsetLeft: expect.any(Number), + offsetTop: expect.any(Number), + pageLeft: expect.any(Number), + pageTop: expect.any(Number), + height: expect.any(Number), + width: expect.any(Number), }, }) }) @@ -46,17 +49,18 @@ describe('trackViewportResize', () => { it('collects visual viewport on scroll', () => { visualViewport!.dispatchEvent(createNewEvent('scroll')) - expect(emitRecordCallback).toHaveBeenCalledOnceWith({ + expect(emitRecordCallback).toHaveBeenCalledTimes(1) + expect(emitRecordCallback).toHaveBeenCalledExactlyOnceWith({ type: RecordType.VisualViewport, - timestamp: jasmine.any(Number), + timestamp: expect.any(Number), data: { - scale: jasmine.any(Number), - offsetLeft: jasmine.any(Number), - offsetTop: jasmine.any(Number), - pageLeft: jasmine.any(Number), - pageTop: jasmine.any(Number), - height: jasmine.any(Number), - width: jasmine.any(Number), + scale: expect.any(Number), + offsetLeft: expect.any(Number), + offsetTop: expect.any(Number), + pageLeft: expect.any(Number), + pageTop: expect.any(Number), + height: expect.any(Number), + width: expect.any(Number), }, }) }) diff --git a/packages/browser-rum/src/domain/replayStats.spec.ts b/packages/browser-rum/src/domain/replayStats.spec.ts index 6d365b2a3f..ddf8481aa1 100644 --- a/packages/browser-rum/src/domain/replayStats.spec.ts +++ b/packages/browser-rum/src/domain/replayStats.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { getReplayStats, MAX_STATS_HISTORY, addSegment, addRecord, addWroteData } from './replayStats' describe('replayStats', () => { diff --git a/packages/browser-rum/src/domain/segmentCollection/buildReplayPayload.spec.ts b/packages/browser-rum/src/domain/segmentCollection/buildReplayPayload.spec.ts index d37d9e35ba..42505a79c6 100644 --- a/packages/browser-rum/src/domain/segmentCollection/buildReplayPayload.spec.ts +++ b/packages/browser-rum/src/domain/segmentCollection/buildReplayPayload.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { deflate } from 'pako' import type { Uint8ArrayBuffer } from '@datadog/browser-core' import type { BrowserSegment, BrowserSegmentMetadata } from '../../types' diff --git a/packages/browser-rum/src/domain/segmentCollection/segment.spec.ts b/packages/browser-rum/src/domain/segmentCollection/segment.spec.ts index 786afdb09c..383c525303 100644 --- a/packages/browser-rum/src/domain/segmentCollection/segment.spec.ts +++ b/packages/browser-rum/src/domain/segmentCollection/segment.spec.ts @@ -1,5 +1,6 @@ -import type { DeflateEncoder, Uint8ArrayBuffer } from '@datadog/browser-core' +import { vi, beforeEach, describe, expect, it } from 'vitest' import type { TimeStamp } from '@datadog/js-core/time' +import type { DeflateEncoder } from '@datadog/browser-core' import { noop, DeflateEncoderStreamId } from '@datadog/browser-core' import { setDebugMode } from '@datadog/js-core/util' import { registerCleanupTask } from '@datadog/browser-core/test' @@ -49,8 +50,8 @@ describe('Segment', () => { }) it('writes a segment', () => { - const addRecordCallbackSpy = jasmine.createSpy() - const flushCallbackSpy = jasmine.createSpy() + const addRecordCallbackSpy = vi.fn() + const flushCallbackSpy = vi.fn() const segment = createTestSegment() segment.addRecord(RECORD, addRecordCallbackSpy) @@ -63,7 +64,7 @@ describe('Segment', () => { expect(addRecordCallbackSpy).toHaveBeenCalledTimes(1) expect(flushCallbackSpy).toHaveBeenCalledTimes(1) - expect(parseSegment(flushCallbackSpy.calls.mostRecent().args[2].output)).toEqual({ + expect(parseSegment(flushCallbackSpy.mock.lastCall![2].output)).toEqual({ source: 'browser' as const, creation_reason: 'init' as const, end: 10, @@ -82,22 +83,24 @@ describe('Segment', () => { }) it('compressed bytes count is updated when a record is added', () => { - const addRecordCallbackSpy = jasmine.createSpy() + const addRecordCallbackSpy = vi.fn() const segment = createTestSegment() segment.addRecord(RECORD, addRecordCallbackSpy) worker.processAllMessages() - expect(addRecordCallbackSpy).toHaveBeenCalledOnceWith( + expect(addRecordCallbackSpy).toHaveBeenCalledTimes(1) + expect(addRecordCallbackSpy).toHaveBeenCalledExactlyOnceWith( ENCODED_SEGMENT_HEADER_BYTES_COUNT + ENCODED_RECORD_BYTES_COUNT ) }) it('calls the flush callback with metadata and encoder output as argument', () => { - const flushCallbackSpy = jasmine.createSpy() + const flushCallbackSpy = vi.fn() const segment = createTestSegment() segment.addRecord(RECORD, noop) segment.flush(flushCallbackSpy) worker.processAllMessages() - expect(flushCallbackSpy).toHaveBeenCalledOnceWith( + expect(flushCallbackSpy).toHaveBeenCalledTimes(1) + expect(flushCallbackSpy).toHaveBeenCalledExactlyOnceWith( { start: 10, end: 10, @@ -110,7 +113,7 @@ describe('Segment', () => { }, RECORD_STATS, { - output: jasmine.any(Uint8Array) as unknown as Uint8ArrayBuffer, + output: expect.any(Uint8Array), outputBytesCount: ENCODED_SEGMENT_HEADER_BYTES_COUNT + ENCODED_RECORD_BYTES_COUNT + @@ -123,7 +126,7 @@ describe('Segment', () => { }) it('resets the encoder when a segment is flushed', () => { - const flushCallbackSpy = jasmine.createSpy() + const flushCallbackSpy = vi.fn() const segment1 = createTestSegment({ creationReason: 'init' }) const stats1: SerializationStats = { @@ -144,10 +147,10 @@ describe('Segment', () => { segment2.flush(flushCallbackSpy) worker.processAllMessages() - expect(flushCallbackSpy.calls.argsFor(0)[1]).toEqual(stats1) - expect(parseSegment(flushCallbackSpy.calls.argsFor(0)[2].output).records.length).toBe(1) - expect(flushCallbackSpy.calls.argsFor(1)[1]).toEqual(stats2) - expect(parseSegment(flushCallbackSpy.calls.argsFor(1)[2].output).records.length).toBe(1) + expect(flushCallbackSpy.mock.calls[0][1]).toEqual(stats1) + expect(parseSegment(flushCallbackSpy.mock.calls[0][2].output).records.length).toBe(1) + expect(flushCallbackSpy.mock.calls[1][1]).toEqual(stats2) + expect(parseSegment(flushCallbackSpy.mock.calls[1][2].output).records.length).toBe(1) }) it('throws when trying to flush an empty segment', () => { @@ -302,7 +305,7 @@ describe('Segment', () => { createTestSegment() worker.processAllMessages() expect(getReplayStats('b')).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ segments_count: 1, records_count: 0, segments_total_raw_size: 0, @@ -316,7 +319,7 @@ describe('Segment', () => { segment.flush(noop) worker.processAllMessages() expect(getReplayStats('b')).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ segments_count: 1, segments_total_raw_size: ENCODED_SEGMENT_HEADER_BYTES_COUNT + ENCODED_RECORD_BYTES_COUNT + ENCODED_META_BYTES_COUNT, diff --git a/packages/browser-rum/src/domain/segmentCollection/segmentCollection.spec.ts b/packages/browser-rum/src/domain/segmentCollection/segmentCollection.spec.ts index 2fa18f5932..bdca699f87 100644 --- a/packages/browser-rum/src/domain/segmentCollection/segmentCollection.spec.ts +++ b/packages/browser-rum/src/domain/segmentCollection/segmentCollection.spec.ts @@ -1,3 +1,4 @@ +import { vi, afterEach, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { ClocksState } from '@datadog/js-core/time' import type { HttpRequest, HttpRequestEvent } from '@datadog/browser-core' import { DeflateEncoderStreamId, Observable, PageExitReason } from '@datadog/browser-core' @@ -41,8 +42,8 @@ describe('startSegmentCollection', () => { let worker: MockWorker let httpRequestSpy: { observable: Observable> - sendOnExit: jasmine.Spy['sendOnExit']> - send: jasmine.Spy['send']> + sendOnExit: Mock['sendOnExit']> + send: Mock['send']> } let addRecord: (record: BrowserRecord) => void let context: SegmentContext | undefined @@ -59,8 +60,8 @@ describe('startSegmentCollection', () => { lifeCycle.notify(LifeCycleEventType.PREPARE_URGENT_FLUSH, PageExitReason.UNLOADING) } - function readMostRecentMetadata(spy: jasmine.Spy) { - return readMetadataFromReplayPayload(spy.calls.mostRecent().args[0]) + function readMostRecentMetadata(spy: Mock<(...args: any[]) => any>) { + return readMetadataFromReplayPayload(spy.mock.lastCall![0]) } beforeEach(() => { @@ -68,8 +69,8 @@ describe('startSegmentCollection', () => { worker = new MockWorker() httpRequestSpy = { observable: new Observable>(), - sendOnExit: jasmine.createSpy(), - send: jasmine.createSpy(), + sendOnExit: vi.fn(), + send: vi.fn(), } context = CONTEXT ;({ stop: stopSegmentCollection, addRecord } = doStartSegmentCollection( @@ -117,14 +118,14 @@ describe('startSegmentCollection', () => { it('includes metadata for segment telemetry in the segment payload', () => { addRecordAndFlushSegment() - expect(httpRequestSpy.sendOnExit.calls.mostRecent().args[0]).toEqual({ - data: jasmine.anything(), - bytesCount: jasmine.anything(), - cssText: jasmine.anything(), - isFullSnapshot: jasmine.anything(), - rawSize: jasmine.anything(), - recordCount: jasmine.anything(), - serializationDuration: jasmine.anything(), + expect(httpRequestSpy.sendOnExit.mock.lastCall![0]).toEqual({ + data: expect.anything(), + bytesCount: expect.anything(), + cssText: expect.anything(), + isFullSnapshot: expect.anything(), + rawSize: expect.anything(), + recordCount: expect.anything(), + serializationDuration: expect.anything(), }) }) diff --git a/packages/browser-rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts b/packages/browser-rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts index a14d1c17ad..3271b9b3d8 100644 --- a/packages/browser-rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts +++ b/packages/browser-rum/src/domain/segmentCollection/startSegmentTelemetry.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { Telemetry, HttpRequestEvent, BandwidthStats } from '@datadog/browser-core' import { Observable } from '@datadog/browser-core' import type { MockTelemetry } from '@datadog/browser-core/test' @@ -55,7 +56,7 @@ describe('segmentTelemetry', () => { generateReplayRequest({ result, isFullSnapshot: true }) expect(await telemetry.getEvents()).toEqual([ - jasmine.objectContaining({ + expect.objectContaining({ type: 'log', status: 'debug', message: 'Segment network request metrics', @@ -100,7 +101,7 @@ describe('segmentTelemetry', () => { generateReplayRequest({ result, isFullSnapshot: false }) expect(await telemetry.getEvents()).toEqual([ - jasmine.objectContaining({ + expect.objectContaining({ type: 'log', status: 'debug', message: 'Segment network request metrics', diff --git a/packages/browser-rum/src/domain/startRecorderInitTelemetry.spec.ts b/packages/browser-rum/src/domain/startRecorderInitTelemetry.spec.ts index 5b27fe2daa..5feb819fb7 100644 --- a/packages/browser-rum/src/domain/startRecorderInitTelemetry.spec.ts +++ b/packages/browser-rum/src/domain/startRecorderInitTelemetry.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import type { Telemetry, RawTelemetryEvent } from '@datadog/browser-core' import { Observable } from '@datadog/browser-core' import type { MockTelemetry } from '@datadog/browser-core/test' @@ -142,10 +143,10 @@ function expectedRecorderInitTelemetry(overrides: Partial = message: 'Recorder init metrics', metrics: { forced: false, - loadRecorderModuleDuration: jasmine.any(Number), - recorderInitDuration: jasmine.any(Number), + loadRecorderModuleDuration: expect.any(Number), + recorderInitDuration: expect.any(Number), result: 'succeeded', - waitForDocReadyDuration: jasmine.any(Number), + waitForDocReadyDuration: expect.any(Number), ...overrides, }, } diff --git a/packages/browser-worker/src/boot/startWorker.spec.ts b/packages/browser-worker/src/boot/startWorker.spec.ts index 40ebcaa2c4..41a6b3cafc 100644 --- a/packages/browser-worker/src/boot/startWorker.spec.ts +++ b/packages/browser-worker/src/boot/startWorker.spec.ts @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, expect, it, type Mock } from 'vitest' import type { DeflateWorkerAction, DeflateWorkerResponse } from '@datadog/browser-core' import type { WorkerScope } from './startWorker' import { startWorker } from './startWorker' @@ -32,26 +33,27 @@ const FOO_BAZ_COMPRESSED_TRAILER = [3, 0, 8, 179, 2, 130] describe('startWorker', () => { let workerScope: { - addEventListener: jasmine.Spy - postMessage: jasmine.Spy + addEventListener: Mock + postMessage: Mock } beforeEach(() => { workerScope = { - addEventListener: jasmine.createSpy(), - postMessage: jasmine.createSpy(), + addEventListener: vi.fn(), + postMessage: vi.fn(), } startWorker(workerScope) }) function emulateAction(message: DeflateWorkerAction): DeflateWorkerResponse { - workerScope.postMessage.calls.reset() - workerScope.addEventListener.calls.allArgs().forEach(([eventName, listener]) => { + workerScope.postMessage.mockClear() + workerScope.addEventListener.mock.calls.forEach(([eventName, listener]) => { if (eventName === 'message') { listener({ data: message } as MessageEvent) } }) - return workerScope.postMessage.calls.mostRecent()?.args[0] + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return workerScope.postMessage.mock.lastCall?.[0] as any } it('buffers data and responds with the buffer deflated result when writing', () => { @@ -166,13 +168,17 @@ describe('startWorker', () => { it('reports an error when an unexpected exception occurs', () => { expect(emulateAction(null as any)).toEqual({ type: 'errored', - error: jasmine.any(TypeError), + error: expect.any(TypeError), streamId: undefined, }) }) - it('reports an error when an unexpected exception occurs while writing on a stream', () => { - spyOn(TextEncoder.prototype, 'encode').and.callFake(() => { + it('reports an error when an unexpected exception occurs while writing on a stream', (ctx) => { + if (!window.TextEncoder) { + ctx.skip(true, 'TextEncoder not supported') + return + } + vi.spyOn(TextEncoder.prototype, 'encode').mockImplementation(() => { throw new Error('Something went wrong!') }) expect( @@ -190,14 +196,14 @@ describe('startWorker', () => { }) it('use the string representation of the error when it fails to send it through postMessage', () => { - workerScope.postMessage.and.callFake((response) => { + workerScope.postMessage.mockImplementation((response) => { if (response.type === 'errored' && response.error instanceof Error) { throw new DOMException("Failed to execute 'postMessage' on 'WorkerScope'") } }) expect(emulateAction(null as any)).toEqual({ type: 'errored', - error: jasmine.stringContaining('TypeError'), + error: expect.stringContaining('TypeError'), streamId: undefined, }) }) diff --git a/packages/js-core/src/assembly/hook.spec.ts b/packages/js-core/src/assembly/hook.spec.ts index 5e3785aef6..d0e08aa730 100644 --- a/packages/js-core/src/assembly/hook.spec.ts +++ b/packages/js-core/src/assembly/hook.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import type { RelativeTime } from '../entries/time' import { DISCARDED, SKIPPED, createHook } from './hook' @@ -6,7 +7,7 @@ describe('createHook', () => { it('unregisters a callback', () => { const hook = createHook() - const callback = jasmine.createSpy().and.returnValue({ service: 'foo' }) + const callback = vi.fn().mockReturnValue({ service: 'foo' }) const { unregister } = hook.register(callback) unregister() @@ -19,8 +20,8 @@ describe('createHook', () => { it('combines results from multiple callbacks', () => { const hook = createHook() - const callback1 = jasmine.createSpy().and.returnValue({ type: 'action', service: 'foo' }) - const callback2 = jasmine.createSpy().and.returnValue({ type: 'action', version: 'bar' }) + const callback1 = vi.fn().mockReturnValue({ type: 'action', service: 'foo' }) + const callback2 = vi.fn().mockReturnValue({ type: 'action', version: 'bar' }) hook.register(callback1) hook.register(callback2) @@ -34,8 +35,8 @@ describe('createHook', () => { it('does not combine undefined results from callbacks', () => { const hook = createHook() - const callback1 = jasmine.createSpy().and.returnValue({ type: 'action', service: 'foo' }) - const callback2 = jasmine.createSpy().and.returnValue(undefined) + const callback1 = vi.fn().mockReturnValue({ type: 'action', service: 'foo' }) + const callback2 = vi.fn().mockReturnValue(undefined) hook.register(callback1) hook.register(callback2) @@ -47,9 +48,9 @@ describe('createHook', () => { it('returns DISCARDED if one callback returns DISCARDED', () => { const hook = createHook() - const callback1 = jasmine.createSpy().and.returnValue({ type: 'action', service: 'foo' }) - const callback2 = jasmine.createSpy().and.returnValue(DISCARDED) - const callback3 = jasmine.createSpy().and.returnValue({ type: 'action', version: 'bar' }) + const callback1 = vi.fn().mockReturnValue({ type: 'action', service: 'foo' }) + const callback2 = vi.fn().mockReturnValue(DISCARDED) + const callback3 = vi.fn().mockReturnValue({ type: 'action', version: 'bar' }) hook.register(callback1) hook.register(callback2) @@ -65,8 +66,8 @@ describe('createHook', () => { it('skips callbacks that return SKIPPED', () => { const hook = createHook() - const callback1 = jasmine.createSpy().and.returnValue(SKIPPED) - const callback2 = jasmine.createSpy().and.returnValue({ type: 'action', service: 'foo' }) + const callback1 = vi.fn().mockReturnValue(SKIPPED) + const callback2 = vi.fn().mockReturnValue({ type: 'action', service: 'foo' }) hook.register(callback1) hook.register(callback2) diff --git a/packages/js-core/src/entries/monitor.spec.ts b/packages/js-core/src/entries/monitor.spec.ts index 658c62a4a3..8cf8b863ae 100644 --- a/packages/js-core/src/entries/monitor.spec.ts +++ b/packages/js-core/src/entries/monitor.spec.ts @@ -1,26 +1,27 @@ +import { vi, describe, expect, it, beforeEach, afterEach, type Mock } from 'vitest' import { setDebugMode } from './util' import type { Display } from './util' import type { Monitor } from './monitor' import { createMonitor } from './monitor' describe('monitor', () => { - let onMonitorErrorCollectedSpy: jasmine.Spy<(error: unknown) => void> - let displayErrorSpy: jasmine.Spy + let onMonitorErrorCollectedSpy: Mock<(error: unknown) => void> + let displayErrorSpy: Mock let currentMonitor: Monitor function createFakeDisplay(): Display { - displayErrorSpy = jasmine.createSpy('display.error') + displayErrorSpy = vi.fn() return { - debug: jasmine.createSpy(), - log: jasmine.createSpy(), - info: jasmine.createSpy(), - warn: jasmine.createSpy(), + debug: vi.fn(), + log: vi.fn(), + info: vi.fn(), + warn: vi.fn(), error: displayErrorSpy, } } beforeEach(() => { - onMonitorErrorCollectedSpy = jasmine.createSpy() + onMonitorErrorCollectedSpy = vi.fn() currentMonitor = createMonitor(createFakeDisplay(), onMonitorErrorCollectedSpy) }) @@ -81,19 +82,19 @@ describe('monitor', () => { it('should report error', () => { candidate.monitoredThrowing() - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith(new Error('monitored')) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith(new Error('monitored')) }) it('should report string error', () => { candidate.monitoredStringErrorThrowing() - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith('string error') + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith('string error') }) it('should report object error', () => { candidate.monitoredObjectErrorThrowing() - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith({ foo: 'bar' }) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith({ foo: 'bar' }) }) }) @@ -115,7 +116,7 @@ describe('monitor', () => { it('should report error', () => { currentMonitor.callMonitored(throwing) - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith(new Error('error')) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith(new Error('error')) }) }) @@ -133,7 +134,7 @@ describe('monitor', () => { it('should report error', () => { currentMonitor.monitor(throwing)() - expect(onMonitorErrorCollectedSpy).toHaveBeenCalledOnceWith(new Error('error')) + expect(onMonitorErrorCollectedSpy).toHaveBeenCalledExactlyOnceWith(new Error('error')) }) }) }) @@ -163,7 +164,9 @@ describe('monitor', () => { it('logs errors thrown by the onMonitorErrorCollected callback when debug mode is enabled', () => { setDebugMode(true) - onMonitorErrorCollectedSpy.and.throwError(new Error('unexpected')) + onMonitorErrorCollectedSpy.mockImplementation(() => { + throw new Error('unexpected') + }) currentMonitor.callMonitored(() => { throw new Error('message') diff --git a/packages/js-core/src/util/debug.spec.ts b/packages/js-core/src/util/debug.spec.ts index e1e9304cfa..f06c4b1bbd 100644 --- a/packages/js-core/src/util/debug.spec.ts +++ b/packages/js-core/src/util/debug.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, afterEach } from 'vitest' import { getDebugMode, setDebugMode } from './debug' describe('debug mode', () => { diff --git a/packages/js-core/src/util/display.spec.ts b/packages/js-core/src/util/display.spec.ts index ed5d63d774..31cbb08df8 100644 --- a/packages/js-core/src/util/display.spec.ts +++ b/packages/js-core/src/util/display.spec.ts @@ -1,3 +1,4 @@ +import { vi, describe, expect, it } from 'vitest' import { createDisplay, originalConsoleMethods } from './display' const CONSOLE_METHODS = ['debug', 'log', 'info', 'warn', 'error'] as const @@ -7,12 +8,12 @@ describe('display', () => { it(`${method}() forwards to the console method, prefixed`, () => { // Spy on the captured original *before* creating the display: createDisplay binds // `originalConsoleMethods[method]` at call time, so the bound method forwards to the spy. - const consoleSpy = spyOn(originalConsoleMethods, method) + const consoleSpy = vi.spyOn(originalConsoleMethods, method).mockImplementation(() => undefined) const display = createDisplay('[PREFIX]') display[method]('message') - expect(consoleSpy).toHaveBeenCalledOnceWith('[PREFIX]', 'message') + expect(consoleSpy).toHaveBeenCalledExactlyOnceWith('[PREFIX]', 'message') }) }) }) diff --git a/packages/js-core/src/util/mergeInto.spec.ts b/packages/js-core/src/util/mergeInto.spec.ts index 83841b3640..b5b988cb9e 100644 --- a/packages/js-core/src/util/mergeInto.spec.ts +++ b/packages/js-core/src/util/mergeInto.spec.ts @@ -1,3 +1,4 @@ +import { afterEach, describe, expect, it } from 'vitest' import { deepClone, mergeInto, combine } from './mergeInto' describe('mergeInto', () => { diff --git a/packages/js-core/src/util/typeUtils.spec.ts b/packages/js-core/src/util/typeUtils.spec.ts index dc5d281201..13e0b28c11 100644 --- a/packages/js-core/src/util/typeUtils.spec.ts +++ b/packages/js-core/src/util/typeUtils.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' import { getType, isIndexableObject } from './typeUtils' describe('typeUtils', () => { @@ -25,23 +26,23 @@ describe('typeUtils', () => { describe('isIndexableObject', () => { it('returns true for plain objects', () => { - expect(isIndexableObject({})).toBeTrue() + expect(isIndexableObject({})).toBe(true) }) it('returns false for primitives', () => { - expect(isIndexableObject(null)).toBeFalse() - expect(isIndexableObject(undefined)).toBeFalse() - expect(isIndexableObject('')).toBeFalse() - expect(isIndexableObject(0)).toBeFalse() - expect(isIndexableObject(false)).toBeFalse() + expect(isIndexableObject(null)).toBe(false) + expect(isIndexableObject(undefined)).toBe(false) + expect(isIndexableObject('')).toBe(false) + expect(isIndexableObject(0)).toBe(false) + expect(isIndexableObject(false)).toBe(false) }) it("returned value don't matter too much for non-plain objects", () => { // This test assertions are not strictly relevent. The goal of this function is to be able to // use the value as a plain object. Using an array or a date as a plain object is fine, but // it doesn't make much sense, so we don't really care. - expect(isIndexableObject([])).toBeFalse() - expect(isIndexableObject(new Date())).toBeTrue() + expect(isIndexableObject([])).toBe(false) + expect(isIndexableObject(new Date())).toBe(true) }) }) }) diff --git a/renovate.json b/renovate.json index e0e88fc5a8..48b11093d8 100644 --- a/renovate.json +++ b/renovate.json @@ -19,7 +19,7 @@ "groupName": "all non-major dependencies", "groupSlug": "all-minor-patch", "matchUpdateTypes": ["minor", "patch"], - "matchPackageNames": ["!karma-webpack", "!@playwright/test"] + "matchPackageNames": ["!@playwright/test"] }, { "matchFileNames": ["test/apps/vanilla/**"], @@ -40,6 +40,6 @@ } ], "toolSettings": { - "nodeMaxMemory": 2048 + "nodeMaxMemory": 4096 } } diff --git a/scripts/test/bs-wrapper.ts b/scripts/test/bs-wrapper.ts index 32578649c4..568f9b74e5 100644 --- a/scripts/test/bs-wrapper.ts +++ b/scripts/test/bs-wrapper.ts @@ -6,12 +6,9 @@ // It used to re-run the test command based on its output (in particular, when the BrowserStack // session failed to be created), but we observed that: // -// * The retry logic of karma and wdio was more efficient to retry this kind of tests (the -// BrowserStack connection is re-created on each retry) +// * The previous runner retried this kind of failure by recreating the BrowserStack connection. // -// * Aborting the test command via a SIGTERM signal was buggy and the command continued to run even -// after killing it. There might be a better way of prematurely aborting the test command if we need -// to in the future. +// * Aborting the previous test command via SIGTERM was buggy and the command continued to run. import { randomUUID } from 'node:crypto' import { spawn, type ChildProcess } from 'node:child_process' @@ -108,20 +105,26 @@ function runTests(): Promise { }) let output = '' - let timeoutId: NodeJS.Timeout - - child.stdout!.pipe(process.stdout) - child.stdout!.on('data', onOutput) - - child.stderr!.pipe(process.stderr) - child.stderr!.on('data', onOutput) + let timeoutId = setTimeout(() => killIt('no output timeout'), NO_OUTPUT_TIMEOUT) + child.stdout!.on('data', (data) => onOutput(data, process.stdout)) + child.stderr!.on('data', (data) => onOutput(data, process.stderr)) child.on('exit', (code, signal) => { + clearTimeout(timeoutId) + // Only the test process owns the result. Inferring success from human-readable output can + // hide setup, collection, reporter, or unhandled errors. resolve(!signal && code === 0) }) + child.on('error', (error) => { + clearTimeout(timeoutId) + printError('Unable to start the BrowserStack test process:', error) + resolve(false) + }) - function onOutput(data: Buffer | string): void { - output += data.toString() + function onOutput(data: Buffer | string, destination: NodeJS.WriteStream): void { + const chunk = redactBrowserStackCredentials(data.toString()) + output += chunk + destination.write(chunk) clearTimeout(timeoutId) @@ -134,14 +137,21 @@ function runTests(): Promise { function killIt(message: string): void { printError(`Killing the browserstack job because of ${message}`) - // use 'SIGKILL' instead of 'SIGTERM' because Karma intercepts 'SIGTERM' and terminates the process with a 0 exit code, - // which is not what we want here (we want to indicate a failure). - // see https://github.com/karma-runner/karma/blob/master/lib/server.js#L391 child.kill('SIGKILL') } }) } +function redactBrowserStackCredentials(output: string): string { + let redactedOutput = output + for (const credential of [process.env.BS_USERNAME, process.env.BS_ACCESS_KEY]) { + if (credential) { + redactedOutput = redactedOutput.replaceAll(credential, '[REDACTED]') + } + } + return redactedOutput +} + function hasUnrecoverableFailure(stdout: string): boolean { return stdout.includes('is set to true but local testing through BrowserStack is not connected.') } diff --git a/scripts/test/ci-bs.ts b/scripts/test/ci-bs.ts index 9b233c13dc..ca9e7d2398 100644 --- a/scripts/test/ci-bs.ts +++ b/scripts/test/ci-bs.ts @@ -9,6 +9,8 @@ const RELEVANT_FILE_PATTERNS = [ 'test/*', 'developer-extension/*', 'scripts/test/*', + 'vitest.config.ts', + 'vitest.bs.config.ts', 'package.json', 'yarn.lock', 'tsconfig*', diff --git a/scripts/test/lib/browserstackUnitConfig.spec.ts b/scripts/test/lib/browserstackUnitConfig.spec.ts new file mode 100644 index 0000000000..b87262aaf2 --- /dev/null +++ b/scripts/test/lib/browserstackUnitConfig.spec.ts @@ -0,0 +1,99 @@ +import assert from 'node:assert' +import { describe, it } from 'node:test' +import { getBrowserStackCapabilities, getBrowserStackInstance } from './browserstackUnitConfig.ts' + +const options = { + username: 'user', + accessKey: 'key', + localIdentifier: 'local', + build: 'build', + playwrightVersion: '1.59.1', +} + +describe('BrowserStack unit configuration', () => { + it('maps branded desktop browsers to Chromium with an explicit version', () => { + const capabilities = getBrowserStackCapabilities( + { + sessionName: 'Edge', + name: 'edge', + version: '88.0', + os: 'Windows', + osVersion: '11', + }, + options + ) + + const instance = getBrowserStackInstance( + { + sessionName: 'Edge', + name: 'edge', + version: '88.0', + os: 'Windows', + osVersion: '11', + }, + options + ) + assert.equal(instance.browser, 'chromium') + assert.equal(capabilities.browser_version, '88.0') + assert.equal(capabilities['browserstack.playwrightVersion'], undefined) + assert.equal(capabilities['client.playwrightVersion'], '1.59.1') + }) + + it('uses BrowserStack bundled-browser capabilities for Firefox and WebKit', () => { + const capabilities = getBrowserStackCapabilities( + { + sessionName: 'Firefox', + name: 'playwright-firefox', + os: 'Windows', + osVersion: '11', + }, + options + ) + + const firefoxInstance = getBrowserStackInstance( + { + sessionName: 'Firefox', + name: 'playwright-firefox', + os: 'Windows', + osVersion: '11', + }, + options + ) + const webkitInstance = getBrowserStackInstance( + { + sessionName: 'WebKit', + name: 'playwright-webkit', + os: 'OS X', + osVersion: 'Sequoia', + }, + options + ) + assert.equal(firefoxInstance.browser, 'chromium') + assert.equal(webkitInstance.browser, 'chromium') + assert.equal(capabilities.browser_version, undefined) + assert.equal(capabilities['browserstack.playwrightVersion'], '1.59.1') + }) + + it('configures the remote connection on the instance provider', () => { + const instance = getBrowserStackInstance( + { + sessionName: 'Firefox', + name: 'playwright-firefox', + os: 'Windows', + osVersion: '11', + }, + options + ) + + assert.equal(instance.browser, 'chromium') + assert.equal(instance.name, 'Firefox') + assert.equal(instance.provider.name, 'playwright') + assert.equal('playwright' in instance, false) + + const wsEndpoint = instance.provider.options.connectOptions?.wsEndpoint + assert.ok(wsEndpoint) + const capabilities = JSON.parse(new URL(wsEndpoint).searchParams.get('caps')!) as Record + assert.equal(capabilities.browser, 'playwright-firefox') + assert.equal(capabilities['browserstack.playwrightVersion'], '1.59.1') + }) +}) diff --git a/scripts/test/lib/browserstackUnitConfig.ts b/scripts/test/lib/browserstackUnitConfig.ts new file mode 100644 index 0000000000..4b736ae590 --- /dev/null +++ b/scripts/test/lib/browserstackUnitConfig.ts @@ -0,0 +1,66 @@ +import { playwright } from '@vitest/browser-playwright' +import type { BrowserInstanceOption } from 'vitest/node' + +export interface BrowserStackUnitConfiguration { + sessionName: string + name: string + version?: string + os?: string + osVersion?: string +} + +export interface BrowserStackCapabilityOptions { + username?: string + accessKey?: string + localIdentifier?: string + build?: string + playwrightVersion: string +} + +export function getBrowserStackCapabilities( + configuration: BrowserStackUnitConfiguration, + options: BrowserStackCapabilityOptions +): Record { + const isBundledBrowser = configuration.name.startsWith('playwright-') + + return { + os: configuration.os, + os_version: configuration.osVersion, + browser: configuration.name, + ...(configuration.version ? { browser_version: configuration.version } : {}), + 'browserstack.username': options.username, + 'browserstack.accessKey': options.accessKey, + project: 'browser sdk unit', + build: options.build, + name: configuration.sessionName, + 'browserstack.local': true, + 'browserstack.localIdentifier': options.localIdentifier ?? '', + ...(isBundledBrowser ? { 'browserstack.playwrightVersion': options.playwrightVersion } : {}), + 'client.playwrightVersion': options.playwrightVersion, + 'browserstack.debug': false, + 'browserstack.console': 'info', + 'browserstack.networkLogs': false, + 'browserstack.interactiveDebugging': false, + } +} + +export function getBrowserStackInstance( + configuration: BrowserStackUnitConfiguration, + options: BrowserStackCapabilityOptions +) { + const capabilities = getBrowserStackCapabilities(configuration, options) + + return { + // BrowserStack's Playwright endpoint is always reached through chromium.connect(); the + // capability selects whether the remote browser is Chrome, Edge, Firefox, or WebKit. + browser: 'chromium' as const, + name: configuration.sessionName, + // Vitest reads remote connection options from each instance provider. Other instance keys are + // ignored and make Playwright fall back to launching a browser on the CI runner. + provider: playwright({ + connectOptions: { + wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(capabilities))}`, + }, + }), + } satisfies BrowserInstanceOption +} diff --git a/scripts/test/lib/unitTestArguments.spec.ts b/scripts/test/lib/unitTestArguments.spec.ts new file mode 100644 index 0000000000..a5225d1ed6 --- /dev/null +++ b/scripts/test/lib/unitTestArguments.spec.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert' +import { describe, it } from 'node:test' +import { buildVitestCommand } from './unitTestArguments.ts' + +describe('buildVitestCommand', () => { + it('preserves regular Vitest arguments', () => { + assert.deepStrictEqual(buildVitestCommand(['--bail=1', '-t', 'my test']), { + arguments: ['--bail=1', '-t', 'my test'], + watch: false, + }) + }) + + it('translates repeated spec filters', () => { + assert.deepStrictEqual(buildVitestCommand(['--spec', 'first.spec.ts', '--spec=second.spec.ts']), { + arguments: ['first.spec.ts', 'second.spec.ts'], + watch: false, + }) + }) + + it('translates the randomization seed', () => { + assert.deepStrictEqual(buildVitestCommand(['--seed', '123', '--seed=456']), { + arguments: ['--sequence.seed=123', '--sequence.seed=456'], + watch: false, + }) + }) + + it('recognizes watch mode options', () => { + assert.deepStrictEqual(buildVitestCommand(['--no-single-run']), { arguments: [], watch: true }) + assert.deepStrictEqual(buildVitestCommand(['--watch']), { arguments: [], watch: true }) + }) + + it('rejects an option without a value', () => { + assert.throws(() => buildVitestCommand(['--spec']), /Missing value for --spec/) + assert.throws(() => buildVitestCommand(['--seed']), /Missing value for --seed/) + }) +}) diff --git a/scripts/test/lib/unitTestArguments.ts b/scripts/test/lib/unitTestArguments.ts new file mode 100644 index 0000000000..736697c427 --- /dev/null +++ b/scripts/test/lib/unitTestArguments.ts @@ -0,0 +1,45 @@ +interface VitestCommand { + arguments: string[] + watch: boolean +} + +/** + * Preserve the Browser SDK unit-test CLI while translating legacy options to Vitest. + */ +export function buildVitestCommand(arguments_: string[]): VitestCommand { + const vitestArguments: string[] = [] + let watch = false + + for (let index = 0; index < arguments_.length; index += 1) { + const argument = arguments_[index] + + if (argument === '--watch' || argument === '--no-single-run') { + watch = true + continue + } + + if (argument === '--spec' || argument === '--seed') { + const value = arguments_[index + 1] + if (!value) { + throw new Error(`Missing value for ${argument}`) + } + index += 1 + vitestArguments.push(argument === '--spec' ? value : `--sequence.seed=${value}`) + continue + } + + if (argument.startsWith('--spec=')) { + vitestArguments.push(argument.slice('--spec='.length)) + continue + } + + if (argument.startsWith('--seed=')) { + vitestArguments.push(`--sequence.seed=${argument.slice('--seed='.length)}`) + continue + } + + vitestArguments.push(argument) + } + + return { arguments: vitestArguments, watch } +} diff --git a/scripts/test/lib/unitTestReporter.spec.ts b/scripts/test/lib/unitTestReporter.spec.ts new file mode 100644 index 0000000000..87b2918bac --- /dev/null +++ b/scripts/test/lib/unitTestReporter.spec.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert' +import { afterEach, describe, it, mock } from 'node:test' +import type { TestCase, TestModule } from 'vitest/node' +import { recordUnexpectedConsoleLog, UnitTestReporter } from './unitTestReporter.ts' + +const initialExitCode = process.exitCode + +afterEach(() => { + process.exitCode = initialExitCode + mock.restoreAll() +}) + +describe('UnitTestReporter', () => { + it('fails the run when a test writes unexpected console output', () => { + const reporter = new UnitTestReporter() + const consoleError = mock.method(console, 'error', () => undefined) + reporter.onTestRunStart() + + recordUnexpectedConsoleLog('unexpected', 'stderr', makeTestCase()) + reporter.onTestRunEnd() + + assert.equal(process.exitCode, 1) + assert.match(String(consoleError.mock.calls[0].arguments[0]), /Unexpected console calls/) + }) + + it('allows console output when the test opts in explicitly', () => { + const reporter = new UnitTestReporter() + const consoleError = mock.method(console, 'error', () => undefined) + reporter.onTestRunStart() + + recordUnexpectedConsoleLog('expected', 'stderr', makeTestCase({ allowUnexpectedConsoleLogs: true })) + reporter.onTestRunEnd() + + assert.equal(process.exitCode, initialExitCode) + assert.equal(consoleError.mock.callCount(), 0) + }) + + it('fails the run when test names are duplicated', () => { + const reporter = new UnitTestReporter() + const consoleError = mock.method(console, 'error', () => undefined) + const testCase = makeTestCase() + const testModule = { + children: { allTests: () => [testCase, testCase] }, + } as unknown as TestModule + reporter.onTestRunStart() + + reporter.onTestModuleCollected(testModule) + reporter.onTestRunEnd() + + assert.equal(process.exitCode, 1) + assert.match(String(consoleError.mock.calls[0].arguments[0]), /Duplicate tests/) + }) +}) + +function makeTestCase(meta: Record = {}): TestCase { + return { + type: 'test', + fullName: 'suite > test', + module: { moduleId: 'feature.spec.ts' }, + project: { name: 'chromium' }, + meta: () => meta, + } as unknown as TestCase +} diff --git a/scripts/test/lib/unitTestReporter.ts b/scripts/test/lib/unitTestReporter.ts new file mode 100644 index 0000000000..3741cd5edc --- /dev/null +++ b/scripts/test/lib/unitTestReporter.ts @@ -0,0 +1,76 @@ +import type { Reporter, TestCase, TestModule, TestSuite } from 'vitest/node' + +interface UnexpectedConsoleLog { + entity: TestModule | TestSuite | TestCase | undefined + log: string + type: 'stderr' | 'stdout' +} + +const unexpectedConsoleLogs: UnexpectedConsoleLog[] = [] + +/** + * Record test console output so CI can reject it after reporters have written their results. + */ +export function recordUnexpectedConsoleLog( + log: string, + type: UnexpectedConsoleLog['type'], + entity: TestModule | TestSuite | TestCase | undefined +): void { + // Task metadata is synchronized after the test finishes, so defer the opt-in check until the + // run ends instead of deciding while the console call is being streamed. + unexpectedConsoleLogs.push({ entity, log, type }) +} + +export class UnitTestReporter implements Reporter { + private testNames = new Set() + private duplicateTestNames = new Set() + + onTestRunStart(): void { + this.testNames.clear() + this.duplicateTestNames.clear() + unexpectedConsoleLogs.length = 0 + } + + onTestModuleCollected(testModule: TestModule): void { + for (const testCase of testModule.children.allTests()) { + const testName = `${testCase.project.name}:${testCase.fullName}` + if (this.testNames.has(testName)) { + this.duplicateTestNames.add(testName) + } + this.testNames.add(testName) + } + } + + onTestRunEnd(): void { + if (this.duplicateTestNames.size > 0) { + process.exitCode = 1 + console.error(`Duplicate tests:\n${[...this.duplicateTestNames].sort().join('\n')}`) + } + + const remainingConsoleLogs = unexpectedConsoleLogs.filter(({ entity }) => !allowsConsoleLogs(entity)) + if (remainingConsoleLogs.length > 0) { + process.exitCode = 1 + console.error( + `Unexpected console calls:\n${remainingConsoleLogs + .map(({ entity, log, type }) => `${formatEntity(entity)}\n${type}: ${log}`) + .join('\n')}` + ) + } + } +} + +function allowsConsoleLogs(entity: TestModule | TestSuite | TestCase | undefined): boolean { + return Boolean( + entity?.type === 'test' && (entity.meta() as { allowUnexpectedConsoleLogs?: boolean }).allowUnexpectedConsoleLogs + ) +} + +function formatEntity(entity: TestModule | TestSuite | TestCase | undefined): string { + if (!entity) { + return 'outside a test' + } + if (entity.type === 'module') { + return entity.moduleId + } + return `${entity.module.moduleId} > ${entity.fullName}` +} diff --git a/scripts/test/lib/vitestBrowserPolyfills.spec.ts b/scripts/test/lib/vitestBrowserPolyfills.spec.ts new file mode 100644 index 0000000000..af6ade3046 --- /dev/null +++ b/scripts/test/lib/vitestBrowserPolyfills.spec.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert' +import { readFileSync } from 'node:fs' +import { describe, it } from 'node:test' +import { createContext, runInContext } from 'node:vm' + +const polyfillsSource = readFileSync(new URL('../../../test/unit/vitestBrowserPolyfills.js', import.meta.url), 'utf8') + +describe('Vitest browser polyfills', () => { + it('installs the APIs needed by the Vitest client in legacy browsers', () => { + const crypto = { + getRandomValues(bytes: Uint8Array) { + for (let index = 0; index < bytes.length; index += 1) { + bytes[index] = index + } + return bytes + }, + } + const context = createContext({ crypto }) + + runInContext( + ` + delete Object.hasOwn + delete Array.prototype.at + delete Array.prototype.findLastIndex + `, + context + ) + runInContext(polyfillsSource, context) + + assert.equal(runInContext("Object.hasOwn({ value: true }, 'value')", context), true) + assert.equal(runInContext("Object.hasOwn(Object.create({ inherited: true }), 'inherited')", context), false) + assert.equal(runInContext("['first', 'last'].at(-1)", context), 'last') + assert.equal(runInContext('[1, 2, 3, 4].findLastIndex((value) => value % 2 === 1)', context), 2) + assert.equal(runInContext('crypto.randomUUID()', context), '00010203-0405-4607-8809-0a0b0c0d0e0f') + assert.equal(runInContext("Object.prototype.propertyIsEnumerable.call(Object, 'hasOwn')", context), false) + assert.equal(runInContext("Array.prototype.propertyIsEnumerable('at')", context), false) + assert.equal(runInContext("Array.prototype.propertyIsEnumerable('findLastIndex')", context), false) + }) +}) diff --git a/scripts/test/run-unit-tests.ts b/scripts/test/run-unit-tests.ts new file mode 100644 index 0000000000..9da78f44ba --- /dev/null +++ b/scripts/test/run-unit-tests.ts @@ -0,0 +1,10 @@ +import { command } from '../lib/command.ts' +import { runMain } from '../lib/executionUtils.ts' +import { buildVitestCommand } from './lib/unitTestArguments.ts' + +runMain(() => { + const vitestCommand = buildVitestCommand(process.argv.slice(2)) + const vitestArguments = [...(vitestCommand.watch ? [] : ['run']), ...vitestCommand.arguments] + + command`yarn vitest ${vitestArguments}`.withLogs().run() +}) diff --git a/test/unit/browsers.conf.ts b/test/unit/browsers.conf.ts index e9fbdb5059..ce01d5c059 100644 --- a/test/unit/browsers.conf.ts +++ b/test/unit/browsers.conf.ts @@ -2,50 +2,47 @@ import type { BrowserConfiguration } from '../browsers.conf' -// The ECMAScript version supported by the oldest browser in the list below (Edge/Chrome 80 β†’ ES2020). +// Vitest Browser Mode supports Chrome >=87, Edge >=88, Firefox >=78 and Safari >=15.4. Firefox +// and WebKit are selected through Playwright's bundled-browser capabilities, so BrowserStack uses +// the versions paired with the pinned `playwright` dependency. +// +// The ECMAScript version supported by the oldest branded browser below (Chrome 87 β†’ ES2020). // Used by tests that validate runtime-generated code strings (e.g. the expression compiler) which // bypass TypeScript/webpack transpilation and must only use syntax supported by all target browsers. export const OLDEST_BROWSER_ECMA_VERSION = 2020 +// BrowserStack's real Android integration uses Playwright's separate `_android` API, which is not +// supported by Vitest's Playwright provider. Keep this matrix to browser-type connections only. export const browserConfigurations: BrowserConfiguration[] = [ { id: 'edge', sessionName: 'Edge', - name: 'Edge', - version: '80.0', + name: 'edge', + version: '88.0', os: 'Windows', osVersion: '11', }, { id: 'firefox', - sessionName: 'Firefox', - name: 'Firefox', - version: '78.0', + sessionName: 'Firefox (Playwright)', + name: 'playwright-firefox', os: 'Windows', osVersion: '11', }, { - id: 'safari-desktop', - sessionName: 'Safari desktop', - name: 'Safari', - version: '14.0', + id: 'webkit-desktop', + sessionName: 'WebKit desktop', + name: 'playwright-webkit', os: 'OS X', - osVersion: 'Big Sur', + // BrowserStack does not provide WebKit on Ventura or Sonoma for Playwright 1.59. + osVersion: 'Sequoia', }, { id: 'chrome-desktop', sessionName: 'Chrome desktop', - name: 'Chrome', - version: '80.0', + name: 'chrome', + version: '87.0', os: 'Windows', osVersion: '11', }, - { - id: 'chrome-mobile', - sessionName: 'Chrome mobile', - name: 'chrome', - os: 'android', - osVersion: '12.0', - device: 'Google Pixel 6', - }, ] diff --git a/test/unit/browserstack.keepalive.ts b/test/unit/browserstack.keepalive.ts new file mode 100644 index 0000000000..847ca2ddf9 --- /dev/null +++ b/test/unit/browserstack.keepalive.ts @@ -0,0 +1,37 @@ +import { client } from '@vitest/browser/client' + +const KEEPALIVE_INTERVAL = 25_000 +const keepaliveWindow = window as Window & { vitestBrowserStackKeepalive?: number } + +if (keepaliveWindow.vitestBrowserStackKeepalive === undefined) { + let keepaliveInFlight = false + + const sendKeepalive = async () => { + if (keepaliveInFlight) { + return + } + + keepaliveInFlight = true + try { + await client.waitForConnection() + await client.rpc.getCountOfFailedTests() + } catch { + // A transient reconnect can race with the heartbeat; the next interval retries it. + } finally { + keepaliveInFlight = false + } + } + + keepaliveWindow.vitestBrowserStackKeepalive = window.setInterval(() => { + void sendKeepalive() + }, KEEPALIVE_INTERVAL) + + window.addEventListener( + 'pagehide', + () => { + window.clearInterval(keepaliveWindow.vitestBrowserStackKeepalive) + delete keepaliveWindow.vitestBrowserStackKeepalive + }, + { once: true } + ) +} diff --git a/test/unit/globalThisPolyfill.js b/test/unit/globalThisPolyfill.js deleted file mode 100644 index cd39cadf99..0000000000 --- a/test/unit/globalThisPolyfill.js +++ /dev/null @@ -1,6 +0,0 @@ -// Polyfill globalThis for browsers that don't support it (e.g. Chrome < 71) -// Required because @angular/core uses globalThis internally. -/* eslint-disable no-undef */ -if (typeof globalThis === 'undefined') { - window.globalThis = window -} diff --git a/test/unit/jasmineSeedReporterPlugin.js b/test/unit/jasmineSeedReporterPlugin.js deleted file mode 100644 index b426c2642e..0000000000 --- a/test/unit/jasmineSeedReporterPlugin.js +++ /dev/null @@ -1,15 +0,0 @@ -function seedReporter(logger) { - const log = logger.create('jasmine-seed-reporter') - this.onBrowserComplete = function (browser, result) { - if (result.order?.random && result.order.seed) { - log.info(`${browser}: Randomized with seed ${result.order.seed}\n`) - } - } -} - -seedReporter.$inject = ['logger'] - -// eslint-disable-next-line import-x/no-default-export -export default { - 'reporter:jasmine-seed': ['type', seedReporter], -} diff --git a/test/unit/karma.base.conf.js b/test/unit/karma.base.conf.js deleted file mode 100644 index f1b3bdcb5d..0000000000 --- a/test/unit/karma.base.conf.js +++ /dev/null @@ -1,151 +0,0 @@ -import { parseArgs } from 'node:util' -import webpackBase from '../../webpack.base.ts' - -import { getTestReportDirectory } from '../envUtils.ts' -import jasmineSeedReporterPlugin from './jasmineSeedReporterPlugin.js' -import karmaSkippedFailedReporterPlugin from './karmaSkippedFailedReporterPlugin.js' -import karmaDuplicateTestNameReporterPlugin from './karmaDuplicateTestNameReporterPlugin.js' -import karmaUnexpectedErrorLogReporterPlugin from './karmaUnexpectedErrorLogReporterPlugin.js' - -const webpackConfig = webpackBase({ - mode: 'development', - types: ['jasmine', 'chrome'], - // do not replace some build env variables in unit test in order to test different build behaviors - keepBuildEnvVariables: ['SDK_VERSION'], -}) - -const reporters = ['spec', 'jasmine-seed', 'karma-skipped-failed', 'karma-duplicate-test-name'] -if (process.env.CI) { - reporters.push('karma-unexpected-error-log') -} - -const testReportDirectory = getTestReportDirectory() -if (testReportDirectory) { - reporters.push('junit') -} - -const FILES = [ - // Polyfill globalThis for older browsers (e.g. Chrome 63) that don't support it. - // Required because @angular/core uses globalThis internally. - { pattern: 'test/unit/globalThisPolyfill.js', watched: false }, - // Make sure 'forEach.spec' is the first file to be loaded, so its `beforeEach` hook is executed - // before all other `beforeEach` hooks, and its `afterEach` hook is executed after all other - // `afterEach` hooks. - 'packages/browser-core/test/forEach.spec.ts', - 'packages/browser-rum/test/record/toto.css', -] - -const FILES_SPECS = [ - 'packages/*/@(src|test)/**/*.spec.@(ts|tsx)', - 'developer-extension/@(src|test)/**/*.spec.@(ts|tsx)', -] - -const { values } = parseArgs({ - allowPositionals: true, - strict: false, - options: { - spec: { - type: 'string', - multiple: true, - }, - seed: { - type: 'string', - }, - }, -}) - -// eslint-disable-next-line import-x/no-default-export -export default { - basePath: '../..', - files: [...FILES, ...(values.spec || FILES_SPECS)], - frameworks: ['jasmine', 'webpack'], - client: { - jasmine: { - random: true, - seed: values.seed, - stopSpecOnExpectationFailure: true, - }, - }, - preprocessors: { - '**/*.+(ts|tsx)': ['webpack', 'sourcemap'], - // Apply sourcemaps to webpack common chunk - '/**/*.js': ['sourcemap'], - }, - reporters, - specReporter: { - suppressErrorSummary: true, - suppressPassed: true, - suppressSkipped: true, - showBrowser: true, - }, - junitReporter: { - outputDir: testReportDirectory, - }, - singleRun: true, - webpack: { - stats: 'minimal', - module: overrideTsLoaderRule(webpackConfig.module), - resolve: webpackConfig.resolve, - target: webpackConfig.target, - devtool: false, - mode: 'development', - plugins: webpackConfig.plugins, - ignoreWarnings: [ - // we will see warnings about missing exports in some files - // this is because we set transpileOnly option in ts-loader - { message: /export .* was not found in/ }, - ], - }, - webpackMiddleware: { - stats: 'errors-only', - logLevel: 'warn', - }, - plugins: [ - 'karma-*', - jasmineSeedReporterPlugin, - karmaSkippedFailedReporterPlugin, - karmaDuplicateTestNameReporterPlugin, - karmaUnexpectedErrorLogReporterPlugin, - ], - - // Running tests on low performance environments (ex: BrowserStack) can block JS execution for a - // few seconds. We need to increase those two timeout values to make sure Karma (and underlying - // Socket.io) does not consider that the browser crashed. - pingTimeout: 60_000, - browserNoActivityTimeout: 2 * 60_000, -} - -function overrideTsLoaderRule(module) { - // We set transpileOnly to true to avoid type checking in unit tests - module.rules = module.rules.map((rule) => { - if (rule.loader === 'ts-loader') { - return { - ...rule, - options: { - ...rule.options, - transpileOnly: true, - }, - } - } - return rule - }) - - // TODO next major see if we can remove this transpilation when bumping browsers - // We use swc-loader to transpile dependencies that are using syntax not compatible with browsers we use for testing - module.rules.push({ - test: /\.m?js$/, - include: /node_modules/, - use: { - loader: 'swc-loader', - options: { - env: { - targets: { - chrome: '63', - }, - }, - }, - }, - }) - - return module -} diff --git a/test/unit/karma.bs.conf.js b/test/unit/karma.bs.conf.js deleted file mode 100644 index c2fd55bfd2..0000000000 --- a/test/unit/karma.bs.conf.js +++ /dev/null @@ -1,54 +0,0 @@ -import { getBuildInfos } from '../envUtils.ts' -import { browserConfigurations } from './browsers.conf.ts' -import karmaBaseConf from './karma.base.conf.js' - -const selectedBrowser = process.env.BS_BROWSER -const filteredConfigurations = selectedBrowser - ? browserConfigurations.filter((configuration) => configuration.id === selectedBrowser) - : browserConfigurations - -if (selectedBrowser && filteredConfigurations.length === 0) { - const availableIds = browserConfigurations.map((c) => c.id).join(', ') - throw new Error(`Unknown BS_BROWSER "${selectedBrowser}". Available: ${availableIds}`) -} - -// eslint-disable-next-line import-x/no-default-export -export default function (config) { - config.set({ - ...karmaBaseConf, - exclude: [ - // Exclude developer-extension from BrowserStack because it is is only compatible with Chrome - // so there is no point to test it on other browsers. - 'developer-extension/**', - ], - plugins: [...karmaBaseConf.plugins, 'karma-browserstack-launcher'], - reporters: [...karmaBaseConf.reporters, 'BrowserStack'], - browsers: filteredConfigurations.map((configuration) => configuration.sessionName), - concurrency: filteredConfigurations.length, - browserDisconnectTolerance: 3, - captureTimeout: 2 * 60 * 1000, - browserStack: { - username: process.env.BS_USERNAME, - accessKey: process.env.BS_ACCESS_KEY, - localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER, - project: 'browser sdk unit', - build: getBuildInfos(), - video: false, - }, - customLaunchers: Object.fromEntries( - filteredConfigurations.map((configuration) => [ - configuration.sessionName, - // See https://github.com/karma-runner/karma-browserstack-launcher#per-browser-options - { - base: 'BrowserStack', - os: configuration.os, - os_version: configuration.osVersion, - browser: configuration.name, - browser_version: configuration.version, - device: configuration.device, - name: configuration.sessionName, - }, - ]) - ), - }) -} diff --git a/test/unit/karma.local.conf.js b/test/unit/karma.local.conf.js deleted file mode 100644 index 1be082a573..0000000000 --- a/test/unit/karma.local.conf.js +++ /dev/null @@ -1,50 +0,0 @@ -import path from 'node:path' -import { getCoverageReportDirectory } from '../envUtils.ts' -import karmaBaseConf from './karma.base.conf.js' - -const coverageReports = ['text-summary', 'html'] - -if (process.env.CI) { - coverageReports.push('clover') -} - -// eslint-disable-next-line import-x/no-default-export -export default function (config) { - config.set({ - ...karmaBaseConf, - reporters: ['coverage-istanbul', ...karmaBaseConf.reporters], - browsers: ['ChromeHeadlessNoSandbox'], - coverageIstanbulReporter: { - reports: coverageReports, - dir: getCoverageReportDirectory(), - }, - customLaunchers: { - ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'], - }, - }, - webpack: { - ...karmaBaseConf.webpack, - module: withIstanbulRule(karmaBaseConf.webpack.module), - }, - sourceMapLoader: { - remapSource: (source) => source.replace(/webpack:\/\//g, path.join(import.meta.dirname, '../../')), - }, - }) -} - -function withIstanbulRule(module) { - module.rules.push({ - test: /^.*\.ts$/, - exclude: [/.*\.spec\.ts$/, /.*\.d\.ts$/, /.*capturedExceptions\.ts$/, /.*specHelper\.ts$/, /node_modules/], - enforce: 'post', - use: { - loader: '@jsdevtools/coverage-istanbul-loader', - options: { - esModules: true, - }, - }, - }) - return module -} diff --git a/test/unit/karmaDuplicateTestNameReporterPlugin.js b/test/unit/karmaDuplicateTestNameReporterPlugin.js deleted file mode 100644 index f2fdc7b25b..0000000000 --- a/test/unit/karmaDuplicateTestNameReporterPlugin.js +++ /dev/null @@ -1,56 +0,0 @@ -function getPrefix(browser) { - return `[${browser.name}] ` -} - -function KarmaDuplicateTestNameReporter(logger) { - var log = logger.create('karma-duplicate-test-name') - - var testNames = new Set() - var duplicatedTestNames = new Map() - - this.onRunStart = () => { - testNames.clear() - duplicatedTestNames.clear() - } - - this.onRunComplete = () => { - if (duplicatedTestNames.size > 0) { - log.error('Duplicate tests:') - - for (const [name, count] of duplicatedTestNames) { - log.error(`${name} (${count} times)`) - } - } - } - - this.onBrowserStart = (browser) => { - const prefix = getPrefix(browser) - for (const name of testNames) { - if (name.startsWith(prefix)) { - testNames.delete(name) - duplicatedTestNames.delete(name) - } - } - } - - this.onSpecComplete = (browser, result) => { - const testName = `${getPrefix(browser)}${result.fullName}` - - if (testNames.has(testName)) { - if (!duplicatedTestNames.has(testName)) { - duplicatedTestNames.set(testName, 2) - } else { - duplicatedTestNames.set(testName, duplicatedTestNames.get(testName) + 1) - } - } - - testNames.add(testName) - } -} - -KarmaDuplicateTestNameReporter.$inject = ['logger'] - -// eslint-disable-next-line import-x/no-default-export -export default { - 'reporter:karma-duplicate-test-name': ['type', KarmaDuplicateTestNameReporter], -} diff --git a/test/unit/karmaSkippedFailedReporterPlugin.js b/test/unit/karmaSkippedFailedReporterPlugin.js deleted file mode 100644 index b560a20c16..0000000000 --- a/test/unit/karmaSkippedFailedReporterPlugin.js +++ /dev/null @@ -1,19 +0,0 @@ -function KarmaSkippedFailedReporter(logger) { - var log = logger.create('karma-skipped-failed') - - this.onSpecComplete = (browser, result) => { - if (result.skipped && !result.success) { - log.warn('Failing skipped test:') - log.warn(browser.name) - log.warn(result.fullName) - log.warn(result.log.join('\n')) - } - } -} - -KarmaSkippedFailedReporter.$inject = ['logger'] - -// eslint-disable-next-line import-x/no-default-export -export default { - 'reporter:karma-skipped-failed': ['type', KarmaSkippedFailedReporter], -} diff --git a/test/unit/karmaUnexpectedErrorLogReporterPlugin.js b/test/unit/karmaUnexpectedErrorLogReporterPlugin.js deleted file mode 100644 index e405137092..0000000000 --- a/test/unit/karmaUnexpectedErrorLogReporterPlugin.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Karma reporter that fails any spec that produces an unexpected console log. - * - * Tests that intentionally trigger console output should spy on the relevant method beforehand, - * which prevents the call from reaching the browser console (and thus this reporter). - */ -function KarmaUnexpectedErrorLogReporter(logger) { - const log = logger.create('karma-unexpected-error-log') - - // Logs are buffered here between two onSpecComplete calls - const pendingLogs = new Map() // browser.id -> string[] - - this.onBrowserLog = (browser, message, type) => { - pendingLogs.getOrInsert(browser.id, []).push(`${type}: ${message}`) - } - - this.onSpecComplete = (browser, result) => { - if (!pendingLogs.has(browser.id)) { - return - } - const logs = pendingLogs.get(browser.id) - pendingLogs.delete(browser.id) - - for (const message of logs) { - const failure = `Unexpected console call: ${message}` - log.error(failure) - result.log.push(failure) - } - result.success = false - browser.lastResult.failed++ - } -} - -KarmaUnexpectedErrorLogReporter.$inject = ['logger'] - -// eslint-disable-next-line import-x/no-default-export -export default { - 'reporter:karma-unexpected-error-log': ['type', KarmaUnexpectedErrorLogReporter], -} diff --git a/test/unit/vitest.setup.ts b/test/unit/vitest.setup.ts new file mode 100644 index 0000000000..96d16e0a94 --- /dev/null +++ b/test/unit/vitest.setup.ts @@ -0,0 +1,54 @@ +import { beforeEach, afterEach } from 'vitest' +import { resetAllowUntrustedEvents } from '../../packages/browser-core/src/browser/addEventListener' +import { resetGetCurrentSite } from '../../packages/browser-core/src/browser/cookie' +import { resetValueHistoryGlobals } from '../../packages/browser-core/src/tools/valueHistory' +import { resetFetchObservable } from '../../packages/browser-core/src/browser/fetchObservable' +import { resetWebSocketObservable } from '../../packages/browser-core/src/browser/webSocketObservable' +import { resetConsoleObservable } from '../../packages/browser-core/src/domain/console/consoleObservable' +import { resetXhrObservable } from '../../packages/browser-core/src/browser/xhrObservable' +import { resetSampleDecisionCache } from '../../packages/browser-core/src/domain/sampler' +import { resetReplayStats } from '../../packages/browser-rum/src/domain/replayStats' +import { resetManageResourceTimingBufferFull } from '../../packages/browser-rum-core/src/browser/performanceObservable' +import { resetInteractionCountPolyfill } from '../../packages/browser-rum-core/src/domain/view/viewMetrics/interactionCountPolyfill' +import { resetMonitor } from '../../packages/browser-core/src/tools/monitor' +import { resetTelemetry } from '../../packages/browser-core/src/domain/telemetry' +import { resetExperimentalFeatures } from '../../packages/browser-core/src/tools/experimentalFeatures' +import { ignoreConsoleLogs, startLeakDetection } from '../../packages/browser-core/test' + +beforeEach(() => { + ;(window as any).IS_REACT_ACT_ENVIRONMENT = true + // prevent 'Some of your tests did a full page reload!' issue + window.onbeforeunload = () => 'stop' + startLeakDetection() + ignoreConsoleLogs('warn', 'using an outdated JSX transform') + // Note: clearing cookies should be done in `beforeEach` rather than `afterEach`, because in some + // cases the test patches the `document.cookie` getter (ex: `spyOnProperty(document, 'cookie', + // 'get')`), which would prevent the `clearAllCookies` function from working properly. + clearAllCookies() +}) + +afterEach(() => { + // reset globals + delete (window as any).DD_LOGS + delete (window as any).DD_RUM + resetValueHistoryGlobals() + resetFetchObservable() + resetConsoleObservable() + resetXhrObservable() + resetWebSocketObservable() + resetGetCurrentSite() + resetReplayStats() + resetMonitor() + resetTelemetry() + resetInteractionCountPolyfill() + resetSampleDecisionCache() + resetExperimentalFeatures() + resetManageResourceTimingBufferFull() + resetAllowUntrustedEvents() +}) + +function clearAllCookies() { + document.cookie.split(';').forEach((c) => { + document.cookie = c.replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/;samesite=strict`) + }) +} diff --git a/test/unit/vitest.tester.html b/test/unit/vitest.tester.html new file mode 100644 index 0000000000..38abdf9de4 --- /dev/null +++ b/test/unit/vitest.tester.html @@ -0,0 +1,10 @@ + + + + + Browser SDK unit tests + + + + + diff --git a/test/unit/vitestBrowserClient.d.ts b/test/unit/vitestBrowserClient.d.ts new file mode 100644 index 0000000000..5da8bacf05 --- /dev/null +++ b/test/unit/vitestBrowserClient.d.ts @@ -0,0 +1,8 @@ +declare module '@vitest/browser/client' { + export const client: { + waitForConnection(): Promise + rpc: { + getCountOfFailedTests(): Promise + } + } +} diff --git a/test/unit/vitestBrowserPolyfills.js b/test/unit/vitestBrowserPolyfills.js new file mode 100644 index 0000000000..9ef5ff3ca4 --- /dev/null +++ b/test/unit/vitestBrowserPolyfills.js @@ -0,0 +1,91 @@ +;(function installVitestBrowserPolyfills() { + 'use strict' + + const MAX_SAFE_INTEGER = 0x1fffffffffffff + + function toIntegerOrInfinity(value) { + const numericValue = Number(value) + if (numericValue === 0 || numericValue !== numericValue) { + return 0 + } + return numericValue < 0 ? Math.ceil(numericValue) : Math.floor(numericValue) + } + + function toLength(value) { + return Math.min(Math.max(toIntegerOrInfinity(value), 0), MAX_SAFE_INTEGER) + } + + if (typeof Object.hasOwn !== 'function') { + Object.defineProperty(Object, 'hasOwn', { + configurable: true, + enumerable: false, + writable: true, + value: function hasOwn(object, property) { + return Object.prototype.hasOwnProperty.call(object, property) + }, + }) + } + + if (typeof Array.prototype.at !== 'function') { + Object.defineProperty(Array.prototype, 'at', { + configurable: true, + enumerable: false, + writable: true, + value: function at(index) { + if (this === null || this === undefined) { + throw new TypeError('Array.prototype.at called on null or undefined') + } + + const object = Object(this) + const length = toLength(object.length) + const relativeIndex = toIntegerOrInfinity(index) + const actualIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex + return actualIndex < 0 || actualIndex >= length ? undefined : object[actualIndex] + }, + }) + } + + if (typeof Array.prototype.findLastIndex !== 'function') { + Object.defineProperty(Array.prototype, 'findLastIndex', { + configurable: true, + enumerable: false, + writable: true, + value: function findLastIndex(predicate, thisArg) { + if (this === null || this === undefined) { + throw new TypeError('Array.prototype.findLastIndex called on null or undefined') + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function') + } + + const object = Object(this) + const length = toLength(object.length) + for (let index = length - 1; index >= 0; index -= 1) { + if (predicate.call(thisArg, object[index], index, object)) { + return index + } + } + return -1 + }, + }) + } + + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') { + Object.defineProperty(crypto, 'randomUUID', { + configurable: true, + enumerable: false, + writable: true, + value: function randomUUID() { + const bytes = crypto.getRandomValues(new Uint8Array(16)) + bytes[6] = (bytes[6] % 0x10) + 0x40 + bytes[8] = (bytes[8] % 0x40) + 0x80 + + let hex = '' + for (const byte of bytes) { + hex += byte.toString(16).padStart(2, '0') + } + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` + }, + }) + } +})() diff --git a/tsconfig.default.json b/tsconfig.default.json index 984b2a415e..29cfbc082d 100644 --- a/tsconfig.default.json +++ b/tsconfig.default.json @@ -3,7 +3,6 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "types": ["jasmine"], "allowImportingTsExtensions": true, "noEmit": true }, @@ -36,6 +35,10 @@ "packages/browser-rum-nextjs", // Skip typechecking for these folders for now - "test/apps" + "test/apps", + + // Files included in ./tsconfig.unit.json + "vitest.config.ts", + "vitest.bs.config.ts" ] } diff --git a/tsconfig.json b/tsconfig.json index 013707afb2..8364be1498 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./tsconfig.default.json" }, { "path": "./tsconfig.scripts.json" }, + { "path": "./tsconfig.unit.json" }, { "path": "./developer-extension/tsconfig.json" }, { "path": "./test/e2e/tsconfig.json" }, { "path": "./test/performance/tsconfig.json" } diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json index 9d55946f85..9a27630eb8 100644 --- a/tsconfig.scripts.json +++ b/tsconfig.scripts.json @@ -15,9 +15,5 @@ "checkJs": false, "allowImportingTsExtensions": true }, - "include": ["scripts", "**/webpack.*.*", "test/envUtils.ts", "eslint.config.ts", "eslint-local-rules", "test/unit"], - // `test/apps` is excluded (like in tsconfig.default.json): its generated `node_modules` contain - // `@datadog/webpack-plugin` types that import browser source, which would otherwise be pulled into - // this Node-only program via the `**/webpack.*.*` glob and fail for lack of DOM libs. - "exclude": ["test/apps"] + "include": ["scripts", "**/webpack.*.*", "test/envUtils.ts", "eslint.config.ts", "eslint-local-rules"] } diff --git a/tsconfig.unit.json b/tsconfig.unit.json new file mode 100644 index 0000000000..adb48a55df --- /dev/null +++ b/tsconfig.unit.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "preserve", + "target": "ES2024", + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "types": ["node", "vitest/globals"], + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["vitest.config.ts", "vitest.bs.config.ts", "test/unit/**/*.ts", "test/unit/vitestBrowserPolyfills.js"] +} diff --git a/vitest.bs.config.ts b/vitest.bs.config.ts new file mode 100644 index 0000000000..0685e6ac1e --- /dev/null +++ b/vitest.bs.config.ts @@ -0,0 +1,156 @@ +import path from 'node:path' +import { registerHooks } from 'node:module' + +import { playwright } from '@vitest/browser-playwright' +import { defineConfig } from 'vitest/config' + +/* eslint-disable local-rules/disallow-test-import-export-from-src -- Vitest configuration composes test infrastructure. */ +import { getBuildInfos } from './test/envUtils.ts' +import { browserConfigurations as allBrowserConfigurations } from './test/unit/browsers.conf.ts' +import { UnitTestReporter, recordUnexpectedConsoleLog } from './scripts/test/lib/unitTestReporter.ts' +import { getBrowserStackInstance } from './scripts/test/lib/browserstackUnitConfig.ts' +/* eslint-enable local-rules/disallow-test-import-export-from-src */ +import { getBuildEnvValue } from './scripts/lib/buildEnv.ts' + +const browserStackPlaywrightVersion = '1.59.1' + +// The E2E suite follows the latest Playwright, while BrowserStack currently supports an older +// client. Redirect only the Vitest provider's lazy Playwright import to the separately pinned +// package, leaving every other Playwright consumer untouched. +registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier === 'playwright' && context.parentURL?.includes('/@vitest/browser-playwright/')) { + return nextResolve('playwright-browserstack', context) + } + return nextResolve(specifier, context) + }, +}) + +// Filter to a single browser when BS_BROWSER is set (CI parallel matrix). +// Without this, all browsers run in a single Vitest process. +const selectedBrowser = process.env.BS_BROWSER +const browserConfigurations = selectedBrowser + ? allBrowserConfigurations.filter((c) => c.id === selectedBrowser) + : allBrowserConfigurations + +if (selectedBrowser && browserConfigurations.length === 0) { + const availableIds = allBrowserConfigurations.map((c) => c.id).join(', ') + throw new Error(`Unknown BS_BROWSER "${selectedBrowser}". Available: ${availableIds}`) +} + +// Build env variables that should be replaced at compile time (same as webpack.base.ts) +const buildEnvDefines: Record = { + __BUILD_ENV__SDK_VERSION__: JSON.stringify('test'), + __BUILD_ENV__SDK_SETUP__: JSON.stringify('npm'), + __BUILD_ENV__WORKER_STRING__: JSON.stringify(getBuildEnvValue('WORKER_STRING')), +} + +function createBrowserStackInstance(configuration: (typeof browserConfigurations)[number]) { + return getBrowserStackInstance(configuration, { + username: process.env.BS_USERNAME, + accessKey: process.env.BS_ACCESS_KEY, + localIdentifier: process.env.BROWSERSTACK_LOCAL_IDENTIFIER, + build: getBuildInfos(), + playwrightVersion: browserStackPlaywrightVersion, + }) +} + +// eslint-disable-next-line import-x/no-default-export +export default defineConfig({ + resolve: { + alias: { + // Test utility subpath imports (must come before main package aliases) + '@datadog/browser-core/test': path.resolve('./packages/browser-core/test'), + '@datadog/browser-rum-core/test': path.resolve('./packages/browser-rum-core/test'), + + // Main package aliases (matching tsconfig.base.json paths) + '@datadog/js-core/assembly': path.resolve('./packages/js-core/src/entries/assembly'), + '@datadog/js-core/time': path.resolve('./packages/js-core/src/entries/time'), + '@datadog/js-core/monitor': path.resolve('./packages/js-core/src/entries/monitor'), + '@datadog/js-core/transport': path.resolve('./packages/js-core/src/entries/transport'), + '@datadog/js-core/util': path.resolve('./packages/js-core/src/entries/util'), + '@datadog/browser-core': path.resolve('./packages/browser-core/src'), + '@datadog/browser-flagging': path.resolve('./packages/browser-flagging/src/entries/main'), + '@datadog/browser-logs': path.resolve('./packages/browser-logs/src/entries/main'), + '@datadog/browser-rum-core': path.resolve('./packages/browser-rum-core/src'), + '@datadog/browser-rum/internal': path.resolve('./packages/browser-rum/src/entries/internal'), + '@datadog/browser-rum/internal-synthetics': path.resolve('./packages/browser-rum/src/entries/internalSynthetics'), + '@datadog/browser-rum': path.resolve('./packages/browser-rum/src/entries/main'), + '@datadog/browser-rum-slim': path.resolve('./packages/browser-rum-slim/src/entries/main'), + '@datadog/browser-rum-react/react-router-v6': path.resolve( + './packages/browser-rum-react/src/entries/reactRouterV6' + ), + '@datadog/browser-rum-react/react-router-v7': path.resolve( + './packages/browser-rum-react/src/entries/reactRouterV7' + ), + '@datadog/browser-rum-react/internal': path.resolve('./packages/browser-rum-react/src/entries/internal'), + '@datadog/browser-rum-react': path.resolve('./packages/browser-rum-react/src/entries/main'), + '@datadog/browser-worker': path.resolve('./packages/browser-worker/src/entries/main'), + }, + }, + + define: { + ...buildEnvDefines, + // Some CJS modules use `global` for environment detection; map it to globalThis + // so they patch the real window instead of an empty object. + global: 'globalThis', + }, + + // Transpile project modules to the syntax level supported by the oldest branded browsers in + // the matrix. The Vitest client itself still determines the runner's documented browser floor. + // See https://github.com/vitest-dev/vitest/issues/4304 + oxc: { + target: 'es2020', + }, + + optimizeDeps: { + include: ['pako'], + }, + + test: { + browser: { + enabled: true, + provider: playwright(), + // Use bs-local.com instead of localhost so Safari on BrowserStack can access cookies. + // BrowserStack replaces localhost with bs-local.com for Safari, which breaks cookie-based + // tests when the server hostname doesn't match. + // See https://www.browserstack.com/support/faq/local-testing/local-exceptions/i-face-issues-while-testing-localhost-urls-or-private-servers-in-safari-on-macos-os-x-and-ios + api: { + host: 'bs-local.com', + }, + // Vitest's orchestrator and tester iframe have separate globals, so both need the bootstrap. + // Keep this a classic script so it executes before Vitest's module bundle. + orchestratorScripts: [ + { + src: path.resolve('./test/unit/vitestBrowserPolyfills.js'), + type: 'text/javascript', + }, + ], + testerHtmlPath: './test/unit/vitest.tester.html', + instances: browserConfigurations.map(createBrowserStackInstance), + }, + + // Exclude developer-extension: only compatible with Chrome, no point testing on other browsers + include: ['packages/*/{src,test}/**/*.spec.{ts,tsx}'], + + exclude: ['**/node_modules/**'], + + restoreMocks: true, + + // Many SDK modules intentionally expose test-only reset hooks for process-wide browser state + // and are not safe across parallel files. + fileParallelism: false, + + setupFiles: ['./test/unit/browserstack.keepalive.ts', './test/unit/vitest.setup.ts'], + + sequence: { + shuffle: true, + }, + + onConsoleLog: process.env.CI ? recordUnexpectedConsoleLog : undefined, + + reporters: process.env.CI + ? ['verbose', ['junit', { outputFile: 'test-report/unit-bs/results.xml' }], new UnitTestReporter()] + : ['default', new UnitTestReporter()], + }, +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000..7433e45759 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,114 @@ +import path from 'node:path' + +import { playwright } from '@vitest/browser-playwright' +import { defineConfig } from 'vitest/config' +// eslint-disable-next-line local-rules/disallow-test-import-export-from-src +import { UnitTestReporter, recordUnexpectedConsoleLog } from './scripts/test/lib/unitTestReporter.ts' +import { getBuildEnvValue } from './scripts/lib/buildEnv.ts' +// eslint-disable-next-line local-rules/disallow-test-import-export-from-src +import { getCoverageReportDirectory } from './test/envUtils.ts' + +// Build env variables that should be replaced at compile time (same as webpack.base.ts) +const buildEnvDefines: Record = { + __BUILD_ENV__SDK_VERSION__: JSON.stringify('test'), + __BUILD_ENV__SDK_SETUP__: JSON.stringify('npm'), + __BUILD_ENV__WORKER_STRING__: JSON.stringify(getBuildEnvValue('WORKER_STRING')), +} +const localBrowser = (process.env.VITEST_BROWSER as 'chromium' | 'firefox' | 'webkit' | undefined) ?? 'chromium' + +// eslint-disable-next-line import-x/no-default-export +export default defineConfig({ + resolve: { + alias: { + // Test utility subpath imports (must come before main package aliases) + '@datadog/browser-core/test': path.resolve('./packages/browser-core/test'), + '@datadog/browser-rum-core/test': path.resolve('./packages/browser-rum-core/test'), + + // Main package aliases (matching tsconfig.base.json paths) + '@datadog/js-core/assembly': path.resolve('./packages/js-core/src/entries/assembly'), + '@datadog/js-core/time': path.resolve('./packages/js-core/src/entries/time'), + '@datadog/js-core/monitor': path.resolve('./packages/js-core/src/entries/monitor'), + '@datadog/js-core/transport': path.resolve('./packages/js-core/src/entries/transport'), + '@datadog/js-core/util': path.resolve('./packages/js-core/src/entries/util'), + '@datadog/browser-core': path.resolve('./packages/browser-core/src'), + '@datadog/browser-flagging': path.resolve('./packages/browser-flagging/src/entries/main'), + '@datadog/browser-logs': path.resolve('./packages/browser-logs/src/entries/main'), + '@datadog/browser-rum-core': path.resolve('./packages/browser-rum-core/src'), + '@datadog/browser-rum/internal': path.resolve('./packages/browser-rum/src/entries/internal'), + '@datadog/browser-rum/internal-synthetics': path.resolve('./packages/browser-rum/src/entries/internalSynthetics'), + '@datadog/browser-rum': path.resolve('./packages/browser-rum/src/entries/main'), + '@datadog/browser-rum-slim': path.resolve('./packages/browser-rum-slim/src/entries/main'), + '@datadog/browser-rum-react/react-router-v6': path.resolve( + './packages/browser-rum-react/src/entries/reactRouterV6' + ), + '@datadog/browser-rum-react/react-router-v7': path.resolve( + './packages/browser-rum-react/src/entries/reactRouterV7' + ), + '@datadog/browser-rum-react/internal': path.resolve('./packages/browser-rum-react/src/entries/internal'), + '@datadog/browser-rum-react': path.resolve('./packages/browser-rum-react/src/entries/main'), + '@datadog/browser-worker': path.resolve('./packages/browser-worker/src/entries/main'), + }, + }, + + define: { + ...buildEnvDefines, + // Some CJS modules use `global` for environment detection; map it to globalThis + // so they patch the real window instead of an empty object. + global: 'globalThis', + }, + + optimizeDeps: { + include: ['pako'], + }, + + test: { + browser: { + enabled: true, + provider: playwright(), + headless: true, + // Vitest's client runs in the main page before setupFiles and needs the same legacy-browser + // polyfills as the tester iframe. A classic script executes before its module bundle. + orchestratorScripts: [ + { + src: path.resolve('./test/unit/vitestBrowserPolyfills.js'), + type: 'text/javascript', + }, + ], + // Empty body prevents DOM serialization tests from breaking on Vitest's default tester HTML + testerHtmlPath: './test/unit/vitest.tester.html', + instances: [{ browser: localBrowser }], + }, + + include: ['packages/*/{src,test}/**/*.spec.{ts,tsx}', 'developer-extension/{src,test}/**/*.spec.{ts,tsx}'], + + exclude: ['**/node_modules/**'], + + // Auto-restore all spies after each test. + restoreMocks: true, + + // Browser SDK tests share module-level state. Running files concurrently can make one suite + // mutate another suite's globals and mocks. + fileParallelism: false, + + coverage: { + enabled: true, + provider: 'istanbul', + reportsDirectory: getCoverageReportDirectory(), + reporter: ['text-summary', 'html', ...(process.env.CI ? ['clover'] : [])], + exclude: ['**/*.spec.{ts,tsx}', '**/*.specHelper.{ts,tsx}', '**/*.d.ts', '**/capturedExceptions.ts'], + }, + + setupFiles: ['./test/unit/vitest.setup.ts'], + + // Randomize test order so isolation issues are reproducible with a printed seed. + sequence: { + shuffle: true, + }, + + onConsoleLog: process.env.CI ? recordUnexpectedConsoleLog : undefined, + + reporters: process.env.CI + ? ['default', ['junit', { outputFile: 'test-report/unit/results.xml' }], new UnitTestReporter()] + : ['default', new UnitTestReporter()], + }, +}) diff --git a/yarn.lock b/yarn.lock index c9b64e7ad4..fd813bf891 100644 --- a/yarn.lock +++ b/yarn.lock @@ -132,7 +132,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.7.5": +"@babel/core@npm:^7.29.0": version: 7.29.7 resolution: "@babel/core@npm:7.29.7" dependencies: @@ -345,10 +345,10 @@ __metadata: languageName: node linkType: hard -"@colors/colors@npm:1.5.0": - version: 1.5.0 - resolution: "@colors/colors@npm:1.5.0" - checksum: 10c0/eb42729851adca56d19a08e48d5a1e95efd2a32c55ae0323de8119052be0510d4b7a1611f2abcbf28c044a6c11e6b7d38f99fccdad7429300c37a8ea5fb95b44 +"@blazediff/core@npm:1.9.1": + version: 1.9.1 + resolution: "@blazediff/core@npm:1.9.1" + checksum: 10c0/fd45cdd0544002341d74831a179ef693a81414abd348c1ff0c01086c0ea03f5e5ee284c4e16c2e6fb3670c265f90a3d85752b9360320efa9a835928e604dae77 languageName: node linkType: hard @@ -1345,14 +1345,14 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": +"@istanbuljs/schema@npm:^0.1.3": version: 0.1.6 resolution: "@istanbuljs/schema@npm:0.1.6" checksum: 10c0/bb0d370bf3dd454d2f37f1bccb8921e2da99adacef2da56ef47850e25d7a4de69cf639ead8c189755aef38921369024b4afea3535a5c2ac9082b3e1171bcbc3a languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.13, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: @@ -1396,7 +1396,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:0.3.31, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -1406,19 +1406,6 @@ __metadata: languageName: node linkType: hard -"@jsdevtools/coverage-istanbul-loader@npm:3.0.5": - version: 3.0.5 - resolution: "@jsdevtools/coverage-istanbul-loader@npm:3.0.5" - dependencies: - convert-source-map: "npm:^1.7.0" - istanbul-lib-instrument: "npm:^4.0.3" - loader-utils: "npm:^2.0.0" - merge-source-map: "npm:^1.1.0" - schema-utils: "npm:^2.7.0" - checksum: 10c0/68e885163bada885bcaf7866437072b60d622698228c88919b11c8b9f86fbeae9d8777a32f18638b57b9a745b7343404c55f7e44a0db08218ec7c80c2db0bf9d - languageName: node - linkType: hard - "@jsdevtools/ono@npm:^7.1.3": version: 7.1.3 resolution: "@jsdevtools/ono@npm:7.1.3" @@ -1879,6 +1866,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@puppeteer/browsers@npm:3.0.6": version: 3.0.6 resolution: "@puppeteer/browsers@npm:3.0.6" @@ -2154,10 +2148,10 @@ __metadata: languageName: node linkType: hard -"@socket.io/component-emitter@npm:~3.1.0": - version: 3.1.2 - resolution: "@socket.io/component-emitter@npm:3.1.2" - checksum: 10c0/c4242bad66f67e6f7b712733d25b43cbb9e19a595c8701c3ad99cbeb5901555f78b095e24852f862fffb43e96f1d8552e62def885ca82ae1bb05da3668fd87d7 +"@standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 languageName: node linkType: hard @@ -2429,6 +2423,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f + languageName: node + linkType: hard + "@types/chrome@npm:0.2.2": version: 0.2.2 resolution: "@types/chrome@npm:0.2.2" @@ -2448,7 +2452,7 @@ __metadata: languageName: node linkType: hard -"@types/cors@npm:2.8.19, @types/cors@npm:^2.8.12": +"@types/cors@npm:2.8.19": version: 2.8.19 resolution: "@types/cors@npm:2.8.19" dependencies: @@ -2457,6 +2461,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + "@types/esrecurse@npm:^4.3.1": version: 4.3.1 resolution: "@types/esrecurse@npm:4.3.1" @@ -2540,13 +2551,6 @@ __metadata: languageName: node linkType: hard -"@types/jasmine@npm:3.10.19": - version: 3.10.19 - resolution: "@types/jasmine@npm:3.10.19" - checksum: 10c0/162b97e986e73ad9e32fadc565bc2cbde8295d39d6b20980a40647c4fe4c8867fc75be05593b6d143df2978b4720aa5648379451336121644cd52729ae77d31c - languageName: node - linkType: hard - "@types/jsesc@npm:^2.5.0": version: 2.5.1 resolution: "@types/jsesc@npm:2.5.1" @@ -2554,7 +2558,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -2584,7 +2588,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0": +"@types/node@npm:*": version: 26.1.1 resolution: "@types/node@npm:26.1.1" dependencies: @@ -2674,7 +2678,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:8.18.1, @types/ws@npm:^8.5.12": +"@types/ws@npm:8.18.1": version: 8.18.1 resolution: "@types/ws@npm:8.18.1" dependencies: @@ -2732,6 +2736,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.63.0": + version: 8.63.0 + resolution: "@typescript-eslint/project-service@npm:8.63.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.63.0" + "@typescript-eslint/types": "npm:^8.63.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/d49ce51b128d83a0e87e01d7a30f2295d77713d913abddc817df7ac3aeb1333b5dbb0c634ad0d0941f51cdd84ad86a036178b90bb370aa819ff59082377ca0ee + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.62.1": version: 8.62.1 resolution: "@typescript-eslint/scope-manager@npm:8.62.1" @@ -2742,6 +2759,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.63.0, @typescript-eslint/scope-manager@npm:^8.58.0": + version: 8.63.0 + resolution: "@typescript-eslint/scope-manager@npm:8.63.0" + dependencies: + "@typescript-eslint/types": "npm:8.63.0" + "@typescript-eslint/visitor-keys": "npm:8.63.0" + checksum: 10c0/394245a25f852170adbdaaca9364d5d36a0e97e69e691e5594d5a3ced892a7eb78cf6e7a17cbc095490a168b60bb3ad6f6d48cd167c4b9cbc568bf0f785ab926 + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.62.1": version: 8.62.1 resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.1" @@ -2751,7 +2778,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:^8.62.1": +"@typescript-eslint/tsconfig-utils@npm:8.63.0, @typescript-eslint/tsconfig-utils@npm:^8.62.1, @typescript-eslint/tsconfig-utils@npm:^8.63.0": version: 8.63.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.63.0" peerDependencies: @@ -2783,7 +2810,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.59.4, @typescript-eslint/types@npm:^8.62.1": +"@typescript-eslint/types@npm:8.63.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.59.4, @typescript-eslint/types@npm:^8.62.1, @typescript-eslint/types@npm:^8.63.0": version: 8.63.0 resolution: "@typescript-eslint/types@npm:8.63.0" checksum: 10c0/270bd2b2affdc173dd7e9e1bf1419a6f7a2fa8ff1fca5c613da88f6e44c433a1412887e513d24233a4a6112bdc3439d3e4cadc4f492eb1a6dc2a377495a38836 @@ -2809,6 +2836,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.63.0": + version: 8.63.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.63.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.63.0" + "@typescript-eslint/tsconfig-utils": "npm:8.63.0" + "@typescript-eslint/types": "npm:8.63.0" + "@typescript-eslint/visitor-keys": "npm:8.63.0" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/84ca87141a0e7d59fb91e7baef36e20d4f00f1996aec6dfb1909c40496f13e54a13102c12b7cbe89285d04ca7975faa64d5683ff0d44ec9baa7be5207540142c + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.62.1": version: 8.62.1 resolution: "@typescript-eslint/utils@npm:8.62.1" @@ -2824,6 +2870,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.58.0": + version: 8.63.0 + resolution: "@typescript-eslint/utils@npm:8.63.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.63.0" + "@typescript-eslint/types": "npm:8.63.0" + "@typescript-eslint/typescript-estree": "npm:8.63.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/1b0bb66c2dc11d7c3ea4e840294e35062e5f85b6d1d7d94b37e94e52a2fa71e76adf277602a183832c23d3deeb1f129d7aeb3ce960c1fc9b6d1136a4d61bd54a + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.62.1": version: 8.62.1 resolution: "@typescript-eslint/visitor-keys@npm:8.62.1" @@ -2834,6 +2895,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.63.0": + version: 8.63.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.63.0" + dependencies: + "@typescript-eslint/types": "npm:8.63.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/595b47b08efecc35d93a8ac072798f9dc2371a371f03a1a03ab134a8505fe422fc647014bd096eac75a3d487c5eecf24393048ca88ed06f759d00115c11dd8bc + languageName: node + linkType: hard + "@unrs/resolver-binding-android-arm-eabi@npm:1.12.2": version: 1.12.2 resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.12.2" @@ -3010,6 +3081,165 @@ __metadata: languageName: node linkType: hard +"@vitest/browser-playwright@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/browser-playwright@npm:4.1.8" + dependencies: + "@vitest/browser": "npm:4.1.8" + "@vitest/mocker": "npm:4.1.8" + tinyrainbow: "npm:^3.1.0" + peerDependencies: + playwright: "*" + vitest: 4.1.8 + peerDependenciesMeta: + playwright: + optional: false + checksum: 10c0/aa2a9b7a9614f6a4860271e1eef76873a1970a534e59c5ffc7147d13611cbcab38f57f1de418ebaeb89b2c3e675be3b4f17425ac6d0198a1eecf68fe9e517857 + languageName: node + linkType: hard + +"@vitest/browser@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/browser@npm:4.1.8" + dependencies: + "@blazediff/core": "npm:1.9.1" + "@vitest/mocker": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + magic-string: "npm:^0.30.21" + pngjs: "npm:^7.0.0" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.1.0" + ws: "npm:^8.19.0" + peerDependencies: + vitest: 4.1.8 + checksum: 10c0/9e78c4b2273f5defd43e822361ec9c6f2804e9144fc5679a69171a588476c945a1ae3de313ad77239683490a3df703766257efc8a509c1f244c4b8f3b9ab4fe6 + languageName: node + linkType: hard + +"@vitest/coverage-istanbul@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/coverage-istanbul@npm:4.1.8" + dependencies: + "@babel/core": "npm:^7.29.0" + "@istanbuljs/schema": "npm:^0.1.3" + "@jridgewell/gen-mapping": "npm:^0.3.13" + "@jridgewell/trace-mapping": "npm:0.3.31" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.2" + obug: "npm:^2.1.1" + tinyrainbow: "npm:^3.1.0" + peerDependencies: + vitest: 4.1.8 + checksum: 10c0/cb4d1df51fbe078391f17da0d09f2e7d765aa001bbaebaa6665ae9d356d9fd34f62b3739da175492b566cd2f35a3fd9d1cda61f35544f99b88611566931a772e + languageName: node + linkType: hard + +"@vitest/eslint-plugin@npm:1.6.22": + version: 1.6.22 + resolution: "@vitest/eslint-plugin@npm:1.6.22" + dependencies: + "@typescript-eslint/scope-manager": "npm:^8.58.0" + "@typescript-eslint/utils": "npm:^8.58.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": "*" + eslint: ">=8.57.0" + typescript: ">=5.0.0" + vitest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + typescript: + optional: true + vitest: + optional: true + checksum: 10c0/894b87936c98ea9eeeb26ce08fa297fa52f1c48c27d97d2ec437acad28644d639fb21c7fe8fe58ce47e483f7596d80100b45a14bf95d9b6d3454bd274bfa28f0 + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/expect@npm:4.1.8" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/f7bf6c720d2427c3bd0b35472ebd84d963be7d09ecf52a0fb05e8c4d5d0c9ee164a8c28eee6360947be1b245b47faefab54560cb98e5cb678c1c1074260b9149 + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/mocker@npm:4.1.8" + dependencies: + "@vitest/spy": "npm:4.1.8" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f8cb2b8b55dc2cba0b2399aeee528b0187042f22cbc2d50a4fd6141f5aa246ebc41700f45dd1d73eca44ddfb57dcde48b2eb317bfbb1198f5ab2cc4fd04b2ea0 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/pretty-format@npm:4.1.8" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/553c456692a4b9ae13cd116c234c74b4495e0f1a0d5c51ffc3fab8ea085e3550769967e29db79bdac0cf127b1bf88b7f70bfba3dcc72be6bddf834433e30cc91 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/runner@npm:4.1.8" + dependencies: + "@vitest/utils": "npm:4.1.8" + pathe: "npm:^2.0.3" + checksum: 10c0/706808a4b7b95ea9a9268fc152dd39e15a9a754f37c7990aea167486a9094caa913dae454771ae02c18dccfabd667f8cc38eed33a1307a79d32a89878b5bcce1 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/snapshot@npm:4.1.8" + dependencies: + "@vitest/pretty-format": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/ba4c32112491d42d24986f921c50ede5edbdb4b7eafa16c72cf8d2c9ecc44121fdb3d9365236747a9841f0d6776affc6457470fcbb082df9dbc28c24792a0c6d + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/spy@npm:4.1.8" + checksum: 10c0/3c10c0325a09d16bc0e77c0be96c47c15416186e33332880c0d1dd0a51d51a866091067b81f2a2ef6fb422a7760e6cf15c04d91a0eca4d59f62e8c8401fa53fc + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/utils@npm:4.1.8" + dependencies: + "@vitest/pretty-format": "npm:4.1.8" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/acda9d3d640c1ebc81afb358ac30589d7d7d583af81e2d09419f0af9cbe41f3ce0b90527326943bf0da51614be5fc31afcd32259f6beb32b3417999d6ef380f3 + languageName: node + linkType: hard + "@vue-macros/common@npm:^3.1.1": version: 3.1.2 resolution: "@vue-macros/common@npm:3.1.2" @@ -3431,16 +3661,6 @@ __metadata: languageName: node linkType: hard -"accepts@npm:~1.3.4": - version: 1.3.8 - resolution: "accepts@npm:1.3.8" - dependencies: - mime-types: "npm:~2.1.34" - negotiator: "npm:0.6.3" - checksum: 10c0/3a35c5f5586cfb9a21163ca47a5f77ac34fa8ceb5d17d2fa2c0d81f41cbd7f8c6fa52c77e2c039acc0f4d09e71abdc51144246900f6bef5e3c4b333f77d89362 - languageName: node - linkType: hard - "acorn-import-phases@npm:^1.0.3": version: 1.0.4 resolution: "acorn-import-phases@npm:1.0.4" @@ -3484,15 +3704,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^4.3.0": - version: 4.3.0 - resolution: "agent-base@npm:4.3.0" - dependencies: - es6-promisify: "npm:^5.0.0" - checksum: 10c0/a618d4e4ca7c0c2023b2664346570773455c501a930718764f65016a8a9eea6d2ab5ba54255589e46de529bab4026a088523dce17f94e34ba385af1f644febe1 - languageName: node - linkType: hard - "ajv-draft-04@npm:~1.0.0": version: 1.0.0 resolution: "ajv-draft-04@npm:1.0.0" @@ -3533,15 +3744,6 @@ __metadata: languageName: node linkType: hard -"ajv-keywords@npm:^3.5.2": - version: 3.5.2 - resolution: "ajv-keywords@npm:3.5.2" - peerDependencies: - ajv: ^6.9.1 - checksum: 10c0/0c57a47cbd656e8cdfd99d7c2264de5868918ffa207c8d7a72a7f63379d4333254b2ba03d69e3c035e996a3fd3eb6d5725d7a1597cca10694296e32510546360 - languageName: node - linkType: hard - "ajv-keywords@npm:^5.1.0": version: 5.1.0 resolution: "ajv-keywords@npm:5.1.0" @@ -3565,7 +3767,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4, ajv@npm:^6.14.0": +"ajv@npm:^6.14.0": version: 6.15.0 resolution: "ajv@npm:6.15.0" dependencies: @@ -3644,16 +3846,6 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:~3.1.2": - version: 3.1.3 - resolution: "anymatch@npm:3.1.3" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac - languageName: node - linkType: hard - "are-docs-informative@npm:^0.0.2": version: 0.0.2 resolution: "are-docs-informative@npm:0.0.2" @@ -3691,6 +3883,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "ast-kit@npm:^2.1.2, ast-kit@npm:^2.2.0": version: 2.2.0 resolution: "ast-kit@npm:2.2.0" @@ -3784,13 +3983,6 @@ __metadata: languageName: node linkType: hard -"base64id@npm:2.0.0, base64id@npm:~2.0.0": - version: 2.0.0 - resolution: "base64id@npm:2.0.0" - checksum: 10c0/6919efd237ed44b9988cbfc33eca6f173a10e810ce50292b271a1a421aac7748ef232a64d1e6032b08f19aae48dce6ee8f66c5ae2c9e5066c82b884861d4d453 - languageName: node - linkType: hard - "baseline-browser-mapping@npm:^2.10.42, baseline-browser-mapping@npm:^2.9.19": version: 2.10.42 resolution: "baseline-browser-mapping@npm:2.10.42" @@ -3809,20 +4001,6 @@ __metadata: languageName: node linkType: hard -"big.js@npm:^5.2.2": - version: 5.2.2 - resolution: "big.js@npm:5.2.2" - checksum: 10c0/230520f1ff920b2d2ce3e372d77a33faa4fa60d802fe01ca4ffbc321ee06023fe9a741ac02793ee778040a16b7e497f7d60c504d1c402b8fdab6f03bb785a25f - languageName: node - linkType: hard - -"binary-extensions@npm:^2.0.0": - version: 2.3.0 - resolution: "binary-extensions@npm:2.3.0" - checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 - languageName: node - linkType: hard - "birpc@npm:^2.6.1": version: 2.9.0 resolution: "birpc@npm:2.9.0" @@ -3844,26 +4022,6 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:^1.19.0": - version: 1.20.5 - resolution: "body-parser@npm:1.20.5" - dependencies: - bytes: "npm:~3.1.2" - content-type: "npm:~1.0.5" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - destroy: "npm:~1.2.0" - http-errors: "npm:~2.0.1" - iconv-lite: "npm:~0.4.24" - on-finished: "npm:~2.4.1" - qs: "npm:~6.15.1" - raw-body: "npm:~2.5.3" - type-is: "npm:~1.6.18" - unpipe: "npm:~1.0.0" - checksum: 10c0/ad777ca5e4711eae253c93f50fdc4608c60b76a9710d79e5e5b84581c76691e6ad21ecc9158986d9ea2b365df73e403ca33c27a8bccc1a7cfc2ccc248548118d - languageName: node - linkType: hard - "body-parser@npm:^2.2.1": version: 2.3.0 resolution: "body-parser@npm:2.3.0" @@ -3939,22 +4097,12 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:~3.0.2": - version: 3.0.3 - resolution: "braces@npm:3.0.3" - dependencies: - fill-range: "npm:^7.1.1" - checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 - languageName: node - linkType: hard - "browser-sdk@workspace:.": version: 0.0.0-use.local resolution: "browser-sdk@workspace:." dependencies: "@datadog/rum-events-format": "DataDog/rum-events-format#commit=31cc845ba1cb52086a25424de86731c316b2385e" "@eslint/js": "npm:10.0.1" - "@jsdevtools/coverage-istanbul-loader": "npm:3.0.5" "@microsoft/api-extractor": "npm:7.58.9" "@playwright/test": "npm:1.61.1" "@swc/core": "npm:1.15.43" @@ -3962,11 +4110,14 @@ __metadata: "@types/chrome": "npm:0.2.2" "@types/cors": "npm:2.8.19" "@types/express": "npm:5.0.6" - "@types/jasmine": "npm:3.10.19" "@types/node": "npm:26.1.0" "@types/node-forge": "npm:1.3.14" "@types/ws": "npm:8.18.1" "@typescript-eslint/utils": "npm:8.62.1" + "@vitest/browser": "npm:4.1.8" + "@vitest/browser-playwright": "npm:4.1.8" + "@vitest/coverage-istanbul": "npm:4.1.8" + "@vitest/eslint-plugin": "npm:1.6.22" ajv: "npm:8.20.0" browserstack-local: "npm:1.5.13" busboy: "npm:1.6.0" @@ -3976,26 +4127,17 @@ __metadata: eslint: "npm:10.6.0" eslint-import-resolver-typescript: "npm:4.4.5" eslint-plugin-import-x: "npm:4.17.1" - eslint-plugin-jasmine: "npm:4.2.2" eslint-plugin-jsdoc: "npm:63.0.12" eslint-plugin-unicorn: "npm:65.0.1" express: "npm:5.2.1" globals: "npm:17.7.0" html-webpack-plugin: "npm:5.6.7" http-server: "npm:14.1.1" - jasmine-core: "npm:3.99.1" json-schema-to-typescript: "bcaudan/json-schema-to-typescript#bcaudan/add-readonly-support" - karma: "npm:6.4.4" - karma-browserstack-launcher: "npm:1.6.0" - karma-chrome-launcher: "npm:3.2.0" - karma-coverage-istanbul-reporter: "npm:3.0.3" - karma-jasmine: "npm:4.0.2" - karma-junit-reporter: "npm:2.0.1" - karma-sourcemap-loader: "npm:0.4.0" - karma-spec-reporter: "npm:0.0.36" - karma-webpack: "npm:5.0.0" minimatch: "npm:10.2.5" node-forge: "npm:1.4.0" + playwright: "npm:1.61.1" + playwright-browserstack: "npm:playwright@1.59.1" prettier: "npm:3.9.4" puppeteer: "npm:25.3.0" swc-loader: "npm:0.2.7" @@ -4007,6 +4149,7 @@ __metadata: typescript: "npm:6.0.3" typescript-eslint: "npm:8.62.1" undici: "npm:8.7.0" + vitest: "npm:4.1.8" webpack: "npm:5.108.4" webpack-cli: "npm:7.2.1" webpack-dev-middleware: "npm:8.0.3" @@ -4029,7 +4172,7 @@ __metadata: languageName: node linkType: hard -"browserstack-local@npm:1.5.13, browserstack-local@npm:^1.3.7": +"browserstack-local@npm:1.5.13": version: 1.5.13 resolution: "browserstack-local@npm:1.5.13" dependencies: @@ -4041,15 +4184,6 @@ __metadata: languageName: node linkType: hard -"browserstack@npm:~1.5.1": - version: 1.5.3 - resolution: "browserstack@npm:1.5.3" - dependencies: - https-proxy-agent: "npm:^2.2.1" - checksum: 10c0/118704796f0a83aab4d23c7aa506a80cb4cd8338ddafe1fb184cb664c6d9c5527d4748df1529deeeffce53cd6f1e123f329c328ff8a4443ebc6698255a4b3c75 - languageName: node - linkType: hard - "buffer-crc32@npm:^1.0.0": version: 1.0.0 resolution: "buffer-crc32@npm:1.0.0" @@ -4152,7 +4286,7 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": +"call-bound@npm:^1.0.2": version: 1.0.4 resolution: "call-bound@npm:1.0.4" dependencies: @@ -4186,6 +4320,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + "chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -4210,25 +4351,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.1": - version: 3.6.0 - resolution: "chokidar@npm:3.6.0" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 - languageName: node - linkType: hard - "chokidar@npm:^5.0.0": version: 5.0.0 resolution: "chokidar@npm:5.0.0" @@ -4344,17 +4466,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^7.0.2": - version: 7.0.4 - resolution: "cliui@npm:7.0.4" - dependencies: - string-width: "npm:^4.2.0" - strip-ansi: "npm:^6.0.0" - wrap-ansi: "npm:^7.0.0" - checksum: 10c0/6035f5daf7383470cef82b3d3db00bec70afb3423538c50394386ffbbab135e26c3689c41791f911fa71b62d13d3863c712fdd70f0fbdffd938a1e6fd09aac00 - languageName: node - linkType: hard - "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -4411,13 +4522,6 @@ __metadata: languageName: node linkType: hard -"colors@npm:1.4.0": - version: 1.4.0 - resolution: "colors@npm:1.4.0" - checksum: 10c0/9af357c019da3c5a098a301cf64e3799d27549d8f185d86f79af23069e4f4303110d115da98483519331f6fb71c8568d5688fa1c6523600044fd4a54e97c4efb - languageName: node - linkType: hard - "commander@npm:2.9.0": version: 2.9.0 resolution: "commander@npm:2.9.0" @@ -4524,18 +4628,6 @@ __metadata: languageName: node linkType: hard -"connect@npm:^3.7.0": - version: 3.7.0 - resolution: "connect@npm:3.7.0" - dependencies: - debug: "npm:2.6.9" - finalhandler: "npm:1.1.2" - parseurl: "npm:~1.3.3" - utils-merge: "npm:1.0.1" - checksum: 10c0/f120c6116bb16a0a7d2703c0b4a0cd7ed787dc5ec91978097bf62aa967289020a9f41a9cd3c3276a7b92aaa36f382d2cd35fed7138fd466a55c8e9fdbed11ca8 - languageName: node - linkType: hard - "consola@npm:^3.4.2": version: 3.4.2 resolution: "consola@npm:3.4.2" @@ -4550,7 +4642,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.5, content-type@npm:~1.0.5": +"content-type@npm:^1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -4564,13 +4656,6 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.7.0": - version: 1.9.0 - resolution: "convert-source-map@npm:1.9.0" - checksum: 10c0/281da55454bf8126cbc6625385928c43479f2060984180c42f3a86c8b8c12720a24eac260624a7d1e090004028d2dee78602330578ceec1a08e27cb8bb0a8a5b - languageName: node - linkType: hard - "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -4592,7 +4677,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.7.1, cookie@npm:~0.7.2": +"cookie@npm:^0.7.1": version: 0.7.2 resolution: "cookie@npm:0.7.2" checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 @@ -4622,7 +4707,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:2.8.6, cors@npm:~2.8.5": +"cors@npm:2.8.6": version: 2.8.6 resolution: "cors@npm:2.8.6" dependencies: @@ -4704,20 +4789,6 @@ __metadata: languageName: node linkType: hard -"custom-event@npm:~1.0.0": - version: 1.0.1 - resolution: "custom-event@npm:1.0.1" - checksum: 10c0/86cd8497328b1e17dcda894c8df34a73b7a99f915123940d39b33c709482b2d3a2e689cd5e79e4775eb4167227689f57a2ae2f99a3f0bc9c54c0ac1b06853bd5 - languageName: node - linkType: hard - -"date-format@npm:^4.0.14": - version: 4.0.14 - resolution: "date-format@npm:4.0.14" - checksum: 10c0/1c67a4d77c677bb880328c81d81f5b9ed7fbf672bdaff74e5a0f7314b21188f3a829b06acf120c70cc1df876a7724e3e5c23d511e86d64656a3035a76ac3930b - languageName: node - linkType: hard - "debounce@npm:1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -4725,16 +4796,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9": - version: 2.6.9 - resolution: "debug@npm:2.6.9" - dependencies: - ms: "npm:2.0.0" - checksum: 10c0/121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3, debug@npm:~4.4.1": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -4746,15 +4808,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.1.0": - version: 3.2.7 - resolution: "debug@npm:3.2.7" - dependencies: - ms: "npm:^2.1.1" - checksum: 10c0/37d96ae42cbc71c14844d2ae3ba55adf462ec89fd3a999459dec3833944cd999af6007ff29c780f1c61153bcaaf2c842d1e4ce1ec621e4fc4923244942e4a02a - languageName: node - linkType: hard - "debug@npm:~4.3.1": version: 4.3.7 resolution: "debug@npm:4.3.7" @@ -4819,7 +4872,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": +"depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c @@ -4840,13 +4893,6 @@ __metadata: languageName: node linkType: hard -"destroy@npm:~1.2.0": - version: 1.2.0 - resolution: "destroy@npm:1.2.0" - checksum: 10c0/bd7633942f57418f5a3b80d5cb53898127bcf53e24cdf5d5f4396be471417671f0fee48a4ebe9a1e9defbde2a31280011af58a57e090ff822f589b443ed4e643 - languageName: node - linkType: hard - "detect-indent@npm:^7.0.2": version: 7.0.2 resolution: "detect-indent@npm:7.0.2" @@ -4875,13 +4921,6 @@ __metadata: languageName: node linkType: hard -"di@npm:^0.0.1": - version: 0.0.1 - resolution: "di@npm:0.0.1" - checksum: 10c0/fbca4cc93e8c493d50f82df3a9ecaa5d8b2935674aabddeb8f68db3ab03c942c201f9c3d920de094407392ee6f488eac16b96f500c0ea6b408634864b7b939d1 - languageName: node - linkType: hard - "diff@npm:~8.0.2": version: 8.0.4 resolution: "diff@npm:8.0.4" @@ -4898,18 +4937,6 @@ __metadata: languageName: node linkType: hard -"dom-serialize@npm:^2.2.1": - version: 2.2.1 - resolution: "dom-serialize@npm:2.2.1" - dependencies: - custom-event: "npm:~1.0.0" - ent: "npm:~2.2.0" - extend: "npm:^3.0.0" - void-elements: "npm:^2.0.0" - checksum: 10c0/ceb6e62b73c658986ca4c9b8b2fae358d8ae914eb06712d137da595a327c3bbca45a762f412a6d181f892ce5e3cffb855c2db2b64c53ad0534b2a0ad8e65b05e - languageName: node - linkType: hard - "dom-serializer@npm:^1.0.1": version: 1.4.1 resolution: "dom-serializer@npm:1.4.1" @@ -5154,13 +5181,6 @@ __metadata: languageName: node linkType: hard -"emojis-list@npm:^3.0.0": - version: 3.0.0 - resolution: "emojis-list@npm:3.0.0" - checksum: 10c0/7dc4394b7b910444910ad64b812392159a21e1a7ecc637c775a440227dcb4f80eff7fe61f4453a7d7603fa23d23d30cc93fe9e4b5ed985b88d6441cd4a35117b - languageName: node - linkType: hard - "empathic@npm:^2.0.1": version: 2.0.1 resolution: "empathic@npm:2.0.1" @@ -5175,57 +5195,13 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:~1.0.2": - version: 1.0.2 - resolution: "encodeurl@npm:1.0.2" - checksum: 10c0/f6c2387379a9e7c1156c1c3d4f9cb7bb11cf16dd4c1682e1f6746512564b053df5781029b6061296832b59fb22f459dbe250386d217c2f6e203601abb2ee0bec - languageName: node - linkType: hard - -"engine.io-parser@npm:~5.2.1": - version: 5.2.3 - resolution: "engine.io-parser@npm:5.2.3" - checksum: 10c0/ed4900d8dbef470ab3839ccf3bfa79ee518ea8277c7f1f2759e8c22a48f64e687ea5e474291394d0c94f84054749fd93f3ef0acb51fa2f5f234cc9d9d8e7c536 - languageName: node - linkType: hard - -"engine.io@npm:~6.6.0": - version: 6.6.9 - resolution: "engine.io@npm:6.6.9" - dependencies: - "@types/cors": "npm:^2.8.12" - "@types/node": "npm:>=10.0.0" - "@types/ws": "npm:^8.5.12" - accepts: "npm:~1.3.4" - base64id: "npm:2.0.0" - cookie: "npm:~0.7.2" - cors: "npm:~2.8.5" - debug: "npm:~4.4.1" - engine.io-parser: "npm:~5.2.1" - ws: "npm:~8.21.0" - checksum: 10c0/8e4a64b6e3861ffc3ab3fa7f864fd06358b918b85696c5fb53113ddde6661bb2f31f2d091a2613511ff16dbb0bff970a166e5c2dfaf2be51a022f7e6573b1153 - languageName: node - linkType: hard - -"enhanced-resolve@npm:^5.22.2, enhanced-resolve@npm:^5.7.0": - version: 5.24.2 - resolution: "enhanced-resolve@npm:5.24.2" - dependencies: - graceful-fs: "npm:^4.2.4" - tapable: "npm:^2.3.3" - checksum: 10c0/eede58444780fc10d881af1c6bfdf6be561a834445e96f433de357a996fced4f58500674f3763679bf7242d775c24742f4fe858366950e7c238512ea51d7d23b - languageName: node - linkType: hard - -"ent@npm:~2.2.0": - version: 2.2.2 - resolution: "ent@npm:2.2.2" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - punycode: "npm:^1.4.1" - safe-regex-test: "npm:^1.1.0" - checksum: 10c0/83673cc952bb1ca01473460eb4f1289448d887ef2bfcdd142bfe83cd20a794a4393b6bca543922bf1eb913d1ae0ab69ca2d2f1f6a5e9f3de6e68464b3a3b9096 +"enhanced-resolve@npm:^5.22.2, enhanced-resolve@npm:^5.7.0": + version: 5.24.2 + resolution: "enhanced-resolve@npm:5.24.2" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.3.3" + checksum: 10c0/eede58444780fc10d881af1c6bfdf6be561a834445e96f433de357a996fced4f58500674f3763679bf7242d775c24742f4fe858366950e7c238512ea51d7d23b languageName: node linkType: hard @@ -5326,22 +5302,6 @@ __metadata: languageName: node linkType: hard -"es6-promise@npm:^4.0.3": - version: 4.2.8 - resolution: "es6-promise@npm:4.2.8" - checksum: 10c0/2373d9c5e9a93bdd9f9ed32ff5cb6dd3dd785368d1c21e9bbbfd07d16345b3774ae260f2bd24c8f836a6903f432b4151e7816a7fa8891ccb4e1a55a028ec42c3 - languageName: node - linkType: hard - -"es6-promisify@npm:^5.0.0": - version: 5.0.0 - resolution: "es6-promisify@npm:5.0.0" - dependencies: - es6-promise: "npm:^4.0.3" - checksum: 10c0/23284c6a733cbf7842ec98f41eac742c9f288a78753c4fe46652bae826446ced7615b9e8a5c5f121a08812b1cd478ea58630f3e1c3d70835bd5dcd69c7cd75c9 - languageName: node - linkType: hard - "esbuild@npm:^0.27.1": version: 0.27.7 resolution: "esbuild@npm:0.27.7" @@ -5445,7 +5405,7 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": +"escape-html@npm:^1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 @@ -5531,13 +5491,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jasmine@npm:4.2.2": - version: 4.2.2 - resolution: "eslint-plugin-jasmine@npm:4.2.2" - checksum: 10c0/dccc3b54f3312b4917b4e9c816c9d128a264a36fa127ea65a72587cb47c7d094dc92f1bcbb40ae6770d68c5f5bbbb02b263bd6e4d6d8b7b35427c46bca1f20e6 - languageName: node - linkType: hard - "eslint-plugin-jsdoc@npm:63.0.12": version: 63.0.12 resolution: "eslint-plugin-jsdoc@npm:63.0.12" @@ -5762,6 +5715,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.3.0": + version: 1.4.0 + resolution: "expect-type@npm:1.4.0" + checksum: 10c0/d40d76b8570695d36587beb3cc28494da2ca3ec8f04e67f5622ed2d372d850e401a9adef19c6835e1a8173903f157c79540b34c7b3fbd7cd8ce726cc903c57b7 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.3 resolution: "exponential-backoff@npm:3.1.3" @@ -5812,13 +5772,6 @@ __metadata: languageName: node linkType: hard -"extend@npm:^3.0.0": - version: 3.0.2 - resolution: "extend@npm:3.0.2" - checksum: 10c0/73bf6e27406e80aa3e85b0d1c4fd987261e628064e170ca781125c0b635a3dabad5e05adbf07595ea0cf1e6c5396cacb214af933da7cbaf24fe75ff14818e8f9 - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -5882,30 +5835,6 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.1.1": - version: 7.1.1 - resolution: "fill-range@npm:7.1.1" - dependencies: - to-regex-range: "npm:^5.0.1" - checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 - languageName: node - linkType: hard - -"finalhandler@npm:1.1.2": - version: 1.1.2 - resolution: "finalhandler@npm:1.1.2" - dependencies: - debug: "npm:2.6.9" - encodeurl: "npm:~1.0.2" - escape-html: "npm:~1.0.3" - on-finished: "npm:~2.3.0" - parseurl: "npm:~1.3.3" - statuses: "npm:~1.5.0" - unpipe: "npm:~1.0.0" - checksum: 10c0/6a96e1f5caab085628c11d9fdceb82ba608d5e426c6913d4d918409baa271037a47f28fbba73279e8ad614f0b8fa71ea791d265e408d760793829edd8c2f4584 - languageName: node - linkType: hard - "finalhandler@npm:^2.1.0": version: 2.1.1 resolution: "finalhandler@npm:2.1.1" @@ -5981,7 +5910,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.7, flatted@npm:^3.2.9": +"flatted@npm:^3.2.9": version: 3.4.2 resolution: "flatted@npm:3.4.2" checksum: 10c0/a65b67aae7172d6cdf63691be7de6c5cd5adbdfdfe2e9da1a09b617c9512ed794037741ee53d93114276bff3f93cd3b0d97d54f9b316e1e4885dde6e9ffdf7ed @@ -6047,24 +5976,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^8.1.0": - version: 8.1.0 - resolution: "fs-extra@npm:8.1.0" - dependencies: - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^4.0.0" - universalify: "npm:^0.1.0" - checksum: 10c0/259f7b814d9e50d686899550c4f9ded85c46c643f7fe19be69504888e007fcbc08f306fae8ec495b8b998635e997c9e3e175ff2eeed230524ef1c1684cc96423 - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 - languageName: node - linkType: hard - "fsevents@npm:2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -6075,7 +5986,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": +"fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -6094,7 +6005,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -6235,15 +6146,6 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:~5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: "npm:^4.0.1" - checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee - languageName: node - linkType: hard - "glob-to-regex.js@npm:^1.0.0, glob-to-regex.js@npm:^1.0.1": version: 1.2.0 resolution: "glob-to-regex.js@npm:1.2.0" @@ -6276,20 +6178,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.3, glob@npm:^7.1.7": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.1.1" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10c0/65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe - languageName: node - linkType: hard - "global-directory@npm:^4.0.1": version: 4.0.1 resolution: "global-directory@npm:4.0.1" @@ -6320,7 +6208,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -6348,22 +6236,13 @@ __metadata: languageName: node linkType: hard -"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": +"has-symbols@npm:^1.1.0": version: 1.1.0 resolution: "has-symbols@npm:1.1.0" checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e languageName: node linkType: hard -"has-tostringtag@npm:^1.0.2": - version: 1.0.2 - resolution: "has-tostringtag@npm:1.0.2" - dependencies: - has-symbols: "npm:^1.0.3" - checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c - languageName: node - linkType: hard - "hasown@npm:^2.0.2, hasown@npm:^2.0.3": version: 2.0.4 resolution: "hasown@npm:2.0.4" @@ -6535,16 +6414,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^2.2.1": - version: 2.2.4 - resolution: "https-proxy-agent@npm:2.2.4" - dependencies: - agent-base: "npm:^4.3.0" - debug: "npm:^3.1.0" - checksum: 10c0/4bdde8fcd9ea0adc4a77282de2b4f9e27955e0441425af0f27f0fe01006946b80eaee6749e08e838d350c06ed2ebd5d11347d3beb88c45eacb0667e27276cdad - languageName: node - linkType: hard - "https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" @@ -6580,15 +6449,6 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:~0.4.24": - version: 0.4.24 - resolution: "iconv-lite@npm:0.4.24" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3" - checksum: 10c0/c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 - languageName: node - linkType: hard - "ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -6657,17 +6517,7 @@ __metadata: languageName: node linkType: hard -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: "npm:^1.3.0" - wrappy: "npm:1" - checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 - languageName: node - linkType: hard - -"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:~2.0.3, inherits@npm:~2.0.4": +"inherits@npm:^2.0.3, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -6725,15 +6575,6 @@ __metadata: languageName: node linkType: hard -"is-binary-path@npm:~2.1.0": - version: 2.1.0 - resolution: "is-binary-path@npm:2.1.0" - dependencies: - binary-extensions: "npm:^2.0.0" - checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 - languageName: node - linkType: hard - "is-builtin-module@npm:^5.0.0": version: 5.0.0 resolution: "is-builtin-module@npm:5.0.0" @@ -6802,7 +6643,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -6855,13 +6696,6 @@ __metadata: languageName: node linkType: hard -"is-number@npm:^7.0.0": - version: 7.0.0 - resolution: "is-number@npm:7.0.0" - checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 - languageName: node - linkType: hard - "is-path-inside@npm:^4.0.0": version: 4.0.0 resolution: "is-path-inside@npm:4.0.0" @@ -6899,18 +6733,6 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.2.1": - version: 1.2.1 - resolution: "is-regex@npm:1.2.1" - dependencies: - call-bound: "npm:^1.0.2" - gopd: "npm:^1.2.0" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10c0/1d3715d2b7889932349241680032e85d0b492cfcb045acb75ffc2c3085e8d561184f1f7e84b6f8321935b4aea39bc9c6ba74ed595b57ce4881a51dfdbc214e04 - languageName: node - linkType: hard - "is-relative@npm:^0.1.0": version: 0.1.3 resolution: "is-relative@npm:0.1.3" @@ -6950,13 +6772,6 @@ __metadata: languageName: node linkType: hard -"isbinaryfile@npm:^4.0.8": - version: 4.0.10 - resolution: "isbinaryfile@npm:4.0.10" - checksum: 10c0/0703d8cfeb69ed79e6d173120f327450011a066755150a6bbf97ffecec1069a5f2092777868315b21359098c84b54984871cad1abce877ad9141fb2caf3dcabf - languageName: node - linkType: hard - "isbot@npm:^5.1.22": version: 5.2.0 resolution: "isbot@npm:5.2.0" @@ -6992,33 +6807,14 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^2.0.5": - version: 2.0.5 - resolution: "istanbul-lib-coverage@npm:2.0.5" - checksum: 10c0/370bd6940532ea2737a85532870f3024b113774ce3e637438cccfaf979a7598aa29d342f4adcc3d73e306155c907b519bc1c5c6887a29234f605a1a472747821 - languageName: node - linkType: hard - -"istanbul-lib-coverage@npm:^3.0.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b languageName: node linkType: hard -"istanbul-lib-instrument@npm:^4.0.3": - version: 4.0.3 - resolution: "istanbul-lib-instrument@npm:4.0.3" - dependencies: - "@babel/core": "npm:^7.7.5" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-coverage: "npm:^3.0.0" - semver: "npm:^6.3.0" - checksum: 10c0/7f1005566a912e33e847576b2c1072d48a7c556810a54d912f3e2f0bd966171e68b30c40b0c1ce6ee9b8864de422d0c10e2d0dfd2d25b48723950cc78cd437c2 - languageName: node - linkType: hard - -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -7029,20 +6825,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-source-maps@npm:^3.0.6": - version: 3.0.6 - resolution: "istanbul-lib-source-maps@npm:3.0.6" - dependencies: - debug: "npm:^4.1.1" - istanbul-lib-coverage: "npm:^2.0.5" - make-dir: "npm:^2.1.0" - rimraf: "npm:^2.6.3" - source-map: "npm:^0.6.1" - checksum: 10c0/0d2c0e6b301fd964d74137000b7f949d18856ad6e40e065a9f28eec041b33b901e3ff6f4f61505230558b26a1be0be044ae2e2bd6c692a9a7985e762fb300722 - languageName: node - linkType: hard - -"istanbul-reports@npm:^3.0.2": +"istanbul-reports@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-reports@npm:3.2.0" dependencies: @@ -7065,13 +6848,6 @@ __metadata: languageName: node linkType: hard -"jasmine-core@npm:3.99.1, jasmine-core@npm:^3.6.0": - version: 3.99.1 - resolution: "jasmine-core@npm:3.99.1" - checksum: 10c0/f89721874e2af19a098e214b8f3f201a644d625339c010733b6b0dba4ff9149f16997712a639c2bb427e7533526f82e0477b7927e175d3a2b2f9ff92928a5329 - languageName: node - linkType: hard - "jest-worker@npm:^27.4.5": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" @@ -7217,7 +6993,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": +"json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -7226,18 +7002,6 @@ __metadata: languageName: node linkType: hard -"jsonfile@npm:^4.0.0": - version: 4.0.0 - resolution: "jsonfile@npm:4.0.0" - dependencies: - graceful-fs: "npm:^4.1.6" - dependenciesMeta: - graceful-fs: - optional: true - checksum: 10c0/7dc94b628d57a66b71fb1b79510d460d662eb975b5f876d723f81549c2e9cd316d58a2ddf742b2b93a4fa6b17b2accaf1a738a0e2ea114bdfb13a32e5377e480 - languageName: node - linkType: hard - "jsonfile@npm:^6.0.1": version: 6.2.1 resolution: "jsonfile@npm:6.2.1" @@ -7309,131 +7073,6 @@ __metadata: languageName: node linkType: hard -"karma-browserstack-launcher@npm:1.6.0": - version: 1.6.0 - resolution: "karma-browserstack-launcher@npm:1.6.0" - dependencies: - browserstack: "npm:~1.5.1" - browserstack-local: "npm:^1.3.7" - q: "npm:~1.5.0" - peerDependencies: - karma: ">=0.9" - checksum: 10c0/d3e326732157000710d3c7a3751e82611ba7b3fdb25cdc86e059a6441855446273094887854e67a68ded654d7a033142fa2355b457e416a074c17fc16795e2c9 - languageName: node - linkType: hard - -"karma-chrome-launcher@npm:3.2.0": - version: 3.2.0 - resolution: "karma-chrome-launcher@npm:3.2.0" - dependencies: - which: "npm:^1.2.1" - checksum: 10c0/0cec1ae7d922110dc29cee36389d597157c82f019c8917259f9fa93d1f5ee8e19141c2eb74bfe30797cdb3adbc51a6b65fd18a9ebc1527c725c4acf62cd46d04 - languageName: node - linkType: hard - -"karma-coverage-istanbul-reporter@npm:3.0.3": - version: 3.0.3 - resolution: "karma-coverage-istanbul-reporter@npm:3.0.3" - dependencies: - istanbul-lib-coverage: "npm:^3.0.0" - istanbul-lib-report: "npm:^3.0.0" - istanbul-lib-source-maps: "npm:^3.0.6" - istanbul-reports: "npm:^3.0.2" - minimatch: "npm:^3.0.4" - checksum: 10c0/543722fd63c98d6e3c9e7bba91d7cb0e3ccb0db0e4b9703f091f2c25ab5a87f330f38ba6d62361c4bb99f3de9e19c5c918555083bb24c41c24400ebe5b772865 - languageName: node - linkType: hard - -"karma-jasmine@npm:4.0.2": - version: 4.0.2 - resolution: "karma-jasmine@npm:4.0.2" - dependencies: - jasmine-core: "npm:^3.6.0" - peerDependencies: - karma: "*" - checksum: 10c0/4cc4e812130898e59ae125fa9bf5cada2e228b246c7612175906ec44beb41725f6bab01097b08f642c3a1f99c3e0c1780aaec786fb383c33e9b4f2669dea521e - languageName: node - linkType: hard - -"karma-junit-reporter@npm:2.0.1": - version: 2.0.1 - resolution: "karma-junit-reporter@npm:2.0.1" - dependencies: - path-is-absolute: "npm:^1.0.0" - xmlbuilder: "npm:12.0.0" - peerDependencies: - karma: ">=0.9" - checksum: 10c0/ab243908a950ee67cc03fff53cedf0be32cfd05e544af3115cfccae850688b49d62a060aafc244763fac75f67bf1454312430bc5269ff17afba671dd010191b9 - languageName: node - linkType: hard - -"karma-sourcemap-loader@npm:0.4.0": - version: 0.4.0 - resolution: "karma-sourcemap-loader@npm:0.4.0" - dependencies: - graceful-fs: "npm:^4.2.10" - checksum: 10c0/8f77516330bc78d7c4d22469bdfd96b82acf7a285e2c19b8f30ae9d9bb21c7d817b0c32b9e5ea6ae002b16365e124afea121910c089c641320ce5595cafaadac - languageName: node - linkType: hard - -"karma-spec-reporter@npm:0.0.36": - version: 0.0.36 - resolution: "karma-spec-reporter@npm:0.0.36" - dependencies: - colors: "npm:1.4.0" - peerDependencies: - karma: ">=0.9" - checksum: 10c0/ae2e9cb5e2af74f0d4d8faa752c7c7204c296566906ea0d1a0eb98e76a02224b3aa60fe58c2b7cb40df4161ecd8a89feb0bd3ced5fffcae4fd5a40b72d48db9d - languageName: node - linkType: hard - -"karma-webpack@npm:5.0.0": - version: 5.0.0 - resolution: "karma-webpack@npm:5.0.0" - dependencies: - glob: "npm:^7.1.3" - minimatch: "npm:^3.0.4" - webpack-merge: "npm:^4.1.5" - peerDependencies: - webpack: ^5.0.0 - checksum: 10c0/82e6169e137444cd61cf3f4f43e3bd5f8766519f7708acc43e4031f1ddffa25dcee390aaae9a1635b3fa5db0e1d6198d8f2ab28cfac859e107f3b4eeba68d54d - languageName: node - linkType: hard - -"karma@npm:6.4.4": - version: 6.4.4 - resolution: "karma@npm:6.4.4" - dependencies: - "@colors/colors": "npm:1.5.0" - body-parser: "npm:^1.19.0" - braces: "npm:^3.0.2" - chokidar: "npm:^3.5.1" - connect: "npm:^3.7.0" - di: "npm:^0.0.1" - dom-serialize: "npm:^2.2.1" - glob: "npm:^7.1.7" - graceful-fs: "npm:^4.2.6" - http-proxy: "npm:^1.18.1" - isbinaryfile: "npm:^4.0.8" - lodash: "npm:^4.17.21" - log4js: "npm:^6.4.1" - mime: "npm:^2.5.2" - minimatch: "npm:^3.0.4" - mkdirp: "npm:^0.5.5" - qjobs: "npm:^1.2.0" - range-parser: "npm:^1.2.1" - rimraf: "npm:^3.0.2" - socket.io: "npm:^4.7.2" - source-map: "npm:^0.6.1" - tmp: "npm:^0.2.1" - ua-parser-js: "npm:^0.7.30" - yargs: "npm:^16.1.1" - bin: - karma: bin/karma - checksum: 10c0/1658c4b7396c0edf6f048289182e075b561902e02992e1a3eb72f56f67090ff0c7ad7c91ab099e88a790c60f9500c5a6f974d75f1769e3ea2dfccda52876ec0b - languageName: node - linkType: hard - "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -7683,17 +7322,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^2.0.0": - version: 2.0.4 - resolution: "loader-utils@npm:2.0.4" - dependencies: - big.js: "npm:^5.2.2" - emojis-list: "npm:^3.0.0" - json5: "npm:^2.1.2" - checksum: 10c0/d5654a77f9d339ec2a03d88221a5a695f337bf71eb8dea031b3223420bb818964ba8ed0069145c19b095f6c8b8fd386e602a3fc7ca987042bd8bb1dcc90d7100 - languageName: node - linkType: hard - "local-pkg@npm:^1.1.2": version: 1.2.1 resolution: "local-pkg@npm:1.2.1" @@ -7779,7 +7407,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21": +"lodash@npm:^4.17.20, lodash@npm:^4.17.21": version: 4.18.1 resolution: "lodash@npm:4.18.1" checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 @@ -7799,19 +7427,6 @@ __metadata: languageName: node linkType: hard -"log4js@npm:^6.4.1": - version: 6.9.1 - resolution: "log4js@npm:6.9.1" - dependencies: - date-format: "npm:^4.0.14" - debug: "npm:^4.3.4" - flatted: "npm:^3.2.7" - rfdc: "npm:^1.3.0" - streamroller: "npm:^3.1.5" - checksum: 10c0/05846e48f72d662800c8189bd178c42b4aa2f0c574cfc90a1942cf90b76f621c44019e26796c8fd88da1b6f0fe8272cba607cbaad6ae6ede50a7a096b58197ea - languageName: node - linkType: hard - "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -7873,16 +7488,6 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^2.1.0": - version: 2.1.0 - resolution: "make-dir@npm:2.1.0" - dependencies: - pify: "npm:^4.0.1" - semver: "npm:^5.6.0" - checksum: 10c0/ada869944d866229819735bee5548944caef560d7a8536ecbc6536edca28c72add47cc4f6fc39c54fb25d06b58da1f8994cf7d9df7dadea047064749efc085d8 - languageName: node - linkType: hard - "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -7943,13 +7548,6 @@ __metadata: languageName: node linkType: hard -"media-typer@npm:0.3.0": - version: 0.3.0 - resolution: "media-typer@npm:0.3.0" - checksum: 10c0/d160f31246907e79fed398470285f21bafb45a62869dc469b1c8877f3f064f5eabc4bcc122f9479b8b605bc5c76187d7871cf84c4ee3ecd3e487da1993279928 - languageName: node - linkType: hard - "media-typer@npm:^1.1.0": version: 1.1.0 resolution: "media-typer@npm:1.1.0" @@ -7988,15 +7586,6 @@ __metadata: languageName: node linkType: hard -"merge-source-map@npm:^1.1.0": - version: 1.1.0 - resolution: "merge-source-map@npm:1.1.0" - dependencies: - source-map: "npm:^0.6.1" - checksum: 10c0/ac0e0192c9c7e30056c5baa939434c0d1015faa5c7ce7936ad77600f1752c03099134cb33c50f1bb32ec25350e191ca2392c6b76b1eaca89c7c989e42403655f - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -8004,13 +7593,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa - languageName: node - linkType: hard - "mime-db@npm:^1.54.0": version: 1.54.0 resolution: "mime-db@npm:1.54.0" @@ -8027,15 +7609,6 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: "npm:1.52.0" - checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 - languageName: node - linkType: hard - "mime@npm:^1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -8045,15 +7618,6 @@ __metadata: languageName: node linkType: hard -"mime@npm:^2.5.2": - version: 2.6.0 - resolution: "mime@npm:2.6.0" - bin: - mime: cli.js - checksum: 10c0/a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c - languageName: node - linkType: hard - "mimic-function@npm:^5.0.0": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" @@ -8079,7 +7643,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1": +"minimatch@npm:^3.0.4": version: 3.1.5 resolution: "minimatch@npm:3.1.5" dependencies: @@ -8166,17 +7730,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.5": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" - dependencies: - minimist: "npm:^1.2.6" - bin: - mkdirp: bin/cmd.js - checksum: 10c0/e2e2be789218807b58abced04e7b49851d9e46e88a2f9539242cc8a92c9b5c3a0b9bab360bd3014e02a140fc4fbc58e31176c408b493f8a2a6f4986bd7527b01 - languageName: node - linkType: hard - "mlly@npm:^1.7.4, mlly@npm:^1.8.2": version: 1.8.2 resolution: "mlly@npm:1.8.2" @@ -8196,10 +7749,10 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.0.0": - version: 2.0.0 - resolution: "ms@npm:2.0.0" - checksum: 10c0/f8fda810b39fd7255bbdc451c46286e549794fcc700dc9cd1d25658bbc4dc2563a5de6fe7c60f798a16a60c6ceb53f033cb353f493f0cf63e5199b702943159d +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 languageName: node linkType: hard @@ -8270,13 +7823,6 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:0.6.3": - version: 0.6.3 - resolution: "negotiator@npm:0.6.3" - checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 - languageName: node - linkType: hard - "negotiator@npm:^1.0.0": version: 1.0.0 resolution: "negotiator@npm:1.0.0" @@ -8438,7 +7984,7 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": +"normalize-path@npm:^3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 @@ -8529,7 +8075,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:^2.4.1, on-finished@npm:~2.4.1": +"on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -8538,16 +8084,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:~2.3.0": - version: 2.3.0 - resolution: "on-finished@npm:2.3.0" - dependencies: - ee-first: "npm:1.1.1" - checksum: 10c0/c904f9e518b11941eb60279a3cbfaf1289bd0001f600a950255b1dede9fe3df8cd74f38483550b3bb9485165166acb5db500c3b4c4337aec2815c88c96fcc2ea - languageName: node - linkType: hard - -"once@npm:^1.3.0, once@npm:^1.4.0": +"once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -8735,7 +8272,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.3, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 @@ -8759,13 +8296,6 @@ __metadata: languageName: node linkType: hard -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 - languageName: node - linkType: hard - "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -8818,27 +8348,20 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": +"picomatch@npm:^2.3.1": version: 2.3.2 resolution: "picomatch@npm:2.3.2" checksum: 10c0/a554d1709e59be97d1acb9eaedbbc700a5c03dbd4579807baed95100b00420bc729335440ef15004ae2378984e2487a7c1cebd743cfdb72b6fa9ab69223c0d61 languageName: node linkType: hard -"picomatch@npm:^4.0.0, picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": +"picomatch@npm:^4.0.0, picomatch@npm:^4.0.3, picomatch@npm:^4.0.4, picomatch@npm:^4.0.5": version: 4.0.5 resolution: "picomatch@npm:4.0.5" checksum: 10c0/947bc6b6e1ff1e6c5aaf95b107a0839d12802f4f7b867663f67d47accba939ca1cb582cf99dfc30438efa1c4648ac5990967e783e8929c36b03e8440704ef1bd languageName: node linkType: hard -"pify@npm:^4.0.1": - version: 4.0.1 - resolution: "pify@npm:4.0.1" - checksum: 10c0/6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf - languageName: node - linkType: hard - "pino-abstract-transport@npm:^2.0.0": version: 2.0.0 resolution: "pino-abstract-transport@npm:2.0.0" @@ -8907,6 +8430,30 @@ __metadata: languageName: node linkType: hard +"playwright-browserstack@npm:playwright@1.59.1": + version: 1.59.1 + resolution: "playwright@npm:1.59.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.59.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/dfe38396e616e5c4f98825ce90037bb96e477c5a2bd9258a24854f8ce72a8a41427b19098863866f85aa0216e70287dd537c4438d761aca93995e31ae099c533 + languageName: node + linkType: hard + +"playwright-core@npm:1.59.1": + version: 1.59.1 + resolution: "playwright-core@npm:1.59.1" + bin: + playwright-core: cli.js + checksum: 10c0/d41a74d9681ce3beb3d5239e9ed577710b4ad099a6ca2476219c6599d51e9cb4b80bd72ed82c528da6a5d929c18ae3b872cf02bb83f78fa1c2cb9199c501abee + languageName: node + linkType: hard + "playwright-core@npm:1.61.1": version: 1.61.1 resolution: "playwright-core@npm:1.61.1" @@ -8938,6 +8485,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^7.0.0": + version: 7.0.0 + resolution: "pngjs@npm:7.0.0" + checksum: 10c0/0d4c7a0fd476a9c33df7d0a2a73e1d56537628a668841f6995c2bca070cf30819f9254a64363266bc14ef2fee47659dd3b4f2b18eec7ab65143015139f497b38 + languageName: node + linkType: hard + "portfinder@npm:^1.0.28": version: 1.0.38 resolution: "portfinder@npm:1.0.38" @@ -9086,13 +8640,6 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^1.4.1": - version: 1.4.1 - resolution: "punycode@npm:1.4.1" - checksum: 10c0/354b743320518aef36f77013be6e15da4db24c2b4f62c5f1eb0529a6ed02fbaf1cb52925785f6ab85a962f2b590d9cd5ad730b70da72b5f180e2556b8bd3ca08 - languageName: node - linkType: hard - "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -9139,21 +8686,7 @@ __metadata: languageName: node linkType: hard -"q@npm:~1.5.0": - version: 1.5.1 - resolution: "q@npm:1.5.1" - checksum: 10c0/7855fbdba126cb7e92ef3a16b47ba998c0786ec7fface236e3eb0135b65df36429d91a86b1fff3ab0927b4ac4ee88a2c44527c7c3b8e2a37efbec9fe34803df4 - languageName: node - linkType: hard - -"qjobs@npm:^1.2.0": - version: 1.2.0 - resolution: "qjobs@npm:1.2.0" - checksum: 10c0/772207772b856a3b1ec673b11a6cda074f1b82821644f2d042504b438ea3ea1fe918555547491e717e8694ec105379fe5139fc5ddd7937b21f7712bb648ed01d - languageName: node - linkType: hard - -"qs@npm:^6.14.0, qs@npm:^6.15.2, qs@npm:^6.4.0, qs@npm:~6.15.1": +"qs@npm:^6.14.0, qs@npm:^6.15.2, qs@npm:^6.4.0": version: 6.15.3 resolution: "qs@npm:6.15.3" dependencies: @@ -9203,18 +8736,6 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:~2.5.3": - version: 2.5.3 - resolution: "raw-body@npm:2.5.3" - dependencies: - bytes: "npm:~3.1.2" - http-errors: "npm:~2.0.1" - iconv-lite: "npm:~0.4.24" - unpipe: "npm:~1.0.0" - checksum: 10c0/449844344fc90547fb994383a494b83300e4f22199f146a79f68d78a199a8f2a923ea9fd29c3be979bfd50291a3884733619ffc15ba02a32e703b612f8d3f74a - languageName: node - linkType: hard - "rc9@npm:^3.0.1": version: 3.0.1 resolution: "rc9@npm:3.0.1" @@ -9407,15 +8928,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:~3.6.0": - version: 3.6.0 - resolution: "readdirp@npm:3.6.0" - dependencies: - picomatch: "npm:^2.2.1" - checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b - languageName: node - linkType: hard - "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0" @@ -9570,35 +9082,13 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.3.0, rfdc@npm:^1.4.1": +"rfdc@npm:^1.4.1": version: 1.4.1 resolution: "rfdc@npm:1.4.1" checksum: 10c0/4614e4292356cafade0b6031527eea9bc90f2372a22c012313be1dcc69a3b90c7338158b414539be863fa95bfcb2ddcd0587be696841af4e6679d85e62c060c7 languageName: node linkType: hard -"rimraf@npm:^2.6.3": - version: 2.7.1 - resolution: "rimraf@npm:2.7.1" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: ./bin.js - checksum: 10c0/4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40 - languageName: node - linkType: hard - -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: bin.js - checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 - languageName: node - linkType: hard - "rolldown-plugin-dts@npm:^0.26.0": version: 0.26.0 resolution: "rolldown-plugin-dts@npm:0.26.0" @@ -9731,17 +9221,6 @@ __metadata: languageName: node linkType: hard -"safe-regex-test@npm:^1.1.0": - version: 1.1.0 - resolution: "safe-regex-test@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - is-regex: "npm:^1.2.1" - checksum: 10c0/f2c25281bbe5d39cddbbce7f86fca5ea9b3ce3354ea6cd7c81c31b006a5a9fff4286acc5450a3b9122c56c33eba69c56b9131ad751457b2b4a585825e6a10665 - languageName: node - linkType: hard - "safe-stable-stringify@npm:^2.3.1": version: 2.5.0 resolution: "safe-stable-stringify@npm:2.5.0" @@ -9749,7 +9228,7 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": +"safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 @@ -9770,17 +9249,6 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^2.7.0": - version: 2.7.1 - resolution: "schema-utils@npm:2.7.1" - dependencies: - "@types/json-schema": "npm:^7.0.5" - ajv: "npm:^6.12.4" - ajv-keywords: "npm:^3.5.2" - checksum: 10c0/f484f34464edd8758712d5d3ba25a306e367dac988aecaf4ce112e99baae73f33a807b5cf869240bb6648c80720b36af2d7d72be3a27faa49a2d4fc63fa3f85f - languageName: node - linkType: hard - "schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3": version: 4.3.3 resolution: "schema-utils@npm:4.3.3" @@ -9807,16 +9275,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^5.6.0": - version: 5.7.2 - resolution: "semver@npm:5.7.2" - bin: - semver: bin/semver - checksum: 10c0/e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25 - languageName: node - linkType: hard - -"semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -10092,6 +9551,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -10099,6 +9565,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.2": + version: 3.0.2 + resolution: "sirv@npm:3.0.2" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/5930e4397afdb14fbae13751c3be983af4bda5c9aadec832607dc2af15a7162f7d518c71b30e83ae3644b9a24cea041543cc969e5fe2b80af6ce8ea3174b2d04 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -10126,41 +9603,6 @@ __metadata: languageName: node linkType: hard -"socket.io-adapter@npm:~2.5.2": - version: 2.5.8 - resolution: "socket.io-adapter@npm:2.5.8" - dependencies: - debug: "npm:~4.4.1" - ws: "npm:~8.21.0" - checksum: 10c0/17fc68a7953d6b640b78b2191d8a575b30c7efb9fdec91b4f0788e2f9d093dfb9a84f6fcb396f9b922c32dd0584b01c2d3410a8f69b4cae3f309530b8859bc8a - languageName: node - linkType: hard - -"socket.io-parser@npm:~4.2.4": - version: 4.2.6 - resolution: "socket.io-parser@npm:4.2.6" - dependencies: - "@socket.io/component-emitter": "npm:~3.1.0" - debug: "npm:~4.4.1" - checksum: 10c0/ba0a0b541b0a8e9d02b45c04c4c93a02331be5ea3478073c65bb9ff87032f12469c9adb309728eb90c0a352618d645ab88999c167a11c783cac861d7fd35c9d1 - languageName: node - linkType: hard - -"socket.io@npm:^4.7.2": - version: 4.8.3 - resolution: "socket.io@npm:4.8.3" - dependencies: - accepts: "npm:~1.3.4" - base64id: "npm:~2.0.0" - cors: "npm:~2.8.5" - debug: "npm:~4.4.1" - engine.io: "npm:~6.6.0" - socket.io-adapter: "npm:~2.5.2" - socket.io-parser: "npm:~4.2.4" - checksum: 10c0/1f7c4118cdbcb346e63db9d8fd657c3dc5caf148404762ed98ac14c4e7b74984a65fe51657bfe1696adcf7c168d1a3aad4a26b52864ce8491556f38218598f0b - languageName: node - linkType: hard - "sonic-boom@npm:^4.0.1": version: 4.2.1 resolution: "sonic-boom@npm:4.2.1" @@ -10187,7 +9629,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": +"source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 @@ -10265,6 +9707,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + "statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2": version: 2.0.2 resolution: "statuses@npm:2.0.2" @@ -10272,21 +9721,10 @@ __metadata: languageName: node linkType: hard -"statuses@npm:~1.5.0": - version: 1.5.0 - resolution: "statuses@npm:1.5.0" - checksum: 10c0/e433900956357b3efd79b1c547da4d291799ac836960c016d10a98f6a810b1b5c0dcc13b5a7aa609a58239b5190e1ea176ad9221c2157d2fd1c747393e6b2940 - languageName: node - linkType: hard - -"streamroller@npm:^3.1.5": - version: 3.1.5 - resolution: "streamroller@npm:3.1.5" - dependencies: - date-format: "npm:^4.0.14" - debug: "npm:^4.3.4" - fs-extra: "npm:^8.1.0" - checksum: 10c0/0bdeec34ad37487d959ba908f17067c938f544db88b5bb1669497a67a6b676413229ce5a6145c2812d06959ebeb8842e751076647d4b323ca06be612963b9099 +"std-env@npm:^4.0.0-rc.1": + version: 4.2.0 + resolution: "std-env@npm:4.2.0" + checksum: 10c0/40ac525ce7b7c556abc332a7376f14356eeb1a7f17f6ff9a003eb9f52326ff1f3745d3e1b43452675b1ec6fcc319f1b1d6f3b0d386cf3f91058479ad883cff69 languageName: node linkType: hard @@ -10605,7 +10043,14 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.2.4": +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2, tinyexec@npm:^1.2.4": version: 1.2.4 resolution: "tinyexec@npm:1.2.4" checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff @@ -10622,6 +10067,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^3.1.0": + version: 3.1.0 + resolution: "tinyrainbow@npm:3.1.0" + checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 + languageName: node + linkType: hard + "tmp@npm:0.2.5": version: 0.2.5 resolution: "tmp@npm:0.2.5" @@ -10629,22 +10081,6 @@ __metadata: languageName: node linkType: hard -"tmp@npm:^0.2.1": - version: 0.2.7 - resolution: "tmp@npm:0.2.7" - checksum: 10c0/59eb55584f2f07210d3231b6a1f6b5c2b9794d8a7b509c8ee867ed2acad6d2245ee2448b7937b676ffbff3155a70077edde8a69f9d7cf0f90c86a62e8910c357 - languageName: node - linkType: hard - -"to-regex-range@npm:^5.0.1": - version: 5.0.1 - resolution: "to-regex-range@npm:5.0.1" - dependencies: - is-number: "npm:^7.0.0" - checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 - languageName: node - linkType: hard - "to-valid-identifier@npm:^1.0.0": version: 1.0.0 resolution: "to-valid-identifier@npm:1.0.0" @@ -10662,6 +10098,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + "tree-dump@npm:^1.0.3, tree-dump@npm:^1.1.0": version: 1.1.0 resolution: "tree-dump@npm:1.1.0" @@ -10834,16 +10277,6 @@ __metadata: languageName: node linkType: hard -"type-is@npm:~1.6.18": - version: 1.6.18 - resolution: "type-is@npm:1.6.18" - dependencies: - media-typer: "npm:0.3.0" - mime-types: "npm:~2.1.24" - checksum: 10c0/a23daeb538591b7efbd61ecf06b6feb2501b683ffdc9a19c74ef5baba362b4347e42f1b4ed81f5882a8c96a3bfff7f93ce3ffaf0cbbc879b532b04c97a55db9d - languageName: node - linkType: hard - "typed-query-selector@npm:^2.12.2": version: 2.12.2 resolution: "typed-query-selector@npm:2.12.2" @@ -10930,15 +10363,6 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.30": - version: 0.7.41 - resolution: "ua-parser-js@npm:0.7.41" - bin: - ua-parser-js: script/cli.js - checksum: 10c0/b134bc0d8da10c76e07740a0ade61c193fd4c4d120ba2cb2530e26931f6b550dd60b6e801d8891f6d9c23dfebadf5590294739069edff94396f173cc8cc5767e - languageName: node - linkType: hard - "uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": version: 2.1.0 resolution: "uc.micro@npm:2.1.0" @@ -11023,13 +10447,6 @@ __metadata: languageName: node linkType: hard -"universalify@npm:^0.1.0": - version: 0.1.2 - resolution: "universalify@npm:0.1.2" - checksum: 10c0/e70e0339f6b36f34c9816f6bf9662372bd241714dc77508d231d08386d94f2c4aa1ba1318614f92015f40d45aae1b9075cd30bd490efbe39387b60a76ca3f045 - languageName: node - linkType: hard - "universalify@npm:^2.0.0": version: 2.0.1 resolution: "universalify@npm:2.0.1" @@ -11272,13 +10689,6 @@ __metadata: languageName: node linkType: hard -"utils-merge@npm:1.0.1": - version: 1.0.1 - resolution: "utils-merge@npm:1.0.1" - checksum: 10c0/02ba649de1b7ca8854bfe20a82f1dfbdda3fb57a22ab4a8972a63a34553cf7aa51bc9081cf7e001b035b88186d23689d69e71b510e610a09a4c66f68aa95b672 - languageName: node - linkType: hard - "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -11367,10 +10777,128 @@ __metadata: languageName: node linkType: hard -"void-elements@npm:^2.0.0": - version: 2.0.1 - resolution: "void-elements@npm:2.0.1" - checksum: 10c0/23b4f35bbeabcaa5c87a9f638ae80862a9313dccbaa8973b0eada81dbe97488ae11baf4d8aa2846bc397d31456afdfd8d791bb44c542f83735e6d04af6996f4d +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.1.4 + resolution: "vite@npm:8.1.4" + dependencies: + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.5" + postcss: "npm:^8.5.16" + rolldown: "npm:~1.1.4" + tinyglobby: "npm:^0.2.17" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/4262cdd0dd00ca99b00a8aef91fc00e58b66e80b54adbef49353bfbbae7342c2d6ddf1180351956086ff53ad186bec03784f57b96e36ab4dad1246547236d79b + languageName: node + linkType: hard + +"vitest@npm:4.1.8": + version: 4.1.8 + resolution: "vitest@npm:4.1.8" + dependencies: + "@vitest/expect": "npm:4.1.8" + "@vitest/mocker": "npm:4.1.8" + "@vitest/pretty-format": "npm:4.1.8" + "@vitest/runner": "npm:4.1.8" + "@vitest/snapshot": "npm:4.1.8" + "@vitest/spy": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.8 + "@vitest/browser-preview": 4.1.8 + "@vitest/browser-webdriverio": 4.1.8 + "@vitest/coverage-istanbul": 4.1.8 + "@vitest/coverage-v8": 4.1.8 + "@vitest/ui": 4.1.8 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: vitest.mjs + checksum: 10c0/f459c500f8818c7a2318cd23228b4e5c0b5efb25bf8e5cb7f116c6d26e51482b2f800a8bb19837c0b5f0d05c51519edbf502bc8ceb5bd86868e8facf1d2c498e languageName: node linkType: hard @@ -11547,15 +11075,6 @@ __metadata: languageName: node linkType: hard -"webpack-merge@npm:^4.1.5": - version: 4.2.2 - resolution: "webpack-merge@npm:4.2.2" - dependencies: - lodash: "npm:^4.17.15" - checksum: 10c0/283cb4ffe4d4ae6de23d595154868780126835ded241748da0b070c6cca6974c229493ac0b6b7160c2c92950c950c8e5edf036a192da78e32e22a9c81593ad16 - languageName: node - linkType: hard - "webpack-merge@npm:^6.0.1": version: 6.0.1 resolution: "webpack-merge@npm:6.0.1" @@ -11651,17 +11170,6 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.1": - version: 1.3.1 - resolution: "which@npm:1.3.1" - dependencies: - isexe: "npm:^2.0.0" - bin: - which: ./bin/which - checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 - languageName: node - linkType: hard - "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -11684,6 +11192,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "widest-line@npm:^5.0.0": version: 5.0.0 resolution: "widest-line@npm:5.0.0" @@ -11765,7 +11285,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.21.0, ws@npm:^8.21.0, ws@npm:~8.21.0": +"ws@npm:8.21.0, ws@npm:^8.19.0, ws@npm:^8.21.0": version: 8.21.0 resolution: "ws@npm:8.21.0" peerDependencies: @@ -11865,13 +11385,6 @@ __metadata: languageName: node linkType: hard -"xmlbuilder@npm:12.0.0": - version: 12.0.0 - resolution: "xmlbuilder@npm:12.0.0" - checksum: 10c0/9d166aef70a4264f8650b5fcf11a29cd811ca6bbd63b830389ffb496ac253ade30f67f3cf22250468f374a2dbf84ed6f7a183b4623953d906e20057349e74f12 - languageName: node - linkType: hard - "xmlbuilder@npm:~11.0.0": version: 11.0.1 resolution: "xmlbuilder@npm:11.0.1" @@ -11909,13 +11422,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2": - version: 20.2.9 - resolution: "yargs-parser@npm:20.2.9" - checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 - languageName: node - linkType: hard - "yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -11930,21 +11436,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.1.1": - version: 16.2.2 - resolution: "yargs@npm:16.2.2" - dependencies: - cliui: "npm:^7.0.2" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.0" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^20.2.2" - checksum: 10c0/1ca2152581ee7c9c9fb4174767ff7294b1c272d2d0a1f6eb13c39ce177fded85eb9c96711e00cd7913c0a9b88b6e763713ee11cae81b9b62fd97bdd0b03d5549 - languageName: node - linkType: hard - "yargs@npm:^17.5.1": version: 17.7.3 resolution: "yargs@npm:17.7.3"