@grafana/create-plugin 7.12.1 → 7.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.12.2](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.12.1...@grafana/create-plugin@7.12.2) (2026-10-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **jest:** fill in unimplemented interfaces with jest-canvas-mock and intersection-observer ([#2933](https://github.com/grafana/plugin-tools/issues/2933)) ([a111afe](https://github.com/grafana/plugin-tools/commit/a111afe941b7cd3c4b3bc87c30a0c40853c65383))
9
+
3
10
  ## [7.12.1](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.12.0...@grafana/create-plugin@7.12.1) (2026-10-06)
4
11
 
5
12
 
@@ -84,6 +84,18 @@ var defaultMigrations = [
84
84
  version: "7.11.1",
85
85
  description: "Update @grafana/eslint-config to v10: drop the removed ./flat.js subpath import and swap @stylistic/eslint-plugin-ts for @stylistic/eslint-plugin.",
86
86
  scriptPath: import.meta.resolve("./scripts/014-eslint-config-v10.js")
87
+ },
88
+ {
89
+ name: "015-jest-canvas-mock",
90
+ version: "7.12.2",
91
+ description: "Fix @grafana/ui Combobox crashing in Jest: replace the scaffolded no-op HTMLCanvasElement.getContext stub with jest-canvas-mock, which returns a spec-shaped CanvasRenderingContext2D (and TextMetrics) so measureText() no longer throws.",
92
+ scriptPath: import.meta.resolve("./scripts/015-jest-canvas-mock.js")
93
+ },
94
+ {
95
+ name: "016-jest-intersection-observer",
96
+ version: "7.12.2",
97
+ description: "Fix @grafana/ui Combobox dropdown crashing in Jest: add an IntersectionObserver polyfill stub so ScrollIndicators (mounted when the dropdown opens) no longer throws ReferenceError in jsdom.",
98
+ scriptPath: import.meta.resolve("./scripts/016-jest-intersection-observer.js")
87
99
  }
88
100
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
89
101
  // for those written before the switch to updates as migrations.
@@ -0,0 +1,35 @@
1
+ import { addDependenciesToPackageJson } from '../../utils.js';
2
+
3
+ const JEST_SETUP_PATH = ".config/jest-setup.js";
4
+ const BROKEN_CANVAS_STUB_PATTERN = /^HTMLCanvasElement\.prototype\.getContext\s*=\s*\(\)\s*=>\s*\{\};\s*\n?/m;
5
+ const CANVAS_MOCK_IMPORT = "import 'jest-canvas-mock';";
6
+ const JEST_CANVAS_MOCK_VERSION = "^2.5.2";
7
+ function migrate(context) {
8
+ if (!context.doesFileExist(JEST_SETUP_PATH)) {
9
+ return context;
10
+ }
11
+ const original = context.getFile(JEST_SETUP_PATH) ?? "";
12
+ let updated = original;
13
+ if (BROKEN_CANVAS_STUB_PATTERN.test(updated)) {
14
+ updated = updated.replace(BROKEN_CANVAS_STUB_PATTERN, "");
15
+ }
16
+ if (!updated.includes(CANVAS_MOCK_IMPORT)) {
17
+ updated = addCanvasMockImport(updated);
18
+ }
19
+ if (updated !== original) {
20
+ context.updateFile(JEST_SETUP_PATH, updated);
21
+ }
22
+ addDependenciesToPackageJson(context, {}, { "jest-canvas-mock": JEST_CANVAS_MOCK_VERSION });
23
+ return context;
24
+ }
25
+ function addCanvasMockImport(content) {
26
+ const anchor = "import '@testing-library/jest-dom';";
27
+ if (content.includes(anchor)) {
28
+ return content.replace(anchor, `${anchor}
29
+ ${CANVAS_MOCK_IMPORT}`);
30
+ }
31
+ return `${CANVAS_MOCK_IMPORT}
32
+ ${content}`;
33
+ }
34
+
35
+ export { migrate as default };
@@ -0,0 +1,28 @@
1
+ import { addDependenciesToPackageJson } from '../../utils.js';
2
+
3
+ const JEST_SETUP_PATH = ".config/jest-setup.js";
4
+ const INTERSECTION_OBSERVER_IMPORT = "import 'intersection-observer';";
5
+ const INTERSECTION_OBSERVER_VERSION = "^0.12.2";
6
+ function migrate(context) {
7
+ if (!context.doesFileExist(JEST_SETUP_PATH)) {
8
+ return context;
9
+ }
10
+ const original = context.getFile(JEST_SETUP_PATH) ?? "";
11
+ if (!original.includes(INTERSECTION_OBSERVER_IMPORT)) {
12
+ const updated = addIntersectionObserverImport(original);
13
+ context.updateFile(JEST_SETUP_PATH, updated);
14
+ }
15
+ addDependenciesToPackageJson(context, {}, { "intersection-observer": INTERSECTION_OBSERVER_VERSION });
16
+ return context;
17
+ }
18
+ function addIntersectionObserverImport(content) {
19
+ const anchor = "import 'jest-canvas-mock';";
20
+ if (content.includes(anchor)) {
21
+ return content.replace(anchor, `${anchor}
22
+ ${INTERSECTION_OBSERVER_IMPORT}`);
23
+ }
24
+ return `${INTERSECTION_OBSERVER_IMPORT}
25
+ ${content}`;
26
+ }
27
+
28
+ export { migrate as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "7.12.1",
3
+ "version": "7.12.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "directory": "packages/create-plugin",
@@ -99,6 +99,20 @@ export default [
99
99
  'Update @grafana/eslint-config to v10: drop the removed ./flat.js subpath import and swap @stylistic/eslint-plugin-ts for @stylistic/eslint-plugin.',
100
100
  scriptPath: import.meta.resolve('./scripts/014-eslint-config-v10.js'),
101
101
  },
102
+ {
103
+ name: '015-jest-canvas-mock',
104
+ version: '7.12.2',
105
+ description:
106
+ 'Fix @grafana/ui Combobox crashing in Jest: replace the scaffolded no-op HTMLCanvasElement.getContext stub with jest-canvas-mock, which returns a spec-shaped CanvasRenderingContext2D (and TextMetrics) so measureText() no longer throws.',
107
+ scriptPath: import.meta.resolve('./scripts/015-jest-canvas-mock.js'),
108
+ },
109
+ {
110
+ name: '016-jest-intersection-observer',
111
+ version: '7.12.2',
112
+ description:
113
+ 'Fix @grafana/ui Combobox dropdown crashing in Jest: add an IntersectionObserver polyfill stub so ScrollIndicators (mounted when the dropdown opens) no longer throws ReferenceError in jsdom.',
114
+ scriptPath: import.meta.resolve('./scripts/016-jest-intersection-observer.js'),
115
+ },
102
116
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
103
117
  // for those written before the switch to updates as migrations.
104
118
  //
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import migrate from './015-jest-canvas-mock.js';
3
+ import { Context } from '../../context.js';
4
+
5
+ const JEST_SETUP_PATH = '.config/jest-setup.js';
6
+
7
+ const SCAFFOLD_JEST_SETUP = `/*
8
+ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
9
+ */
10
+
11
+ import '@testing-library/jest-dom';
12
+
13
+ Object.defineProperty(global, 'matchMedia', { writable: true, value: () => ({}) });
14
+
15
+ HTMLCanvasElement.prototype.getContext = () => {};
16
+ `;
17
+
18
+ function setupContext(setup = SCAFFOLD_JEST_SETUP, extraDevDeps: Record<string, string> = {}) {
19
+ const context = new Context('/virtual');
20
+ context.addFile(JEST_SETUP_PATH, setup);
21
+ context.addFile('package.json', JSON.stringify({ devDependencies: { jest: '^29.7.0', ...extraDevDeps } }));
22
+ return context;
23
+ }
24
+
25
+ describe('015-jest-canvas-mock', () => {
26
+ it('should replace the broken canvas stub, add the jest-canvas-mock import and devDependency, and preserve surrounding content', () => {
27
+ const result = migrate(setupContext());
28
+ const updated = result.getFile(JEST_SETUP_PATH) || '';
29
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
30
+
31
+ expect(updated).not.toMatch(/HTMLCanvasElement\.prototype\.getContext\s*=\s*\(\)\s*=>\s*\{\}/);
32
+ expect(updated).toContain("import 'jest-canvas-mock';");
33
+ expect(updated).toContain('DO NOT EDIT THIS FILE DIRECTLY');
34
+ expect(updated).toContain('matchMedia');
35
+ expect(packageJson.devDependencies['jest-canvas-mock']).toBe('^2.5.2');
36
+ });
37
+
38
+ it('should not modify anything when the jest-setup file does not exist', () => {
39
+ const context = new Context('/virtual');
40
+
41
+ const result = migrate(context);
42
+
43
+ expect(result.hasChanges()).toBe(false);
44
+ });
45
+
46
+ it('should be idempotent', async () => {
47
+ await expect(migrate).toBeIdempotent(setupContext());
48
+ });
49
+ });
@@ -0,0 +1,40 @@
1
+ import type { Context } from '../../context.js';
2
+ import { addDependenciesToPackageJson } from '../../utils.js';
3
+
4
+ const JEST_SETUP_PATH = '.config/jest-setup.js';
5
+ const BROKEN_CANVAS_STUB_PATTERN = /^HTMLCanvasElement\.prototype\.getContext\s*=\s*\(\)\s*=>\s*\{\};\s*\n?/m;
6
+ const CANVAS_MOCK_IMPORT = "import 'jest-canvas-mock';";
7
+ const JEST_CANVAS_MOCK_VERSION = '^2.5.2';
8
+
9
+ export default function migrate(context: Context) {
10
+ if (!context.doesFileExist(JEST_SETUP_PATH)) {
11
+ return context;
12
+ }
13
+
14
+ const original = context.getFile(JEST_SETUP_PATH) ?? '';
15
+ let updated = original;
16
+
17
+ if (BROKEN_CANVAS_STUB_PATTERN.test(updated)) {
18
+ updated = updated.replace(BROKEN_CANVAS_STUB_PATTERN, '');
19
+ }
20
+
21
+ if (!updated.includes(CANVAS_MOCK_IMPORT)) {
22
+ updated = addCanvasMockImport(updated);
23
+ }
24
+
25
+ if (updated !== original) {
26
+ context.updateFile(JEST_SETUP_PATH, updated);
27
+ }
28
+
29
+ addDependenciesToPackageJson(context, {}, { 'jest-canvas-mock': JEST_CANVAS_MOCK_VERSION });
30
+
31
+ return context;
32
+ }
33
+
34
+ function addCanvasMockImport(content: string) {
35
+ const anchor = "import '@testing-library/jest-dom';";
36
+ if (content.includes(anchor)) {
37
+ return content.replace(anchor, `${anchor}\n${CANVAS_MOCK_IMPORT}`);
38
+ }
39
+ return `${CANVAS_MOCK_IMPORT}\n${content}`;
40
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import migrate from './016-jest-intersection-observer.js';
3
+ import { Context } from '../../context.js';
4
+
5
+ const JEST_SETUP_PATH = '.config/jest-setup.js';
6
+
7
+ const SCAFFOLD_JEST_SETUP = `/*
8
+ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
9
+ */
10
+
11
+ import '@testing-library/jest-dom';
12
+ import 'jest-canvas-mock';
13
+
14
+ Object.defineProperty(global, 'matchMedia', { writable: true, value: () => ({}) });
15
+ `;
16
+
17
+ function setupContext(setup = SCAFFOLD_JEST_SETUP, extraDevDeps: Record<string, string> = {}) {
18
+ const context = new Context('/virtual');
19
+ context.addFile(JEST_SETUP_PATH, setup);
20
+ context.addFile('package.json', JSON.stringify({ devDependencies: { jest: '^29.7.0', ...extraDevDeps } }));
21
+ return context;
22
+ }
23
+
24
+ describe('016-jest-intersection-observer', () => {
25
+ it('should add the intersection-observer import and devDependency, and preserve surrounding content', () => {
26
+ const result = migrate(setupContext());
27
+ const updated = result.getFile(JEST_SETUP_PATH) || '';
28
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
29
+
30
+ expect(updated).toContain("import 'intersection-observer';");
31
+ expect(updated).toContain('DO NOT EDIT THIS FILE DIRECTLY');
32
+ expect(updated).toContain('matchMedia');
33
+ expect(updated).toContain("import 'jest-canvas-mock';");
34
+ expect(packageJson.devDependencies['intersection-observer']).toBe('^0.12.2');
35
+ });
36
+
37
+ it('should not modify anything when the jest-setup file does not exist', () => {
38
+ const context = new Context('/virtual');
39
+
40
+ const result = migrate(context);
41
+
42
+ expect(result.hasChanges()).toBe(false);
43
+ });
44
+
45
+ it('should be idempotent', async () => {
46
+ await expect(migrate).toBeIdempotent(setupContext());
47
+ });
48
+ });
@@ -0,0 +1,31 @@
1
+ import type { Context } from '../../context.js';
2
+ import { addDependenciesToPackageJson } from '../../utils.js';
3
+
4
+ const JEST_SETUP_PATH = '.config/jest-setup.js';
5
+ const INTERSECTION_OBSERVER_IMPORT = "import 'intersection-observer';";
6
+ const INTERSECTION_OBSERVER_VERSION = '^0.12.2';
7
+
8
+ export default function migrate(context: Context) {
9
+ if (!context.doesFileExist(JEST_SETUP_PATH)) {
10
+ return context;
11
+ }
12
+
13
+ const original = context.getFile(JEST_SETUP_PATH) ?? '';
14
+
15
+ if (!original.includes(INTERSECTION_OBSERVER_IMPORT)) {
16
+ const updated = addIntersectionObserverImport(original);
17
+ context.updateFile(JEST_SETUP_PATH, updated);
18
+ }
19
+
20
+ addDependenciesToPackageJson(context, {}, { 'intersection-observer': INTERSECTION_OBSERVER_VERSION });
21
+
22
+ return context;
23
+ }
24
+
25
+ function addIntersectionObserverImport(content: string) {
26
+ const anchor = "import 'jest-canvas-mock';";
27
+ if (content.includes(anchor)) {
28
+ return content.replace(anchor, `${anchor}\n${INTERSECTION_OBSERVER_IMPORT}`);
29
+ }
30
+ return `${INTERSECTION_OBSERVER_IMPORT}\n${content}`;
31
+ }
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import '@testing-library/jest-dom';
9
+ import 'jest-canvas-mock';
10
+ import 'intersection-observer';
9
11
  import { TextEncoder, TextDecoder } from 'util';
10
12
  import { MessageChannel, MessagePort } from 'worker_threads';
11
13
 
@@ -26,5 +28,3 @@ Object.defineProperty(global, 'matchMedia', {
26
28
  dispatchEvent: jest.fn(),
27
29
  }),
28
30
  });
29
-
30
- HTMLCanvasElement.prototype.getContext = () => {};
@@ -48,7 +48,9 @@
48
48
  "glob": "^11.1.0",
49
49
  "identity-obj-proxy": "^3.0.0",
50
50
  "imports-loader": "^5.0.0",
51
+ "intersection-observer": "^0.12.2",
51
52
  "jest": "^29.7.0",
53
+ "jest-canvas-mock": "^2.5.2",
52
54
  "jest-environment-jsdom": "^29.7.0",
53
55
  "prettier": "^3.6.0",
54
56
  "replace-in-file-webpack-plugin": "^1.0.0",{{#if useExperimentalRspack}}