-
Notifications
You must be signed in to change notification settings - Fork 0
feat: #749 inline binding creation from RefinementCard #792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9268b6e
60d7a0b
a8c4336
1694530
582e42c
2ead77b
6109576
381dd7a
769eac3
e18865f
b746804
a456dd4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -285,4 +285,37 @@ describe('RefinementCard', () => { | |
| expect(captured[0].trigger).toBeTruthy(); | ||
| }); | ||
| }); | ||
|
|
||
| // === Inline binding creation (#749) === | ||
|
|
||
| it('shows "Quick Bind" button for unbound source', async () => { | ||
| const RefinementCard = (await import('./RefinementCard.svelte')).default; | ||
| const { container } = render(RefinementCard, { | ||
| props: { suggestion: noteSuggestion, eventHistory: [], deviceName: 'Test', sourceDeviceId: 'Unknown Port' }, | ||
| }); | ||
| const bindBtn = container.querySelector('.source-bind-btn'); | ||
| expect(bindBtn).toBeTruthy(); | ||
| expect(bindBtn?.textContent?.trim()).toBe('Quick Bind'); | ||
| }); | ||
|
|
||
| it('shows unbound UI when device has no configured binding', async () => { | ||
| // Default mock returns empty bindings → sourceBinding is null → unbound path | ||
| const RefinementCard = (await import('./RefinementCard.svelte')).default; | ||
| const { container } = render(RefinementCard, { | ||
| props: { suggestion: noteSuggestion, eventHistory: [], deviceName: 'Test', sourceDeviceId: 'some-device' }, | ||
| }); | ||
| expect(container.querySelector('.source-unbound')).toBeTruthy(); | ||
| expect(container.querySelector('.source-bind-btn')).toBeTruthy(); | ||
| }); | ||
|
Comment on lines
+291
to
+309
|
||
|
|
||
| it('Quick Bind button is disabled when config not loaded', async () => { | ||
| // configStore defaults to no config in mock → configReady = false → button disabled | ||
| const RefinementCard = (await import('./RefinementCard.svelte')).default; | ||
| const { container } = render(RefinementCard, { | ||
| props: { suggestion: noteSuggestion, eventHistory: [], deviceName: 'Test', sourceDeviceId: 'Mikro MK3' }, | ||
| }); | ||
| const bindBtn = container.querySelector('.source-bind-btn') as HTMLButtonElement; | ||
| expect(bindBtn).toBeTruthy(); | ||
| expect(bindBtn.disabled).toBe(true); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright 2025 Amiable | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| import { describe, it, expect } from 'vitest'; | ||
| import { generateQuickBinding } from './quick-bind'; | ||
|
|
||
| describe('generateQuickBinding', () => { | ||
| it('generates alias from first word of device ID', () => { | ||
| const result = generateQuickBinding('Maschine Mikro MK3', new Set()); | ||
| expect(result.alias).toBe('maschine'); | ||
| expect(result.matchers[0].type).toBe('NameContains'); | ||
| expect(result.matchers[0].value).toBe('Maschine Mikro MK3'); | ||
| expect(result.enabled).toBe(true); | ||
| }); | ||
|
|
||
| it('handles device ID with special characters (underscore-replaced)', () => { | ||
| const result = generateQuickBinding('USB-MIDI_Device', new Set()); | ||
| expect(result.alias).toBe('usb_midi_device'); | ||
| }); | ||
|
|
||
| it('falls back to "device" for empty/unparseable names', () => { | ||
| const result = generateQuickBinding('---', new Set()); | ||
| expect(result.alias).toBe('device'); | ||
| }); | ||
|
|
||
| it('appends suffix for alias collision', () => { | ||
| const existing = new Set(['maschine']); | ||
| const result = generateQuickBinding('Maschine Mikro MK3', existing); | ||
| expect(result.alias).toBe('maschine2'); | ||
| }); | ||
|
|
||
| it('increments suffix until unique', () => { | ||
| const existing = new Set(['maschine', 'maschine2', 'maschine3']); | ||
| const result = generateQuickBinding('Maschine Mikro MK3', existing); | ||
| expect(result.alias).toBe('maschine4'); | ||
| }); | ||
|
|
||
| it('returns NameContains matcher with full device ID', () => { | ||
| const result = generateQuickBinding('nanoKONTROL2 MIDI', new Set()); | ||
| expect(result.matchers).toEqual([ | ||
| { type: 'NameContains', value: 'nanoKONTROL2 MIDI' }, | ||
| ]); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Copyright 2025 Amiable | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| /** | ||
| * quick-bind.ts — Inline binding creation logic (#749) | ||
| * | ||
| * Generates a new device identity config from a source device ID. | ||
| * Extracted for testability. | ||
| */ | ||
|
|
||
| export interface QuickBindConfig { | ||
| alias: string; | ||
| matchers: { type: string; value: string }[]; | ||
| enabled: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Generate a new device identity config for quick binding. | ||
| * | ||
| * @param sourceDeviceId - Raw port name / device ID | ||
| * @param existingAliases - Set of already-used aliases | ||
| * @returns New device config with unique alias and NameContains matcher | ||
| */ | ||
| export function generateQuickBinding( | ||
| sourceDeviceId: string, | ||
| existingAliases: Set<string>, | ||
| ): QuickBindConfig { | ||
| // Based on DeviceEditor alias convention (first word, lowercase, non-alphanumeric → underscore) | ||
| // with additional trim of leading/trailing underscores and 'device' fallback | ||
| const base = sourceDeviceId | ||
| .split(' ')[0] | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]/g, '_') | ||
| .replace(/^_+|_+$/g, '') || 'device'; | ||
|
|
||
| let alias = base; | ||
| let suffix = 2; | ||
| while (existingAliases.has(alias)) { | ||
| alias = `${base}${suffix++}`; | ||
| } | ||
|
|
||
| return { | ||
| alias, | ||
| matchers: [{ type: 'NameContains', value: sourceDeviceId }], | ||
| enabled: true, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new Quick Bind behavior in
RefinementCardisn’t exercised end-to-end in tests yet (no test clicks the button with a loaded config and assertssaveDeviceConfigis called, progress label changes toBinding..., and errors render on failure). Adding those assertions would prevent regressions in the inline binding flow.