feat(routes): auto-select route variables on click (#24)
* feat(routes): auto-select route variables on click * style: apply TS style fixes
This commit is contained in:
@@ -10,13 +10,14 @@ import {
|
||||
AppSelectTrigger,
|
||||
AppSelectValue,
|
||||
} from '@/components/base/select';
|
||||
import AppTooltipWrapper from '@/components/base/tooltip/AppTooltipWrapper.vue';
|
||||
import { useRouteSegmentSelection } from '@/composables/request/useRouteSegmentSelection';
|
||||
import { RouteDefinition } from '@/interfaces/routes/routes';
|
||||
import { useConfigStore, useRequestStore } from '@/stores';
|
||||
import { generateCurlCommand } from '@/utils/request';
|
||||
import { cn } from '@/utils/ui';
|
||||
import { CodeXml, CornerDownLeftIcon } from 'lucide-vue-next';
|
||||
import { computed, HTMLAttributes, ref } from 'vue';
|
||||
|
||||
import AppTooltipWrapper from '@/components/base/tooltip/AppTooltipWrapper.vue';
|
||||
import { useConfigStore } from '@/stores';
|
||||
import { generateCurlCommand } from '@/utils/request';
|
||||
import CurlExportDialog from './CurlExportDialog.vue';
|
||||
|
||||
interface RequestBuilderEndpointProps {
|
||||
@@ -25,9 +26,6 @@ interface RequestBuilderEndpointProps {
|
||||
|
||||
const props = defineProps<RequestBuilderEndpointProps>();
|
||||
|
||||
import { RouteDefinition } from '@/interfaces/routes/routes';
|
||||
import { useRequestStore } from '@/stores';
|
||||
|
||||
const availableMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
|
||||
/*
|
||||
@@ -75,6 +73,13 @@ const currentRouteUnsupportedMethods = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* Route segment selection.
|
||||
*/
|
||||
|
||||
const { handleClick: autoSelectRouteVariableSegmentWhenApplicable } =
|
||||
useRouteSegmentSelection({ endpoint });
|
||||
|
||||
/*
|
||||
* Actions.
|
||||
*/
|
||||
@@ -157,6 +162,7 @@ const populateCurlCommandExporterDialog = () => {
|
||||
v-model="endpoint"
|
||||
class="h-full flex-1 rounded-none border-0 text-xs shadow-none focus:ring-0 focus-visible:ring-0"
|
||||
placeholder="<endpoint>"
|
||||
@click="autoSelectRouteVariableSegmentWhenApplicable"
|
||||
@keydown="executeCurrentRequestWhenEnterIsPressed"
|
||||
/>
|
||||
<div class="flex gap-2 pr-2">
|
||||
|
||||
285
resources/js/composables/request/useRouteSegmentSelection.ts
Normal file
285
resources/js/composables/request/useRouteSegmentSelection.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
// composables/useRouteSegmentSelection.ts
|
||||
import { nextTick, ref, Ref, watch } from 'vue';
|
||||
|
||||
export interface UseRouteSegmentSelectionOptions {
|
||||
/**
|
||||
* The endpoint URL to watch for changes
|
||||
*/
|
||||
endpoint: Ref<string>;
|
||||
}
|
||||
|
||||
export interface UseRouteSegmentSelectionReturn {
|
||||
/**
|
||||
* Handler to be attached to the input's click event
|
||||
*/
|
||||
handleClick: (event: MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Manually identify variable segments in a URL
|
||||
*/
|
||||
identifyVariableSegments: (url: string) => number[];
|
||||
|
||||
/**
|
||||
* The indices of segments that were originally variables
|
||||
*/
|
||||
variableSegmentIndices: Ref<number[]>;
|
||||
}
|
||||
|
||||
interface SegmentPosition {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for handling automatic selection of route segments in endpoint inputs.
|
||||
*
|
||||
* This composable tracks variable segments (enclosed in braces like {id}) in route URLs
|
||||
* and enables automatic selection when users click on those segments, even after
|
||||
* they've been modified to contain actual values.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const endpoint = ref('api/users/{id}/posts/{postId}');
|
||||
* const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
*
|
||||
* // In template: <input v-model="endpoint" @click="handleClick" />
|
||||
* // Clicking on {id} or {postId} will select the entire segment
|
||||
* // Even after changing to 'api/users/123/posts/456', clicking on '123' or '456' still selects the whole segment
|
||||
* ```
|
||||
*/
|
||||
export function useRouteSegmentSelection(
|
||||
options: UseRouteSegmentSelectionOptions,
|
||||
): UseRouteSegmentSelectionReturn {
|
||||
const { endpoint } = options;
|
||||
|
||||
const variableSegmentIndices = ref<number[]>([]);
|
||||
|
||||
/**
|
||||
* Identifies which segments in a URL path are variables (wrapped in braces).
|
||||
*
|
||||
* @param url - The URL path to analyze (e.g., 'api/users/{id}/posts')
|
||||
* @returns Array of segment indices that are variables (0-based)
|
||||
*/
|
||||
const identifyVariableSegments = (url: string): number[] => {
|
||||
const segments = url.split('/');
|
||||
const indices: number[] = [];
|
||||
|
||||
segments.forEach((segment: string, index: number) => {
|
||||
const isVariable = segment.startsWith('{') && segment.endsWith('}');
|
||||
|
||||
if (isVariable) {
|
||||
indices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
return indices;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a character is an opening brace.
|
||||
*/
|
||||
const isOpeningBrace = (char: string): boolean => char === '{';
|
||||
|
||||
/**
|
||||
* Checks if a character is a closing brace.
|
||||
*/
|
||||
const isClosingBrace = (char: string): boolean => char === '}';
|
||||
|
||||
/**
|
||||
* Searches backward from cursor position to find an opening brace.
|
||||
* Returns -1 if a closing brace is found first or no opening brace exists.
|
||||
*/
|
||||
const findOpeningBracePosition = (text: string, cursorPos: number): number => {
|
||||
for (let i = cursorPos; i >= 0; i--) {
|
||||
if (isOpeningBrace(text[i])) {
|
||||
return i;
|
||||
}
|
||||
if (isClosingBrace(text[i])) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches forward from a position to find a closing brace.
|
||||
* Returns -1 if an opening brace is found first or no closing brace exists.
|
||||
*/
|
||||
const findClosingBracePosition = (text: string, startPos: number): number => {
|
||||
for (let i = startPos; i < text.length; i++) {
|
||||
if (isClosingBrace(text[i])) {
|
||||
return i + 1; // +1 to include the closing brace
|
||||
}
|
||||
if (isOpeningBrace(text[i])) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the position of a segment containing braces at the cursor position.
|
||||
*
|
||||
* @param text - The full input text
|
||||
* @param cursorPos - Current cursor position
|
||||
* @returns Segment position or null if not found
|
||||
*/
|
||||
const findBraceSegment = (
|
||||
text: string,
|
||||
cursorPos: number,
|
||||
): SegmentPosition | null => {
|
||||
const openingBracePos = findOpeningBracePosition(text, cursorPos);
|
||||
|
||||
if (openingBracePos === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const closingBracePos = findClosingBracePosition(text, cursorPos);
|
||||
|
||||
if (closingBracePos === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
start: openingBracePos,
|
||||
end: closingBracePos,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds which segment index the cursor is currently in.
|
||||
*/
|
||||
const findSegmentIndexAtCursor = (text: string, cursorPos: number): number | null => {
|
||||
const segments = text.split('/');
|
||||
let charCount = 0;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segmentLength = segments[i].length;
|
||||
const segmentStart = charCount;
|
||||
const segmentEnd = charCount + segmentLength;
|
||||
|
||||
const isCursorInSegment =
|
||||
cursorPos >= segmentStart && cursorPos <= segmentEnd;
|
||||
|
||||
if (isCursorInSegment) {
|
||||
return i;
|
||||
}
|
||||
|
||||
charCount += segmentLength + 1; // +1 for the '/' separator
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the start and end positions of a segment by its index.
|
||||
*/
|
||||
const getSegmentPosition = (
|
||||
text: string,
|
||||
segmentIndex: number,
|
||||
): SegmentPosition | null => {
|
||||
const segments = text.split('/');
|
||||
|
||||
if (segmentIndex >= segments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let charCount = 0;
|
||||
|
||||
for (let i = 0; i < segmentIndex; i++) {
|
||||
charCount += segments[i].length + 1; // +1 for the '/' separator
|
||||
}
|
||||
|
||||
return {
|
||||
start: charCount,
|
||||
end: charCount + segments[segmentIndex].length,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the position of a segment that was originally a variable.
|
||||
*
|
||||
* @param text - The full input text
|
||||
* @param cursorPos - Current cursor position
|
||||
* @returns Segment position or null if not found
|
||||
*/
|
||||
const findOriginalVariableSegment = (
|
||||
text: string,
|
||||
cursorPos: number,
|
||||
): SegmentPosition | null => {
|
||||
const segmentIndex = findSegmentIndexAtCursor(text, cursorPos);
|
||||
|
||||
if (segmentIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOriginalVariable = variableSegmentIndices.value.includes(segmentIndex);
|
||||
|
||||
if (!isOriginalVariable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getSegmentPosition(text, segmentIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a text range in the input element.
|
||||
*/
|
||||
const selectRange = (input: HTMLInputElement, position: SegmentPosition): void => {
|
||||
nextTick(() => {
|
||||
input.setSelectionRange(position.start, position.end);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles click events on the input to auto-select route segments.
|
||||
* Priority: 1) Segments with braces, 2) Segments that were originally variables
|
||||
*/
|
||||
const handleClick = (event: MouseEvent): void => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
|
||||
if (!input || input.selectionStart === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = input.selectionStart;
|
||||
const text = input.value;
|
||||
|
||||
// First priority: Check if we're inside a segment with braces
|
||||
const braceSegment = findBraceSegment(text, cursorPos);
|
||||
|
||||
if (braceSegment) {
|
||||
selectRange(input, braceSegment);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Second priority: Check if cursor is in a segment that was originally a variable
|
||||
const originalSegment = findOriginalVariableSegment(text, cursorPos);
|
||||
|
||||
if (originalSegment) {
|
||||
selectRange(input, originalSegment);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for endpoint changes to track variable segments
|
||||
watch(
|
||||
endpoint,
|
||||
(newValue: string) => {
|
||||
const hasVariableSegments = newValue?.includes('{');
|
||||
|
||||
if (hasVariableSegments) {
|
||||
variableSegmentIndices.value = identifyVariableSegments(newValue);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
handleClick,
|
||||
identifyVariableSegments,
|
||||
variableSegmentIndices,
|
||||
};
|
||||
}
|
||||
329
resources/js/tests/composables/useRouteSegmentSelection.test.ts
Normal file
329
resources/js/tests/composables/useRouteSegmentSelection.test.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { useRouteSegmentSelection } from '@/composables/request/useRouteSegmentSelection';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
describe('useRouteSegmentSelection', () => {
|
||||
describe('identifyVariableSegments', () => {
|
||||
it('identifies single variable segment', () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { identifyVariableSegments } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const result = identifyVariableSegments('api/users/{id}');
|
||||
|
||||
expect(result).toEqual([2]);
|
||||
});
|
||||
|
||||
it('identifies multiple variable segments', () => {
|
||||
const endpoint = ref('api/users/{userId}/posts/{postId}');
|
||||
const { identifyVariableSegments } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const result = identifyVariableSegments('api/users/{userId}/posts/{postId}');
|
||||
|
||||
expect(result).toEqual([2, 4]);
|
||||
});
|
||||
|
||||
it('returns empty array when no variable segments exist', () => {
|
||||
const endpoint = ref('api/users/list');
|
||||
const { identifyVariableSegments } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const result = identifyVariableSegments('api/users/list');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
const endpoint = ref('');
|
||||
const { identifyVariableSegments } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const result = identifyVariableSegments('');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores partial braces', () => {
|
||||
const endpoint = ref('api/{users/posts}');
|
||||
const { identifyVariableSegments } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const result = identifyVariableSegments('api/{users/posts}');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('variableSegmentIndices tracking', () => {
|
||||
it('initializes with variable segments from endpoint', () => {
|
||||
const endpoint = ref('api/users/{id}/posts/{postId}');
|
||||
const { variableSegmentIndices } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
expect(variableSegmentIndices.value).toEqual([2, 4]);
|
||||
});
|
||||
|
||||
it('updates when endpoint changes to include braces', async () => {
|
||||
const endpoint = ref('api/users/123');
|
||||
const { variableSegmentIndices } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
expect(variableSegmentIndices.value).toEqual([]);
|
||||
|
||||
endpoint.value = 'api/users/{id}';
|
||||
await nextTick();
|
||||
|
||||
expect(variableSegmentIndices.value).toEqual([2]);
|
||||
});
|
||||
|
||||
it('does not update when endpoint changes without braces', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { variableSegmentIndices } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
expect(variableSegmentIndices.value).toEqual([2]);
|
||||
|
||||
endpoint.value = 'api/users/123';
|
||||
await nextTick();
|
||||
|
||||
expect(variableSegmentIndices.value).toEqual([2]); // Should remain unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleClick', () => {
|
||||
let mockInput: HTMLInputElement;
|
||||
|
||||
beforeEach(() => {
|
||||
mockInput = document.createElement('input');
|
||||
mockInput.setSelectionRange = vi.fn();
|
||||
document.body.appendChild(mockInput);
|
||||
});
|
||||
|
||||
it('selects segment with braces when clicked', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/{id}';
|
||||
mockInput.selectionStart = 12; // Inside {id}
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 14); // <- Selects {id}
|
||||
});
|
||||
|
||||
it('selects entire braced segment when clicking at opening brace', async () => {
|
||||
const endpoint = ref('api/users/{userId}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/{userId}';
|
||||
mockInput.selectionStart = 10; // <- At the {
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 18);
|
||||
});
|
||||
|
||||
it('selects entire braced segment when clicking at closing brace', async () => {
|
||||
const endpoint = ref('api/users/{userId}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/{userId}';
|
||||
mockInput.selectionStart = 17; // <- At the }
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 18);
|
||||
});
|
||||
|
||||
it('selects replaced variable segment when clicked', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
// User has replaced {id} with 123
|
||||
mockInput.value = 'api/users/123';
|
||||
mockInput.selectionStart = 11; // <- Inside 123
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 13); // <- Selects 123
|
||||
});
|
||||
|
||||
it('selects multiple replaced variable segments independently', async () => {
|
||||
const endpoint = ref('api/users/{userId}/posts/{postId}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/123/posts/456';
|
||||
|
||||
// Click on first replaced segment (123)
|
||||
mockInput.selectionStart = 11;
|
||||
let event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 13);
|
||||
|
||||
// Click on second replaced segment (456)
|
||||
mockInput.selectionStart = 21;
|
||||
event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(20, 23);
|
||||
});
|
||||
|
||||
it('does not select when clicking on non-variable segment', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/{id}';
|
||||
mockInput.selectionStart = 4; // <- Inside "users"
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prioritizes braced segments over original variable segments', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
// User added braces back after replacing
|
||||
mockInput.value = 'api/users/{newId}';
|
||||
mockInput.selectionStart = 12; // <- Inside {newId}
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 17); // Selects {newId}
|
||||
});
|
||||
|
||||
it('handles clicking at start of segment', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/123';
|
||||
mockInput.selectionStart = 10; // <- At the start of 123
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 13);
|
||||
});
|
||||
|
||||
it('handles clicking at end of segment', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/123';
|
||||
mockInput.selectionStart = 13; // <- At end of 123
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 13);
|
||||
});
|
||||
|
||||
it('handles empty segments gracefully', async () => {
|
||||
const endpoint = ref('api/users/{}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
mockInput.value = 'api/users/{}';
|
||||
mockInput.selectionStart = 11; // <- Inside {}
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInput,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
handleClick(event);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInput.setSelectionRange).toHaveBeenCalledWith(10, 12);
|
||||
});
|
||||
|
||||
it('does not error when event target is null', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
|
||||
expect(() => handleClick(event)).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not error when selectionStart is null', async () => {
|
||||
const endpoint = ref('api/users/{id}');
|
||||
const { handleClick } = useRouteSegmentSelection({ endpoint });
|
||||
|
||||
const mockInputWithoutSelection = document.createElement('input');
|
||||
Object.defineProperty(mockInputWithoutSelection, 'selectionStart', {
|
||||
value: null,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
const event = new MouseEvent('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', {
|
||||
value: mockInputWithoutSelection,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
expect(() => handleClick(event)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user