diff --git a/.github/workflows/pr-frontend-unit-tests.yml b/.github/workflows/pr-frontend-unit-tests.yml index 25d2450e002..283a186c50d 100644 --- a/.github/workflows/pr-frontend-unit-tests.yml +++ b/.github/workflows/pr-frontend-unit-tests.yml @@ -107,6 +107,24 @@ jobs: - run: yarn install --immutable --check-cache - run: yarn run plugin:test:ci + frontend-packages-unit-tests: + needs: detect-changes + if: needs.detect-changes.outputs.changed == 'true' + runs-on: ubuntu-latest-8-cores + name: "Packages unit tests" + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + cache-dependency-path: 'yarn.lock' + - run: yarn install --immutable --check-cache + - run: yarn run packages:test:ci + + # This is the job that is actually required by rulesets. # We need to require EITHER the OSS or the Enterprise job to pass. # However, if one is skipped, GitHub won't flat-map the shards, @@ -116,6 +134,7 @@ jobs: - frontend-unit-tests - frontend-unit-tests-enterprise - frontend-decoupled-plugin-tests + - frontend-packages-unit-tests # always() is the best function here. # success() || failure() will skip this function if any need is also skipped. # That means conditional test suites will fail the entire requirement check. diff --git a/package.json b/package.json index 79bbdd20cbe..278ffb6efcc 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact", "packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", "packages:typecheck": "nx run-many -t typecheck --projects='tag:scope:package'", + "packages:test:ci": "nx run-many -t test:ci --projects='tag:scope:package'", "prettier:check": "prettier --check --ignore-path .prettierignore --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\"", "prettier:checkDocs": "prettier --check --list-different=false --log-level=warn \"docs/**/*.md\" \"*.md\" \"packages/**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\"", "prettier:write": "prettier --ignore-path .prettierignore --list-different \"**/*.{js,ts,tsx,scss,md,mdx,json,cjs}\" --write", diff --git a/packages/grafana-plugin-configs/jest/jest-setup.js b/packages/grafana-plugin-configs/jest/jest-setup.js index 00a84b42b68..05a2b03634c 100644 --- a/packages/grafana-plugin-configs/jest/jest-setup.js +++ b/packages/grafana-plugin-configs/jest/jest-setup.js @@ -1,6 +1,10 @@ import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; +import { matchers } from '@grafana/test-utils/matchers'; + +expect.extend(matchers); + Object.assign(global, { TextDecoder, TextEncoder }); // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom diff --git a/packages/grafana-test-utils/README.md b/packages/grafana-test-utils/README.md index 2e864d4ad7b..370987a8931 100644 --- a/packages/grafana-test-utils/README.md +++ b/packages/grafana-test-utils/README.md @@ -1,3 +1,24 @@ # Grafana test utils This package is a collection of test utils and a mock API (using MSW) for use with core Grafana UI development. + +## Matchers + +To add the matchers to your Jest config, import them then extend `expect`. This should be done in the `setupFilesAfterEnv` file declared in `jest.config.{js,ts}`. + +```ts +// setupTests.ts +import { matchers } from '@grafana/test-utils'; + +expect.extend(matchers); +``` + +Included in this package are the following matchers: + +### `toEmitValues` + +Tests that an Observable emits the expected values in the correct order. This matcher collects all emitted values (including errors) and compares them against the expected array using deep equality. + +### `toEmitValuesWith` + +Tests that an Observable emits values that satisfy custom expectations. This matcher collects all emitted values and passes them to a callback function where you can perform custom assertions. diff --git a/packages/grafana-test-utils/jest-setup.js b/packages/grafana-test-utils/jest-setup.js new file mode 100644 index 00000000000..57d87c0c2ad --- /dev/null +++ b/packages/grafana-test-utils/jest-setup.js @@ -0,0 +1,3 @@ +import { matchers } from '@grafana/test-utils/matchers'; + +expect.extend(matchers); diff --git a/packages/grafana-test-utils/jest.config.js b/packages/grafana-test-utils/jest.config.js new file mode 100644 index 00000000000..468cf6bae51 --- /dev/null +++ b/packages/grafana-test-utils/jest.config.js @@ -0,0 +1,29 @@ +process.env.TZ = 'Pacific/Easter'; // UTC-06:00 or UTC-05:00 depending on daylight savings + +export default { + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + setupFilesAfterEnv: ['/jest-setup.js'], + testMatch: ['/**/__tests__/**/*.{js,jsx,ts,tsx}', '/**/*.{spec,test,jest}.{js,jsx,ts,tsx}'], + testEnvironment: 'node', + transform: { + '^.+\\.(t|j)sx?$': [ + '@swc/jest', + { + sourceMaps: 'inline', + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: false, + dynamicImport: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index 7f58c62ac0d..9a9f5998621 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -40,17 +40,28 @@ "./unstable": { "import": "./src/unstable.ts", "require": "./src/unstable.ts" + }, + "./matchers": { + "types": "./src/matchers/index.ts", + "import": "./src/matchers/index.ts", + "require": "./src/matchers/index.ts" } }, "scripts": { - "typecheck": "tsc --emitDeclarationOnly false --noEmit" + "typecheck": "tsc --emitDeclarationOnly false --noEmit", + "test": "jest --watch --onlyChanged", + "test:ci": "jest --maxWorkers 4" }, "dependencies": { "msw": "2.10.4" }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", + "@swc/core": "1.10.12", + "@swc/jest": "^0.2.26", + "@types/jest": "29.5.14", "@types/node": "22.16.5", + "jest": "29.7.0", "typescript": "5.8.3" } } diff --git a/packages/grafana-test-utils/src/index.ts b/packages/grafana-test-utils/src/index.ts index f5d42f308b0..55005c34d54 100644 --- a/packages/grafana-test-utils/src/index.ts +++ b/packages/grafana-test-utils/src/index.ts @@ -5,4 +5,6 @@ * @packageDocumentation */ -export {}; +// This is also exported as `@grafana/test-utils/matchers` but we cannot use that in places +// where the tsconfig is not set to moduleResolution: bundler so we export it here also. +export { matchers } from './matchers'; diff --git a/public/test/matchers/index.ts b/packages/grafana-test-utils/src/matchers/index.ts similarity index 86% rename from public/test/matchers/index.ts rename to packages/grafana-test-utils/src/matchers/index.ts index b62de8d6974..11a1891accd 100644 --- a/public/test/matchers/index.ts +++ b/packages/grafana-test-utils/src/matchers/index.ts @@ -1,4 +1,4 @@ -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; import { toEmitValues } from './toEmitValues'; import { toEmitValuesWith } from './toEmitValuesWith'; diff --git a/public/test/matchers/toEmitValues.test.ts b/packages/grafana-test-utils/src/matchers/toEmitValues.test.ts similarity index 100% rename from public/test/matchers/toEmitValues.test.ts rename to packages/grafana-test-utils/src/matchers/toEmitValues.test.ts diff --git a/public/test/matchers/toEmitValues.ts b/packages/grafana-test-utils/src/matchers/toEmitValues.ts similarity index 100% rename from public/test/matchers/toEmitValues.ts rename to packages/grafana-test-utils/src/matchers/toEmitValues.ts diff --git a/public/test/matchers/toEmitValuesWith.test.ts b/packages/grafana-test-utils/src/matchers/toEmitValuesWith.test.ts similarity index 100% rename from public/test/matchers/toEmitValuesWith.test.ts rename to packages/grafana-test-utils/src/matchers/toEmitValuesWith.test.ts diff --git a/public/test/matchers/toEmitValuesWith.ts b/packages/grafana-test-utils/src/matchers/toEmitValuesWith.ts similarity index 100% rename from public/test/matchers/toEmitValuesWith.ts rename to packages/grafana-test-utils/src/matchers/toEmitValuesWith.ts diff --git a/packages/grafana-test-utils/src/matchers/types.ts b/packages/grafana-test-utils/src/matchers/types.ts new file mode 100644 index 00000000000..c7c420e9355 --- /dev/null +++ b/packages/grafana-test-utils/src/matchers/types.ts @@ -0,0 +1,28 @@ +import { Observable } from 'rxjs'; + +export const OBSERVABLE_TEST_TIMEOUT_IN_MS = 1000; + +export interface ObservableMatchers extends jest.ExpectExtendMap { + toEmitValues(received: Observable, expected: T[]): Promise; + toEmitValuesWith( + received: Observable, + expectations: (received: T[]) => void + ): Promise; +} + +type ObservableType = T extends Observable ? V : never; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toEmitValues>(expected: E[]): Promise; + /** + * Collect all the values emitted by the observables (also errors) and pass them to the expectations functions after + * the observable ended (or emitted error). If Observable does not complete within OBSERVABLE_TEST_TIMEOUT_IN_MS the + * test fails. + */ + toEmitValuesWith>(expectations: (received: E[]) => void): Promise; + } + } +} diff --git a/public/test/matchers/utils.ts b/packages/grafana-test-utils/src/matchers/utils.ts similarity index 100% rename from public/test/matchers/utils.ts rename to packages/grafana-test-utils/src/matchers/utils.ts diff --git a/public/app/plugins/datasource/azuremonitor/jest-setup.js b/public/app/plugins/datasource/azuremonitor/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/azuremonitor/jest-setup.js +++ b/public/app/plugins/datasource/azuremonitor/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/cloud-monitoring/jest-setup.js b/public/app/plugins/datasource/cloud-monitoring/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/cloud-monitoring/jest-setup.js +++ b/public/app/plugins/datasource/cloud-monitoring/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js b/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js b/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js b/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js +++ b/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/jaeger/jest-setup.js b/public/app/plugins/datasource/jaeger/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/jaeger/jest-setup.js +++ b/public/app/plugins/datasource/jaeger/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/mysql/jest-setup.js b/public/app/plugins/datasource/mysql/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/mysql/jest-setup.js +++ b/public/app/plugins/datasource/mysql/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/parca/jest-setup.js b/public/app/plugins/datasource/parca/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/parca/jest-setup.js +++ b/public/app/plugins/datasource/parca/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/tempo/jest-setup.js b/public/app/plugins/datasource/tempo/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/tempo/jest-setup.js +++ b/public/app/plugins/datasource/tempo/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/zipkin/jest-setup.js b/public/app/plugins/datasource/zipkin/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/zipkin/jest-setup.js +++ b/public/app/plugins/datasource/zipkin/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/test/matchers/types.ts b/public/test/matchers/types.ts deleted file mode 100644 index 2c8cd92ff3b..00000000000 --- a/public/test/matchers/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Observable } from 'rxjs'; - -export const OBSERVABLE_TEST_TIMEOUT_IN_MS = 1000; - -export interface ObservableMatchers extends jest.ExpectExtendMap { - toEmitValues(received: Observable, expected: T[]): Promise; - toEmitValuesWith( - received: Observable, - expectations: (received: T[]) => void - ): Promise; -} diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index 3f053609138..d6c97a0d1c8 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -7,9 +7,9 @@ import i18next from 'i18next'; import failOnConsole from 'jest-fail-on-console'; import { initReactI18next } from 'react-i18next'; -import getEnvConfig from '../../scripts/webpack/env-util'; +import { matchers } from '@grafana/test-utils'; -import { matchers } from './matchers'; +import getEnvConfig from '../../scripts/webpack/env-util'; const config = getEnvConfig() as Record; diff --git a/yarn.lock b/yarn.lock index dab33c9e9bc..7d72c331da7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3714,7 +3714,11 @@ __metadata: resolution: "@grafana/test-utils@workspace:packages/grafana-test-utils" dependencies: "@grafana/tsconfig": "npm:^2.0.0" + "@swc/core": "npm:1.10.12" + "@swc/jest": "npm:^0.2.26" + "@types/jest": "npm:29.5.14" "@types/node": "npm:22.16.5" + jest: "npm:29.7.0" msw: "npm:2.10.4" typescript: "npm:5.8.3" languageName: unknown