@atlassian/chippy 0.0.0-stage → 0.0.1-beta.1

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.
Files changed (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -2
  3. package/bin/chippy.mjs +134 -0
  4. package/bin/pack.mjs +184 -0
  5. package/lib/avalanche.js +2570 -0
  6. package/lib/avalanche.js.map +7 -0
  7. package/lib/bootstrap.d.ts +1 -0
  8. package/lib/bootstrap.js +51 -0
  9. package/lib/bootstrap.js.map +7 -0
  10. package/lib/browser.js +15 -0
  11. package/lib/browser.js.map +7 -0
  12. package/lib/extension/avalanche.es.js +5396 -0
  13. package/lib/extension/avalanche.es.js.map +1 -0
  14. package/lib/extension/chippy-page-bridge.js +1833 -0
  15. package/lib/extension/inspect-react.js +216 -0
  16. package/lib/extension/layout-lab.js +1453 -0
  17. package/lib/extension/panel-dom.js +90 -0
  18. package/lib/extension/resolve-probes.js +342 -0
  19. package/lib/mcp-skill/LICENSE +201 -0
  20. package/lib/mcp-skill/README.md +150 -0
  21. package/lib/mcp-skill/SKILL.md +330 -0
  22. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  24. package/lib/mcp-skill/extension/avalanche.es.js +5396 -0
  25. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  26. package/lib/mcp-skill/extension/avalanche.iife.js +5397 -0
  27. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  28. package/lib/mcp-skill/extension/background.js +323 -0
  29. package/lib/mcp-skill/extension/chippy-page-bridge.js +1833 -0
  30. package/lib/mcp-skill/extension/content-script.js +167 -0
  31. package/lib/mcp-skill/extension/devtools-hook-shim.js +68 -0
  32. package/lib/mcp-skill/extension/early-bootstrap.js +47 -0
  33. package/lib/mcp-skill/extension/icon-128.png +0 -0
  34. package/lib/mcp-skill/extension/icon-16.png +0 -0
  35. package/lib/mcp-skill/extension/icon-32.png +0 -0
  36. package/lib/mcp-skill/extension/icon-48.png +0 -0
  37. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  38. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  39. package/lib/mcp-skill/extension/logo.svg +14 -0
  40. package/lib/mcp-skill/extension/manifest.json +69 -0
  41. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  42. package/lib/mcp-skill/extension/panel-settings.js +191 -0
  43. package/lib/mcp-skill/extension/popup.html +41 -0
  44. package/lib/mcp-skill/extension/popup.js +80 -0
  45. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  46. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  47. package/lib/mcp-skill/package.json +1 -0
  48. package/lib/mcp-skill/references/README.md +56 -0
  49. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  50. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  51. package/lib/mcp-skill/references/bdd-router.md +310 -0
  52. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  53. package/lib/mcp-skill/references/command-output-formats.md +1075 -0
  54. package/lib/mcp-skill/references/command-surface.md +183 -0
  55. package/lib/mcp-skill/references/cynefin.md +99 -0
  56. package/lib/mcp-skill/references/debug-hook.md +161 -0
  57. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  58. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  59. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  60. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  61. package/lib/mcp-skill/references/mcp.md +122 -0
  62. package/lib/mcp-skill/references/mode-a-injection.md +68 -0
  63. package/lib/mcp-skill/references/operating-model.md +128 -0
  64. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  65. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  66. package/lib/mcp-skill/references/procedures.md +545 -0
  67. package/lib/mcp-skill/references/recipes.txt +114 -0
  68. package/lib/mcp-skill/references/stability-render.md +428 -0
  69. package/lib/mcp-skill/references/test-mode-playbook.md +193 -0
  70. package/lib/mcp-skill/references/troubleshooting.md +54 -0
  71. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  72. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  73. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  74. package/lib/mcp-skill/scripts/chippy-helper-server.js +1710 -0
  75. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  76. package/lib/mcp-skill/scripts/chippy.sh +1218 -0
  77. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  78. package/lib/mcp-skill/scripts/helper-access.js +144 -0
  79. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  80. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  81. package/lib/mcp-skill/snippets/README.md +89 -0
  82. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  83. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  84. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  85. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  86. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  87. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  88. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  89. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  90. package/lib/package.json +1 -0
  91. package/lib/plugin.js +119 -0
  92. package/lib/plugin.js.map +7 -0
  93. package/lib/storybook/avalanche-panel-model.js +79 -0
  94. package/lib/storybook/brand.jsx +6 -0
  95. package/lib/storybook/connection-fallback.jsx +47 -0
  96. package/lib/storybook/constants.js +15 -0
  97. package/lib/storybook/context-card.jsx +171 -0
  98. package/lib/storybook/manager.jsx +403 -0
  99. package/lib/storybook/preset.js +7 -0
  100. package/lib/storybook/preview.js +263 -0
  101. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  102. package/lib/storybook/probe-intent.js +23 -0
  103. package/lib/storybook/setup.js +93 -0
  104. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  105. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  106. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  107. package/lib/types/avalanche/detector.d.ts +183 -0
  108. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  109. package/lib/types/avalanche/internals.d.ts +189 -0
  110. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  111. package/lib/types/avalanche/reporter.d.ts +610 -0
  112. package/lib/types/chippy-bridge/fiber-source.d.ts +37 -0
  113. package/lib/types/chippy-bridge/react-renderer.d.ts +49 -0
  114. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  115. package/lib/types/debug-render/core.d.ts +72 -0
  116. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  117. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  118. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  119. package/lib/types/package/avalanche.d.ts +6 -0
  120. package/lib/types/package/browser.d.ts +10 -0
  121. package/lib/types/package/plugin.d.ts +15 -0
  122. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  123. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  124. package/lib/types/plugins/index.d.ts +49 -0
  125. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  126. package/package.json +104 -4
@@ -0,0 +1,167 @@
1
+ /* eslint-disable */
2
+ // The content script owns extension UX/configuration and delegates MAIN-world
3
+ // execution to the background service worker. It cannot inspect React fibers
4
+ // directly, so it communicates through page events and runtime messages;
5
+ // keeping this split also avoids injecting inline code through page CSP.
6
+ (function chippyContentScript() {
7
+
8
+ const DEFAULT_HELPER = 'http://127.0.0.1:7823';
9
+ const BUNDLE_ATTR = 'data-chippy-bundle';
10
+ const LOG_PREFIX = '[chippy-ext]';
11
+
12
+ const pageStatus = {
13
+ status: 'booting',
14
+ detail: '',
15
+ updatedAt: new Date().toISOString(),
16
+ helperBaseUrl: DEFAULT_HELPER,
17
+ activated: false,
18
+ };
19
+
20
+ function error(...args) { console.error(LOG_PREFIX, ...args); }
21
+
22
+ function getInjectionParent() {
23
+ return document.head || document.documentElement || document.body || null;
24
+ }
25
+
26
+ function appendToDocument(node, label, onAppended = null, attempt = 0) {
27
+ const parent = getInjectionParent();
28
+ if (parent) {
29
+ parent.appendChild(node);
30
+ onAppended?.();
31
+ return true;
32
+ }
33
+
34
+ if (attempt >= 50) {
35
+ error('failed to append node; no injection parent', { label, readyState: document.readyState });
36
+ return false;
37
+ }
38
+
39
+ window.setTimeout(() => appendToDocument(node, label, onAppended, attempt + 1), 20);
40
+ return false;
41
+ }
42
+
43
+ function normalizeBaseUrl(value) {
44
+ return String(value || DEFAULT_HELPER).replace(/\/+$/, '');
45
+ }
46
+
47
+ function getOriginKey() {
48
+ try {
49
+ return window.location.origin || null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function setStatus(status, detail = '') {
56
+ pageStatus.status = status;
57
+ pageStatus.detail = detail || '';
58
+ pageStatus.updatedAt = new Date().toISOString();
59
+ }
60
+
61
+ function writePageConfig(helperBaseUrl, activated) {
62
+ const normalizedBaseUrl = normalizeBaseUrl(helperBaseUrl);
63
+ pageStatus.helperBaseUrl = normalizedBaseUrl;
64
+ pageStatus.activated = Boolean(activated);
65
+ }
66
+
67
+ function injectBundle(helperBaseUrl) {
68
+ const base = normalizeBaseUrl(helperBaseUrl);
69
+
70
+ writePageConfig(base, true);
71
+ setStatus('injecting', 'background bundle');
72
+
73
+ chrome.runtime.sendMessage({ type: 'chippy-inject-bundle', helperBaseUrl: base }, (response) => {
74
+ if (chrome.runtime.lastError) {
75
+ error('injectBundle:background-error', { message: chrome.runtime.lastError.message });
76
+ setStatus('load-error', chrome.runtime.lastError.message);
77
+ return;
78
+ }
79
+
80
+ if (!response?.ok) {
81
+ error('injectBundle:background-failed', response || null);
82
+ setStatus('load-error', response?.error || 'Background injection failed');
83
+ return;
84
+ }
85
+
86
+ setStatus('loaded', response.detail || 'background bundle injected');
87
+ });
88
+ }
89
+
90
+ function disableBundle() {
91
+ writePageConfig(DEFAULT_HELPER, false);
92
+ // Dispatch a custom event so the page bridge can pick up deactivation
93
+ // without inline script injection (CSP-safe).
94
+ try {
95
+ document.dispatchEvent(new CustomEvent('chippy-ext-command', {
96
+ detail: { type: 'deactivate' },
97
+ }));
98
+ } catch {}
99
+ document.querySelectorAll(`script[${BUNDLE_ATTR}]`).forEach((node) => node.remove());
100
+ setStatus('disabled', getOriginKey() || 'disabled');
101
+ }
102
+
103
+ function applyStoredConfig() {
104
+ const origin = getOriginKey();
105
+ if (!origin) {
106
+ setStatus('unsupported', 'No origin');
107
+ return;
108
+ }
109
+
110
+ chrome.storage.local.get({
111
+ helperBaseUrl: DEFAULT_HELPER,
112
+ enabledOrigins: {},
113
+ }, (stored) => {
114
+ const enabled = Boolean(stored.enabledOrigins?.[origin]);
115
+ if (!enabled) {
116
+ setStatus('disabled', origin);
117
+ return;
118
+ }
119
+ injectBundle(normalizeBaseUrl(stored.helperBaseUrl));
120
+ });
121
+ }
122
+
123
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
124
+ if (message.type === 'chippy-get-page-status') {
125
+ const bundle = document.querySelector(`script[${BUNDLE_ATTR}]`);
126
+ sendResponse({
127
+ ok: true,
128
+ result: {
129
+ origin: getOriginKey(),
130
+ status: pageStatus.status || 'unknown',
131
+ detail: pageStatus.detail || '',
132
+ updatedAt: pageStatus.updatedAt || null,
133
+ helperBaseUrl: pageStatus.helperBaseUrl || null,
134
+ activated: Boolean(pageStatus.activated),
135
+ bundlePresent: Boolean(bundle),
136
+ bundleSrc: bundle?.src || null,
137
+ },
138
+ });
139
+ return;
140
+ }
141
+
142
+ if (message.type === 'chippy-disable-now') {
143
+ disableBundle();
144
+ sendResponse({ ok: true });
145
+ return;
146
+ }
147
+
148
+ if (message.type === 'chippy-update-helper-url') {
149
+ const enabled = Boolean(message.enabled);
150
+ const helperBaseUrl = normalizeBaseUrl(message.helperBaseUrl);
151
+ writePageConfig(helperBaseUrl, enabled);
152
+ if (enabled) {
153
+ try {
154
+ document.dispatchEvent(new CustomEvent('chippy-ext-command', {
155
+ detail: { type: 'apply-config', helperBaseUrl, activated: true },
156
+ }));
157
+ } catch {}
158
+ }
159
+ setStatus('configured', helperBaseUrl);
160
+ sendResponse({ ok: true });
161
+ return;
162
+ }
163
+ });
164
+
165
+ applyStoredConfig();
166
+
167
+ })();
@@ -0,0 +1,68 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * DevTools-hook shim.
4
+ *
5
+ * Must be installed BEFORE react-dom evaluates. React-DOM checks for
6
+ * window.__REACT_DEVTOOLS_GLOBAL_HOOK__
7
+ * at module evaluation time. If a hook with `inject(internals)` is present,
8
+ * react-dom registers itself into hook.renderers, which exposes the
9
+ * `currentDispatcherRef` that Avalanche debug-render needs to capture hook
10
+ * call stacks.
11
+ *
12
+ * Without this shim in place at evaluation time, hook.renderers stays empty,
13
+ * debug-render cannot patch the dispatcher, and `debugHook()` collapses to
14
+ * a flat fallback (no custom-hook ancestry, no folders).
15
+ *
16
+ * Idempotent: never overwrites an existing real hook (e.g. real React DevTools
17
+ * extension). Only installs an empty shim when nothing is there yet.
18
+ *
19
+ * Browser extension content-script and the local Playwright path both load
20
+ * this script BEFORE the page bootstraps React.
21
+ */
22
+ (function chippyDevtoolsHookShim(global) {
23
+ try {
24
+ if (global.__REACT_DEVTOOLS_GLOBAL_HOOK__) return;
25
+
26
+ var renderers = new Map();
27
+ var hook = {
28
+ __chippyShim: true,
29
+ supportsFiber: true,
30
+ renderers: renderers,
31
+ inject: function inject(renderer) {
32
+ var id = (renderers.size || 0) + 1;
33
+ renderers.set(id, renderer);
34
+ return id;
35
+ },
36
+ onScheduleFiberRoot: function () {},
37
+ onCommitFiberRoot: function () {},
38
+ onCommitFiberUnmount: function () {},
39
+ onPostCommitFiberRoot: function () {},
40
+ };
41
+
42
+ Object.defineProperty(global, '__REACT_DEVTOOLS_GLOBAL_HOOK__', {
43
+ configurable: true,
44
+ enumerable: true,
45
+ get: function () { return hook; },
46
+ set: function (next) {
47
+ // Real DevTools may try to install later. Accept it but preserve any
48
+ // renderers we already collected so debug-render can still find them.
49
+ if (next && typeof next === 'object') {
50
+ if (!next.renderers || (next.renderers.size === 0 && renderers.size > 0)) {
51
+ try { next.renderers = renderers; } catch (_) {}
52
+ }
53
+ hook = next;
54
+ }
55
+ },
56
+ });
57
+
58
+ // Cross-iframe note: if running in a top-frame, mirror so child iframes
59
+ // (Storybook iframe.html) can rely on it too. Safe no-op otherwise.
60
+ try {
61
+ if (global.top && global.top !== global && !global.top.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
62
+ global.top.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
63
+ }
64
+ } catch (_) {}
65
+ } catch (_) {
66
+ // Never throw at page boot. A failed shim must degrade silently.
67
+ }
68
+ })(typeof globalThis !== 'undefined' ? globalThis : window);
@@ -0,0 +1,47 @@
1
+ /* eslint-disable */
2
+ // Runs in MAIN before React evaluates. This phase exists only to establish
3
+ // timing-sensitive page contracts (startup config and the registered commit
4
+ // metrics adapter's pre-page hook). The actual Chippy bridge is injected
5
+ // later, after owner arbitration, so a user-controlled Storybook/import
6
+ // integration can win.
7
+ (function chippyEarlyBootstrap() {
8
+ if (globalThis.__CHIPPY_EARLY_BOOTSTRAP_INSTALLED__) {
9
+ return;
10
+ }
11
+ globalThis.__CHIPPY_EARLY_BOOTSTRAP_INSTALLED__ = true;
12
+
13
+ const cfg = globalThis.__CHIPPY_STARTUP_CONFIG || null;
14
+ if (!cfg || !cfg.enabled) {
15
+ return;
16
+ }
17
+
18
+ globalThis.__CHIPPY = globalThis.__CHIPPY || {};
19
+
20
+ globalThis.__CHIPPY.config = {
21
+ ...(globalThis.__CHIPPY.config || {}),
22
+ helperBaseUrl: cfg.helperBaseUrl,
23
+ activated: true,
24
+ avalanche: cfg.avalanche ? {
25
+ autoStart: true,
26
+ max: (cfg.avalanche && cfg.avalanche.max) || 500,
27
+ options: (cfg.avalanche && cfg.avalanche.options) || {},
28
+ } : { autoStart: false, max: 500 },
29
+ };
30
+
31
+ // Run the registered commit metrics adapter's pre-page hook. It must run
32
+ // before the page's React evaluates (for example, the ATL adapter installs a
33
+ // configurable metrics-slot accessor). avalanche.iife.js loads before this
34
+ // script, so the registry is populated. A failure here is non-fatal:
35
+ // Avalanche falls back to DevTools integration.
36
+ try {
37
+ if (globalThis.__AVALANCHE__ && typeof globalThis.__AVALANCHE__.runPrePageHooks === 'function') {
38
+ globalThis.__AVALANCHE__.runPrePageHooks(cfg);
39
+ }
40
+ } catch (_e) {
41
+ // Non-fatal.
42
+ }
43
+
44
+ if (globalThis.__AVALANCHE__) {
45
+ return;
46
+ }
47
+ })();
@@ -0,0 +1,216 @@
1
+ /* eslint-disable */
2
+ (function installInspectReact(global) {
3
+ const chippy = global.__CHIPPY = global.__CHIPPY || {};
4
+ // Always reinstall — guards caused stale code to win after extension reload.
5
+
6
+ function getReactInternalKeys(node) {
7
+ if (!node) {
8
+ return [];
9
+ }
10
+
11
+ return Object.getOwnPropertyNames(node).filter(
12
+ (key) =>
13
+ key.startsWith('__reactFiber$') ||
14
+ key.startsWith('__reactProps$') ||
15
+ key.startsWith('__reactContainer$'),
16
+ );
17
+ }
18
+
19
+ function getFiberFromNode(node) {
20
+ if (!node) {
21
+ return null;
22
+ }
23
+
24
+ const fiberKey = getReactInternalKeys(node).find((key) => key.startsWith('__reactFiber$'));
25
+ const containerKey = getReactInternalKeys(node).find((key) => key.startsWith('__reactContainer$'));
26
+
27
+ return node[fiberKey] || node[containerKey] || null;
28
+ }
29
+
30
+ function getClosestFiber(node) {
31
+ let current = node;
32
+
33
+ while (current) {
34
+ const fiber = getFiberFromNode(current);
35
+ if (fiber) {
36
+ return fiber;
37
+ }
38
+ current = current.parentNode || current.host || null;
39
+ }
40
+
41
+ return null;
42
+ }
43
+
44
+ function getDisplayName(type) {
45
+ if (!type) {
46
+ return null;
47
+ }
48
+ if (typeof type === 'string') {
49
+ return type;
50
+ }
51
+ return type.displayName || type.name || type.render?.displayName || type.render?.name || 'Anonymous';
52
+ }
53
+
54
+ function normalizeSource(source) {
55
+ if (!source) {
56
+ return null;
57
+ }
58
+
59
+ return {
60
+ fileName: source.fileName || null,
61
+ lineNumber: source.lineNumber || null,
62
+ columnNumber: source.columnNumber || null,
63
+ };
64
+ }
65
+
66
+ function sourceOf(fiber) {
67
+ const describeFiberSource = global.__AVALANCHE__?.layoutLab?.describeFiberSource;
68
+ return typeof describeFiberSource === 'function'
69
+ ? describeFiberSource(fiber)
70
+ : normalizeSource(fiber?._debugSource || fiber?.type?._debugSource || null);
71
+ }
72
+
73
+ function describeFiber(fiber) {
74
+ if (!fiber) {
75
+ return null;
76
+ }
77
+
78
+ return {
79
+ key: fiber.key ?? null,
80
+ tag: fiber.tag ?? null,
81
+ index: fiber.index ?? null,
82
+ elementType: getDisplayName(fiber.elementType),
83
+ type: getDisplayName(fiber.type),
84
+ stateNodeType:
85
+ fiber.stateNode && fiber.stateNode.nodeType === 1
86
+ ? fiber.stateNode.tagName.toLowerCase()
87
+ : typeof fiber.stateNode,
88
+ debugSource: sourceOf(fiber),
89
+ memoizedPropsKeys: fiber.memoizedProps ? Object.keys(fiber.memoizedProps) : [],
90
+ };
91
+ }
92
+
93
+ function isCompositeFiber(fiber) {
94
+ if (!fiber) {
95
+ return false;
96
+ }
97
+
98
+ const name = getDisplayName(fiber.type);
99
+ return Boolean(name && typeof fiber.type !== 'string');
100
+ }
101
+
102
+ function getOwnerChain(fiber) {
103
+ const owners = [];
104
+ let current = fiber ? fiber._debugOwner : null;
105
+
106
+ while (current) {
107
+ owners.push(describeFiber(current));
108
+ current = current._debugOwner || null;
109
+ }
110
+
111
+ return owners;
112
+ }
113
+
114
+ function getReturnChain(fiber) {
115
+ const parents = [];
116
+ let current = fiber ? fiber.return : null;
117
+
118
+ while (current) {
119
+ parents.push(describeFiber(current));
120
+ current = current.return || null;
121
+ }
122
+
123
+ return parents;
124
+ }
125
+
126
+ function getNearestCompositeFiber(fiber) {
127
+ let current = fiber;
128
+
129
+ while (current) {
130
+ if (isCompositeFiber(current)) {
131
+ return current;
132
+ }
133
+ current = current.return || null;
134
+ }
135
+
136
+ return null;
137
+ }
138
+
139
+
140
+ function getNearestTestIdParent(node) {
141
+ // Walk includes the node itself — if the probed element has data-testid, that wins.
142
+ let current = node || null;
143
+ while (current) {
144
+ const testId = current.dataset ? current.dataset.testid : null;
145
+ if (testId) {
146
+ const fiber = getFiberFromNode(current) || getClosestFiber(current);
147
+ const componentFiber = getNearestCompositeFiber(fiber);
148
+ return {
149
+ testId,
150
+ tagName: current.tagName ? current.tagName.toLowerCase() : null,
151
+ component: describeFiber(componentFiber),
152
+ fiber: describeFiber(fiber),
153
+ };
154
+ }
155
+ current = current.parentElement || null;
156
+ }
157
+ return null;
158
+ }
159
+
160
+ function inspectNode(node) {
161
+ const fiber = getFiberFromNode(node) || getClosestFiber(node);
162
+ const componentFiber = getNearestCompositeFiber(fiber);
163
+
164
+ return {
165
+ dom: node
166
+ ? {
167
+ tagName: node.tagName ? node.tagName.toLowerCase() : null,
168
+ text: node.textContent?.trim() || '',
169
+ reactKeys: getReactInternalKeys(node),
170
+ }
171
+ : null,
172
+ fiber: describeFiber(fiber),
173
+ component: describeFiber(componentFiber),
174
+ owners: getOwnerChain(componentFiber || fiber),
175
+ returns: getReturnChain(componentFiber || fiber),
176
+ nearestTestIdParent: getNearestTestIdParent(node),
177
+ };
178
+ }
179
+
180
+ function findByText(text, root = document) {
181
+ return Array.from(root.querySelectorAll('*'))
182
+ .filter((node) => node.textContent?.trim() === text)
183
+ .map((node) => ({
184
+ node,
185
+ inspection: inspectNode(node),
186
+ }));
187
+ }
188
+
189
+ function findBySelector(selector, root = document) {
190
+ return Array.from(root.querySelectorAll(selector)).map((node) => ({
191
+ node,
192
+ inspection: inspectNode(node),
193
+ }));
194
+ }
195
+
196
+ function inspectAtPoint(x, y) {
197
+ return document.elementsFromPoint(x, y).map((node) => ({
198
+ node,
199
+ inspection: inspectNode(node),
200
+ }));
201
+ }
202
+
203
+ chippy.inspectReact = {
204
+ getReactInternalKeys,
205
+ getFiberFromNode,
206
+ getClosestFiber,
207
+ getNearestCompositeFiber,
208
+ describeFiber,
209
+ getOwnerChain,
210
+ getReturnChain,
211
+ inspectNode,
212
+ inspectAtPoint,
213
+ findByText,
214
+ findBySelector,
215
+ };
216
+ })(window);