├── .editorconfig ├── .eslintignore ├── .github └── workflows │ ├── docs.yml │ └── nodejs.yml ├── .gitignore ├── CHANGELOG.md ├── INTERNALS.md ├── LICENSE ├── PRIVACY.md ├── README.md ├── media └── preact-chrome-light.png ├── package-lock.json ├── package.json ├── playwright.config.ts ├── src ├── adapter │ ├── 10 │ │ ├── bindings.ts │ │ ├── log.ts │ │ ├── options.ts │ │ └── renderReason.ts │ ├── 11 │ │ ├── bindings.ts │ │ ├── log.ts │ │ ├── options.ts │ │ └── renderReason.ts │ ├── adapter │ │ ├── adapter.ts │ │ ├── filter.ts │ │ ├── highlight.ts │ │ ├── highlightUpdates.ts │ │ ├── picker.ts │ │ ├── port.ts │ │ └── profiler.ts │ ├── debug.test.ts │ ├── debug.ts │ ├── dom.ts │ ├── hook.ts │ ├── marks.ts │ ├── parse-semverish.test.ts │ ├── parse-semverish.ts │ ├── protocol │ │ ├── events.test.ts │ │ ├── events.ts │ │ ├── legacy │ │ │ └── operationsV1.ts │ │ ├── operations.test.ts │ │ ├── operations.ts │ │ ├── string-table.test.ts │ │ └── string-table.ts │ ├── renderer.ts │ ├── setup.ts │ ├── shared │ │ ├── bindings.ts │ │ ├── hooks.ts │ │ ├── idMapper.ts │ │ ├── inspectVNode.ts │ │ ├── renderReasons.ts │ │ ├── renderer.test.tsx │ │ ├── renderer.ts │ │ ├── serialize.test.tsx │ │ ├── serialize.ts │ │ ├── stats.ts │ │ ├── timings.ts │ │ ├── traverse.ts │ │ ├── update.test.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ └── store.test.ts ├── constants.ts ├── debug.ts ├── global.css ├── global.d.ts ├── shells │ ├── chrome │ │ └── manifest.json │ ├── edge │ │ └── manifest.json │ ├── firefox │ │ └── manifest.json │ ├── inline │ │ └── index.ts │ └── shared │ │ ├── background │ │ ├── background.ts │ │ └── emitter.ts │ │ ├── content-script.ts │ │ ├── icons │ │ ├── icon-128-disabled.png │ │ ├── icon-128.png │ │ ├── icon-16-disabled.png │ │ ├── icon-16.png │ │ ├── icon-192-disabled.png │ │ ├── icon-192.png │ │ ├── icon-32-disabled.png │ │ ├── icon-32.png │ │ ├── icon-48-disabled.png │ │ └── icon-48.png │ │ ├── installHook.ts │ │ ├── panel │ │ ├── empty-panel.html │ │ ├── panel.html │ │ ├── panel.ts │ │ └── settings.ts │ │ ├── popup │ │ ├── disabled.html │ │ ├── enabled.html │ │ └── popup.ts │ │ └── utils.ts └── view │ ├── components │ ├── Actions.tsx │ ├── AutoSizeInput.tsx │ ├── CanvasHighlight │ │ ├── CanvasHighlight.module.css │ │ └── CanvasHighlight.tsx │ ├── Checkbox │ │ └── Checkbox.tsx │ ├── ComponentName.tsx │ ├── DataInput │ │ ├── DataInput.module.css │ │ ├── index.tsx │ │ ├── parseValue.test.ts │ │ └── parseValue.ts │ ├── Devtools.module.css │ ├── Devtools.tsx │ ├── FilterPopup │ │ ├── FilterPopup.module.css │ │ └── FilterPopup.tsx │ ├── Highlighter.module.css │ ├── Highlighter.tsx │ ├── IconBtn.tsx │ ├── Message │ │ └── Message.tsx │ ├── OutsideClick.tsx │ ├── RadioBar.tsx │ ├── SidebarLayout.tsx │ ├── ThemeSwitcher.tsx │ ├── devtools.css │ ├── elements │ │ ├── Elements.tsx │ │ ├── OwnerInfo.tsx │ │ ├── TreeBar.module.css │ │ ├── TreeBar.tsx │ │ ├── TreeView.test.tsx │ │ ├── TreeView.tsx │ │ ├── VirtualizedList.tsx │ │ ├── background-logo.tsx │ │ └── useAutoIndent.ts │ ├── icons.tsx │ ├── profiler │ │ ├── components │ │ │ ├── CommitInfo │ │ │ │ ├── CommitInfo.tsx │ │ │ │ └── DebugInfo.tsx │ │ │ ├── CommitTimeline │ │ │ │ ├── CommitTimeline.test.tsx │ │ │ │ └── CommitTimeline.tsx │ │ │ ├── Profiler.tsx │ │ │ ├── ProfilerInfo │ │ │ │ └── ProfilerInfo.tsx │ │ │ ├── RenderReasons.tsx │ │ │ ├── RenderedAt │ │ │ │ ├── DebugNodeNav.tsx │ │ │ │ └── RenderedAt.tsx │ │ │ ├── SidebarHeader.tsx │ │ │ ├── Tabs │ │ │ │ └── Tabs.tsx │ │ │ └── TimelineBar │ │ │ │ ├── TimelineBar.module.css │ │ │ │ └── TimelineBar.tsx │ │ ├── data │ │ │ ├── commits.ts │ │ │ └── gradient.ts │ │ ├── flamegraph │ │ │ ├── FlameGraph.tsx │ │ │ ├── FlameGraphMode.tsx │ │ │ ├── FlameNode.tsx │ │ │ ├── FlamegraphStore.ts │ │ │ ├── inline-pattern.svg │ │ │ ├── modes │ │ │ │ ├── FlamegraphLayout.tsx │ │ │ │ ├── flamegraph-utils.ts │ │ │ │ ├── patchTree.test.ts │ │ │ │ └── patchTree.ts │ │ │ ├── placeNodes.ts │ │ │ ├── ranked │ │ │ │ ├── RankedLayout.tsx │ │ │ │ ├── ranked-utils.test.ts │ │ │ │ └── ranked-utils.ts │ │ │ ├── shared.ts │ │ │ ├── testHelpers.test.ts │ │ │ └── testHelpers.ts │ │ └── util.ts │ ├── settings │ │ └── Settings.tsx │ ├── sidebar │ │ ├── DebugNodeNavTree.tsx │ │ ├── DebugTreeStats.tsx │ │ ├── HocPanel.tsx │ │ ├── KeyPanel.tsx │ │ ├── Sidebar.tsx │ │ ├── SidebarActions.tsx │ │ ├── SidebarPanel.tsx │ │ └── inspect │ │ │ ├── ElementProps.module.css │ │ │ ├── ElementProps.tsx │ │ │ ├── NewProp.tsx │ │ │ ├── PropsPanel.tsx │ │ │ ├── parseProps.test.ts │ │ │ ├── parseProps.ts │ │ │ ├── serializeProps.test.ts │ │ │ └── serializeProps.ts │ ├── stats │ │ ├── StatsPanel.module.css │ │ └── StatsPanel.tsx │ ├── tree │ │ ├── keyboard.ts │ │ ├── windowing.test.ts │ │ └── windowing.ts │ └── utils.ts │ ├── sprite.svg │ └── store │ ├── collapser.ts │ ├── filter.ts │ ├── index.ts │ ├── props.ts │ ├── react-bindings.ts │ ├── search.ts │ ├── selection.ts │ ├── types.ts │ └── utils.ts ├── test-e2e ├── fixtures │ ├── apps │ │ ├── context-displayName.jsx │ │ ├── counter.jsx │ │ ├── data-input.jsx │ │ ├── deep-tree-2.jsx │ │ ├── deep-tree.jsx │ │ ├── forwardRef-update.jsx │ │ ├── forwardRef.jsx │ │ ├── fragment-filter.jsx │ │ ├── goober.jsx │ │ ├── highlight-fragment.jsx │ │ ├── highlight-margin.jsx │ │ ├── highlight-scroll.jsx │ │ ├── highlight-text.jsx │ │ ├── hoc-update.jsx │ │ ├── hoc.jsx │ │ ├── holes.jsx │ │ ├── hooks-debug.jsx │ │ ├── hooks-depth-limit.jsx │ │ ├── hooks-expand.jsx │ │ ├── hooks-multiple.jsx │ │ ├── hooks-name-custom.jsx │ │ ├── hooks-name.jsx │ │ ├── hooks-support.jsx │ │ ├── hooks.jsx │ │ ├── iframe.jsx │ │ ├── inspect-map-set-hooks.jsx │ │ ├── inspect-map-set.jsx │ │ ├── islands-order-virtual.jsx │ │ ├── islands-order.jsx │ │ ├── keys.jsx │ │ ├── memo-highlight.jsx │ │ ├── memo-stats.jsx │ │ ├── memo.jsx │ │ ├── memo2.jsx │ │ ├── message-connected.jsx │ │ ├── message-no-results.jsx │ │ ├── non-vnode.jsx │ │ ├── profile-reload.jsx │ │ ├── profiler-1.jsx │ │ ├── profiler-2.jsx │ │ ├── profiler-3.jsx │ │ ├── profiler-highlight.jsx │ │ ├── props.jsx │ │ ├── render-reasons-memo.jsx │ │ ├── render-reasons.jsx │ │ ├── root-multi.jsx │ │ ├── signals-subscribe.jsx │ │ ├── signals-text.jsx │ │ ├── signals.jsx │ │ ├── simple-stats.jsx │ │ ├── static-subtree.jsx │ │ ├── suspense.jsx │ │ ├── symbols.jsx │ │ ├── todo.jsx │ │ ├── truncate.jsx │ │ ├── update-all.jsx │ │ ├── update-fake-hoc.jsx │ │ ├── update-hoc.jsx │ │ ├── update-middle.jsx │ │ └── use-ref-element.jsx │ ├── babel.ts │ ├── devtools.html │ ├── devtools.ts │ ├── favicon.ico │ ├── iframe.html │ ├── iframe1.tsx │ ├── iframe2.html │ ├── iframe2.tsx │ ├── index.html │ ├── index.ts │ ├── inject-sprite.ts │ ├── list-fixtures.ts │ ├── list-preact-versions.ts │ ├── load-preact-version.ts │ ├── rewrite-preact-version.ts │ ├── style.css │ ├── utils.ts │ ├── vendor │ │ ├── goober.js │ │ └── htm.js │ └── vite.config.ts ├── pw-utils.ts └── tests │ ├── collapse.test.ts │ ├── context-displayname.test.ts │ ├── debug-mode.test.ts │ ├── element-keyboard.test.ts │ ├── element-scroll.test.ts │ ├── element-search-keyboard.test.ts │ ├── filter-fragment.test.ts │ ├── filter-text-signal.test.ts │ ├── highlight-iframe.test.ts │ ├── highlight-margin.test.ts │ ├── highlight-suspense.test.ts │ ├── highlighter.test.ts │ ├── hoc-filter-disable.test.ts │ ├── hoc-filter-search.test.ts │ ├── hoc-filter-update.test.ts │ ├── hoc-filter.test.ts │ ├── hoc-forward-update.test.ts │ ├── hoc-highlight.test.ts │ ├── hoc-update.test.ts │ ├── hooks │ ├── hook-name.test.ts │ ├── hooks-depth-limit.test.ts │ ├── hooks-expand-state.test.ts │ ├── hooks-multiple.test.ts │ ├── hooks-number.test.ts │ ├── hooks-support.test.ts │ ├── useCallback.test.ts │ ├── useContext-10.5.14.test.ts │ ├── useContext.test.ts │ ├── useCustomHooks.test.ts │ ├── useDebugValue-complex.test.ts │ ├── useDebugValue.test.ts │ ├── useDeepHook.test.ts │ ├── useEffect.test.ts │ ├── useErrorBoundary.test.ts │ ├── useImperativeHandle.test.ts │ ├── useLayoutEffect.test.ts │ ├── useMemo.test.ts │ ├── useRef-element.test.ts │ ├── useRef.test.ts │ └── useState.test.ts │ ├── inspect-click.test.ts │ ├── inspect-fragment.test.ts │ ├── inspect-highlight.test.ts │ ├── inspect-key.test.ts │ ├── inspect-map-set.test.ts │ ├── inspect-non-vnode.test.ts │ ├── inspect-owner-hoc.test.ts │ ├── inspect-owner-memo.test.ts │ ├── inspect-owner-no-hoc.test.ts │ ├── inspect-owner-update.test.ts │ ├── inspect-owner.test.ts │ ├── inspect-props-sort.test.ts │ ├── inspect-scroll.test.ts │ ├── inspect-select.test.ts │ ├── inspect-signal.test.ts │ ├── inspect-truncate.test.ts │ ├── inspect-virtual-2.test.ts │ ├── inspect-virtual.test.ts │ ├── inspect.test.ts │ ├── message-connected.test.ts │ ├── message-no-results.test.ts │ ├── new-prop.test.ts │ ├── profiler │ ├── flamegraph │ │ ├── highlight-flamegraph.test.ts │ │ ├── memo-sibling.test.ts │ │ ├── memo.test.ts │ │ ├── profiler-flamegraph-focus.test.ts │ │ ├── profiler-hoc.test.ts │ │ ├── profiler-static-subtree.test.ts │ │ ├── profiler-unaffected.test.ts │ │ ├── profiler-unmount.test.ts │ │ └── utils.ts │ ├── highlight-updates-holes.test.ts │ ├── highlight-updates-text.test.ts │ ├── highlight-updates.test.ts │ ├── ranked │ │ ├── highlight-ranked.test.ts │ │ ├── profiler-ranked-focus.test.ts │ │ ├── profiler-ranked-selected.test.ts │ │ └── profiler-ranked.test.ts │ ├── render-reason-disabled.test.ts │ ├── render-reason-support.test.ts │ ├── render-reasons-memo.test.ts │ ├── render-reasons.test.ts │ └── rendered-at.test.ts │ ├── prop-input.test.ts │ ├── root-islands.test.ts │ ├── root-multiple.test.ts │ ├── state-edit.test.ts │ ├── state.test.ts │ ├── stats │ ├── stats-empty.test.ts │ ├── stats-memo.test.ts │ ├── stats-simple.test.ts │ └── stats-single-child.test.ts │ ├── suspense-toggle.test.ts │ ├── suspense.test.ts │ ├── symbol-value.test.ts │ ├── sync-selection.test.ts │ └── update-copy.test.ts ├── tools ├── build-plugins │ ├── babel-plugin-css-module.mjs │ ├── babel-plugin-dead-code.mjs │ └── esbuild-plugins.mjs ├── build.mjs ├── fetch-preact-versions.mjs ├── release.js ├── run-chrome.js └── test-setup.js ├── tsconfig.cjs.json ├── tsconfig.inline.json ├── tsconfig.json └── types └── devtools.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | [**/*.{js,jsx,ts,tsx,json}] 2 | indent_style = tab 3 | tab_width = 2 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate documentation 2 | 3 | on: 4 | create: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | doc: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | ref: gh-pages 17 | env: 18 | GIT_TRACE: 1 19 | GIT_CURL_VERBOSE: 1 20 | 21 | - name: Build documentation 22 | run: | 23 | PKG_VERSION=$(curl -s https://api.github.com/repos/preactjs/preact-devtools/releases/latest | jq -r '.tag_name') 24 | echo $PKG_VERSION 25 | sed -i -- "s/Version: v[0-9]\+.[0-9]\+.[0-9]\+/Version: $PKG_VERSION/g" index.html 26 | 27 | - name: Upload to gh-pages 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: . 32 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 22 20 | - name: Install Deps 21 | run: npm i 22 | - name: Install Playwright 23 | run: npx playwright install --with-deps && npx playwright install-deps chromium 24 | - name: Lint 25 | run: npm run lint 26 | - name: Unit tests 27 | run: npm run test 28 | - name: Run e2e tests 29 | run: npm run build:chrome && npm run test:e2e --retries=5 30 | env: 31 | PREACT_VERSION: 10 32 | CI: true 33 | - name: Upload test results 34 | if: always() 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: playwright-report 38 | path: playwright-report 39 | retention-days: 5 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | /dist/ 73 | .rts* 74 | *.pdf 75 | test-e2e/screenshots 76 | test-e2e/fixtures/extension 77 | 78 | # Browser profiles 79 | profiles/firefox/* 80 | profiles/chrome/* 81 | 82 | # macOS 83 | .DS_Store 84 | 85 | test-results/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Marvin Hagemeister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | ## What type of data does the extension collect? 4 | 5 | **None**, your data is yours and we do not want it. The extension does not collect any data. It never communicates in any form over the network. 6 | 7 | ## What data does the extension store? 8 | 9 | Any options you may set in the `Settings` tab are saved, so that they don't get lost when you close and re-open your browser. 10 | 11 | ## How can I be informed of any changes to the privacy policy? 12 | 13 | Since we don't collect any information on you, we also cannot notify you of changes to this privacy policy. To receive updates, please subscribe to this repository on Github. 14 | 15 | ## How can I ask questions about this policy? 16 | 17 | You can ask questions by filing an issue in the [issue tracker](https://github.com/preactjs/preact-devtools/issues) on GitHub. 18 | -------------------------------------------------------------------------------- /media/preact-chrome-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/media/preact-chrome-light.png -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | import path from "path"; 3 | 4 | const config: PlaywrightTestConfig = { 5 | use: { 6 | viewport: { width: 1280, height: 768 }, 7 | ignoreHTTPSErrors: true, 8 | video: "on-first-retry", 9 | trace: "retain-on-failure", 10 | }, 11 | testDir: path.join(__dirname, "test-e2e", "tests"), 12 | testMatch: "**/*.test.ts", 13 | // retries: 3, 14 | timeout: 10 * 1000, 15 | webServer: { 16 | command: "npm run dev", 17 | url: "http://localhost:8100/", 18 | timeout: 120 * 1000, 19 | reuseExistingServer: true, 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /src/adapter/10/log.ts: -------------------------------------------------------------------------------- 1 | import { getDisplayName, getComponent } from "./bindings"; 2 | import { ID } from "../../view/store/types"; 3 | import { getVNodeById, IdMappingState } from "../shared/idMapper"; 4 | import { SharedVNode } from "../shared/bindings"; 5 | import { RendererConfig } from "../shared/renderer"; 6 | 7 | /** 8 | * Pretty print a `VNode` to the browser console. This can be triggered 9 | * by clicking a button in the devtools ui. 10 | */ 11 | export function logVNode( 12 | ids: IdMappingState, 13 | config: RendererConfig, 14 | id: ID, 15 | children: ID[], 16 | ) { 17 | const vnode = getVNodeById(ids, id); 18 | if (vnode == null) { 19 | // eslint-disable-next-line no-console 20 | console.warn(`Could not find vnode with id ${id}`); 21 | return; 22 | } 23 | const display = getDisplayName(vnode, config); 24 | const name = display === "#text" ? display : `<${display || "Component"} />`; 25 | 26 | /* eslint-disable no-console */ 27 | console.group(`LOG %c${name}`, "color: #ea88fd; font-weight: normal"); 28 | console.log("props:", vnode.props); 29 | const c = getComponent(vnode); 30 | if (c != null) { 31 | console.log("state:", c.state); 32 | } 33 | console.log("vnode:", vnode); 34 | console.log("devtools id:", id); 35 | console.log("devtools children:", children); 36 | console.groupEnd(); 37 | /* eslint-enable no-console */ 38 | } 39 | -------------------------------------------------------------------------------- /src/adapter/11/log.ts: -------------------------------------------------------------------------------- 1 | import { ID } from "../../view/store/types"; 2 | import { RendererConfig } from "../shared/renderer"; 3 | import { getVNodeById, IdMappingState } from "../shared/idMapper"; 4 | import { getComponent, getDisplayName, Internal } from "./bindings"; 5 | 6 | export function logInternal( 7 | ids: IdMappingState, 8 | config: RendererConfig, 9 | id: ID, 10 | children: ID[], 11 | ) { 12 | const internal = getVNodeById(ids, id); 13 | if (internal == null) { 14 | // eslint-disable-next-line no-console 15 | console.warn(`Could not find internal with id ${id}`); 16 | return; 17 | } 18 | const display = getDisplayName(internal, config); 19 | const name = display === "#text" ? display : `<${display || "Component"} />`; 20 | 21 | /* eslint-disable no-console */ 22 | console.group(`LOG %c${name}`, "color: #ea88fd; font-weight: normal"); 23 | console.log("props:", internal.props); 24 | const c = getComponent(internal); 25 | if (c != null) { 26 | console.log("state:", c.state); 27 | } 28 | console.log("internal:", internal); 29 | console.log("vnodeId:", internal._vnodeId ?? internal.__v); 30 | console.log("devtools id:", id); 31 | console.log("devtools children:", children); 32 | console.groupEnd(); 33 | /* eslint-enable no-console */ 34 | } 35 | -------------------------------------------------------------------------------- /src/adapter/adapter/filter.ts: -------------------------------------------------------------------------------- 1 | import { RawFilter } from "../../view/store/filter"; 2 | 3 | export interface RawFilterState { 4 | regex: RawFilter[]; 5 | type: { 6 | fragment: boolean; 7 | dom: boolean; 8 | hoc?: boolean; 9 | root?: boolean; 10 | textSignal?: boolean; 11 | }; 12 | } 13 | 14 | export type TypeFilterValue = 15 | | "dom" 16 | | "fragment" 17 | | "hoc" 18 | | "root" 19 | | "textSignal"; 20 | 21 | export interface FilterState { 22 | regex: RegExp[]; 23 | type: Set; 24 | } 25 | 26 | export interface RegexFilter { 27 | type: "name"; 28 | value: string; 29 | } 30 | 31 | export interface TypeFilter { 32 | type: "type"; 33 | value: TypeFilterValue; 34 | } 35 | 36 | export type Filter = RegexFilter | TypeFilter; 37 | 38 | export const DEFAULT_FIlTERS: FilterState = { 39 | regex: [], 40 | type: new Set(["dom", "fragment", "root", "hoc", "textSignal"]), 41 | }; 42 | 43 | export function parseFilters(raw: RawFilterState): FilterState { 44 | const type = new Set(); 45 | if (raw.type.fragment) type.add("fragment"); 46 | if (raw.type.dom) type.add("dom"); 47 | if (raw.type.hoc) type.add("hoc"); 48 | if (raw.type.root) type.add("root"); 49 | if (raw.type.textSignal) type.add("textSignal"); 50 | 51 | return { 52 | regex: raw.regex.filter(x => x.enabled).map(x => new RegExp(x.value, "gi")), 53 | type, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/adapter/adapter/profiler.ts: -------------------------------------------------------------------------------- 1 | import { UpdateRects } from "./highlightUpdates"; 2 | 3 | export interface RawTimelineFilterState { 4 | filterCommitsUnder: number | false; 5 | } 6 | export type ProfilerState = { 7 | isProfiling: boolean; 8 | highlightUpdates: boolean; 9 | pendingHighlightUpdates: Set; 10 | updateRects: UpdateRects; 11 | captureRenderReasons: boolean; 12 | recordStats: boolean; 13 | } & RawTimelineFilterState; 14 | 15 | export function newProfiler(): ProfilerState { 16 | return { 17 | highlightUpdates: false, 18 | updateRects: new Map(), 19 | pendingHighlightUpdates: new Set(), 20 | captureRenderReasons: false, 21 | isProfiling: false, 22 | recordStats: false, 23 | filterCommitsUnder: false, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/adapter/marks.ts: -------------------------------------------------------------------------------- 1 | // Here we use the "User Timing API" to collect samples for the 2 | // native profiling tools of browsers. These timings will show 3 | // up in the "Timing" category. 4 | 5 | const markName = (s: string) => `⚛ ${s}`; 6 | 7 | const supportsPerformance = 8 | globalThis.performance && 9 | typeof globalThis.performance.getEntriesByName === "function"; 10 | 11 | export function recordMark(s: string) { 12 | if (supportsPerformance) { 13 | performance.mark(markName(s)); 14 | } 15 | } 16 | 17 | export function endMark(nodeName: string) { 18 | if (supportsPerformance) { 19 | const name = markName(nodeName); 20 | const start = `${name}_diff`; 21 | const end = `${name}_diffed`; 22 | try { 23 | performance.mark(end); 24 | performance.measure(name, start, end); 25 | 26 | performance.clearMarks(start); 27 | performance.clearMarks(end); 28 | } catch (e) { 29 | // Do nothing 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/adapter/parse-semverish.test.ts: -------------------------------------------------------------------------------- 1 | import parseSemverish from "./parse-semverish"; 2 | import { expect } from "chai"; 3 | 4 | describe("parse-semverish", () => { 5 | it("should parse normal", () => { 6 | expect(parseSemverish("10.0.1")).to.deep.equal({ 7 | major: 10, 8 | minor: 0, 9 | patch: 1, 10 | preRelease: undefined, 11 | }); 12 | }); 13 | 14 | it("should parse big numbers", () => { 15 | expect(parseSemverish("10.10.10")).to.deep.equal({ 16 | major: 10, 17 | minor: 10, 18 | patch: 10, 19 | preRelease: undefined, 20 | }); 21 | }); 22 | 23 | it("should parse tags", () => { 24 | expect(parseSemverish("10.2.3-alpha.0")).to.deep.equal({ 25 | major: 10, 26 | minor: 2, 27 | patch: 3, 28 | preRelease: { 29 | tag: "alpha", 30 | version: 0, 31 | }, 32 | }); 33 | }); 34 | 35 | it("should parse incomplete tags", () => { 36 | expect(parseSemverish("10.2.3-alpha")).to.deep.equal({ 37 | major: 10, 38 | minor: 2, 39 | patch: 3, 40 | preRelease: { 41 | tag: "alpha", 42 | version: -1, 43 | }, 44 | }); 45 | }); 46 | 47 | it("should parse tags with dash", () => { 48 | expect(parseSemverish("10.2.3-alpha-hehe.2")).to.deep.equal({ 49 | major: 10, 50 | minor: 2, 51 | patch: 3, 52 | preRelease: { 53 | tag: "alpha-hehe", 54 | version: 2, 55 | }, 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/adapter/parse-semverish.ts: -------------------------------------------------------------------------------- 1 | const MAJOR = 1; 2 | const MINOR = 2; 3 | const PATCH = 3; 4 | const PRERELEASE = 5; 5 | const PRERELEASE_TAG = 5; 6 | const PRERELEASE_VERSION = 6; 7 | const REGEXP_SEMVERISH = /^(\d+)\.(\d+)\.(\d+)(-([\w_-]+)(?:\.(\d+))?)?$/i; 8 | 9 | /** 10 | * semver-ish parsing based on https://github.com/npm/node-semver/blob/master/semver.js 11 | * 12 | * @param version Version to parse 13 | * @param allowPreRelease Flag to indicate whether pre-releases should be allowed & parsed (e.g. -rc.1) 14 | */ 15 | export default function parseSemverish( 16 | version: string, 17 | ): { 18 | major: number; 19 | minor: number; 20 | patch: number; 21 | preRelease?: { tag: string; version: number }; 22 | } | null { 23 | const match = version.match(REGEXP_SEMVERISH); 24 | 25 | if (match) { 26 | let preRelease = undefined; 27 | if (match[PRERELEASE]) { 28 | preRelease = { 29 | tag: match[PRERELEASE_TAG], 30 | version: 31 | match[PRERELEASE_VERSION] !== undefined 32 | ? +match[PRERELEASE_VERSION] 33 | : -1, 34 | }; 35 | } 36 | 37 | return { 38 | major: +match[MAJOR], 39 | minor: +match[MINOR], 40 | patch: +match[PATCH], 41 | preRelease, 42 | }; 43 | } 44 | 45 | return null; 46 | } 47 | -------------------------------------------------------------------------------- /src/adapter/protocol/string-table.test.ts: -------------------------------------------------------------------------------- 1 | import { parseTable, flushTable } from "./string-table"; 2 | import { expect } from "chai"; 3 | 4 | describe("StringTable", () => { 5 | describe("flushTable", () => { 6 | it("should flush", () => { 7 | const table = new Map([ 8 | ["abc", 1], 9 | ["foo", 2], 10 | ]); 11 | expect(flushTable(table)).to.deep.equal([ 12 | 8, 13 | 3, 14 | 97, 15 | 98, 16 | 99, 17 | 3, 18 | 102, 19 | 111, 20 | 111, 21 | ]); 22 | }); 23 | }); 24 | 25 | describe("parseTable", () => { 26 | it("should parse single string", () => { 27 | const data = [4, 3, 97, 98, 99]; 28 | expect(parseTable(data)).to.deep.equal(["abc"]); 29 | }); 30 | 31 | it("should parse multiple strings", () => { 32 | const data = [8, 3, 97, 98, 99, 3, 102, 111, 111]; 33 | expect(parseTable(data)).to.deep.equal(["abc", "foo"]); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/adapter/setup.ts: -------------------------------------------------------------------------------- 1 | import { DevtoolsHook } from "./hook"; 2 | import { Options, Fragment, Component } from "preact"; 3 | 4 | export async function init(options: Options, getHook: () => DevtoolsHook) { 5 | const hook = getHook(); 6 | if (hook.attachPreact) { 7 | return hook.attachPreact("10.10.6", options, { 8 | Fragment: Fragment as any, 9 | Component: Component as any, 10 | }); 11 | } else { 12 | // eslint-disable-next-line no-console 13 | console.error( 14 | "Devtools hook is missing attachPreact() method. " + 15 | "This happens when the running 'preact-devtools' extension is too old. Please update it.", 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/adapter/shared/timings.ts: -------------------------------------------------------------------------------- 1 | import { ID } from "../../view/store/types"; 2 | 3 | /** 4 | * Store timings in a Map instead of mutating the vnode for 5 | * performance. 6 | */ 7 | export interface VNodeTimings { 8 | start: Map; 9 | end: Map; 10 | } 11 | 12 | export function storeTime(timings: Map, id: ID, time: number) { 13 | timings.set(id, time); 14 | } 15 | 16 | export function getTime(timings: Map, id: ID): number { 17 | return timings.get(id) || 0; 18 | } 19 | 20 | export function removeTime(timings: VNodeTimings, id: ID) { 21 | timings.start.delete(id); 22 | timings.end.delete(id); 23 | } 24 | 25 | export function createVNodeTimings(): VNodeTimings { 26 | return { 27 | start: new Map(), 28 | end: new Map(), 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ClientToDevtools = "preact-client-to-devtools"; 2 | export const DevtoolsToClient = "preact-devtools-to-client"; 3 | export const ContentScriptName = "preact-content-script"; 4 | export const DevtoolsPanelName = "preact-devtools-panel"; 5 | export const DevtoolsPanelInlineName = "preact-devtools-panel/inline"; 6 | export const PageHookName = "preact-page-hook"; 7 | 8 | export const PROFILE_RELOAD = "preact-devtools_profile-and-reload"; 9 | export const STATS_RELOAD = "preact-devtools_stats-and-reload"; 10 | 11 | export enum NodeType { 12 | Element = 1, 13 | Text = 3, 14 | CData = 4, 15 | XMLProcessingInstruction = 7, 16 | Comment = 8, 17 | Document = 9, 18 | DocumentType = 10, // like 19 | DocumentFragment = 11, 20 | } 21 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Will be tree-shaken out in prod builds 3 | */ 4 | export function debug(...args: any[]) { 5 | if (__DEBUG__) { 6 | // eslint-disable-next-line no-console 7 | console.log(...args); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | } 11 | 12 | :root { 13 | --color-bg: white; 14 | --color-selected-bg: #8058ca; 15 | --color-selected-text: white; 16 | --color-selected-inactive-bg: gray; 17 | --color-selected-inactive-text: white; 18 | --color-element-text: #673ab8; 19 | --color-dim-bg: #e9e1f8; 20 | --color-inactive-child-bg: #e9e1f8; 21 | --color-hover: #e9e1f8; 22 | --color-border: #dedede; 23 | --color-text-empty: #444; 24 | --color-button-active: #7245c7; 25 | } 26 | 27 | :root :global(.dark) { 28 | --color-bg: #242424; 29 | --color-text: #b0b0b0; 30 | } 31 | 32 | body { 33 | background: var(--color-bg); 34 | color: var(--color-text); 35 | } 36 | 37 | iframe { 38 | border: none; 39 | background: none; 40 | } 41 | 42 | :global(.grid) { 43 | display: grid; 44 | grid-template-columns: auto auto; 45 | } 46 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" { 2 | const styles: Record; 3 | export default styles; 4 | } 5 | 6 | declare module "@preact-list-versions" { 7 | export const preactVersions: string[]; 8 | } 9 | 10 | declare const __DEBUG__: boolean; 11 | -------------------------------------------------------------------------------- /src/shells/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Preact Developer Tools", 4 | "description": "Adds debugging tools for Preact to Chrome", 5 | "version": "5.0.1", 6 | "devtools_page": "panel/empty-panel.html", 7 | "content_security_policy": { 8 | "extension_pages": "script-src 'self'; object-src 'self'" 9 | }, 10 | "permissions": ["scripting", "storage"], 11 | "optional_permissions": ["clipboardWrite"], 12 | "host_permissions": [""], 13 | "icons": { 14 | "16": "icons/icon-16.png", 15 | "32": "icons/icon-32.png", 16 | "48": "icons/icon-48.png", 17 | "128": "icons/icon-128.png", 18 | "192": "icons/icon-192.png" 19 | }, 20 | "action": { 21 | "default_icon": { 22 | "16": "icons/icon-16-disabled.png", 23 | "32": "icons/icon-32-disabled.png", 24 | "48": "icons/icon-48-disabled.png", 25 | "128": "icons/icon-128-disabled.png", 26 | "192": "icons/icon-192-disabled.png" 27 | }, 28 | "default_popup": "popup/disabled.html" 29 | }, 30 | "background": { 31 | "service_worker": "background/background.js", 32 | "type": "module" 33 | }, 34 | "content_scripts": [ 35 | { 36 | "matches": [""], 37 | "js": ["content-script.js"], 38 | "all_frames": true, 39 | "run_at": "document_start" 40 | } 41 | ], 42 | "web_accessible_resources": [ 43 | { 44 | "resources": ["preact-devtools-page.css"], 45 | "matches": [""] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/shells/edge/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Preact Developer Tools", 4 | "description": "Adds debugging tools for Preact to Microsoft Edge", 5 | "version": "5.0.1", 6 | "devtools_page": "panel/empty-panel.html", 7 | "content_security_policy": { 8 | "extension_pages": "script-src 'self'; object-src 'self'" 9 | }, 10 | "permissions": ["scripting", "storage"], 11 | "optional_permissions": ["clipboardWrite"], 12 | "host_permissions": [""], 13 | "icons": { 14 | "16": "icons/icon-16.png", 15 | "32": "icons/icon-32.png", 16 | "48": "icons/icon-48.png", 17 | "128": "icons/icon-128.png", 18 | "192": "icons/icon-192.png" 19 | }, 20 | "action": { 21 | "default_icon": { 22 | "16": "icons/icon-16-disabled.png", 23 | "32": "icons/icon-32-disabled.png", 24 | "48": "icons/icon-48-disabled.png", 25 | "128": "icons/icon-128-disabled.png", 26 | "192": "icons/icon-192-disabled.png" 27 | }, 28 | "default_popup": "popup/disabled.html" 29 | }, 30 | "background": { 31 | "service_worker": "background/background.js", 32 | "type": "module" 33 | }, 34 | "content_scripts": [ 35 | { 36 | "matches": [""], 37 | "js": ["content-script.js"], 38 | "all_frames": true, 39 | "run_at": "document_start" 40 | } 41 | ], 42 | "web_accessible_resources": [ 43 | { 44 | "resources": ["preact-devtools-page.css"], 45 | "matches": [""] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/shells/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Preact Developer Tools", 4 | "description": "Adds debugging tools for Preact to Firefox", 5 | "version": "5.0.1", 6 | "devtools_page": "panel/empty-panel.html", 7 | "content_security_policy": { 8 | "extension_pages": "script-src 'self'; object-src 'self'" 9 | }, 10 | "permissions": ["scripting", "storage"], 11 | "optional_permissions": ["clipboardWrite"], 12 | "host_permissions": [""], 13 | "browser_specific_settings": { 14 | "gecko": { 15 | "id": "devtools@marvinh.dev" 16 | } 17 | }, 18 | "icons": { 19 | "16": "icons/icon-16.png", 20 | "32": "icons/icon-32.png", 21 | "48": "icons/icon-48.png", 22 | "128": "icons/icon-128.png", 23 | "192": "icons/icon-192.png" 24 | }, 25 | "action": { 26 | "default_icon": { 27 | "16": "icons/icon-16-disabled.png", 28 | "32": "icons/icon-32-disabled.png", 29 | "48": "icons/icon-48-disabled.png", 30 | "128": "icons/icon-128-disabled.png", 31 | "192": "icons/icon-192-disabled.png" 32 | }, 33 | "default_popup": "popup/disabled.html" 34 | }, 35 | "background": { 36 | "scripts": ["background/background.js"] 37 | }, 38 | "content_scripts": [ 39 | { 40 | "matches": [""], 41 | "js": ["content-script.js"], 42 | "all_frames": true, 43 | "run_at": "document_start" 44 | } 45 | ], 46 | "web_accessible_resources": [ 47 | { 48 | "resources": ["preact-devtools-page.css"], 49 | "matches": [""] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/shells/inline/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "../../view/store"; 2 | export { createStore } from "../../view/store"; 3 | import { render, h } from "preact"; 4 | import { DevTools } from "../../view/components/Devtools"; 5 | import { applyEvent } from "../../adapter/protocol/events"; 6 | import { Store } from "../../view/store/types"; 7 | import { PageHookName, DevtoolsToClient } from "../../constants"; 8 | 9 | export function setupFrontendStore(ctx: Window) { 10 | const store = createStore(); 11 | 12 | function handleClientEvents(e: MessageEvent) { 13 | if ( 14 | e.data && 15 | (e.data.source === PageHookName || e.data.source === DevtoolsToClient) 16 | ) { 17 | const data = e.data; 18 | applyEvent(store, data.type, data.data); 19 | } 20 | } 21 | ctx.addEventListener("message", handleClientEvents); 22 | 23 | const unsubscribe = store.subscribe((name, data) => { 24 | ctx.postMessage( 25 | { 26 | type: name, 27 | data, 28 | source: DevtoolsToClient, 29 | }, 30 | "*", 31 | ); 32 | }); 33 | 34 | return { 35 | store, 36 | destroy: () => { 37 | ctx.removeEventListener("message", handleClientEvents); 38 | unsubscribe(); 39 | }, 40 | }; 41 | } 42 | 43 | export function setupInlineDevtools(container: HTMLElement, ctx: Window) { 44 | const { store } = setupFrontendStore(ctx); 45 | render(h(DevTools, { store, window: ctx }), container); 46 | return store; 47 | } 48 | 49 | export function renderDevtools(store: Store, container: HTMLElement) { 50 | render(h(DevTools, { store, window }), container); 51 | } 52 | -------------------------------------------------------------------------------- /src/shells/shared/background/emitter.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../../debug"; 2 | 3 | export type Handler = (e: T) => void; 4 | export interface Emitter { 5 | on(source: string, handler: Handler): void; 6 | off(source: string): void; 7 | emit(source: string, event: T): void; 8 | connected(): string[]; 9 | } 10 | 11 | /** 12 | * Emitter which will dispatch to everyone but the 13 | * calling source. 14 | */ 15 | export function BackgroundEmitter() { 16 | const targets: Record | undefined> = {}; 17 | 18 | return { 19 | on(source: string, handler: Handler) { 20 | targets[source] = handler; 21 | }, 22 | off(source: string) { 23 | targets[source] = undefined; 24 | }, 25 | emit(source: string, event: T) { 26 | Object.entries(targets).forEach(([name, f]) => { 27 | if (name !== source && f) { 28 | debug(source, "->", name, event); 29 | f(event); 30 | } 31 | }); 32 | }, 33 | connected() { 34 | return Object.keys(targets); 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-128-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-128-disabled.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-128.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-16-disabled.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-16.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-192-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-192-disabled.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-192.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-32-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-32-disabled.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-32.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-48-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-48-disabled.png -------------------------------------------------------------------------------- /src/shells/shared/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/preactjs/preact-devtools/2e2c47a7809127ecf66265d5a1f0109a5ab5e715/src/shells/shared/icons/icon-48.png -------------------------------------------------------------------------------- /src/shells/shared/installHook.ts: -------------------------------------------------------------------------------- 1 | // Note: This file will be inlined into `content-script.ts` 2 | // when building the extension. 3 | 4 | import { createHook } from "../../adapter/hook"; 5 | import { createPortForHook } from "../../adapter/adapter/port"; 6 | 7 | (window as any).__PREACT_DEVTOOLS__ = createHook(createPortForHook(window)); 8 | -------------------------------------------------------------------------------- /src/shells/shared/panel/empty-panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preact Devtools Extension 6 | 37 | 38 | 39 | 40 | 41 | 42 | 52 |
53 |
Unable to find Preact on the page.
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /src/shells/shared/panel/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preact Devtools Extension 6 | 7 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /src/shells/shared/popup/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disabled Popup 6 | 16 | 17 | 18 |

This page doesn't seem to be using Preact.

19 |

Import preact/debug somewhere to initialize the connection to the extension. Make sure that this import is the first import in your whole app.

20 | 21 | 22 | -------------------------------------------------------------------------------- /src/shells/shared/popup/enabled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Enabled Popup 6 | 12 | 13 | 14 | This page is using Preact 15 |

Open the developer tools and you will see a Preact tab in the list.

16 | 17 | 18 | -------------------------------------------------------------------------------- /src/shells/shared/popup/popup.ts: -------------------------------------------------------------------------------- 1 | // This file is not included at runtime. It is only used by 2 | // the background page to activate or deactivate the icon 3 | 4 | import { debug } from "../../../debug"; 5 | 6 | export function setPopupStatus(tabId: number, enabled?: boolean) { 7 | const status = enabled ? "enabled" : "disabled"; 8 | debug( 9 | `[${tabId}] %cPopup: %c${status}`, 10 | "font-weight: bold", 11 | "font-weight: normal", 12 | ); 13 | const suffix = enabled ? "" : "-disabled"; 14 | 15 | chrome.action.setIcon({ 16 | tabId, 17 | path: { 18 | "16": `icons/icon-16${suffix}.png`, 19 | "32": `icons/icon-32${suffix}.png`, 20 | "48": `icons/icon-48${suffix}.png`, 21 | "128": `icons/icon-128${suffix}.png`, 22 | "192": `icons/icon-192${suffix}.png`, 23 | }, 24 | }); 25 | chrome.action.setPopup({ 26 | tabId, 27 | popup: `popup/${status}.html`, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/view/components/Actions.tsx: -------------------------------------------------------------------------------- 1 | import { h, ComponentChildren } from "preact"; 2 | 3 | export interface Props { 4 | class?: string; 5 | children?: ComponentChildren; 6 | } 7 | 8 | export function Actions(props: Props) { 9 | return ( 10 |
11 | {props.children} 12 |
13 | ); 14 | } 15 | 16 | export function ActionSeparator() { 17 | return
; 18 | } 19 | -------------------------------------------------------------------------------- /src/view/components/CanvasHighlight/CanvasHighlight.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | pointer-events: none; 6 | z-index: 10000000; 7 | /** 8 | * Canvas renders rectangles in the wrong size if this rule is set. 9 | * See: https://twitter.com/marvinhagemeist/status/1263506164144316416 10 | */ 11 | max-width: none !important; 12 | } 13 | -------------------------------------------------------------------------------- /src/view/components/CanvasHighlight/CanvasHighlight.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { useRef } from "preact/hooks"; 3 | import { useResize } from "../utils"; 4 | import s from "./CanvasHighlight.module.css"; 5 | 6 | export function CanvasHighlight() { 7 | const ref = useRef(null); 8 | 9 | useResize(() => { 10 | if (ref.current) { 11 | ref.current.width = window.innerWidth; 12 | ref.current.height = window.innerHeight; 13 | } 14 | }, []); 15 | 16 | return ( 17 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/view/components/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { Icon } from "../icons"; 3 | 4 | export interface CheckboxProps { 5 | testId?: string; 6 | checked: boolean; 7 | onChange: (v: boolean) => void; 8 | children: any; 9 | } 10 | 11 | export function Checkbox(props: CheckboxProps) { 12 | return ( 13 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/view/components/ComponentName.tsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from "preact"; 2 | 3 | export function ComponentName(props: { children: any }) { 4 | return ( 5 | 6 | {props.children ? ( 7 | 8 | < 9 | {props.children} 10 | > 11 | 12 | ) : ( 13 | "-" 14 | )} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/view/components/IconBtn.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { ComponentChildren } from "preact"; 3 | 4 | export interface IconBtnProps { 5 | active?: boolean; 6 | title?: string; 7 | disabled?: boolean; 8 | color?: string; 9 | onClick?: () => void; 10 | styling?: "secondary" | "primary"; 11 | children: ComponentChildren; 12 | testId?: string; 13 | } 14 | 15 | export function IconBtn(props: IconBtnProps) { 16 | return ( 17 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/view/components/Message/Message.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | 3 | const infoIcon = ( 4 | 10 | 11 | 15 | 16 | ); 17 | 18 | const warnIcon = ( 19 | 25 | 26 | 30 | 31 | ); 32 | 33 | export interface MessageProps { 34 | type: "info" | "warning"; 35 | children: any; 36 | testId?: string; 37 | } 38 | 39 | export function Message(props: MessageProps) { 40 | return ( 41 |
42 | 43 | {props.type === "info" ? infoIcon : warnIcon} 44 | 45 | {props.children} 46 |
47 | ); 48 | } 49 | 50 | export interface MessageBtnProps { 51 | onClick: () => void; 52 | testId?: string; 53 | children: any; 54 | } 55 | 56 | export function MessageBtn(props: MessageBtnProps) { 57 | return ( 58 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/view/components/OutsideClick.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { useEffect, useRef } from "preact/hooks"; 3 | 4 | export interface Props { 5 | onClick: () => void; 6 | children: any; 7 | class?: string; 8 | style?: string | Record; 9 | } 10 | 11 | export function OutsideClick(props: Props) { 12 | const ref = useRef(null); 13 | 14 | useEffect(() => { 15 | if (!ref.current) return; 16 | 17 | const listener = (e: MouseEvent) => { 18 | if (ref.current && !ref.current.contains(e.target as any)) { 19 | props.onClick(); 20 | } 21 | }; 22 | 23 | const root = ref.current!.getRootNode() as HTMLElement; 24 | root.addEventListener("click", listener); 25 | return () => root.removeEventListener("click", listener); 26 | }, [props.children, ref.current]); 27 | 28 | return ( 29 |
30 | {props.children} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/view/components/RadioBar.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | 3 | export interface RadioBarProps { 4 | value: string; 5 | name: string; 6 | items: Array<{ label: string; value: string }>; 7 | onChange(v: string): void; 8 | } 9 | 10 | export function RadioBar(props: RadioBarProps) { 11 | return ( 12 | 13 | {props.items.map(x => { 14 | return ( 15 | 26 | ); 27 | })} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/view/components/SidebarLayout.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | 3 | export interface ChildProps { 4 | children: any; 5 | } 6 | 7 | export function SidebarLayout(props: ChildProps) { 8 | return ; 9 | } 10 | 11 | export function SingleLayout(props: ChildProps) { 12 | return ; 13 | } 14 | 15 | export function PageLayout(props: ChildProps) { 16 | return ( 17 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { useEffect, useRef } from "preact/hooks"; 3 | import { useStore } from "../store/react-bindings"; 4 | 5 | export function ThemeSwitcher() { 6 | const store = useStore(); 7 | let theme = store.theme.value; 8 | if (theme === "auto") { 9 | theme = matchMedia("(prefers-color-scheme: dark)").matches 10 | ? "dark" 11 | : "light"; 12 | } 13 | 14 | const ref = useRef(null); 15 | 16 | useEffect(() => { 17 | if (ref.current) { 18 | const doc = ref.current.ownerDocument!; 19 | doc.body.classList.remove("auto", "light", "dark"); 20 | doc.body.classList.add(theme); 21 | } 22 | }, [theme, ref.current]); 23 | 24 | return
; 25 | } 26 | -------------------------------------------------------------------------------- /src/view/components/elements/Elements.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { TreeBar } from "./TreeBar"; 3 | import { TreeView } from "./TreeView"; 4 | import { SidebarActions } from "../sidebar/SidebarActions"; 5 | import { Sidebar } from "../sidebar/Sidebar"; 6 | import s from "../Devtools.module.css"; 7 | import { SidebarLayout } from "../SidebarLayout"; 8 | 9 | export function Elements() { 10 | return ( 11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/view/components/elements/TreeBar.module.css: -------------------------------------------------------------------------------- 1 | .btnIcon { 2 | border: none; 3 | background: none; 4 | cursor: pointer; 5 | display: inline-flex; 6 | align-items: center; 7 | outline: none; 8 | color: var(--color-text); 9 | } 10 | 11 | .btnIcon[data-active="true"] { 12 | color: var(--color-button-active); 13 | color: var(--color-button-active); 14 | } 15 | 16 | .btnWrapper { 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | height: 100%; 21 | padding-left: 0.25rem; 22 | padding-right: 0.25rem; 23 | } 24 | 25 | .searchContainer { 26 | align-items: center; 27 | flex: 1 1 auto; 28 | display: flex; 29 | height: 100%; 30 | margin-left: 0.5rem; 31 | margin-right: 0.5rem; 32 | min-width: 0; 33 | } 34 | 35 | .search { 36 | flex: 1 1 auto; 37 | border: none; 38 | height: 100%; 39 | background: none; 40 | color: var(--color-text); 41 | margin-left: 0.25rem !important; 42 | position: relative; 43 | padding: 0.25rem 0; 44 | border-bottom: 0.0625rem solid transparent; 45 | border-top: 0.025rem solid transparent; 46 | } 47 | 48 | .search:focus { 49 | outline: none; 50 | border-bottom: 0.0625rem solid var(--color-selected-bg); 51 | } 52 | 53 | .searchCounter { 54 | display: inline-flex; 55 | font-size: var(--font-small); 56 | color: var(--color-text-empty); 57 | } 58 | 59 | .removeWrapper { 60 | width: 1.5rem; 61 | display: flex; 62 | flex: 0 0 auto; 63 | justify-content: flex-end; 64 | } 65 | 66 | .vSep { 67 | height: 0.0625rem; 68 | margin: 0.4rem 0; 69 | } 70 | -------------------------------------------------------------------------------- /src/view/components/elements/TreeView.test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { render } from "@testing-library/preact"; 3 | import { TreeItem } from "./TreeView"; 4 | import { expect } from "chai"; 5 | import { AppCtx } from "../../store/react-bindings"; 6 | import { createStore } from "../../store"; 7 | import { DevNodeType } from "../../store/types"; 8 | 9 | describe("TreeItem", () => { 10 | it("should limit key length to 15", () => { 11 | const store = createStore(); 12 | store.nodes.value.set(1, { 13 | children: [], 14 | depth: 1, 15 | endTime: 0, 16 | key: "abcdefghijklmnopqrstuvxyz", 17 | id: 1, 18 | hocs: null, 19 | name: "foo", 20 | owner: -1, 21 | parent: -1, 22 | startTime: 0, 23 | type: DevNodeType.ClassComponent, 24 | }); 25 | const { container, rerender } = render( 26 | 27 | , 28 | , 29 | ); 30 | expect(container.textContent).to.equal('foo key="abcdefghijklmno…",'); 31 | 32 | store.nodes.value.get(1)!.key = "foobar"; 33 | store.nodes.value = new Map(store.nodes.value); 34 | rerender( 35 | 36 | , 37 | , 38 | ); 39 | 40 | expect(container.textContent).to.equal('foo key="foobar",'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/view/components/profiler/components/CommitInfo/CommitInfo.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { SidebarPanel } from "../../../sidebar/SidebarPanel"; 3 | import { formatTime } from "../../util"; 4 | import { useStore } from "../../../../store/react-bindings"; 5 | 6 | export function CommitInfo() { 7 | const store = useStore(); 8 | const commit = store.profiler.activeCommit.value; 9 | const isRecording = store.profiler.isRecording.value; 10 | 11 | if (commit === null || isRecording) { 12 | return null; 13 | } 14 | 15 | const root = commit.nodes.get(commit.commitRootId)!; 16 | if (!root) { 17 | return null; 18 | } 19 | 20 | return ( 21 | 22 |
23 |
Start:
24 |
{formatTime(root.startTime)}
25 |
26 |
Duration:
27 |
{formatTime(commit.duration)}
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/view/components/profiler/components/TimelineBar/TimelineBar.module.css: -------------------------------------------------------------------------------- 1 | .filterPopup { 2 | width: 18rem; 3 | } 4 | 5 | .timelineActions { 6 | display: flex; 7 | justify-content: space-between; 8 | flex: 1 1 auto; 9 | } 10 | 11 | .timelineCommitsEmpty { 12 | font-size: 0.8rem; 13 | padding: 0 0.3rem; 14 | } 15 | -------------------------------------------------------------------------------- /src/view/components/profiler/data/gradient.ts: -------------------------------------------------------------------------------- 1 | export function getGradient(max: number, n: number) { 2 | const maxColor = 9; // Amount of colors, see css variables 3 | let i = 0; 4 | if (!isNaN(n)) { 5 | if (!isFinite(n)) { 6 | i = max; 7 | } else { 8 | const slope = (1 * (maxColor - 0)) / max - 0; 9 | i = 0 + Math.round(slope * (n - 0)); 10 | } 11 | } 12 | 13 | // i can be NaN at this point 14 | if (isNaN(i)) return 0; 15 | return Math.max(i, 0); 16 | } 17 | -------------------------------------------------------------------------------- /src/view/components/profiler/flamegraph/FlameGraphMode.tsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from "preact"; 2 | import { IconTab } from "../components/Tabs/Tabs"; 3 | import { useStore } from "../../../store/react-bindings"; 4 | import { useCallback } from "preact/hooks"; 5 | import { FlamegraphType, getCommitInitalSelectNodeId } from "../data/commits"; 6 | import { Icon } from "../../icons"; 7 | 8 | export function FlameGraphMode() { 9 | const store = useStore(); 10 | const type = store.profiler.flamegraphType.value; 11 | const disabled = !store.profiler.isSupported.value; 12 | 13 | const onClick = useCallback((value: string) => { 14 | const profiler = store.profiler; 15 | profiler.flamegraphType.value = value as any; 16 | profiler.selectedNodeId.value = profiler.activeCommit.value 17 | ? getCommitInitalSelectNodeId( 18 | profiler.activeCommit.value, 19 | profiler.flamegraphType.value, 20 | ) 21 | : -1; 22 | }, []); 23 | 24 | return ( 25 | 26 | } 29 | value={FlamegraphType.FLAMEGRAPH} 30 | checked={type === FlamegraphType.FLAMEGRAPH} 31 | onClick={onClick} 32 | disabled={disabled} 33 | > 34 | Flamegraph 35 | 36 | } 39 | value={FlamegraphType.RANKED} 40 | checked={type === FlamegraphType.RANKED} 41 | onClick={onClick} 42 | disabled={disabled} 43 | > 44 | Ranked 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/view/components/profiler/flamegraph/FlamegraphStore.ts: -------------------------------------------------------------------------------- 1 | import { ID, Tree } from "../../../store/types"; 2 | 3 | export function getRoot(tree: Tree, id: ID) { 4 | let item = tree.get(id); 5 | let last = id; 6 | while (item !== undefined) { 7 | last = item.id; 8 | item = tree.get(item.parent); 9 | } 10 | 11 | return last; 12 | } 13 | 14 | export const normalize = (max: number, min: number, x: number) => { 15 | return (x - min) / (max - min); 16 | }; 17 | -------------------------------------------------------------------------------- /src/view/components/profiler/flamegraph/inline-pattern.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 16 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /src/view/components/profiler/flamegraph/placeNodes.ts: -------------------------------------------------------------------------------- 1 | import { DevNode } from "../../../store/types"; 2 | 3 | /** 4 | * Flatten profiler node tree into a flat list 5 | */ 6 | export function flattenNodeTree( 7 | tree: Map, 8 | id: K, 9 | ): T[] { 10 | const out: T[] = []; 11 | const visited = new Set(); 12 | const stack: K[] = [id]; 13 | 14 | while (stack.length > 0) { 15 | const item = stack.pop(); 16 | if (item == null) continue; 17 | 18 | const node = tree.get(item); 19 | if (!node) continue; 20 | 21 | if (!visited.has(node.id)) { 22 | out.push(node); 23 | visited.add(node.id); 24 | 25 | for (let i = node.children.length; i--; ) { 26 | stack.push(node.children[i]); 27 | } 28 | } 29 | } 30 | 31 | return out; 32 | } 33 | 34 | export const EMPTY: DevNode = { 35 | children: [], 36 | depth: 0, 37 | endTime: 0, 38 | id: -1, 39 | owner: -1, 40 | hocs: null, 41 | key: "", 42 | name: "", 43 | parent: -1, 44 | startTime: 0, 45 | type: 0, 46 | }; 47 | 48 | /** 49 | * The minimum width of a node inside the flamegraph. 50 | * This ensures that the user doesn't miss any nodes 51 | * that would be smaller than <1px because of zooming 52 | */ 53 | export const MIN_WIDTH = 6; 54 | -------------------------------------------------------------------------------- /src/view/components/profiler/flamegraph/ranked/ranked-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { toTransform } from "./ranked-utils"; 3 | import { flames } from "../testHelpers"; 4 | import { NodeTransform } from "../shared"; 5 | 6 | const required: Partial = { 7 | visible: true, 8 | maximized: false, 9 | commitParent: false, 10 | }; 11 | 12 | describe("toTransform", () => { 13 | it("should order nodes by selfDuration", () => { 14 | const tree = flames` 15 | App ********** 16 | Foo **** 17 | Bar * 18 | `; 19 | 20 | expect(toTransform(tree.commit)).to.deep.equal([ 21 | { ...required, id: 1, width: 60, x: 0, row: 0, weight: 9 }, 22 | { ...required, id: 3, width: 50, x: 0, row: 1, weight: 8 }, 23 | { ...required, id: 2, width: 30, x: 0, row: 2, weight: 5 }, 24 | ]); 25 | }); 26 | 27 | it("should order nodes by selfDuration #2", () => { 28 | const tree = flames` 29 | App ****************** 30 | Foo **** Baz ** 31 | Bar * 32 | `; 33 | expect(toTransform(tree.commit)).to.deep.equal([ 34 | { ...required, id: 1, width: 80, x: 0, row: 0, weight: 9 }, 35 | { ...required, id: 4, width: 60, x: 0, row: 1, weight: 7 }, 36 | { ...required, id: 3, width: 50, x: 0, row: 2, weight: 6 }, 37 | { ...required, id: 2, width: 30, x: 0, row: 3, weight: 3 }, 38 | ]); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/view/components/profiler/flamegraph/shared.ts: -------------------------------------------------------------------------------- 1 | import { ID } from "../../../store/types"; 2 | 3 | export interface NodeTransform { 4 | id: ID; 5 | x: number; 6 | row: number; 7 | width: number; 8 | weight: number; 9 | maximized: boolean; 10 | visible: boolean; 11 | commitParent: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/view/components/profiler/util.ts: -------------------------------------------------------------------------------- 1 | const formatter = Intl.NumberFormat(); 2 | 3 | const decimals = 1; 4 | 5 | export function formatTime(time: number) { 6 | if (time < 0.1) return "<0.1ms"; 7 | 8 | if (time > 100) { 9 | const display = formatter.format(Number((time / 1000).toFixed(decimals))); 10 | return `${display}s`; 11 | } 12 | 13 | return `${time.toFixed(decimals)}ms`; 14 | } 15 | -------------------------------------------------------------------------------- /src/view/components/sidebar/DebugNodeNavTree.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { useStore } from "../../store/react-bindings"; 3 | import { SidebarPanel, Empty } from "./SidebarPanel"; 4 | 5 | export function DebugNodeNavTree() { 6 | const store = useStore(); 7 | const selected = store.selection.selected.value; 8 | const nodes = store.nodeList.value.map(id => store.nodes.value.get(id)!); 9 | 10 | return ( 11 | 12 | {nodes.length === 0 ? ( 13 | No nodes found inside commmit 14 | ) : ( 15 | 35 | )} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/view/components/sidebar/DebugTreeStats.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { useStore } from "../../store/react-bindings"; 3 | import { SidebarPanel } from "./SidebarPanel"; 4 | 5 | export function DebugTreeStats() { 6 | const store = useStore(); 7 | const nodeList = store.nodeList.value; 8 | 9 | return ( 10 | 11 |
12 |
13 |
Active displayed node count
14 |
{nodeList.length}
15 |
Selected node index
16 |
{store.selection.selectedIdx.value}
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/sidebar/HocPanel.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | 3 | export interface Props { 4 | hocs: string[]; 5 | } 6 | 7 | export function Hoc(props: { children: any; small?: boolean }) { 8 | return ( 9 | 10 | {props.children} 11 | 12 | ); 13 | } 14 | 15 | export function HocPanel(props: Props) { 16 | return ( 17 |
18 | {props.hocs.map((hoc, i) => ( 19 | {hoc} 20 | ))} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/view/components/sidebar/KeyPanel.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { SidebarPanel } from "./SidebarPanel"; 3 | 4 | export interface Props { 5 | onCopy: () => void; 6 | value: string; 7 | } 8 | 9 | export function KeyPanel(props: Props) { 10 | return ( 11 | 12 | 13 | {props.value} 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/view/components/sidebar/SidebarPanel.tsx: -------------------------------------------------------------------------------- 1 | import { h, ComponentChildren } from "preact"; 2 | import { IconBtn } from "../IconBtn"; 3 | import { Icon } from "../icons"; 4 | 5 | export interface Props { 6 | title: string; 7 | onCopy?: () => void; 8 | children?: ComponentChildren; 9 | testId?: string; 10 | } 11 | 12 | export function SidebarPanel(props: Props) { 13 | return ( 14 | 25 | ); 26 | } 27 | 28 | export function Empty(props: { children?: any }) { 29 | return {props.children}; 30 | } 31 | -------------------------------------------------------------------------------- /src/view/components/sidebar/inspect/serializeProps.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { serializeProps } from "./serializeProps"; 3 | 4 | describe("serializeProps", () => { 5 | it("should serialize primitives", () => { 6 | expect(serializeProps("foo")).to.equal("foo"); 7 | expect(serializeProps("")).to.equal(""); 8 | expect(serializeProps(2)).to.equal(2); 9 | expect(serializeProps(0)).to.equal(0); 10 | expect(serializeProps(true)).to.equal(true); 11 | expect(serializeProps(false)).to.equal(false); 12 | expect(serializeProps(null)).to.equal(null); 13 | }); 14 | 15 | it("should serialize arrays", () => { 16 | expect(serializeProps([1, 2, 3])).to.deep.equal([1, 2, 3]); 17 | expect(serializeProps([{ type: "vnode", name: "div" }])).to.deep.equal([ 18 | "
", 19 | ]); 20 | }); 21 | 22 | it("should serialize VNodes", () => { 23 | expect(serializeProps({ type: "vnode", name: "div" })).to.equal("
"); 24 | }); 25 | 26 | it("should serialize functions", () => { 27 | expect(serializeProps({ type: "function", name: "foo" })).to.equal("foo()"); 28 | }); 29 | 30 | it("should serialize bigints", () => { 31 | expect(serializeProps({ type: "bigint", value: "3" })).to.equal("3n"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/view/components/tree/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "preact/hooks"; 2 | import { ID } from "../../store/types"; 3 | 4 | export interface ListAdapter { 5 | selected: ID; 6 | canCollapse: (id: ID) => boolean; 7 | checkCollapsed: (id: ID) => boolean; 8 | onPrev: (current: ID) => void; 9 | onNext: (current: ID) => void; 10 | onCollapse: (id: ID, open: boolean) => void; 11 | } 12 | 13 | export function useKeyListNav(opts: ListAdapter) { 14 | return useCallback( 15 | (e: KeyboardEvent) => { 16 | if (/^Arrow/.test(e.key)) e.preventDefault(); 17 | 18 | const sel = opts.selected; 19 | const { 20 | onCollapse, 21 | canCollapse, 22 | checkCollapsed: checkCollapssed, 23 | onPrev, 24 | onNext, 25 | } = opts; 26 | if (e.key === "ArrowLeft") { 27 | if (canCollapse(sel) && !checkCollapssed(sel)) { 28 | return onCollapse(sel, true); 29 | } 30 | onPrev(sel); 31 | } else if (e.key === "ArrowUp") { 32 | onPrev(sel); 33 | } else if (e.key === "ArrowRight") { 34 | if (canCollapse(sel) && checkCollapssed(sel)) { 35 | return onCollapse(sel, false); 36 | } 37 | onNext(sel); 38 | } else if (e.key === "ArrowDown") { 39 | onNext(sel); 40 | } 41 | }, 42 | [opts.selected], 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/view/components/tree/windowing.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { flattenChildren } from "./windowing"; 3 | import { ID } from "../../store/types"; 4 | 5 | describe("flattenChildren", () => { 6 | const tree = new Map(); 7 | tree.set(1, { id: 1, children: [2, 3, 6], depth: 1 }); 8 | tree.set(2, { id: 2, children: [4], depth: 2 }); 9 | tree.set(3, { id: 3, children: [5], depth: 2 }); 10 | tree.set(4, { id: 4, children: [], depth: 3 }); 11 | tree.set(5, { id: 5, children: [], depth: 3 }); 12 | tree.set(6, { id: 6, children: [], depth: 2 }); 13 | 14 | it("should flatten tree", () => { 15 | const collapsed = new Set(); 16 | expect(flattenChildren(tree, 1, id => collapsed.has(id))).to.deep.equal([ 17 | 1, 18 | 2, 19 | 4, 20 | 3, 21 | 5, 22 | 6, 23 | ]); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/view/components/tree/windowing.ts: -------------------------------------------------------------------------------- 1 | import { ID } from "../../store/types"; 2 | 3 | export function flattenChildren< 4 | K, 5 | T extends { id: K; children: K[]; depth: number } 6 | >(tree: Map, id: K, isCollapsed: (id: K) => boolean): K[] { 7 | const out: K[] = []; 8 | const visited = new Set(); 9 | const stack: K[] = [id]; 10 | 11 | while (stack.length > 0) { 12 | const item = stack.pop(); 13 | if (item == null) continue; 14 | 15 | const node = tree.get(item); 16 | if (!node) continue; 17 | 18 | if (!visited.has(node.id)) { 19 | out.push(node.id); 20 | visited.add(node.id); 21 | 22 | if (!isCollapsed(node.id)) { 23 | for (let i = node.children.length; i--; ) { 24 | stack.push(node.children[i]); 25 | } 26 | } 27 | } 28 | } 29 | 30 | return out; 31 | } 32 | 33 | export function clamp(n: number, max: number) { 34 | return Math.max(0, Math.min(n, max)); 35 | } 36 | 37 | export interface Traversable { 38 | id: ID; 39 | parent: ID; 40 | children: ID[]; 41 | } 42 | 43 | export function getLastChild(nodes: Map, id: ID): ID { 44 | const stack = [id]; 45 | let item; 46 | let last = id; 47 | while ((item = stack.pop()) != null) { 48 | last = item; 49 | const node = nodes.get(item); 50 | if (node && node.children.length > 0) { 51 | stack.push(node.children[node.children.length - 1]); 52 | } 53 | } 54 | 55 | return last; 56 | } 57 | -------------------------------------------------------------------------------- /src/view/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useLayoutEffect } from "preact/hooks"; 2 | import { WindowCtx } from "../store/react-bindings"; 3 | import { throttle } from "../../shells/shared/utils"; 4 | 5 | export function useResize(fn: () => void, args: any[], init = false) { 6 | // If we're running inside the browser extension context 7 | // we pull the correct window reference from context. And 8 | // yes there are multiple `window` objects to keep track of. 9 | // If you subscribe to the wrong one, nothing will be 10 | // triggered. For testing scenarios we can fall back to 11 | // the global window object instead. 12 | const win = useContext(WindowCtx) || window; 13 | 14 | useLayoutEffect(() => { 15 | if (init) fn(); 16 | 17 | const fn2 = throttle(fn, 60); 18 | win.addEventListener("resize", fn2); 19 | return () => { 20 | win.removeEventListener("resize", fn2); 21 | }; 22 | }, [...args, init]); 23 | } 24 | -------------------------------------------------------------------------------- /src/view/store/collapser.ts: -------------------------------------------------------------------------------- 1 | import { AppCtx } from "./react-bindings"; 2 | import { Signal } from "@preact/signals"; 3 | import { useContext } from "preact/hooks"; 4 | 5 | export interface Collapser { 6 | collapsed: Signal>; 7 | toggle: (item: T) => void; 8 | collapseNode: (item: T, shouldCollapse: boolean) => void; 9 | } 10 | 11 | /** 12 | * The Collapser deals with hiding sections in a tree view 13 | */ 14 | export function createCollapser(collapsed: Signal>): Collapser { 15 | const collapseNode = (id: T, shouldCollapse: boolean) => { 16 | const v = collapsed.value; 17 | shouldCollapse ? v.add(id) : v.delete(id); 18 | collapsed.value = new Set(v); 19 | }; 20 | 21 | const toggle = (id: T) => collapseNode(id, !collapsed.value.has(id)); 22 | 23 | return { 24 | collapsed, 25 | collapseNode, 26 | toggle, 27 | }; 28 | } 29 | 30 | export function useCollapser() { 31 | const c = useContext(AppCtx).collapser; 32 | const collapsed = c.collapsed.value; 33 | return { collapsed, collapseNode: c.collapseNode, toggle: c.toggle }; 34 | } 35 | -------------------------------------------------------------------------------- /src/view/store/react-bindings.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "preact"; 2 | import { useContext } from "preact/hooks"; 3 | import { Store } from "./types"; 4 | import { EmitFn } from "../../adapter/hook"; 5 | 6 | // Make sure we're accessing the right window object. The global window 7 | // reference is not the same and won't trigger any "resize" (and likely 8 | // other) events at all. 9 | export const WindowCtx = createContext(null as any); 10 | export const AppCtx = createContext(null as any); 11 | export const EmitCtx = createContext(() => null); 12 | 13 | export const useEmitter = () => useContext(EmitCtx); 14 | export const useStore = () => useContext(AppCtx); 15 | -------------------------------------------------------------------------------- /src/view/store/selection.ts: -------------------------------------------------------------------------------- 1 | import { AppCtx } from "./react-bindings"; 2 | import { signal, Signal } from "@preact/signals"; 3 | import { clamp } from "../components/tree/windowing"; 4 | import { useContext } from "preact/hooks"; 5 | import { ID } from "./types"; 6 | 7 | /** 8 | * Manages selection state of the TreeView. 9 | */ 10 | export function createSelectionStore(list: Signal) { 11 | const selected = signal(list.value.length > 0 ? list.value[0] : -1); 12 | const selectedIdx = signal(0); 13 | 14 | const selectByIndex = (idx: number) => { 15 | const n = clamp(idx, list.value.length - 1); 16 | selected.value = list.value[n]; 17 | selectedIdx.value = n; 18 | }; 19 | 20 | const selectNext = () => selectByIndex(selectedIdx.value + 1); 21 | const selectPrev = () => selectByIndex(selectedIdx.value - 1); 22 | 23 | const selectById = (id: ID) => { 24 | const idx = list.value.findIndex(x => x === id); 25 | selectByIndex(idx); 26 | }; 27 | 28 | return { 29 | selected, 30 | selectedIdx, 31 | selectByIndex, 32 | selectById, 33 | selectNext, 34 | selectPrev, 35 | }; 36 | } 37 | 38 | export function useSelection() { 39 | const sel = useContext(AppCtx).selection; 40 | const selected = sel.selected.value; 41 | const selectedIdx = sel.selectedIdx.value; 42 | return { 43 | selected, 44 | selectedIdx, 45 | selectByIndex: sel.selectByIndex, 46 | selectById: sel.selectById, 47 | selectPrev: sel.selectPrev, 48 | selectNext: sel.selectNext, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/view/store/utils.ts: -------------------------------------------------------------------------------- 1 | // See: https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript 2 | export function escapeStringRegexp(string: string) { 3 | return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 4 | } 5 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/context-displayName.jsx: -------------------------------------------------------------------------------- 1 | import { h, render, createContext } from "preact"; 2 | 3 | const Ctx = createContext(null); 4 | Ctx.displayName = "Foobar"; 5 | 6 | function App() { 7 | return ( 8 | 9 | {v => `value: ${v}`} 10 | 11 | ); 12 | } 13 | 14 | render(, document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/counter.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | 4 | function Display(props) { 5 | return
Counter: {props.value}
; 6 | } 7 | 8 | function Counter() { 9 | const [v, set] = useState(0); 10 | 11 | return ( 12 |
13 | 14 | 15 |
16 | ); 17 | } 18 | 19 | render(, document.getElementById("app")); 20 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/data-input.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | 4 | function Display({ value }) { 5 | const v = value !== null && typeof value === "object" ? "object" : value; 6 | return
Counter: {v}
; 7 | } 8 | 9 | let i = 0; 10 | function Counter() { 11 | const [v, set] = useState(i); 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | 20 | render(, document.getElementById("app")); 21 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/forwardRef-update.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import { forwardRef } from "preact/compat"; 4 | 5 | function Foo() { 6 | return

Foo

; 7 | } 8 | 9 | const DoubleInner = forwardRef((props, ref) => { 10 | return ; 11 | }); 12 | 13 | function DoubleForward() { 14 | return ; 15 | } 16 | 17 | function App() { 18 | const [v, set] = useState(0); 19 | 20 | return ( 21 |
22 |
Counter: {v}
23 | 24 | 25 |
26 | ); 27 | } 28 | 29 | render(, document.getElementById("app")); 30 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/forwardRef.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import { forwardRef } from "preact/compat"; 4 | 5 | function Foo() { 6 | return

Foo

; 7 | } 8 | 9 | const Named = forwardRef(function Named() { 10 | return

Named

; 11 | }); 12 | 13 | const Unamed = forwardRef((props, ref) => { 14 | return

Unamed

; 15 | }); 16 | 17 | const DoubleInner = forwardRef((props, ref) => { 18 | return ( 19 |
20 | 21 | {null} 22 |
23 | ); 24 | }); 25 | 26 | const DoubleForward = forwardRef((props, ref) => { 27 | return ; 28 | }); 29 | 30 | function Display(props) { 31 | return
Counter: {props.value}
; 32 | } 33 | 34 | function App() { 35 | const [v, set] = useState(0); 36 | 37 | return ( 38 |
39 | 40 | 41 | 42 | 43 | 44 |
45 | ); 46 | } 47 | 48 | render(, document.getElementById("app")); 49 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/fragment-filter.jsx: -------------------------------------------------------------------------------- 1 | import { h, render, Fragment } from "preact"; 2 | 3 | const css = "background: peachpuff; padding: 2rem; margin-bottom: 1rem;"; 4 | 5 | function Foo() { 6 | return
A
; 7 | } 8 | 9 | render( 10 | 11 | 12 | , 13 | document.getElementById("app"), 14 | ); 15 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/goober.jsx: -------------------------------------------------------------------------------- 1 | import "preact/devtools"; 2 | import { h, render } from "preact"; 3 | import { createContext, useContext } from "preact/compat"; 4 | import { styled, setup } from "goober"; 5 | 6 | const ThemeContext = createContext(); 7 | const useTheme = () => useContext(ThemeContext); 8 | 9 | setup(h, undefined, useTheme); 10 | 11 | const StyledButton = styled("button")({ 12 | background: "#fd9", 13 | border: "1px solid #ddd", 14 | borderRadius: "4px", 15 | padding: "4px 8px", 16 | }); 17 | 18 | function Button() { 19 | return Inspect me; 20 | } 21 | 22 | render( 15 |
16 | ); 17 | } 18 | 19 | render(, document.getElementById("app")); 20 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hoc-update.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment, render, Component } from "preact"; 2 | import { memo, forwardRef } from "preact/compat"; 3 | 4 | function Foo() { 5 | return

I am foo

; 6 | } 7 | const MemoFoo = memo(Foo); 8 | 9 | function Wrapped(props) { 10 | return
{props.children}
; 11 | } 12 | 13 | const Forward = forwardRef(function Bar(props, ref) { 14 | return ( 15 |

16 | forward 17 |

18 | ); 19 | }); 20 | 21 | function withBoof(Comp) { 22 | class Boof extends Component { 23 | render() { 24 | return ( 25 |
26 | 31 | 32 | {this.state.condition ? : } 33 | 34 |
35 | ); 36 | } 37 | } 38 | Boof.displayName = "withBoof(" + (Comp.displayName || Comp.name) + ")"; 39 | return Boof; 40 | } 41 | 42 | const Custom = withBoof(Wrapped); 43 | 44 | render( 45 | 46 |
47 | Custom HOC 48 |
49 | 50 |
, 51 | document.getElementById("app"), 52 | ); 53 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hoc.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment, render, Component } from "preact"; 2 | import { memo, forwardRef } from "preact/compat"; 3 | 4 | function Foo() { 5 | return
I am foo
; 6 | } 7 | const MemoFoo = memo(Foo); 8 | 9 | function Last() { 10 | return
I am last
; 11 | } 12 | const MemoLast = memo(Last); 13 | 14 | const Forward = forwardRef(function Bar(props, ref) { 15 | return ( 16 |
17 | forward 18 |
19 | ); 20 | }); 21 | 22 | const ForwardAnonym = forwardRef((props, ref) => { 23 | return ( 24 |
25 | forward anonymous 26 |
27 | ); 28 | }); 29 | 30 | function withBoof(Comp) { 31 | class Boof extends Component { 32 | render() { 33 | return ; 34 | } 35 | } 36 | Boof.displayName = "withBoof(" + (Comp.displayName || Comp.name) + ")"; 37 | return Boof; 38 | } 39 | 40 | const Custom = withBoof(Foo); 41 | const Multiple = withBoof(MemoLast); 42 | 43 | render( 44 | 45 |
46 | MemoFoo 47 |
48 | 49 |
50 | ForwardRef 51 |
52 | 53 |
54 | ForwardRef (Anonymous) 55 |
56 | 57 |
58 | Custom HOC 59 |
60 | 61 |
62 | Multple HOCs 63 |
64 | 65 |
, 66 | document.getElementById("app"), 67 | ); 68 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/holes.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | 4 | function Foo(props) { 5 | return props.children; 6 | } 7 | 8 | function App() { 9 | const [v, set] = useState(0); 10 | 11 | return ( 12 | 13 | {v % 2 === 0 && "Not a placeholder"} 14 | 15 | 16 | ); 17 | } 18 | 19 | render(, document.getElementById("app")); 20 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hooks-debug.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useDebugValue, useRef } from "preact/hooks"; 3 | 4 | const useFoo = () => { 5 | const foo = useRef({ foo: "bar" }).current; 6 | useDebugValue(foo); 7 | return foo; 8 | }; 9 | 10 | function App() { 11 | useFoo(); 12 | return

App

; 13 | } 14 | 15 | render(, document.getElementById("app")); 16 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hooks-depth-limit.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | 4 | const useFoo = () => 5 | useState({ 6 | key1: { 7 | key2: { 8 | key3: { 9 | key4: { 10 | key5: { 11 | key6: { 12 | key7: { 13 | key8: { 14 | key9: 123, 15 | }, 16 | }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | }, 22 | }, 23 | }); 24 | const useBar = () => useFoo(); 25 | const useBob = () => useBar(); 26 | const useBoof = () => useBob(); 27 | const useBlub = () => useBoof(); 28 | const useBread = () => useBlub(); 29 | const useBubby = () => useBread(); 30 | const useBabby = () => useBubby(); 31 | const useBleb = () => useBabby(); 32 | const useBlaBla = () => useBleb(); 33 | const useBrobba = () => useBlaBla(); 34 | 35 | function Hook() { 36 | const [v] = useBrobba(); 37 | return

{JSON.stringify(v)}

; 38 | } 39 | 40 | render(, document.getElementById("app")); 41 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hooks-expand.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment, render, createContext } from "preact"; 2 | import { useMemo, useContext } from "preact/hooks"; 3 | 4 | function Memo() { 5 | const v = useMemo(() => { 6 | return { 7 | bar: { 8 | boof: { 9 | baz: 123, 10 | }, 11 | foo: 123, 12 | bob: 123, 13 | }, 14 | }; 15 | }, []); 16 | 17 | return

{JSON.stringify(v)}

; 18 | } 19 | 20 | const Ctx = createContext({ foo: 123, bar: 123 }); 21 | 22 | function Context() { 23 | useContext(Ctx); 24 | return

Context

; 25 | } 26 | 27 | render( 28 | 29 | 30 | 31 | 32 | 33 | , 34 | document.getElementById("app"), 35 | ); 36 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hooks-multiple.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useState, useCallback, useMemo } from "preact/hooks"; 3 | 4 | function App() { 5 | const [s1, set_s1] = useState(1); 6 | const [s2, set_s2] = useState(2); 7 | const [s3, set_s3] = useState(3); 8 | const update_s3 = useCallback(() => { 9 | set_s3(s3 + 1); 10 | }, [s3, set_s3]); 11 | const test = useMemo(() => s1 + s2 + s3, [s1, s2, s3]); 12 | 13 | return ( 14 |
15 |
16 | {s1}, {s2}, {s3} 17 |
18 | 19 | 20 | 21 |
22 |
{test}
23 |
24 |
25 | ); 26 | } 27 | 28 | render(, document.getElementById("app")); 29 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hooks-name-custom.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { h, render } from "preact"; 4 | import { useMemo } from "preact/hooks"; 5 | import { addHookName } from "preact/devtools"; 6 | 7 | function useFoo() { 8 | const a = addHookName( 9 | useMemo(() => "a", []), 10 | "a", 11 | ); 12 | const b = addHookName( 13 | useMemo(() => "b", []), 14 | "b", 15 | ); 16 | return a + b; 17 | } 18 | 19 | function App() { 20 | const v = useFoo(); 21 | return

{v}

; 22 | } 23 | 24 | render(, document.getElementById("app")); 25 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/hooks-support.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import { useRef } from "preact/hooks"; 3 | 4 | function RefComponent() { 5 | const v = useRef(0); 6 | return

Ref: {v.current}

; 7 | } 8 | 9 | render(, document.getElementById("app")); 10 | -------------------------------------------------------------------------------- /test-e2e/fixtures/apps/iframe.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment, render } from "preact"; 2 | 3 | function View() { 4 | return ( 5 | 6 |
7 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test-e2e/fixtures/inject-sprite.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vite"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | // WARNING: Also update build.js 6 | export function injectSvgSpritePlugin(): Plugin { 7 | return { 8 | name: "inject-svg-sprite", 9 | transformIndexHtml(html) { 10 | if (/\.\/devtools\.ts/.test(html)) { 11 | const filePath = path.join( 12 | __dirname, 13 | "..", 14 | "..", 15 | "src", 16 | "view", 17 | "sprite.svg", 18 | ); 19 | const svg = fs.readFileSync(filePath, "utf-8"); 20 | 21 | const res = html.replace( 22 | //, 23 | "\n\t\t" + svg.split("\n").join("\n\t\t") + "\n", 24 | ); 25 | 26 | return res; 27 | } 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /test-e2e/fixtures/list-fixtures.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vite"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | 5 | /** 6 | * Get all available fixtures and expose them via a virtual 7 | * module. This is used for the fixture drop down 8 | */ 9 | export function listFixtures(): Plugin { 10 | const virtual = "@fixtures"; 11 | return { 12 | name: "preact:fixtures", 13 | resolveId(id) { 14 | if (id === virtual) { 15 | return id; 16 | } 17 | }, 18 | load(id) { 19 | if (id === virtual) { 20 | const dir = path.join(__dirname, "apps"); 21 | const items = fs 22 | .readdirSync(dir) 23 | .map(x => path.basename(x, path.extname(x))) 24 | .sort((a, b) => a.localeCompare(b)); 25 | 26 | return `export const fixtures = [ 27 | ${items.map(x => '"' + x + '"').join(",\n")} 28 | ]`; 29 | } 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /test-e2e/fixtures/list-preact-versions.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vite"; 2 | import { getPreactVersions } from "./utils"; 3 | 4 | /** 5 | * Load all available Preact versions and expose them via 6 | * a virtual module. Used for the version select. 7 | */ 8 | export function listPreactVersions(): Plugin { 9 | const virtual = "@preact-list-versions"; 10 | 11 | return { 12 | name: "preact:list-versions", 13 | resolveId(id) { 14 | if (virtual === id) { 15 | return id; 16 | } 17 | }, 18 | load(id) { 19 | if (virtual === id) { 20 | const items = getPreactVersions(); 21 | return `export const preactVersions = [ 22 | ${items.map(x => '"' + x + '"').join(",\n")} 23 | ]`; 24 | } 25 | }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /test-e2e/fixtures/utils.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | /** 5 | * Get a sorted list of all available preact versions 6 | */ 7 | export function getPreactVersions() { 8 | const dir = path.join(__dirname, "vendor", "preact"); 9 | const versions = fs 10 | .readdirSync(dir) 11 | .filter(name => !name.startsWith(".")) 12 | .map(name => { 13 | if (name.endsWith(".tgz")) { 14 | name = name.slice(0, -".tgz".length); 15 | } 16 | 17 | if (name.startsWith("preact-")) { 18 | name = name.slice("preact-".length); 19 | } 20 | 21 | return name; 22 | }) 23 | .sort((a, b) => { 24 | const semA = a.split(".").map(n => +n); 25 | const semB = b.split(".").map(n => +n); 26 | 27 | // If one is non-semver 28 | if (semA.length === 1 && semB.length > 1) { 29 | return -1; 30 | } else if (semA.length > 1 && semB.length === 1) { 31 | return 1; 32 | } else if (semA.length === 1 && semB.length === 1) { 33 | return a.localeCompare(b); 34 | } 35 | 36 | if (semA[0] < semB[0]) { 37 | return 1; 38 | } else if (semB[0] < semA[0]) { 39 | return -1; 40 | } else if (semA[1] < semB[1]) { 41 | return 1; 42 | } else if (semB[1] < semA[1]) { 43 | return -1; 44 | } else if (semA[2] < semB[2]) { 45 | return 1; 46 | } else if (semB[2] < semA[2]) { 47 | return -1; 48 | } 49 | 50 | // Check if is tagged release: 11.0.0-experimental.0 51 | return a.localeCompare(b) * -1; 52 | }); 53 | 54 | return ["git", ...versions]; 55 | } 56 | -------------------------------------------------------------------------------- /test-e2e/fixtures/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { listFixtures } from "./list-fixtures"; 3 | import { rewritePreactVersion } from "./rewrite-preact-version"; 4 | import { loadPreactVersion } from "./load-preact-version"; 5 | import { listPreactVersions } from "./list-preact-versions"; 6 | import path from "path"; 7 | import { injectSvgSpritePlugin } from "./inject-sprite"; 8 | import prefresh from "@prefresh/vite"; 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig({ 12 | optimizeDeps: { 13 | exclude: ["preact"], 14 | }, 15 | plugins: [ 16 | prefresh(), 17 | { 18 | name: "preact:config", 19 | config() { 20 | return { 21 | esbuild: { 22 | jsxFactory: "h", 23 | jsxFragment: "Fragment", 24 | jsxInject: "", 25 | define: { 26 | __DEBUG__: JSON.stringify(false), 27 | }, 28 | }, 29 | 30 | resolve: { 31 | alias: { 32 | "react-dom/test-utils": "preact/test-utils", 33 | "react-dom": "preact/compat", 34 | react: "preact/compat", 35 | goober: path.join(__dirname, "vendor", "goober.js"), 36 | }, 37 | }, 38 | }; 39 | }, 40 | }, 41 | listPreactVersions(), 42 | listFixtures(), 43 | loadPreactVersion(), 44 | rewritePreactVersion(), 45 | injectSvgSpritePlugin(), 46 | ], 47 | }); 48 | -------------------------------------------------------------------------------- /test-e2e/tests/context-displayname.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, waitForPass } from "../pw-utils"; 3 | 4 | test("Inspect should select node in elements panel", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "context-displayName"); 6 | 7 | await waitForPass(async () => { 8 | const items = await devtools 9 | .locator('[data-testid="tree-item"]') 10 | .allInnerTexts(); 11 | expect(items).toEqual(["App", "Foobar.Provider", "Foobar.Consumer"]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test-e2e/tests/debug-mode.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | gotoTest, 5 | locateFlame, 6 | locateTab, 7 | } from "../pw-utils"; 8 | 9 | test("Debug mode toggles debug views", async ({ page }) => { 10 | const { devtools } = await gotoTest(page, "counter"); 11 | 12 | // Enable Capturing 13 | await devtools.locator(locateTab("SETTINGS")).click(); 14 | const checked = await devtools 15 | .locator('[data-testid="toggle-debug-mode"]') 16 | .isChecked(); 17 | expect(checked).toEqual(false); 18 | 19 | // Start profiling 20 | await devtools.locator(locateTab("PROFILER")).click(); 21 | await clickRecordButton(devtools); 22 | await page.click("button"); 23 | await page.click("button"); 24 | await clickRecordButton(devtools); 25 | await devtools.locator(locateFlame("Counter")).click(); 26 | 27 | await expect( 28 | devtools.locator('[data-testid="profiler-debug-stats"]'), 29 | ).toHaveCount(0); 30 | await expect( 31 | devtools.locator('[data-testid="profiler-debug-nav"]'), 32 | ).toHaveCount(0); 33 | 34 | await devtools.locator(locateTab("SETTINGS")).click(); 35 | await devtools.click('[data-testid="toggle-debug-mode"]'); 36 | 37 | await devtools.locator(locateTab("PROFILER")).click(); 38 | 39 | await devtools.waitForSelector('[data-testid="profiler-debug-stats"]'); 40 | await devtools.waitForSelector('[data-testid="profiler-debug-nav"]'); 41 | }); 42 | -------------------------------------------------------------------------------- /test-e2e/tests/element-keyboard.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getProps, gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("Test keyboard navigation in elements tree", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.locator(locateTreeItem("Counter")).click(); 8 | expect(Object.keys(await getProps(devtools))).toEqual([]); 9 | 10 | await page.keyboard.press("ArrowDown"); 11 | let selected = await devtools.locator('[data-selected="true"]').textContent(); 12 | 13 | const prop = '[data-testid="Props"] [data-testid="props-row"]'; 14 | 15 | expect(selected).toEqual("Display"); 16 | expect((await devtools.$$(prop)).length).toEqual(1); 17 | 18 | await page.keyboard.press("ArrowUp"); 19 | selected = await devtools.locator('[data-selected="true"]').textContent(); 20 | 21 | expect(selected).toEqual("Counter"); 22 | expect((await devtools.$$(prop)).length).toEqual(0); 23 | }); 24 | -------------------------------------------------------------------------------- /test-e2e/tests/element-scroll.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Clicking at the right of element names #144", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "deep-tree"); 6 | 7 | await page.setViewportSize({ 8 | width: 1280, 9 | height: 900, 10 | }); 11 | 12 | const selector = '[data-name="App"]'; 13 | await devtools.waitForSelector(selector); 14 | const { x, y } = await devtools.evaluate((s: string) => { 15 | const rect = document.querySelector(s)!.getBoundingClientRect(); 16 | return { x: rect.right, y: rect.top }; 17 | }, selector); 18 | const offset = await page.evaluate(() => { 19 | return document.querySelector("#devtools")!.getBoundingClientRect().top; 20 | }); 21 | 22 | await page.mouse.click(x - 20, y + offset + 200); 23 | 24 | const text = await devtools.locator('[data-selected="true"]').textContent(); 25 | expect(text).toEqual("ChildItemName"); 26 | }); 27 | -------------------------------------------------------------------------------- /test-e2e/tests/element-search-keyboard.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { gotoTest, waitFor } from "../pw-utils"; 3 | 4 | test("Pressing Enter should scroll marked results into view during search #162", async ({ 5 | page, 6 | }) => { 7 | const { devtools } = await gotoTest(page, "deep-tree"); 8 | 9 | await devtools.waitForSelector('[data-name="App"]'); 10 | await devtools.type('[data-testid="element-search"]', "Child"); 11 | 12 | // Press Enter a bunch of times 13 | for (let i = 0; i < 24; i++) { 14 | await page.keyboard.press("Enter"); 15 | } 16 | 17 | await waitFor(async () => { 18 | const marked = await devtools.$("[data-marked]"); 19 | if (!marked) return false; 20 | return await marked!.isVisible(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/filter-fragment.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Fragment filter should filter Fragment nodes", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "fragment-filter", { 6 | preact: "10.4.1", 7 | }); 8 | 9 | await devtools.locator('[data-testid="elements-tree"] [data-name]').waitFor(); 10 | 11 | const names = await devtools 12 | .locator('[data-testid="elements-tree"] [data-name]') 13 | .allTextContents(); 14 | 15 | expect(names).toEqual(["Foo"]); 16 | }); 17 | -------------------------------------------------------------------------------- /test-e2e/tests/filter-text-signal.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Text Signal filter should filter Text Signal nodes", async ({ page }) => { 5 | test.skip( 6 | process.env.PREACT_VERSION !== "10", 7 | "Signals are not supported in v11 yet.", 8 | ); 9 | const { devtools } = await gotoTest(page, "signals-text"); 10 | 11 | await devtools 12 | .locator('[data-testid="elements-tree"] [data-name]') 13 | .first() 14 | .waitFor(); 15 | 16 | let names = await devtools 17 | .locator('[data-testid="elements-tree"] [data-name]') 18 | .allTextContents(); 19 | expect(names).toEqual(["App", "Counter"]); 20 | 21 | await devtools.click('[data-testid="filter-menu-button"]'); 22 | await devtools.waitForSelector('[data-testid="filter-popup"]'); 23 | await devtools.click( 24 | '[data-testid="filter-popup"] label:has-text("Text Signal nodes")', 25 | ); 26 | await devtools.click('[data-testid="filter-update"]'); 27 | 28 | await expect( 29 | devtools.locator('[data-testid="elements-tree"] [data-name]'), 30 | ).toHaveCount(4); 31 | 32 | names = await devtools 33 | .locator('[data-testid="elements-tree"] [data-name]') 34 | .allTextContents(); 35 | expect(names).toEqual(["App", "Counter", "__TextSignal", "__TextSignal"]); 36 | }); 37 | -------------------------------------------------------------------------------- /test-e2e/tests/highlight-suspense.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem, wait } from "../pw-utils"; 3 | 4 | test("Highlight Suspense nodes without crashing", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "suspense"); 6 | 7 | await devtools.waitForSelector(locateTreeItem("Suspense")); 8 | await devtools.hover(locateTreeItem("Suspense")); 9 | // Wait for possible flickering to occur 10 | await wait(1000); 11 | 12 | const sizeOnPage = await page.$eval('[data-testid="delayed"]', el => 13 | el.getBoundingClientRect(), 14 | ); 15 | const sizeOfHighlight = await page.$eval( 16 | "#preact-devtools-highlighter > div", 17 | el => el.getBoundingClientRect(), 18 | ); 19 | expect(sizeOfHighlight).toEqual(sizeOnPage); 20 | }); 21 | -------------------------------------------------------------------------------- /test-e2e/tests/highlighter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem, wait } from "../pw-utils"; 3 | 4 | test("Highlight item", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.waitForSelector(locateTreeItem("Counter")); 8 | await devtools.hover(locateTreeItem("Counter")); 9 | // Wait for possible flickering to occur 10 | await wait(1000); 11 | 12 | const sizeOnPage = await page.$eval("#app > div", el => 13 | el.getBoundingClientRect(), 14 | ); 15 | const sizeOfHighlight = await page.$eval( 16 | "#preact-devtools-highlighter > div", 17 | el => el.getBoundingClientRect(), 18 | ); 19 | expect(sizeOfHighlight).toEqual(sizeOnPage); 20 | }); 21 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-filter-disable.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("HOC-Component filter should be disabled", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hoc"); 6 | 7 | await devtools.click('[data-testid="filter-menu-button"]'); 8 | await devtools.waitForSelector('[data-testid="filter-popup"]'); 9 | await devtools.click( 10 | '[data-testid="filter-popup"] label:has-text("HOC-Components")', 11 | ); 12 | await devtools.click('[data-testid="filter-update"]'); 13 | 14 | await devtools.waitForSelector(locateTreeItem("Memo(Foo)")); 15 | 16 | const items = await devtools 17 | .locator('[data-testid="tree-item"]') 18 | .evaluateAll(els => 19 | Array.from(els).map(el => el.getAttribute("data-name")), 20 | ); 21 | 22 | expect(items).toEqual([ 23 | "Memo(Foo)", 24 | "Foo", 25 | "ForwardRef(Bar)", 26 | "ForwardRef()", 27 | "withBoof(Foo)", 28 | "Foo", 29 | "withBoof(Memo(Last))", 30 | "Memo(Last)", 31 | "Last", 32 | ]); 33 | 34 | await expect(devtools.locator('[data-testid="hoc-panel"]')).toHaveCount(0); 35 | }); 36 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-filter-search.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("HOC-Component labels should be searchable", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hoc"); 6 | 7 | await devtools.waitForSelector(locateTreeItem("Foo")); 8 | await devtools.type('[data-testid="element-search"]', "forw"); 9 | 10 | let marked = await devtools.$$("mark"); 11 | expect(marked.length).toEqual(2); 12 | expect( 13 | await marked[0].evaluate(el => el.hasAttribute("data-marked")), 14 | ).toEqual(true); 15 | 16 | await devtools 17 | .locator('[data-testid="search-counter"]:has-text("1 | 2")') 18 | .waitFor(); 19 | 20 | await page.keyboard.press("Enter"); 21 | 22 | marked = await devtools.$$("mark"); 23 | expect(marked.length).toEqual(2); 24 | expect( 25 | await marked[1].evaluate(el => el.hasAttribute("data-marked")), 26 | ).toEqual(true); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-filter-update.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { getTreeItems, gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("HOC-Component should work with updates", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hoc-update"); 6 | 7 | await devtools.waitForSelector(locateTreeItem("Wrapped")); 8 | 9 | let items = await getTreeItems(devtools); 10 | expect(items).toEqual([ 11 | { name: "Wrapped", hocs: ["withBoof"] }, 12 | { name: "Bar", hocs: ["ForwardRef"] }, 13 | ]); 14 | 15 | // Trigger update 16 | await page.click("button"); 17 | 18 | items = await getTreeItems(devtools); 19 | expect(items).toEqual([ 20 | { name: "Wrapped", hocs: ["withBoof"] }, 21 | { name: "Foo", hocs: ["Memo"] }, 22 | ]); 23 | }); 24 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-filter.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, waitForPass } from "../pw-utils"; 3 | 4 | test("HOC-Component filter should flatten tree", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hoc"); 6 | 7 | await devtools.waitForSelector('[data-testid="tree-item"][data-name="Foo"]'); 8 | 9 | const items = await devtools 10 | .locator('[data-testid="tree-item"]') 11 | .evaluateAll(els => els.map(el => el.getAttribute("data-name"))); 12 | 13 | expect(items).toEqual(["Foo", "Bar", "Anonymous", "Foo", "Last"]); 14 | 15 | await devtools.click('[data-name="Anonymous"]'); 16 | 17 | const hocs = await devtools 18 | .locator('[data-testid="hoc-panel"] .hoc-item') 19 | .allInnerTexts(); 20 | expect(hocs).toEqual(["ForwardRef"]); 21 | 22 | await devtools.click('[data-name="Last"]'); 23 | 24 | await waitForPass(async () => { 25 | const hocs = await devtools 26 | .locator('[data-testid="hoc-panel"] .hoc-item') 27 | .allInnerTexts(); 28 | expect(hocs).toEqual(["withBoof", "Memo"]); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-forward-update.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Test HOCs on forwardRef update", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "forwardRef-update"); 6 | 7 | await devtools.locator('[data-name="Foo"]').click(); 8 | let labels = devtools.locator('[data-name="Foo"] [data-testid="hoc-labels"]'); 9 | await expect(labels).toHaveCount(0); 10 | 11 | await page.locator("button").click(); 12 | 13 | await page.locator('[data-testid="result"]:has-text("Counter: 1")'); 14 | 15 | await devtools.locator('[data-name="Foo"]').click(); 16 | 17 | labels = devtools.locator('[data-name="Foo"] [data-testid="hoc-labels"]'); 18 | await expect(labels).toHaveCount(0); 19 | }); 20 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-highlight.test.ts: -------------------------------------------------------------------------------- 1 | import { test, Frame, Page } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("HOC-Component original name should show in highlight", async ({ 5 | page, 6 | }) => { 7 | const { devtools } = await gotoTest(page, "hoc"); 8 | 9 | await assertHighlightName(page, devtools, "Bar"); 10 | await assertHighlightName(page, devtools, "Anonymous"); 11 | await assertHighlightName(page, devtools, "Last"); 12 | }); 13 | 14 | async function assertHighlightName(page: Page, devtools: Frame, name: string) { 15 | const selector = `[data-testid="tree-item"][data-name="${name}"]`; 16 | await devtools.waitForSelector(selector); 17 | 18 | await devtools.hover(selector); 19 | 20 | await page 21 | .locator(`[data-testid=highlighter-label]:has-text("${name}")`) 22 | .waitFor(); 23 | } 24 | -------------------------------------------------------------------------------- /test-e2e/tests/hoc-update.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Test HOCs on update", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "static-subtree"); 6 | 7 | await devtools 8 | .locator('[data-name="Static"] [data-testid="hoc-labels"]') 9 | .first() 10 | .waitFor(); 11 | 12 | await page.locator("button").click(); 13 | 14 | const txt = await page.locator("data-testid=result").textContent(); 15 | expect(txt).toEqual("Counter: 1"); 16 | 17 | const items = await devtools 18 | .locator("data-testid=hoc-labels") 19 | .allInnerTexts(); 20 | expect(items).toEqual(["Memo", "Memo"]); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/hooks-expand-state.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickHookItem, 4 | clickTreeItem, 5 | getHooks, 6 | gotoTest, 7 | locateHook, 8 | } from "../../pw-utils"; 9 | 10 | test("Inspect useRef hook", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "hooks-expand"); 12 | 13 | await clickTreeItem(devtools, "Memo"); 14 | await devtools.locator(locateHook("useMemo")).waitFor(); 15 | 16 | await clickHookItem(devtools, "useMemo"); 17 | const hooks = await getHooks(devtools); 18 | expect(hooks.length).toEqual(2); 19 | }); 20 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/hooks-multiple.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickHookItem, 4 | clickTreeItem, 5 | getHooks, 6 | gotoTest, 7 | } from "../../pw-utils"; 8 | 9 | test("Show multiple hook names at the same time", async ({ page }) => { 10 | const { devtools } = await gotoTest(page, "hooks-multiple"); 11 | 12 | await clickTreeItem(devtools, "App"); 13 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 14 | 15 | await clickHookItem(devtools, "useMemo"); 16 | const hooks = await getHooks(devtools); 17 | expect(hooks).toEqual([ 18 | ["useState", "1"], 19 | ["useState", "2"], 20 | ["useState", "3"], 21 | ["useCallback", "ƒ ()"], 22 | ["useMemo", "6"], 23 | ]); 24 | }); 25 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/hooks-number.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { clickTreeItem, gotoTest } from "../../pw-utils"; 3 | 4 | test("Show hook number", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks-multiple"); 6 | 7 | await clickTreeItem(devtools, "App"); 8 | 9 | const nums = await devtools 10 | .locator('[data-testid="Hooks"] .hook-number') 11 | .allTextContents(); 12 | 13 | expect(nums).toEqual(["1", "2", "3", "4", "5"]); 14 | }); 15 | 16 | test("Show hook number only for top level items", async ({ page }) => { 17 | const { devtools } = await gotoTest(page, "hooks-expand"); 18 | 19 | await clickTreeItem(devtools, "Memo"); 20 | 21 | await devtools.click('[data-testid="props-row"] button'); 22 | await devtools.waitForSelector('[data-testid="props-row"][data-depth="2"]'); 23 | 24 | const nums = await devtools 25 | .locator('[data-testid="Hooks"] .hook-number') 26 | .allTextContents(); 27 | 28 | expect(nums).toEqual(["1"]); 29 | }); 30 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/hooks-support.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { clickTreeItem, gotoTest } from "../../pw-utils"; 3 | 4 | test('Show "hooks not supported" warning', async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks-support", { 6 | preact: "10.3.4", 7 | }); 8 | 9 | await clickTreeItem(devtools, "RefComponent"); 10 | await devtools.locator('[data-testid="no-hooks-support-warning"]').waitFor(); 11 | }); 12 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useCallback.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useCallback hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("CallbackOnly")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useCallback", "ƒ ()"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="prop-value"] input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useContext-10.5.14.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useContext hook Preact 10.5.14 (goober)", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "goober"); 6 | 7 | await devtools.locator(locateTreeItem("a")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | await devtools 11 | .locator('[data-testid="Hooks"] [data-depth="1"] button') 12 | .click(); 13 | await devtools.locator('[data-testid="Hooks"] [data-depth="2"]').click(); 14 | 15 | const hooks = await getHooks(devtools); 16 | expect(hooks).toEqual([ 17 | ["useTheme", ""], 18 | ["useContext", "undefined"], 19 | ]); 20 | }); 21 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useContext.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | getHooks, 4 | gotoTest, 5 | locateTreeItem, 6 | waitForPass, 7 | } from "../../pw-utils"; 8 | 9 | test("Inspect useContext hook", async ({ page }) => { 10 | const { devtools } = await gotoTest(page, "hooks"); 11 | 12 | await devtools.locator(locateTreeItem("ContextComponent")).click(); 13 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 14 | 15 | const hooks = await getHooks(devtools); 16 | expect(hooks).toEqual([["useContext", '"foobar"']]); 17 | 18 | // Should not be collapsable 19 | await expect( 20 | devtools.locator('[data-testid="props-row"] > button'), 21 | ).toHaveCount(0); 22 | 23 | // Should not be editable 24 | await expect( 25 | devtools.locator('[data-testid="props-value"] > input'), 26 | ).toHaveCount(0); 27 | 28 | // Check if default value is read when no Provider is present 29 | await devtools.locator(locateTreeItem("ContextNoProvider")).click(); 30 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 31 | 32 | await waitForPass(async () => { 33 | const hooks = await getHooks(devtools); 34 | expect(hooks).toEqual([["useContext", "0"]]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useCustomHooks.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, locateHook, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect custom hooks", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("CustomHooks")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | await devtools 11 | .locator( 12 | '[data-testid="Hooks"] [data-testid="props-row"] button[data-collapsed="true"]', 13 | ) 14 | .waitFor(); 15 | 16 | await expect( 17 | devtools.locator('[data-testid="Hooks"] [data-testid="props-row"]'), 18 | ).toHaveCount(1); 19 | 20 | await devtools.locator(locateHook("useFoo")).click(); 21 | await expect( 22 | devtools.locator('[data-testid="Hooks"] [data-testid="props-row"]'), 23 | ).toHaveCount(2); 24 | 25 | await devtools.locator(locateHook("useBar")).click(); 26 | await expect( 27 | devtools.locator('[data-testid="Hooks"] [data-testid="props-row"]'), 28 | ).toHaveCount(4); 29 | 30 | // Collapse all hooks 31 | await devtools.locator(locateHook("useFoo")).click(); 32 | await expect( 33 | devtools.locator('[data-testid="Hooks"] [data-testid="props-row"]'), 34 | ).toHaveCount(1); 35 | }); 36 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useDebugValue-complex.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Show custom debug value complex", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks-debug"); 6 | 7 | await devtools.locator(locateTreeItem("App")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useFoo", '{foo: "bar"}']]); 12 | }); 13 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useDebugValue.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Show custom debug value", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("DebugValue")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | let hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useMyHook", '"Offline"']]); 12 | 13 | await page.locator('[data-testid="debug-hook-toggle"]').click(); 14 | 15 | hooks = await getHooks(devtools); 16 | expect(hooks).toEqual([["useMyHook", '"Online"']]); 17 | }); 18 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useDeepHook.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateHook, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect deep hook tree", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("CustomHooks3")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | await devtools 11 | .locator( 12 | '[data-testid="Hooks"] [data-testid="props-row"] button[data-collapsed="true"]', 13 | ) 14 | .first() 15 | .waitFor(); 16 | 17 | let hooks = await getHooks(devtools); 18 | expect(hooks.length).toEqual(2); 19 | 20 | await devtools.locator(locateHook("useBoof")).click(); 21 | hooks = await getHooks(devtools); 22 | expect(hooks.length).toEqual(3); 23 | 24 | await devtools.locator(locateHook("useBob")).click(); 25 | hooks = await getHooks(devtools); 26 | expect(hooks.length).toEqual(4); 27 | 28 | await devtools.locator(locateHook("useFoo")).click(); 29 | hooks = await getHooks(devtools); 30 | expect(hooks.length).toEqual(5); 31 | 32 | await devtools.locator(locateHook("useBar")).click(); 33 | hooks = await getHooks(devtools); 34 | expect(hooks.length).toEqual(7); 35 | }); 36 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useEffect.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useEffect hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("Effect")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useEffect", "ƒ ()"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useErrorBoundary.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useErrorBoundary hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("ErrorBoundary1")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | let hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useErrorBoundary", ""]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | 23 | // Error boundary with callback 24 | await devtools.locator(locateTreeItem("ErrorBoundary2")).click(); 25 | hooks = await getHooks(devtools); 26 | expect(hooks).toEqual([["useErrorBoundary", "ƒ ()"]]); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useImperativeHandle.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useImperativeHandle hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("ImperativeHandle")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useImperativeHandle", "ƒ ()"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useLayoutEffect.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useLayoutEffect hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("LayoutEffect")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useLayoutEffect", "ƒ ()"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useMemo.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useMemo hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("Memo")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useMemo", "0"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useRef-element.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useRef-element hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "use-ref-element"); 6 | 7 | await devtools.locator(locateTreeItem("App")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useRef", ""]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useRef.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useRef hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("RefComponent")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useRef", "0"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should not be editable 19 | await expect( 20 | devtools.locator('[data-testid="props-value"] > input'), 21 | ).toHaveCount(0); 22 | }); 23 | -------------------------------------------------------------------------------- /test-e2e/tests/hooks/useState.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getHooks, gotoTest, locateTreeItem } from "../../pw-utils"; 3 | 4 | test("Inspect useState hook", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "hooks"); 6 | 7 | await devtools.locator(locateTreeItem("Counter")).click(); 8 | await devtools.locator('[data-testid="Hooks"]').waitFor(); 9 | 10 | const hooks = await getHooks(devtools); 11 | expect(hooks).toEqual([["useState", "0"]]); 12 | 13 | // Should not be collapsable 14 | await expect( 15 | devtools.locator('[data-testid="props-row"] > button'), 16 | ).toHaveCount(0); 17 | 18 | // Should be editable 19 | await devtools 20 | .locator('[data-testid="Hooks"] [data-testid="prop-value"] input') 21 | .click(); 22 | await page.keyboard.press("ArrowUp"); 23 | await page.keyboard.press("Enter"); 24 | 25 | const text = await page.locator('[data-testid="result"]').textContent(); 26 | expect(text).toEqual("Counter: 1"); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-click.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Don't trigger events on click during inspection", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | let txt = await page.locator('[data-testid="result"]').textContent(); 8 | 9 | await devtools.locator('[data-testid="inspect-btn"]').click(); 10 | await page.locator("button").click(); 11 | 12 | txt = await page.locator('[data-testid="result"]').textContent(); 13 | expect(txt).toEqual("Counter: 0"); 14 | }); 15 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-fragment.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Highlighting combined DOM tree of a Fragment", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "highlight-fragment"); 6 | 7 | // 1st test 8 | let size = await page.locator("data-testid=test1").boundingBox(); 9 | 10 | await devtools.locator("data-testid=tree-item").first().click(); 11 | await page.locator("data-testid=highlight").waitFor(); 12 | 13 | let highlight = await page.locator("data-testid=highlight").boundingBox(); 14 | expect(size!.width).toEqual(highlight!.width); 15 | expect(size!.height).toEqual(highlight!.height); 16 | 17 | // 2nd test 18 | size = await page.locator("data-testid=test2").boundingBox(); 19 | await devtools.locator("data-testid=tree-item").nth(1).click(); 20 | 21 | highlight = await page.locator("data-testid=highlight").boundingBox(); 22 | expect(size!.width).toEqual(highlight!.width); 23 | expect(size!.height).toEqual(highlight!.height); 24 | }); 25 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-highlight.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Highlighting nested elements affects overlay size", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await page.locator('[data-testid="result"]:has-text("Counter: 0")').waitFor(); 8 | 9 | await devtools.click('[data-testid="inspect-btn"]'); 10 | 11 | const highlight = '[data-testid="highlight"]'; 12 | await page.hover('[data-testid="result"]'); 13 | 14 | const sizeTarget = await page.locator(highlight).boundingBox(); 15 | 16 | await page.hover("button"); 17 | const sizeBtn = await page.locator(highlight).boundingBox(); 18 | 19 | expect(sizeTarget).not.toEqual(sizeBtn); 20 | }); 21 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-key.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, waitForPass } from "../pw-utils"; 3 | 4 | test("Show vnode key in the sidebar", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "keys"); 6 | await page.context().grantPermissions(["clipboard-read"]); 7 | 8 | await devtools 9 | .locator(`[data-testid="tree-item"]:has-text('key="ABC"')`) 10 | .click(); 11 | await devtools.waitForSelector('[data-testid="key-panel"]'); 12 | 13 | const text = await devtools 14 | .locator('[data-testid="vnode-key"]') 15 | .textContent(); 16 | expect(text).toEqual("ABC"); 17 | 18 | const copy = '[data-testid="key-panel"] button[title="Copy Key"]'; 19 | await devtools.click(copy); 20 | 21 | const clipboard = await page.evaluate(() => navigator.clipboard.readText()); 22 | expect(JSON.parse(clipboard)).toEqual("ABC"); 23 | 24 | // Check that the keypanel is not present for keyless components 25 | await devtools.click('[data-name="NoKey"]'); 26 | await waitForPass(async () => { 27 | expect(await devtools.locator('[data-testid="key-panel"]').count()).toEqual( 28 | 0, 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-non-vnode.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Inspect should only parse vnodes as vnodes #114", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "non-vnode"); 6 | 7 | await devtools.click('[data-testid="tree-item"]'); 8 | await devtools.waitForSelector('[name="new-prop-name"]'); 9 | 10 | const values = await devtools 11 | .locator('[data-testid="Props"] [data-testid="props-row"] [data-type]') 12 | .evaluateAll(els => els.map(el => el.getAttribute("data-type"))); 13 | 14 | expect(values).toEqual(["blob", "object", "vnode", "vnode"]); 15 | 16 | const blob = await devtools 17 | .locator('[data-testid="props-row"]:first-child [data-testid="prop-value"]') 18 | .textContent(); 19 | expect(blob).toEqual("Blob {}"); 20 | }); 21 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-owner-hoc.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getOwners, gotoTest } from "../pw-utils"; 3 | 4 | // TODO: Something is wrong with owner tracking. 5 | test.skip("Inspect owner with fake HOC", async ({ page }) => { 6 | const { devtools } = await gotoTest(page, "update-hoc"); 7 | 8 | await page.waitForSelector("button"); 9 | 10 | await devtools.click('[data-name="List"]'); 11 | let owners = await getOwners(devtools); 12 | expect(owners).toEqual(["Counter", "App"]); 13 | 14 | // Trigger update 15 | await page.click("button"); 16 | await devtools.waitForSelector('[data-testid="elements-tree"]'); 17 | 18 | await devtools.click('[data-name="ListItem"]'); 19 | owners = await getOwners(devtools); 20 | expect(owners).toEqual(["List", "Counter", "App"]); 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-owner-memo.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getOwners, gotoTest } from "../pw-utils"; 3 | 4 | test("Inspect owner information with filtered nodes", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "static-subtree"); 6 | 7 | await devtools.click('[data-name="App"]'); 8 | 9 | let owners = await getOwners(devtools); 10 | expect(owners).toEqual([]); 11 | 12 | await devtools.click('[data-name="Static"]'); 13 | owners = await getOwners(devtools); 14 | expect(owners).toEqual(["App"]); 15 | 16 | await devtools.click('[data-name="Foo"]'); 17 | owners = await getOwners(devtools); 18 | expect(owners).toEqual(["Static", "App"]); 19 | 20 | await devtools.click('[data-name="Display"]'); 21 | owners = await getOwners(devtools); 22 | expect(owners).toEqual(["App"]); 23 | }); 24 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-owner-no-hoc.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getOwners, gotoTest } from "../pw-utils"; 3 | 4 | // TODO: Something is wrong with owner tracking. 5 | test.skip("Inspect owner with disabled HOC filter", async ({ page }) => { 6 | const { devtools } = await gotoTest(page, "update-hoc"); 7 | 8 | await devtools.click('[data-testid="filter-menu-button"]'); 9 | await devtools.waitForSelector('[data-testid="filter-popup"]'); 10 | await devtools.click( 11 | '[data-testid="filter-popup"] label:has-text("HOC-Components")', 12 | ); 13 | await devtools.click('[data-testid="filter-update"]'); 14 | 15 | await devtools.waitForSelector( 16 | '[data-testid="tree-item"][data-name="Memo(Foo)"]', 17 | ); 18 | 19 | await devtools.click('[data-name="List"]'); 20 | let owners = await getOwners(devtools); 21 | expect(owners).toEqual(["Counter", "App"]); 22 | 23 | // Trigger update 24 | await page.click("button"); 25 | await devtools.waitForSelector('[data-testid="elements-tree"]'); 26 | 27 | await devtools.click('[data-name="ListItem"]'); 28 | owners = await getOwners(devtools); 29 | expect(owners).toEqual(["List", "Counter", "App"]); 30 | }); 31 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-owner-update.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getOwners, gotoTest } from "../pw-utils"; 3 | 4 | test("Inspect owner information with updated nodes", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "update-middle"); 6 | 7 | await page.click("button"); 8 | 9 | await devtools.waitForSelector('[data-testid="elements-tree"]'); 10 | 11 | await devtools.click('[data-name="ListItem"]'); 12 | const owners = await getOwners(devtools); 13 | expect(owners).toEqual(["Counter", "App"]); 14 | }); 15 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-owner.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getOwners, gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("Inspect owner information", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "update-all"); 6 | 7 | await devtools.click(locateTreeItem("App")); 8 | 9 | let owners = await getOwners(devtools); 10 | expect(owners).toEqual([]); 11 | 12 | await devtools.click(locateTreeItem("Props")); 13 | owners = await getOwners(devtools); 14 | expect(owners).toEqual(["App"]); 15 | 16 | await devtools.click(locateTreeItem("State")); 17 | owners = await getOwners(devtools); 18 | expect(owners).toEqual(["App"]); 19 | 20 | await devtools.click(locateTreeItem("Context")); 21 | owners = await getOwners(devtools); 22 | expect(owners).toEqual(["App"]); 23 | 24 | await devtools.click(locateTreeItem("Provider")); 25 | owners = await getOwners(devtools); 26 | expect(owners).toEqual(["Context", "App"]); 27 | 28 | await devtools.click(locateTreeItem("Consumer")); 29 | owners = await getOwners(devtools); 30 | expect(owners).toEqual(["Context", "App"]); 31 | 32 | await devtools.click(locateTreeItem("LegacyContext")); 33 | owners = await getOwners(devtools); 34 | expect(owners).toEqual(["App"]); 35 | 36 | await devtools.click(locateTreeItem("LegacyConsumer")); 37 | owners = await getOwners(devtools); 38 | expect(owners).toEqual(["LegacyContext", "App"]); 39 | }); 40 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-props-sort.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem, getProps } from "../pw-utils"; 3 | 4 | test("Inspect should sort object keys", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "props"); 6 | 7 | await devtools.click(locateTreeItem("NestedObjProps")); 8 | await devtools.waitForSelector('[data-testid="Props"]'); 9 | 10 | await devtools.waitForSelector('[data-testid="props-row"]'); 11 | 12 | const props = await getProps(devtools); 13 | expect(props).toEqual({ 14 | a: "1", 15 | b: "{a: 1, b: 2, c: 3}", 16 | c: "3", 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-scroll.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | import assert from "assert"; 4 | 5 | test("Highlighting should move with scroll", async ({ page }) => { 6 | const { devtools } = await gotoTest(page, "highlight-scroll"); 7 | 8 | const inspect = '[data-testid="inspect-btn"]'; 9 | await devtools.click(inspect); 10 | 11 | await page.hover('[data-testid="0"]'); 12 | 13 | const size = await page.locator('[data-testid="0"]').boundingBox(); 14 | assert(size); 15 | 16 | const highlight = '[data-testid="highlight"]'; 17 | await page.waitForSelector(highlight); 18 | 19 | const before = await page.$eval(highlight, el => el.getBoundingClientRect()); 20 | await page.evaluate(() => { 21 | document.querySelector(".test-case")!.scrollBy(0, 1000); 22 | }); 23 | 24 | await expect(page.locator(highlight)).toHaveCount(0); 25 | 26 | const _x = size.x + size.width / 2; 27 | const _y = size.y + size.height / 2; 28 | await page.mouse.move(_x - 100, _y - 100); 29 | await expect(page.locator(highlight)).toHaveCount(1); 30 | 31 | const after = await page.$eval(highlight, el => el.getBoundingClientRect()); 32 | expect(before.top).not.toEqual(after.top); 33 | }); 34 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-select.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getLog, gotoTest } from "../pw-utils"; 3 | 4 | test("Should inspect during picking", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | const elem1 = '[data-testid="tree-item"][data-name="Counter"]'; 8 | const prop = '[data-testid="Props"] [data-testid="props-row"]'; 9 | await devtools.click(elem1); 10 | await expect(devtools.locator(prop)).toHaveCount(0); 11 | 12 | const target = '[data-testid="result"]'; 13 | const inspect = '[data-testid="inspect-btn"]'; 14 | 15 | await devtools.click(inspect); 16 | let active = await devtools.locator(inspect).getAttribute("data-active"); 17 | expect(active).toEqual("true"); 18 | 19 | await page.hover(target); 20 | 21 | // Move mouse slightly 22 | const rect = await page.$eval(target, el => el.getBoundingClientRect()); 23 | await page.mouse.move(rect.x, rect.y); 24 | 25 | // Should load prop data 26 | await expect(devtools.locator(prop)).toHaveCount(1); 27 | 28 | // Should only fire inspect event once per id 29 | const inspects = (await getLog(page)).filter( 30 | x => x.type === "inspect-result", 31 | ); 32 | expect(inspects.length).toEqual(2); 33 | 34 | // Should select new node in element tree 35 | await page.click(target); 36 | active = await devtools.locator(inspect).getAttribute("data-active"); 37 | expect(active).toEqual("false"); 38 | 39 | // ...and display the newly inspected data 40 | await expect(devtools.locator(prop)).toHaveCount(1); 41 | }); 42 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-truncate.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, waitForPass } from "../pw-utils"; 3 | 4 | test("Format inspected data", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "truncate"); 6 | 7 | await devtools.locator('[data-name="App"]').click(); 8 | 9 | await waitForPass(async () => { 10 | let texts = await devtools 11 | .locator('[data-testid="props-row"]') 12 | .allInnerTexts(); 13 | 14 | texts = texts.map(x => x.replace(/\n/g, "")); 15 | expect(texts).toEqual([ 16 | "arr[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]", 17 | "blobBlob {}", 18 | 'obj{props: null, type: "foo"}', 19 | "obj2{foobarA: 1, foobarB: 1, foobarC: 1, foobarD: 1, foobarE: 1, foobarF: 1, foobarG: 1, foobarH: 1}", 20 | "vnode
", 21 | "vnode2", 22 | ]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-virtual-2.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem, wait } from "../pw-utils"; 3 | 4 | test("Don't scroll a virtualized element if already visible", async ({ 5 | page, 6 | }) => { 7 | const { devtools } = await gotoTest(page, "deep-tree-2"); 8 | 9 | await devtools.click(locateTreeItem("Bar")); 10 | 11 | await wait(1000); 12 | 13 | const scroll = await devtools.evaluate(() => { 14 | return Number(document.querySelector('[data-tree="true"]')?.scrollTop); 15 | }); 16 | 17 | expect(scroll).toEqual(0); 18 | }); 19 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect-virtual.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, wait } from "../pw-utils"; 3 | 4 | test("Scroll a virtualized element into view #333", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "deep-tree-2"); 6 | 7 | const selector = '[data-name="App"]'; 8 | await devtools.waitForSelector(selector); 9 | 10 | await devtools.click('[data-testid="inspect-btn"]'); 11 | 12 | await page.evaluate(() => { 13 | const target = document.querySelector("#select-me") as HTMLHeadingElement; 14 | target.scrollIntoView(); 15 | }); 16 | 17 | await wait(1000); 18 | await page.hover("#select-me"); 19 | 20 | await page.waitForSelector('[data-testid="highlight"]'); 21 | await wait(2000); 22 | await page.waitForSelector('[data-testid="select-me"]'); 23 | await wait(1000); 24 | 25 | const text = await devtools.locator('[data-selected="true"]').textContent(); 26 | expect(text).toEqual("Foo"); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/tests/inspect.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getLog, gotoTest, wait } from "../pw-utils"; 3 | 4 | test("Inspect should select node in elements panel", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.locator("data-testid=inspect-btn").click(); 8 | await page.hover("[data-testid=result]"); 9 | // Wait for possible flickering to occur 10 | await wait(500); 11 | 12 | await page.click("[data-testid=result]"); 13 | 14 | // Wait for possible flickering to occur 15 | await wait(500); 16 | 17 | const log = await getLog(page); 18 | expect(log.filter(x => x.type === "start-picker").length).toEqual(1); 19 | expect(log.filter(x => x.type === "stop-picker").length).toEqual(1); 20 | // expect(log.filter(x => x.type === "start-picker").length).to.equal(1); 21 | // expect(log.filter(x => x.type === "stop-picker").length).to.equal(1); 22 | 23 | await devtools.locator('[data-selected="true"]:has-text("Display)'); 24 | }); 25 | -------------------------------------------------------------------------------- /test-e2e/tests/message-connected.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Display filter no match message", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "message-connected"); 6 | 7 | await devtools.locator("data-testid=msg-only-connected").waitFor(); 8 | }); 9 | -------------------------------------------------------------------------------- /test-e2e/tests/message-no-results.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Display filter no match message", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "message-no-results"); 6 | 7 | await devtools.locator("data-testid=msg-no-results").waitFor(); 8 | }); 9 | -------------------------------------------------------------------------------- /test-e2e/tests/new-prop.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { getProps, gotoTest, locateTreeItem, wait } from "../pw-utils"; 3 | 4 | test("Add new props", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.click(locateTreeItem("Display")); 8 | 9 | await devtools.waitForSelector('[data-testid="props-row"]'); 10 | 11 | const propName = 'input[name="new-prop-name"]'; 12 | const propValue = 'input[name="new-prop-value"]'; 13 | await devtools.fill(propName, "foo"); 14 | await devtools.fill(propValue, "42"); 15 | await page.keyboard.press("Enter"); 16 | 17 | await wait(500); 18 | 19 | const props = await getProps(devtools); 20 | expect(props).toEqual({ 21 | value: "0", 22 | foo: "42", 23 | }); 24 | 25 | // New prop input should be cleared 26 | expect(await devtools.locator(propName).getAttribute("value")).toEqual(null); 27 | expect(await devtools.locator(propValue).getAttribute("value")).toEqual(null); 28 | }); 29 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/highlight-flamegraph.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateFlame, 7 | wait, 8 | } from "../../../pw-utils"; 9 | 10 | test("Should highlight flamegraph node if present in DOM", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "profiler-highlight"); 12 | 13 | await devtools.locator(locateTab("PROFILER")).click(); 14 | await clickRecordButton(devtools); 15 | await page.click("button"); 16 | await clickRecordButton(devtools); 17 | 18 | await devtools.locator(locateFlame("Counter")).first().hover(); 19 | // Wait for possible flickering to occur 20 | await wait(1000); 21 | 22 | const log = (await page.evaluate(() => (window as any).log)) as any[]; 23 | expect(log.filter(x => x.type === "highlight").length).toEqual(1); 24 | }); 25 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/memo.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { clickRecordButton, locateTab, gotoTest } from "../../../pw-utils"; 3 | import assert from "assert"; 4 | import { getFlameNodes } from "./utils"; 5 | 6 | test("Correctly position memoized sub-trees", async ({ page }) => { 7 | const { devtools } = await gotoTest(page, "memo"); 8 | 9 | await devtools.locator(locateTab("PROFILER")).click(); 10 | await clickRecordButton(devtools); 11 | await page.click("button"); 12 | await clickRecordButton(devtools); 13 | 14 | const nodes = await getFlameNodes(devtools); 15 | expect(nodes).toEqual([ 16 | { maximized: true, name: "Fragment", visible: true, hocs: [] }, 17 | { maximized: true, name: "Counter", visible: true, hocs: [] }, 18 | { maximized: false, name: "Display", visible: true, hocs: ["Memo"] }, 19 | { maximized: false, name: "Value", visible: true, hocs: [] }, 20 | { maximized: false, name: "Value", visible: true, hocs: [] }, 21 | ]); 22 | 23 | const memoSize = await devtools 24 | .locator('[data-type="flamegraph"] [data-testid="flame-node"]') 25 | .nth(2) 26 | .boundingBox(); 27 | const staticSize = await devtools 28 | .locator('[data-type="flamegraph"] [data-testid="flame-node"]') 29 | .nth(3) 30 | .boundingBox(); 31 | 32 | assert(memoSize); 33 | assert(staticSize); 34 | 35 | expect(memoSize.x <= staticSize.x).toEqual(true); 36 | expect( 37 | memoSize.x + memoSize.width >= staticSize.x + staticSize.width, 38 | ).toEqual(true); 39 | }); 40 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/profiler-hoc.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateFlame, 7 | wait, 8 | } from "../../../pw-utils"; 9 | import { getFlameNodes } from "./utils"; 10 | 11 | test("Should work with filtered HOC roots", async ({ page }) => { 12 | const { devtools } = await gotoTest(page, "hoc-update"); 13 | 14 | await devtools.locator(locateTab("PROFILER")).click(); 15 | await clickRecordButton(devtools); 16 | await page.click("button"); 17 | await clickRecordButton(devtools); 18 | 19 | await devtools.locator(locateFlame("Wrapped")).waitFor(); 20 | 21 | const nodes = await getFlameNodes(devtools); 22 | expect(nodes.find(x => x.name === "Wrapped")?.hocs).toEqual(["withBoof"]); 23 | 24 | // Disabling HOC-filter should remove hoc labels 25 | await devtools.locator(locateTab("ELEMENTS")).click(); 26 | await devtools.click('[data-testid="filter-menu-button"]'); 27 | await devtools.waitForSelector('[data-testid="filter-popup"]'); 28 | await devtools 29 | .locator('[data-testid="filter-popup"] label:has-text("HOC-Components")') 30 | .click(); 31 | await devtools.click('[data-testid="filter-update"]'); 32 | await wait(1000); 33 | 34 | await devtools.locator(locateTab("PROFILER")).click(); 35 | await clickRecordButton(devtools); 36 | await page.click("button"); 37 | await clickRecordButton(devtools); 38 | 39 | await devtools.locator(locateFlame("withBoof(Wrapped)")).waitFor(); 40 | }); 41 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/profiler-static-subtree.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateFlame, 7 | wait, 8 | } from "../../../pw-utils"; 9 | 10 | test("Static subtree should be smaller in size", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "static-subtree"); 12 | 13 | await devtools.locator(locateTab("PROFILER")).click(); 14 | await clickRecordButton(devtools); 15 | await page.click("button"); 16 | await page.click("button"); 17 | await clickRecordButton(devtools); 18 | 19 | await devtools.locator(locateFlame("App")).waitFor(); 20 | await devtools.locator('[data-testid="next-commit"]').click(); 21 | await devtools 22 | .locator('[data-testid="commit-page-info"]:has-text("2 / 2")') 23 | .waitFor(); 24 | 25 | // Wait for layouting 26 | await wait(500); 27 | 28 | const res = await devtools.evaluate(() => { 29 | const display = document.querySelector('[data-name="Display"]')! 30 | .clientWidth; 31 | const statics = Array.from( 32 | document.querySelectorAll('[data-name="Static"]')!, 33 | ).map(el => el.clientWidth); 34 | 35 | return statics.every(w => w < display); 36 | }); 37 | 38 | // Static nodes were bigger than Display 39 | expect(res).toEqual(true); 40 | }); 41 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/profiler-unaffected.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateFlame, 7 | } from "../../../pw-utils"; 8 | 9 | test("Not rendered nodes that are not parents of the current commit should be striped out", async ({ 10 | page, 11 | }) => { 12 | const { devtools } = await gotoTest(page, "profiler-2"); 13 | 14 | await devtools.locator(locateTab("PROFILER")).click(); 15 | await clickRecordButton(devtools); 16 | await page.locator("button").first().click(); 17 | await clickRecordButton(devtools); 18 | 19 | await devtools.locator(locateFlame("Counter")).first().waitFor(); 20 | 21 | let nodes = await devtools 22 | .locator( 23 | '[data-type="flamegraph"] [data-testid="flame-node"]:not([data-weight="-1"])', 24 | ) 25 | .allTextContents(); 26 | expect(nodes.map(n => n.split(" ")[0])).toEqual(["Counter", "Display"]); 27 | nodes = await devtools 28 | .locator('[data-type="flamegraph"] [data-commit-parent="true"]') 29 | .allTextContents(); 30 | expect(nodes.map(n => n.split(" ")[0])).toEqual(["Fragment", "App"]); 31 | }); 32 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/profiler-unmount.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateFlame, 7 | wait, 8 | locateProfilerTab, 9 | } from "../../../pw-utils"; 10 | 11 | test("Should highlight flamegraph node if present in DOM", async ({ page }) => { 12 | const { devtools } = await gotoTest(page, "profiler-highlight"); 13 | 14 | await devtools.locator(locateTab("PROFILER")).click(); 15 | await clickRecordButton(devtools); 16 | await page.locator("button").first().click(); 17 | await clickRecordButton(devtools); 18 | 19 | await page.click("button"); 20 | await devtools.locator(locateProfilerTab("RANKED")).click(); 21 | await wait(1000); 22 | await devtools.locator(locateProfilerTab("FLAMEGRAPH")).click(); 23 | 24 | await devtools.locator(locateFlame("Counter")).first().waitFor(); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/flamegraph/utils.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from "@playwright/test"; 2 | 3 | export async function getFlameNodes(page: Frame) { 4 | const selector = '[data-type="flamegraph"] [data-id]'; 5 | await page.waitForSelector(selector); 6 | return await page.$$eval(selector, els => { 7 | return els.map(el => { 8 | return { 9 | maximized: el.getAttribute("data-maximized") === "true", 10 | name: el.getAttribute("data-name") || "", 11 | hocs: Array.from(el.querySelectorAll(".hoc-item")).map( 12 | el => el.textContent, 13 | ), 14 | visible: el.getAttribute("data-visible") === "true", 15 | }; 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/highlight-updates-holes.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest } from "../../pw-utils"; 3 | 4 | test("Check if highlight updates is rendered", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "holes"); 6 | 7 | await devtools.locator(locateTab("SETTINGS")).click(); 8 | await devtools.click('[data-testId="toggle-highlight-updates"]'); 9 | 10 | const errors: string[] = []; 11 | page.on("pageerror", err => errors.push(err.toString())); 12 | 13 | await page.click("button"); 14 | await page.click("button"); 15 | 16 | expect(errors).toEqual([]); 17 | }); 18 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/highlight-updates-text.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest, wait } from "../../pw-utils"; 3 | 4 | test("Don't crash on measuring text nodes", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "highlight-text"); 6 | 7 | await devtools.locator(locateTab("SETTINGS")).click(); 8 | await devtools.click('[data-testId="toggle-highlight-updates"]'); 9 | 10 | await page.click("button", { noWaitAfter: true }); 11 | 12 | // Run twice to check if canvas is re-created 13 | const id = "#preact-devtools-highlight-updates"; 14 | await page.waitForSelector(id, { state: "attached" }); 15 | 16 | await wait(1000); 17 | await expect(page.locator(id)).toHaveCount(0); 18 | }); 19 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/highlight-updates.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest, wait } from "../../pw-utils"; 3 | 4 | test("Check if highlight updates is rendered", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "todo"); 6 | 7 | await devtools.locator(locateTab("SETTINGS")).click(); 8 | await devtools.click('[data-testId="toggle-highlight-updates"]'); 9 | 10 | // Run twice to check if canvas is re-created 11 | for (let i = 0; i < 2; i++) { 12 | await page.locator("input").type("foo"); 13 | await page.keyboard.press("Enter"); 14 | 15 | const id = "#preact-devtools-highlight-updates"; 16 | await page.waitForSelector(id, { state: "attached" }); 17 | 18 | await wait(1000); 19 | await expect(page.locator(id)).toHaveCount(0); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/ranked/highlight-ranked.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | wait, 7 | locateProfilerTab, 8 | } from "../../../pw-utils"; 9 | 10 | test("Should highlight ranked node if present in DOM", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "profiler-highlight"); 12 | 13 | await devtools.locator(locateTab("PROFILER")).click(); 14 | await clickRecordButton(devtools); 15 | await page.locator("button").first().click(); 16 | await clickRecordButton(devtools); 17 | 18 | await devtools.locator(locateProfilerTab("RANKED")).click(); 19 | await devtools.hover('[data-type="ranked"] [data-name="Counter"]'); 20 | await wait(1000); 21 | 22 | const log = (await page.evaluate(() => (window as any).log)) as any[]; 23 | expect(log.filter(x => x.type === "highlight").length).toEqual(1); 24 | }); 25 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/ranked/profiler-ranked-selected.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateProfilerTab, 7 | } from "../../../pw-utils"; 8 | 9 | test("Selected node should be changed across commits if not present", async ({ 10 | page, 11 | }) => { 12 | const { devtools } = await gotoTest(page, "profiler-2"); 13 | 14 | await devtools.locator(locateTab("PROFILER")).click(); 15 | await clickRecordButton(devtools); 16 | await page.locator('[data-testid="counter-1"]').click(); 17 | await page.locator('[data-testid="counter-2"]').click(); 18 | await clickRecordButton(devtools); 19 | 20 | await devtools.locator(locateProfilerTab("RANKED")).click(); 21 | await devtools.click('[data-type="ranked"] [data-name="Display"]'); 22 | await devtools.click('[data-testid="next-commit"]'); 23 | 24 | await devtools.waitForSelector('[data-type="ranked"] [data-selected="true"]'); 25 | }); 26 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/ranked/profiler-ranked.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | locateTab, 5 | gotoTest, 6 | locateProfilerTab, 7 | } from "../../../pw-utils"; 8 | 9 | test("Ranked profile view should only show nodes of the current commit", async ({ 10 | page, 11 | }) => { 12 | const { devtools } = await gotoTest(page, "profiler-2"); 13 | 14 | await devtools.locator(locateTab("PROFILER")).click(); 15 | await devtools.locator(locateProfilerTab("RANKED")).click(); 16 | 17 | await clickRecordButton(devtools); 18 | await page.click('[data-testid="counter-1"]'); 19 | await page.click('[data-testid="counter-2"]'); 20 | await clickRecordButton(devtools); 21 | 22 | const nodes = await devtools.$$( 23 | '[data-type="ranked"] [data-id]:not([data-weight])', 24 | ); 25 | expect(nodes.length).toEqual(0); 26 | }); 27 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/render-reason-support.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | locateTab, 4 | gotoTest, 5 | wait, 6 | clickRecordButton, 7 | locateFlame, 8 | } from "../../pw-utils"; 9 | 10 | test("Disables render reason capturing", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "render-reasons"); 12 | 13 | await devtools.locator(locateTab("SETTINGS")).click(); 14 | await devtools.click('[data-testid="toggle-render-reason"]'); 15 | const checked = await devtools 16 | .locator('[data-testid="toggle-render-reason"]') 17 | .isChecked(); 18 | expect(checked).toEqual(true); 19 | 20 | // Start profiling 21 | await devtools.locator(locateTab("PROFILER")).click(); 22 | await clickRecordButton(devtools); 23 | 24 | await page.click('[data-testid="counter-1"]'); 25 | await page.click('[data-testid="counter-2"]'); 26 | await wait(1000); 27 | 28 | await clickRecordButton(devtools); 29 | 30 | await devtools.locator(locateFlame("ComponentState")).click(); 31 | let reasons = await devtools 32 | .locator('[data-testid="render-reasons"]') 33 | .textContent(); 34 | expect(reasons).toEqual("State changed:value"); 35 | 36 | // Reset flamegraph 37 | await devtools.locator(locateFlame("Fragment")).click(); 38 | reasons = await devtools 39 | .locator('[data-testid="render-reasons"]') 40 | .textContent(); 41 | expect(reasons).toEqual("Did not render"); 42 | }); 43 | -------------------------------------------------------------------------------- /test-e2e/tests/profiler/render-reasons-memo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | locateTab, 4 | gotoTest, 5 | wait, 6 | clickRecordButton, 7 | locateFlame, 8 | } from "../../pw-utils"; 9 | 10 | test("Captures render reasons for memo", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "render-reasons-memo"); 12 | 13 | await devtools.locator(locateTab("SETTINGS")).click(); 14 | await devtools.click('[data-testid="toggle-render-reason"]'); 15 | 16 | // Start profiling 17 | await devtools.locator(locateTab("PROFILER")).click(); 18 | await clickRecordButton(devtools); 19 | await page.click("button"); 20 | 21 | await wait(1000); 22 | await clickRecordButton(devtools); 23 | 24 | // Get render reason 25 | await devtools.locator(locateFlame("Foo")).click(); 26 | const reasons = await devtools 27 | .locator('[data-testid="render-reasons"]') 28 | .textContent(); 29 | expect(reasons).toEqual("Did not render"); 30 | 31 | // Elements should be marked as not rendered 32 | const Foo = await devtools 33 | .locator('[data-name="Foo"]') 34 | .getAttribute("data-weight"); 35 | const Inner = await devtools 36 | .locator('[data-name="FooInner"]') 37 | .getAttribute("data-weight"); 38 | 39 | expect(Foo).toEqual("-1"); 40 | expect(Inner).toEqual("-1"); 41 | }); 42 | -------------------------------------------------------------------------------- /test-e2e/tests/root-islands.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getTreeItems, gotoTest } from "../pw-utils"; 3 | 4 | test("Islands roots should be sorted by DOM order", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "islands-order"); 6 | 7 | const items = await getTreeItems(devtools); 8 | expect(items.map(x => x.name)).toEqual(["App1", "App2", "App3"]); 9 | }); 10 | 11 | test("Virtual island roots should be sorted by DOM order", async ({ page }) => { 12 | test.skip( 13 | process.env.PREACT_VERSION !== "10", 14 | "Fake root DOM node is not supported in v11", 15 | ); 16 | const { devtools } = await gotoTest(page, "islands-order-virtual"); 17 | 18 | const items = await getTreeItems(devtools); 19 | expect(items.map(x => x.name)).toEqual([ 20 | "App1", 21 | "App2", 22 | "Virtual1", 23 | "Virtual2", 24 | "App3", 25 | ]); 26 | }); 27 | -------------------------------------------------------------------------------- /test-e2e/tests/root-multiple.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest, waitForPass } from "../pw-utils"; 3 | 4 | test("Inspect should select node in elements panel", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "root-multi"); 6 | 7 | await waitForPass(async () => { 8 | const btns = await page.locator("button").count(); 9 | expect(btns).toEqual(2); 10 | }); 11 | 12 | const txts = await devtools.locator("data-testid=tree-item").allInnerTexts(); 13 | expect(txts).toEqual(["Counter", "Display", "Counter", "Display"]); 14 | }); 15 | -------------------------------------------------------------------------------- /test-e2e/tests/state-edit.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Mirror component state to the devtools", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.locator('[data-name="Display"]').click(); 8 | await devtools.locator('[data-testid="props-row"] input').fill("42"); 9 | await page.keyboard.press("Enter"); 10 | 11 | const value = await devtools 12 | .locator('[data-testid="props-row"] input') 13 | .inputValue(); 14 | expect(value).toEqual("42"); 15 | 16 | await page.locator('[data-testid=result]:has-text("Counter: 42")').waitFor(); 17 | }); 18 | -------------------------------------------------------------------------------- /test-e2e/tests/state.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { gotoTest } from "../pw-utils"; 3 | 4 | test("Mirror component state to the devtools", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.locator('[data-name="Display"]').click(); 8 | 9 | const input = '[data-testid="props-row"] input'; 10 | const result = '[data-testid="result"]'; 11 | 12 | let value = await devtools.locator(input).inputValue(); 13 | let text = await page.locator(result).textContent(); 14 | expect(value).toEqual("0"); 15 | expect(text).toEqual("Counter: 0"); 16 | 17 | await page.click("button"); 18 | 19 | value = await devtools.locator(input).inputValue(); 20 | text = await page.locator(result).textContent(); 21 | expect(value).toEqual("1"); 22 | expect(text).toEqual("Counter: 1"); 23 | }); 24 | -------------------------------------------------------------------------------- /test-e2e/tests/stats/stats-empty.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest } from "../../pw-utils"; 3 | 4 | test("Display no stats initially", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "counter"); 6 | 7 | await devtools.locator(locateTab("STATISTICS")).click(); 8 | await devtools.waitForSelector('[data-testId="stats-info"]'); 9 | 10 | await expect(devtools.locator('[data-testid="vnode-stats"]')).toHaveCount(0); 11 | }); 12 | -------------------------------------------------------------------------------- /test-e2e/tests/stats/stats-memo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest, clickRecordButton } from "../../pw-utils"; 3 | 4 | test("Skip memoized components for stats", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "memo-stats"); 6 | 7 | await devtools.locator(locateTab("STATISTICS")).click(); 8 | await devtools.waitForSelector('[data-testId="stats-info"]'); 9 | 10 | await clickRecordButton(devtools); 11 | 12 | await page.click("button"); 13 | await page.waitForSelector('[data-value="1"]'); 14 | await clickRecordButton(devtools); 15 | 16 | const mountTotal = await devtools 17 | .locator('[data-testid="mount-total"]') 18 | .textContent(); 19 | expect(mountTotal).toEqual("0"); 20 | 21 | const updateTotal = await devtools 22 | .locator('[data-testid="update-total"]') 23 | .textContent(); 24 | expect(updateTotal).toEqual("8"); 25 | 26 | const unmountTotal = await devtools 27 | .locator('[data-testid="unmount-total"]') 28 | .textContent(); 29 | expect(unmountTotal).toEqual("0"); 30 | }); 31 | -------------------------------------------------------------------------------- /test-e2e/tests/stats/stats-simple.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest, clickRecordButton } from "../../pw-utils"; 3 | 4 | test("Display simple stats", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "simple-stats"); 6 | 7 | await devtools.locator(locateTab("STATISTICS")).click(); 8 | await devtools.waitForSelector('[data-testId="stats-info"]'); 9 | 10 | await clickRecordButton(devtools); 11 | await devtools.waitForSelector('[data-testid="stats-info-recording"]'); 12 | 13 | await page.click('[data-testid="update"]'); 14 | await clickRecordButton(devtools); 15 | 16 | const classComponents = await devtools 17 | .locator('[data-testid="class-component-total"]') 18 | .textContent(); 19 | expect(classComponents).toEqual("1"); 20 | 21 | const fnComponents = await devtools 22 | .locator('[data-testid="function-component-total"]') 23 | .textContent(); 24 | expect(fnComponents).toEqual("1"); 25 | 26 | const fragmentsCount = await devtools 27 | .locator('[data-testid="fragment-total"]') 28 | .textContent(); 29 | expect(fragmentsCount).toEqual("0"); 30 | 31 | const elementsCount = await devtools 32 | .locator('[data-testid="element-total"]') 33 | .textContent(); 34 | expect(elementsCount).toEqual("5"); 35 | 36 | const textCount = await devtools 37 | .locator('[data-testid="text-total"]') 38 | .textContent(); 39 | expect(textCount).toEqual("6"); 40 | }); 41 | -------------------------------------------------------------------------------- /test-e2e/tests/stats/stats-single-child.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { locateTab, gotoTest, clickRecordButton } from "../../pw-utils"; 3 | 4 | test("Display single child stats", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "simple-stats"); 6 | 7 | await devtools.locator(locateTab("STATISTICS")).click(); 8 | await devtools.waitForSelector('[data-testId="stats-info"]'); 9 | 10 | await clickRecordButton(devtools); 11 | await devtools.waitForSelector('[data-testid="stats-info-recording"]'); 12 | 13 | await page.click('[data-testid="update"]'); 14 | await clickRecordButton(devtools); 15 | 16 | const classComponents = await devtools 17 | .locator('[data-testid="single-class-component"]') 18 | .textContent(); 19 | expect(classComponents).toEqual("0"); 20 | 21 | const fnComponents = await devtools 22 | .locator('[data-testid="single-function-component"]') 23 | .textContent(); 24 | expect(fnComponents).toEqual("0"); 25 | 26 | const elements = await devtools 27 | .locator('[data-testid="single-element"]') 28 | .textContent(); 29 | expect(elements).toEqual("2"); 30 | 31 | const texts = await devtools 32 | .locator('[data-testid="single-text"]') 33 | .textContent(); 34 | expect(texts).toEqual("3"); 35 | }); 36 | -------------------------------------------------------------------------------- /test-e2e/tests/suspense-toggle.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from "@playwright/test"; 2 | import { 3 | getTreeViewItemNames, 4 | gotoTest, 5 | locateTreeItem, 6 | waitFor, 7 | } from "../pw-utils"; 8 | 9 | function testCase(preactVersion: string) { 10 | return async ({ page }: { page: Page }) => { 11 | const { devtools } = await gotoTest(page, "suspense", { 12 | preact: preactVersion, 13 | }); 14 | 15 | await devtools.click(locateTreeItem("Delayed")); 16 | await devtools.click('[data-testid="suspend-action"]'); 17 | 18 | await waitFor(async () => { 19 | const items = await getTreeViewItemNames(devtools); 20 | expect(items).toEqual( 21 | [ 22 | "Shortly", 23 | "Block", 24 | "Suspense", 25 | // <10.4.5, newer versions use a Fragment 26 | preactVersion === "10.4.1" && "Component", 27 | "Block", 28 | ].filter(Boolean), 29 | ); 30 | return true; 31 | }); 32 | 33 | const selected = await devtools 34 | .locator('[data-testid="tree-item"][data-selected="true"]') 35 | .getAttribute("data-name"); 36 | 37 | expect(selected).toEqual("Suspense"); 38 | 39 | await devtools.click(locateTreeItem("Shortly")); 40 | 41 | await devtools 42 | .locator('[data-testid="inspect-component-name"]:has-text("")') 43 | .textContent(); 44 | 45 | await expect( 46 | devtools.locator('[data-testid="suspend-action"]'), 47 | ).toHaveCount(0); 48 | }; 49 | } 50 | 51 | test.describe("Display Suspense in tree view", () => { 52 | test("Preact 10.5.9", testCase("10.5.9")); 53 | test("Preact 10.4.1", testCase("10.4.1")); 54 | }); 55 | -------------------------------------------------------------------------------- /test-e2e/tests/suspense.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from "@playwright/test"; 2 | import { getTreeViewItemNames, gotoTest, waitForPass } from "../pw-utils"; 3 | 4 | function testCase(version: string) { 5 | return async ({ page }: { page: Page }) => { 6 | const { devtools } = await gotoTest(page, "suspense", { 7 | preact: version, 8 | }); 9 | 10 | await devtools.waitForSelector('[data-testid="tree-item"]'); 11 | 12 | await waitForPass(async () => { 13 | const items = await getTreeViewItemNames(devtools); 14 | expect(items).toEqual( 15 | [ 16 | "Shortly", 17 | "Block", 18 | "Suspense", 19 | version === "10.4.1" && "Component", 20 | "Block", 21 | ].filter(Boolean), 22 | ); 23 | }); 24 | 25 | await waitForPass(async () => { 26 | const items = await getTreeViewItemNames(devtools); 27 | expect(items).toEqual( 28 | [ 29 | "Shortly", 30 | "Block", 31 | "Suspense", 32 | version === "10.4.1" && "Component", 33 | "Delayed", 34 | "Block", 35 | ].filter(Boolean), 36 | ); 37 | }); 38 | }; 39 | } 40 | 41 | test.describe("Display Suspense in tree view", () => { 42 | test("Preact 10.5.9", testCase("10.5.9")); 43 | 44 | // <10.4.5, uses a component instead of a Fragment as the boundary 45 | test("Preact 10.4.1", testCase("10.4.1")); 46 | }); 47 | -------------------------------------------------------------------------------- /test-e2e/tests/symbol-value.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("Display symbol values", async ({ page }) => { 5 | const { devtools } = await gotoTest(page, "symbols"); 6 | 7 | // Hooks 8 | await devtools.click(locateTreeItem("SymbolComponent")); 9 | await devtools.waitForSelector('[data-testid="Hooks"]'); 10 | await devtools 11 | .locator('[data-testid="prop-value"]:has-text("Symbol(foobar)")') 12 | .waitFor(); 13 | 14 | // Props 15 | await devtools.click(locateTreeItem("Child")); 16 | await devtools.waitForSelector('[data-testid="Props"]'); 17 | await devtools 18 | .locator('[data-testid="prop-value"]:has-text("Symbol(foobar)")') 19 | .waitFor(); 20 | 21 | // State 22 | await devtools.click(locateTreeItem("ClassComponent")); 23 | await devtools.waitForSelector('[data-testid="State"]'); 24 | await devtools 25 | .locator('[data-testid="prop-value"]:has-text("Symbol(foobar)")') 26 | .waitFor(); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/tests/sync-selection.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { 3 | clickRecordButton, 4 | gotoTest, 5 | locateFlame, 6 | locateTab, 7 | locateTreeItem, 8 | } from "../pw-utils"; 9 | 10 | test("Sync selection from profiler", async ({ page }) => { 11 | const { devtools } = await gotoTest(page, "counter"); 12 | 13 | await devtools.click(locateTreeItem("Counter")); 14 | await devtools.click(locateTab("PROFILER")); 15 | 16 | await clickRecordButton(devtools); 17 | await page.click("button"); 18 | await clickRecordButton(devtools); 19 | 20 | await devtools.waitForSelector('[data-type="flamegraph"]'); 21 | await devtools.click(locateFlame("Display")); 22 | 23 | await devtools.click(locateTab("ELEMENTS")); 24 | await devtools 25 | .locator('[data-selected="true"]:has-text("Display")') 26 | .waitFor(); 27 | }); 28 | -------------------------------------------------------------------------------- /test-e2e/tests/update-copy.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { gotoTest, locateTreeItem } from "../pw-utils"; 3 | 4 | test("Create a copy when doing props/state/context updates", async ({ 5 | page, 6 | }) => { 7 | const { devtools } = await gotoTest(page, "update-all"); 8 | 9 | // Props 10 | await devtools.click(locateTreeItem("Props")); 11 | await devtools.fill('[data-testid="prop-value"] input', "1"); 12 | await page.keyboard.press("Enter"); 13 | await page 14 | .locator('[data-testid="props-result"]:has-text("props: 1, true")') 15 | .waitFor(); 16 | 17 | // State 18 | await devtools.click(locateTreeItem("State")); 19 | await devtools.fill('[data-testid="prop-value"] input', "1"); 20 | await page.keyboard.press("Enter"); 21 | await page 22 | .locator('[data-testid="state-result"]:has-text("state: 1, true")') 23 | .waitFor(); 24 | 25 | // Legacy Context 26 | await devtools.click(locateTreeItem("LegacyConsumer")); 27 | await devtools.fill('[data-testid="prop-value"] input', "1"); 28 | await page.keyboard.press("Enter"); 29 | await page 30 | .locator( 31 | '[data-testid="legacy-context-result"]:has-text("legacy context: 1")', 32 | ) 33 | .waitFor(); 34 | }); 35 | -------------------------------------------------------------------------------- /tools/build-plugins/babel-plugin-css-module.mjs: -------------------------------------------------------------------------------- 1 | export function babelPluginCssModules({ types: t }) { 2 | return { 3 | visitor: { 4 | ImportDeclaration(path) { 5 | if (!t.isStringLiteral(path.node.source)) return; 6 | const source = path.node.source.value; 7 | if (!/\.module\.css$/.test(source)) return; 8 | const specifier = path.node.specifiers[0]; 9 | if (!t.isImportDefaultSpecifier(specifier)) return; 10 | 11 | const name = specifier.local.name; 12 | 13 | path.replaceWith(t.importDeclaration([], t.StringLiteral(source))); 14 | path.insertAfter( 15 | t.ImportDeclaration( 16 | [t.importNamespaceSpecifier(t.identifier(name))], 17 | t.StringLiteral(source.replace(".module.", ".module-virtual.")), 18 | ), 19 | ); 20 | }, 21 | }, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /tools/build-plugins/babel-plugin-dead-code.mjs: -------------------------------------------------------------------------------- 1 | export function babelPluginDeadCode({ types: t }) { 2 | return { 3 | visitor: { 4 | IfStatement: { 5 | exit(path) { 6 | if ( 7 | (t.isBooleanLiteral(path.node.test) && 8 | path.node.test.value === false) || 9 | (t.isBlockStatement(path.node.consequent) && 10 | path.node.consequent.body.length === 0) 11 | ) { 12 | path.remove(); 13 | } 14 | }, 15 | }, 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /tools/run-chrome.js: -------------------------------------------------------------------------------- 1 | const { chromium } = require("playwright"); 2 | 3 | const path = require("path"); 4 | 5 | async function main() { 6 | const extension = path.join(__dirname, "..", "dist", "chrome-debug"); 7 | const browser = await chromium.launchPersistentContext("./profiles/chrome", { 8 | args: [ 9 | `--disable-extensions-except=${extension}`, 10 | `--load-extension=${extension}`, 11 | ], 12 | headless: false, 13 | devtools: true, 14 | }); 15 | const page = await browser.newPage(); 16 | await page.goto("https://preactjs.com"); 17 | } 18 | 19 | main(); 20 | -------------------------------------------------------------------------------- /tools/test-setup.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | const { JSDOM } = require("jsdom"); 4 | const { performance } = require("perf_hooks"); 5 | 6 | const dom = new JSDOM(` 7 | 8 | 9 | 10 | 11 | 12 | `); 13 | 14 | global.expect = expect; 15 | global.sinon = sinon; 16 | global.document = dom.window.document; 17 | global.window = dom.window; 18 | global.performance = performance; 19 | 20 | global.__DEBUG__ = false; 21 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "jsx": "react" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.inline.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "declaration": true, 6 | "declarationDir": "dist/inline/types/" 7 | }, 8 | "files": [ 9 | "src/shells/shared/panel/panel.ts", 10 | "src/shells/shared/installHook.ts" 11 | ], 12 | "include": ["./src/global.d.ts", "./src"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "target": "ES2020", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "jsxFactory": "h", 10 | "skipLibCheck": true 11 | }, 12 | "include": ["./src/global.d.ts", "./src"] 13 | } 14 | -------------------------------------------------------------------------------- /types/devtools.d.ts: -------------------------------------------------------------------------------- 1 | declare module "preact-devtools" { 2 | import { Options } from "preact"; 3 | 4 | export function attach( 5 | options: Options, 6 | createRenderer: (hook: any) => any, 7 | ): { 8 | store: any; 9 | destroy: () => void; 10 | }; 11 | 12 | export function renderDevtools( 13 | store: any, 14 | container: Element | Document | ShadowRoot | DocumentFragment, 15 | ): void; 16 | } 17 | 18 | declare module "preact-devtools/dist/preact-devtools.css" {} 19 | --------------------------------------------------------------------------------