From 577c6c198a9d5f2789e9342dfcbbc83b73f14a75 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Fri, 22 Nov 2024 14:31:46 +0100 Subject: [PATCH] feat(file-picker): add fuzzy search to file picker (#3517) --- libs/eslint-plugin-mte/index.js | 3 +- package-lock.json | 8 + packages/elements/.vscode/launch.json | 23 +++ packages/elements/package.json | 1 + .../src/components/app/app.component.ts | 11 +- .../elements/src/components/breadcrumb.ts | 4 +- .../file-picker/file-picker.component.ts | 167 ++++++++++-------- .../metrics-table/metrics-table.component.ts | 4 +- .../result-status-bar/result-status-bar.ts | 84 +++------ .../test-file/test-file.component.ts | 6 +- .../theme-switch/theme-switch.component.ts | 2 +- .../test/integration/po/Breadcrumb.po.ts | 5 +- .../test/integration/po/FilePicker.po.ts | 11 ++ .../test/integration/theming.it.spec.ts | 27 ++- ...-match-the-dark-theme-1-chromium-linux.png | Bin 0 -> 169898 bytes ...-match-the-dark-theme-1-chromium-win32.png | Bin 0 -> 162291 bytes ...d-match-the-dark-theme-1-firefox-linux.png | Bin 0 -> 124152 bytes ...d-match-the-dark-theme-1-firefox-win32.png | Bin 0 -> 84862 bytes ...-match-the-dark-theme-1-chromium-linux.png | Bin 183267 -> 180049 bytes ...-match-the-dark-theme-1-chromium-win32.png | Bin 169569 -> 166282 bytes ...d-match-the-dark-theme-1-firefox-linux.png | Bin 134005 -> 132035 bytes ...d-match-the-dark-theme-1-firefox-win32.png | Bin 89048 -> 88460 bytes ...match-the-light-theme-1-chromium-linux.png | Bin 0 -> 183738 bytes ...match-the-light-theme-1-chromium-win32.png | Bin 0 -> 159229 bytes ...-match-the-light-theme-1-firefox-linux.png | Bin 0 -> 126148 bytes ...-match-the-light-theme-1-firefox-win32.png | Bin 0 -> 86301 bytes ...match-the-light-theme-1-chromium-linux.png | Bin 193491 -> 189452 bytes ...match-the-light-theme-1-chromium-win32.png | Bin 165875 -> 162471 bytes ...-match-the-light-theme-1-firefox-linux.png | Bin 134386 -> 134233 bytes ...-match-the-light-theme-1-firefox-win32.png | Bin 89423 -> 90010 bytes .../components/breadcrumb.component.spec.ts | 12 +- .../drawer-mutant.component.spec.ts | 6 +- .../components/drawer-test.component.spec.ts | 9 +- .../unit/components/drawer.component.spec.ts | 4 +- .../components/file-picker.component.spec.ts | 100 ++++++++--- .../unit/components/file.component.spec.ts | 2 +- .../unit/components/totals.component.spec.ts | 6 +- .../test/unit/helpers/CustomElementFixture.ts | 4 +- 38 files changed, 299 insertions(+), 200 deletions(-) create mode 100644 packages/elements/test/integration/po/FilePicker.po.ts create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-dark-theme-when-opening-the-file-picke-6b805-in-the-search-box-should-match-the-dark-theme-1-chromium-linux.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-dark-theme-when-opening-the-file-picke-6b805-in-the-search-box-should-match-the-dark-theme-1-chromium-win32.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-dark-theme-when-opening-the-file-picke-6b805-in-the-search-box-should-match-the-dark-theme-1-firefox-linux.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-dark-theme-when-opening-the-file-picke-6b805-in-the-search-box-should-match-the-dark-theme-1-firefox-win32.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-light-theme-when-opening-the-file-pick-d5814-n-the-search-box-should-match-the-light-theme-1-chromium-linux.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-light-theme-when-opening-the-file-pick-d5814-n-the-search-box-should-match-the-light-theme-1-chromium-win32.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-light-theme-when-opening-the-file-pick-d5814-n-the-search-box-should-match-the-light-theme-1-firefox-linux.png create mode 100644 packages/elements/test/integration/theming.it.spec.ts-snapshots/Theming-light-theme-when-opening-the-file-pick-d5814-n-the-search-box-should-match-the-light-theme-1-firefox-win32.png diff --git a/libs/eslint-plugin-mte/index.js b/libs/eslint-plugin-mte/index.js index 1ea249c9d..ad00d0e08 100644 --- a/libs/eslint-plugin-mte/index.js +++ b/libs/eslint-plugin-mte/index.js @@ -22,12 +22,11 @@ export default [ reportUnusedDisableDirectives: 'error', }, rules: { + eqeqeq: 'error', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/consistent-type-imports': 'error', - // Not useful for a lot of stuff, but mainly `.shadowRoot` - '@typescript-eslint/no-non-null-assertion': 'off', }, }, { diff --git a/package-lock.json b/package-lock.json index c7e59a1f0..cb02c403d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7924,6 +7924,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -16514,6 +16521,7 @@ "esbuild": "0.24.0", "eslint-config-mte": "*", "express": "4.21.1", + "fuzzysort": "3.1.0", "lit": "3.2.1", "mutation-testing-metrics": "3.3.0", "mutation-testing-real-time": "3.3.0", diff --git a/packages/elements/.vscode/launch.json b/packages/elements/.vscode/launch.json index 8708ae224..41851823c 100644 --- a/packages/elements/.vscode/launch.json +++ b/packages/elements/.vscode/launch.json @@ -18,6 +18,29 @@ "url": "http://localhost:5173", "webRoot": "${workspaceFolder}", "sourceMaps": true + }, + { + "type": "node", + "request": "launch", + "name": "Run unit tests", + "program": "${workspaceFolder:parent}/node_modules/vitest/vitest.mjs", + "console": "integratedTerminal", + "skipFiles": ["/**"], + "args": ["--inspect-brk", "--browser", "--no-file-parallelism"] + }, + { + "type": "chrome", + "request": "attach", + "name": "Attach to unit test browser", + "skipFiles": ["/**"], + "port": 9229 + } + ], + "compounds": [ + { + "name": "Debug unit tests", + "configurations": ["Attach to unit test browser", "Run unit tests"], + "stopAll": true } ] } diff --git a/packages/elements/package.json b/packages/elements/package.json index 5a0105e96..165e16c13 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -59,6 +59,7 @@ "esbuild": "0.24.0", "eslint-config-mte": "*", "express": "4.21.1", + "fuzzysort": "3.1.0", "lit": "3.2.1", "mutation-testing-metrics": "3.3.0", "mutation-testing-real-time": "3.3.0", diff --git a/packages/elements/src/components/app/app.component.ts b/packages/elements/src/components/app/app.component.ts index bb043404c..e865c6f13 100644 --- a/packages/elements/src/components/app/app.component.ts +++ b/packages/elements/src/components/app/app.component.ts @@ -2,6 +2,7 @@ import type { PropertyValues } from 'lit'; import { html, isServer, nothing, unsafeCSS } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import type { FileUnderTestModel, Metrics, @@ -370,11 +371,11 @@ export class MutationTestReportAppComponent extends RealTimeElement { .path="${this.context.path}" > ${this.context.view === 'mutant' && this.context.result ? html`${searchIcon} `; + return html` + + `; } #dispatchFilePickerOpenEvent() { diff --git a/packages/elements/src/components/file-picker/file-picker.component.ts b/packages/elements/src/components/file-picker/file-picker.component.ts index 1fb1fa231..c18b3b4b0 100644 --- a/packages/elements/src/components/file-picker/file-picker.component.ts +++ b/packages/elements/src/components/file-picker/file-picker.component.ts @@ -1,30 +1,37 @@ +import fuzzysort from 'fuzzysort'; +import type { TemplateResult } from 'lit'; import { html, LitElement, nothing, type PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; - -import { TestFileModel } from 'mutation-testing-metrics'; import type { FileUnderTestModel, Metrics, MetricsResult, MutationTestMetricsResult, TestMetrics } from 'mutation-testing-metrics'; +import { TestFileModel } from 'mutation-testing-metrics'; -import { mutantFileIcon, searchIcon, testFileIcon } from '../../lib/svg-icons.js'; import { renderIf, toAbsoluteUrl } from '../../lib/html-helpers.js'; -import { tailwind } from '../../style/index.js'; import { View } from '../../lib/router.js'; +import { mutantFileIcon, searchIcon, testFileIcon } from '../../lib/svg-icons.js'; +import { tailwind } from '../../style/index.js'; + +interface ModelEntry { + name: string; + file: FileUnderTestModel | TestFileModel; +} @customElement('mte-file-picker') export class MutationTestReportFilePickerComponent extends LitElement { static styles = [tailwind]; #abortController = new AbortController(); - #searchMap = new Map(); + #searchTargets: (ModelEntry & { prepared: Fuzzysort.Prepared })[] = []; + #originalDocumentOverflow = ''; - @property({ type: Object }) - public declare rootModel: MutationTestMetricsResult; + @property({ attribute: false }) + public declare rootModel: MutationTestMetricsResult | undefined; @state() public declare openPicker: boolean; @state() - public declare filteredFiles: { name: string; file: FileUnderTestModel | TestFileModel }[]; + public declare filteredFiles: (ModelEntry & { template?: (string | TemplateResult)[] })[]; @state() public declare fileIndex: number; @@ -38,29 +45,38 @@ export class MutationTestReportFilePickerComponent extends LitElement { connectedCallback(): void { super.connectedCallback(); + this.#originalDocumentOverflow = document.body.style.overflow; - window.addEventListener('keydown', (e) => this.#handleKeyDown(e), { signal: this.#abortController.signal }); + window.addEventListener('keydown', this.#handleKeyDown, { signal: this.#abortController.signal }); } disconnectedCallback(): void { super.disconnectedCallback(); + fuzzysort.cleanup(); this.#abortController.abort(); } willUpdate(changedProperties: PropertyValues): void { - super.willUpdate(changedProperties); - if (changedProperties.has('rootModel')) { this.#prepareMap(); this.#filter(''); } } + updated(changedProperties: PropertyValues): void { + if (changedProperties.has('openPicker')) { + if (this.openPicker) { + document.body.style.overflow = 'hidden'; + this.#focusInput(); + } else { + document.body.style.overflow = this.#originalDocumentOverflow; + } + } + } + open() { this.openPicker = true; - - void this.updateComplete.then(() => this.#focusInput()); } render() { @@ -71,28 +87,31 @@ export class MutationTestReportFilePickerComponent extends LitElement { return html`
`; @@ -100,27 +119,37 @@ export class MutationTestReportFilePickerComponent extends LitElement { #renderFoundFiles() { return html` -