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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user