Skip to content

Commit ac68709

Browse files
committed
Selection dialog: M11 slow-suite green and final review
Slow suite (./scripts/check.sh --include-slow) surfaced 3 categories of fix work on top of M10: 1. search-modes.spec.ts: update DOM selectors to match the post-M3 ToggleGroup shape. ModeChips now renders .tg-item / .tg-label / aria-selected; the old .mode-chip / .chip-label / .is-active classes are gone. Updates the ACTIVE_MODE_CHIP / MODE_CHIPS constants in search-helpers.ts and the .chip-label query in getActiveMode/hasAiChip. 2. FilterChips.svelte: wrap factory setter refs in arrow-function passthroughs. Const setSizeFilter = filterState.setSizeFilter (etc.) tripped @typescript-eslint/unbound-method even though the factory's setters don't read this. The arrow-wrap pattern silences the rule and keeps the writes going through the same instance. 3. eslint-typecheck-only cleanup across M3/M4/M7 (40+ findings, all in code the default eslint pass doesnt flag): - Drop the any typing on QueryDialog.queryResultsComponent; declare a local QueryResultsAPI interface and bind the ref against it. - Replace 'as any' casts in the recent-items tests (the Svelte 5 generic- mount workaround) with as-unknown-as Component widening. - Remove unnecessary ?? and optional-chain guards that the type system proved redundant. - Add a one-line disable on QueryDialog Props for E generic eslint cant follow through Svelte 5s script generics attribute. Three pre-existing macOS-only e2e flakes did not pass (Escape closes the settings window, Escape closes the viewer window, viewer-window close in afterEach). None touch Selection or Query UI; none reproduce on the Linux shard. Documented in the final-review file as known unrelated infra issues. Final review at docs/specs/selection-dialog-plan-final-review.md. Verdict: SHIP.
1 parent 059cbc5 commit ac68709

17 files changed

Lines changed: 186 additions & 327 deletions

apps/desktop/src/lib/query-ui/FilterChips.svelte

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,18 @@
148148
}: Props = $props()
149149
150150
// Pull the setters from the injected state instance so the template reads
151-
// `setSizeFilter(...)` like before. Local consts (not re-exports) so the component stays
152-
// self-contained; the underlying writes still go through the instance the consumer owns.
153-
const setSizeFilter = filterState.setSizeFilter
154-
const setSizeValue = filterState.setSizeValue
155-
const setSizeUnit = filterState.setSizeUnit
156-
const setSizeValueMax = filterState.setSizeValueMax
157-
const setSizeUnitMax = filterState.setSizeUnitMax
158-
const setDateFilter = filterState.setDateFilter
159-
const setDateValue = filterState.setDateValue
160-
const setDateValueMax = filterState.setDateValueMax
161-
const setQueryFromUserInput = filterState.setQueryFromUserInput
151+
// `setSizeFilter(...)` like before. Arrow-function wrappers (not raw method refs) keep
152+
// `this` binding intact and silence `@typescript-eslint/unbound-method` since the
153+
// factory's setters don't read `this` but the type system can't prove it.
154+
const setSizeFilter: typeof filterState.setSizeFilter = (v) => { filterState.setSizeFilter(v); }
155+
const setSizeValue: typeof filterState.setSizeValue = (v) => { filterState.setSizeValue(v); }
156+
const setSizeUnit: typeof filterState.setSizeUnit = (v) => { filterState.setSizeUnit(v); }
157+
const setSizeValueMax: typeof filterState.setSizeValueMax = (v) => { filterState.setSizeValueMax(v); }
158+
const setSizeUnitMax: typeof filterState.setSizeUnitMax = (v) => { filterState.setSizeUnitMax(v); }
159+
const setDateFilter: typeof filterState.setDateFilter = (v) => { filterState.setDateFilter(v); }
160+
const setDateValue: typeof filterState.setDateValue = (v) => { filterState.setDateValue(v); }
161+
const setDateValueMax: typeof filterState.setDateValueMax = (v) => { filterState.setDateValueMax(v); }
162+
const setQueryFromUserInput: typeof filterState.setQueryFromUserInput = (v) => { filterState.setQueryFromUserInput(v); }
162163
163164
let openChip = $state<FilterKey | 'add' | null>(null)
164165

