Frontend: Decouple unit tests for decoupled plugins from core (#107942)

* test(frontend): ignore azuremonitor tests in root jest config

* test(loki): increase waitfor in monacofieldwrapper test

* feat(plugin-configs): introduce jest config

* feat(azuremonitor): add jest config and test scripts

* ci(pr-frontend-unit-tests): introduce separate decoupled plugin test job

* ci(pr-frontend-unit-tests): run decoupled-plugin tests regardless of fork

* Wip

* test(decoupled-plugins): decouple tests in all plugins except loki and mssql

* test(frontend): ignore decoupled plugins that run their own tests

* test(stackdriver): update snapshot due to reactinlinesvg mock difference

* chore(yarn): update lock file

* chore(plugin-configs): remove create-plugin comments

* ci(frontend-unit-tests): make them run on pr or merge to main

* chore(decoupled-plugins): add plugin:test:ci command for locally running all tests

* ci(frontend-unit-tests): run test:ci instead of calling nx directly

* chore(decoupled-plugins): fix jest-dom types for loki and testdata
This commit is contained in:
Jack Westbrook
2025-07-17 14:47:52 +02:00
committed by GitHub
parent 4b217c601a
commit b95c00a80f
45 changed files with 448 additions and 20 deletions
@@ -0,0 +1,82 @@
import '@testing-library/jest-dom';
import { TextEncoder, TextDecoder } from 'util';
Object.assign(global, { TextDecoder, TextEncoder });
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
writable: true,
value: (query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}),
});
const mockIntersectionObserver = jest.fn().mockImplementation((callback) => ({
observe: jest.fn().mockImplementation((elem) => {
callback([{ target: elem, isIntersecting: true }]);
}),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
global.IntersectionObserver = mockIntersectionObserver;
Object.defineProperty(document, 'fonts', {
value: { ready: Promise.resolve({}) },
});
// Used by useMeasure
global.ResizeObserver = class ResizeObserver {
static #observationEntry = {
contentRect: {
x: 1,
y: 2,
width: 500,
height: 500,
top: 100,
bottom: 0,
left: 100,
right: 0,
},
target: {
// Needed for react-virtual to work in tests
getAttribute: () => 1,
},
};
#isObserving = false;
#callback;
constructor(callback) {
this.#callback = callback;
}
#emitObservation() {
setTimeout(() => {
if (!this.#isObserving) {
return;
}
this.#callback([ResizeObserver.#observationEntry], this);
});
}
observe() {
this.#isObserving = true;
this.#emitObservation();
}
disconnect() {
this.#isObserving = false;
}
unobserve() {
this.#isObserving = false;
}
};
@@ -0,0 +1,46 @@
process.env.TZ = 'Pacific/Easter'; // UTC-06:00 or UTC-05:00 depending on daylight savings
import path from 'path';
import { grafanaESModules, nodeModulesToTransform } from './utils.js';
export default {
moduleNameMapper: {
'\\.(css|scss|sass)$': 'identity-obj-proxy',
'react-inlinesvg': path.resolve(import.meta.dirname, 'mocks', 'react-inlinesvg.tsx'),
'\\.(svg|png|jpg)': path.resolve(import.meta.dirname, 'mocks', 'images.ts'),
'^monaco-editor$': 'monaco-editor/esm/vs/editor/editor.api.js',
'@kusto/monaco-kusto': '@kusto/monaco-kusto/release/esm/monaco.contribution.js',
},
modulePaths: ['<rootDir>'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'cjs'],
setupFiles: ['jest-canvas-mock'],
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/**/__tests__/**/*.{js,jsx,ts,tsx}', '<rootDir>/**/*.{spec,test,jest}.{js,jsx,ts,tsx}'],
transform: {
'^.+\\.(t|j)sx?$': [
'@swc/jest',
{
sourceMaps: 'inline',
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
],
},
// Jest will throw `Cannot use import statement outside module` if it tries to load an
// ES module without it being transformed first. ./config/README.md#esm-errors-with-jest
transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],
watchPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/dist'],
};
@@ -0,0 +1 @@
export default '__DEFAULT_MOCK_IMAGE_CONTENT__';
@@ -0,0 +1,25 @@
// Due to the grafana/ui Icon component making fetch requests to
// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent
// the failed fetch requests from displaying errors in console.
import { Ref } from 'react';
type Callback = (...args: unknown[]) => void;
export interface StorageItem {
content: string;
queue: Callback[];
status: string;
}
export const cacheStore: { [key: string]: StorageItem } = Object.create(null);
const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/;
const InlineSVG = ({ src, innerRef, ...rest }: { src: string; innerRef: Ref<SVGSVGElement> }) => {
// testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)
const testId = src.replace(SVG_FILE_NAME_REGEX, '$2');
return <svg xmlns="http://www.w3.org/2000/svg" data-testid={testId} viewBox="0 0 24 24" ref={innerRef} {...rest} />;
};
export default InlineSVG;
@@ -0,0 +1,31 @@
export const nodeModulesToTransform = (moduleNames) => `.*\/node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`;
// Array of known nested grafana package dependencies that only bundle an ESM version
export const grafanaESModules = [
'@glideapps/glide-data-grid',
'@wojtekmaj/date-utils',
'ol',
'd3',
'd3-color',
'd3-interpolate',
'delaunator',
'get-user-locale',
'internmap',
'robust-predicates',
'leven',
'nanoid',
'marked',
'memoize',
'mimic-function',
'monaco-promql',
'react-calendar',
'@kusto/monaco-kusto',
'monaco-editor',
'@msagl',
'lodash-es',
'vscode-languageserver-types',
'@bsull/augurs',
'react-data-grid',
'@grafana/llm',
'pkce-challenge',
];
@@ -10,6 +10,8 @@
"devDependencies": {
"@grafana/tsconfig": "^2.0.0",
"@swc/core": "1.10.12",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.26",
"@types/eslint": "9.6.1",
"@types/webpack-bundle-analyzer": "^4.7.0",
"copy-webpack-plugin": "12.0.2",
@@ -17,7 +19,10 @@
"eslint-webpack-plugin": "4.2.0",
"fork-ts-checker-webpack-plugin": "9.0.2",
"glob": "11.0.3",
"identity-obj-proxy": "^3.0.0",
"imports-loader": "^5.0.0",
"jest-canvas-mock": "2.5.2",
"jest-environment-jsdom": "29.7.0",
"replace-in-file-webpack-plugin": "1.0.6",
"swc-loader": "0.2.6",
"typescript": "5.8.3",