apps/desktop/src/lib/query-ui/ModeChips.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* `SearchMode` counterpart on purpose: it's a placeholder for the future full-text
3636
* search feature and lives only in the visual chip set so users see it on the horizon.
3737
*/
38-
const options = $derived.by<ToggleGroupOption[]>(() => {
38+
const options = $derived.by<ToggleGroupOption[]>((): ToggleGroupOption[] => {
3939
const list: ToggleGroupOption[] = []
4040
if (aiEnabled) {
4141
list.push({

apps/desktop/src/lib/query-ui/ModeChips.svelte.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ describe('SearchModeChips', () => {
197197
it("AI chip renders its `AI` badge via ToggleGroup's badge slot", async () => {
198198
const { chips, cleanup } = setup({ aiEnabled: true })
199199
await tick()
200-
const aiBadge = chips[0].querySelector('.tg-badge')?.textContent?.trim()
200+
const aiBadge = chips[0].querySelector('.tg-badge')?.textContent.trim()
201201
expect(aiBadge).toBe('AI')
202202
cleanup()
203203
})

apps/desktop/src/lib/query-ui/QueryDialog.svelte

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,22 @@
4646
import { getSetting, onSpecificSettingChange } from '$lib/settings'
4747
4848
interface Props {
49+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments -- E is the Svelte component generic; the explicit <E> binds the inference for callers like SearchDialog/SelectionDialog
4950
config: QueryDialogConfig<E>
5051
}
5152
5253
/* eslint-disable prefer-const -- $props destructuring keeps types clean with const */
5354
let { config }: Props = $props()
5455
/* eslint-enable prefer-const */
5556
57+
/** Shape of the `bind:this` ref for `QueryResults.svelte` — only the exported method we call. */
58+
interface QueryResultsAPI {
59+
scrollCursorIntoView(): void
60+
}
61+
5662
let queryInputElement: HTMLInputElement | undefined = $state()
5763
let dialogElement: HTMLDivElement | undefined = $state()
58-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Svelte 5 bind:this lacks type info
59-
let queryResultsComponent: any = $state()
64+
let queryResultsComponent: QueryResultsAPI | undefined = $state()
6065
let footerRef: HTMLDivElement | undefined = $state()
6166
let recentPopoverOpen = $state(false)
6267
let debounceTimer: ReturnType<typeof setTimeout> | undefined
@@ -345,7 +350,6 @@
345350
346351
async function focusFirstResult(): Promise<void> {
347352
await tick()
348-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte 5 bind:this lacks type info for exports
349353
queryResultsComponent?.scrollCursorIntoView()
350354
}
351355
@@ -597,7 +601,6 @@
597601
config.state.setCursorIndex(next)
598602
// D8: cursor moves keep ⏎ on "go-to-file" as the user browses the list.
599603
config.state.setLastDialogEvent('cursor-moved')
600-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte 5 bind:this lacks type info for exports
601604
queryResultsComponent?.scrollCursorIntoView()
602605
}
603606

apps/desktop/src/lib/query-ui/QueryDialog.svelte.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function mountQueryDialog(opts: MountOptions = {}): MountedDialog {
126126
const fn = opts.translateAi
127127
return async (prompt: string) => {
128128
calls.translateAi.push(prompt)
129-
return fn ? fn(prompt) : null
129+
return fn(prompt)
130130
}
131131
})()
132132
: undefined,
@@ -162,7 +162,7 @@ function mountQueryDialog(opts: MountOptions = {}): MountedDialog {
162162
// Svelte's `mount()` typing of a generic component pins the type parameter at the
163163
// call site; we widen via `unknown` so the test's `HistoryEntry`-typed config still
164164
// passes the type check without losing inference on the rest of the file.
165-
const component = mount(QueryDialog, { target, props: { config: config as unknown as QueryDialogConfig<unknown> } })
165+
const component = mount(QueryDialog, { target, props: { config: config } })
166166

167167
const overlay = target.querySelector('.search-overlay')
168168
if (!overlay) throw new Error('overlay not found')

apps/desktop/src/lib/query-ui/recent-items/RecentItemsFooter.a11y.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88
*/
99

1010
import { describe, it } from 'vitest'
11-
import { mount, tick } from 'svelte'
11+
import { mount, tick, type Component } from 'svelte'
1212
import RecentSearchesFooterRaw from './RecentItemsFooter.svelte'
1313
import type { HistoryEntry } from '$lib/tauri-commands'
1414
import { expectNoA11yViolations } from '$lib/test-a11y'
1515
import type { RecentItemAdapter, RecentItemKey } from './recent-items-types'
1616
import { chipTooltip, modeName, formatAge } from './recent-items-utils'
1717

18-
// Svelte 5 generics+mount type roundtrip workaround — see `RecentItemsFooter.svelte.test.ts`.
19-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20-
const RecentSearchesFooter = RecentSearchesFooterRaw as any
18+
// Svelte 5 generics+mount type roundtrip: the declared Component<unknown> shape rejects typed
19+
// adapters. Cast through unknown to a permissive component shape so the mount() call type-checks
20+
// without unsafe-argument errors. The runtime contract is correct.
21+
const RecentSearchesFooter = RecentSearchesFooterRaw as unknown as Component<Record<string, unknown>>
2122

2223
function makeEntry(overrides: Partial<HistoryEntry> = {}): HistoryEntry {
2324
return {

apps/desktop/src/lib/query-ui/recent-items/RecentItemsFooter.label.svelte.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
* the user can read what those chips are without needing context.
44
*/
55
import { describe, expect, it } from 'vitest'
6-
import { mount, tick } from 'svelte'
6+
import { mount, tick, type Component } from 'svelte'
77
import RecentSearchesFooterRaw from './RecentItemsFooter.svelte'
88
import type { HistoryEntry } from '$lib/tauri-commands'
99
import type { RecentItemAdapter, RecentItemKey } from './recent-items-types'
1010
import { chipTooltip, modeName, formatAge } from './recent-items-utils'
1111

12-
// Svelte 5 generics+mount type roundtrip workaround — see `RecentItemsFooter.svelte.test.ts`.
13-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14-
const RecentSearchesFooter = RecentSearchesFooterRaw as any
12+
// Svelte 5 generics+mount type roundtrip: cast through unknown to avoid unsafe-argument errors.
13+
// See `RecentItemsFooter.svelte.test.ts` for the full explanation.
14+
const RecentSearchesFooter = RecentSearchesFooterRaw as unknown as Component<Record<string, unknown>>
1515

1616
const searchAdapter: RecentItemAdapter<HistoryEntry> = (e) => ({
1717
label: e.query,

apps/desktop/src/lib/query-ui/recent-items/RecentItemsFooter.svelte.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { describe, it, expect, vi } from 'vitest'
2-
import { mount, tick } from 'svelte'
2+
import { mount, tick, type Component } from 'svelte'
33
import RecentSearchesFooterRaw from './RecentItemsFooter.svelte'
44
import type { HistoryEntry, HistoryMode } from '$lib/tauri-commands'
55
import type { RecentItemAdapter, RecentItemKey } from './recent-items-types'
66
import { chipTooltip, modeName, formatAge } from './recent-items-utils'
77

88
// Svelte 5's `generics="E"` declaration doesn't survive the `mount()` type roundtrip: the
99
// declared `Component<unknown>` shape rejects a typed `RecentItemAdapter<HistoryEntry>`. The
10-
// runtime contract is fine; we cast the component reference to the typed variant so the
11-
// test's adapter binding type-checks. This is the same trick the toast suite uses for its
12-
// component-content toasts.
13-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14-
const RecentSearchesFooter = RecentSearchesFooterRaw as any
10+
// runtime contract is fine; we cast through unknown to a permissive Component shape so the
11+
// mount() call type-checks without unsafe-argument errors.
12+
const RecentSearchesFooter = RecentSearchesFooterRaw as unknown as Component<Record<string, unknown>>
1513

1614
function makeEntry(overrides: Partial<HistoryEntry>): HistoryEntry {
1715
return {

apps/desktop/src/lib/query-ui/recent-items/RecentItemsPopover.a11y.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
*/
99

1010
import { describe, it } from 'vitest'
11-
import { mount, tick } from 'svelte'
11+
import { mount, tick, type Component } from 'svelte'
1212
import RecentSearchesPopoverRaw from './RecentItemsPopover.svelte'
1313
import type { HistoryEntry } from '$lib/tauri-commands'
1414
import { expectNoA11yViolations } from '$lib/test-a11y'
1515
import type { RecentItemAdapter, RecentItemKey } from './recent-items-types'
1616
import { chipTooltip, modeName, formatAge } from './recent-items-utils'
1717

18-
// Svelte 5 generics+mount type roundtrip workaround — see `RecentItemsFooter.svelte.test.ts`.
19-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20-
const RecentSearchesPopover = RecentSearchesPopoverRaw as any
18+
// Svelte 5 generics+mount type roundtrip: cast through unknown to avoid unsafe-argument errors.
19+
// See `RecentItemsFooter.svelte.test.ts` for the full explanation.
20+
const RecentSearchesPopover = RecentSearchesPopoverRaw as unknown as Component<Record<string, unknown>>
2121

2222
const searchAdapter: RecentItemAdapter<HistoryEntry> = (e) => ({
2323
label: e.query,

apps/desktop/src/lib/query-ui/recent-items/RecentItemsPopover.svelte.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { describe, it, expect, vi } from 'vitest'
2-
import { mount, tick, unmount } from 'svelte'
2+
import { mount, tick, unmount, type Component } from 'svelte'
33
import RecentSearchesPopoverRaw from './RecentItemsPopover.svelte'
44
import type { HistoryEntry } from '$lib/tauri-commands'
55
import type { RecentItemAdapter, RecentItemKey } from './recent-items-types'
66
import { chipTooltip, modeName, formatAge } from './recent-items-utils'
77

8-
// Svelte 5 generics+mount type roundtrip workaround — see `RecentItemsFooter.svelte.test.ts`.
9-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10-
const RecentSearchesPopover = RecentSearchesPopoverRaw as any
8+
// Svelte 5 generics+mount type roundtrip: cast through unknown to avoid unsafe-argument errors.
9+
// See `RecentItemsFooter.svelte.test.ts` for the full explanation.
10+
const RecentSearchesPopover = RecentSearchesPopoverRaw as unknown as Component<Record<string, unknown>>
1111

1212
function makeEntry(overrides: Partial<HistoryEntry>): HistoryEntry {
1313
return {

0 commit comments

Comments
 (0)