Add pre-commit verification hook

This commit is contained in:
Federico Jaramillo Martinez
2026-05-11 11:14:48 +02:00
parent c82486a2e5
commit 020b8f00ca
6 changed files with 55 additions and 22 deletions
+5 -5
View File
@@ -282,11 +282,11 @@ export class PiWebApp extends LitElement {
selectedStagedDiff: this.state.selectedStagedDiff,
gitStale: this.state.gitStale,
activeTerminalCount: this.state.activeTerminalCount,
onRefreshFiles: () => this.files.refreshFiles(),
onExpandDir: (path: string) => this.files.expandDir(path),
onSelectFile: (path: string) => this.files.selectFile(path),
onRefreshGit: () => this.git.refreshGit(),
onSelectDiff: (path: string) => this.git.selectDiff(path),
onRefreshFiles: () => { void this.files.refreshFiles(); },
onExpandDir: (path: string) => { void this.files.expandDir(path); },
onSelectFile: (path: string) => { void this.files.selectFile(path); },
onRefreshGit: () => { void this.git.refreshGit(); },
onSelectDiff: (path: string) => { void this.git.selectDiff(path); },
};
}
@@ -4,21 +4,21 @@ import { activateSelectableRow, activateSelectableRowFromKeyboard } from "./sele
describe("selectable row activation", () => {
it("activates rows from non-interactive click targets", () => {
const action = vi.fn();
activateSelectableRow(eventWithPath({ matches: () => false }), action);
activateSelectableRow(eventWithPath(matchTarget(() => false)), action);
expect(action).toHaveBeenCalledOnce();
});
it("preserves contributed links and other interactive elements", () => {
const action = vi.fn();
activateSelectableRow(eventWithPath({ matches: (selector: string) => selector.includes("a[href]") }), action);
activateSelectableRow(eventWithPath(matchTarget((selector: string) => selector.includes("a[href]"))), action);
expect(action).not.toHaveBeenCalled();
});
it("activates rows from Enter and Space", () => {
const enterAction = vi.fn();
const spaceAction = vi.fn();
const enter = keyboardEventWithPath("Enter", { matches: () => false });
const space = keyboardEventWithPath(" ", { matches: () => false });
const enter = keyboardEventWithPath("Enter", matchTarget(() => false));
const space = keyboardEventWithPath(" ", matchTarget(() => false));
activateSelectableRowFromKeyboard(enter, enterAction);
activateSelectableRowFromKeyboard(space, spaceAction);
@@ -31,7 +31,7 @@ describe("selectable row activation", () => {
it("does not activate rows from keyboard events inside interactive elements", () => {
const action = vi.fn();
const event = keyboardEventWithPath("Enter", { matches: (selector: string) => selector.includes("button") });
const event = keyboardEventWithPath("Enter", matchTarget((selector: string) => selector.includes("button")));
activateSelectableRowFromKeyboard(event, action);
@@ -40,10 +40,18 @@ describe("selectable row activation", () => {
});
});
function eventWithPath(target: Pick<Element, "matches">): MouseEvent {
return { composedPath: () => [target] } as unknown as MouseEvent;
type EventWithPath = Pick<Event, "composedPath">;
type KeyboardEventWithPath = EventWithPath & Pick<KeyboardEvent, "key" | "preventDefault">;
type MatchTarget = EventTarget & Pick<Element, "matches">;
function matchTarget(matches: Element["matches"]): MatchTarget {
return Object.assign(new EventTarget(), { matches });
}
function keyboardEventWithPath(key: string, target: Pick<Element, "matches">): KeyboardEvent & { preventDefault: ReturnType<typeof vi.fn> } {
return { key, preventDefault: vi.fn(), composedPath: () => [target] } as unknown as KeyboardEvent & { preventDefault: ReturnType<typeof vi.fn> };
function eventWithPath(target: MatchTarget): EventWithPath {
return { composedPath: () => [target] };
}
function keyboardEventWithPath(key: string, target: MatchTarget): KeyboardEventWithPath {
return { key, preventDefault: vi.fn<() => void>(), composedPath: () => [target] };
}
+12 -7
View File
@@ -10,21 +10,26 @@ const interactiveSelector = [
"[contenteditable='true']",
].join(",");
export function isFromInteractiveElement(event: Event): boolean {
return event.composedPath().some((target) => isElementLike(target) && target.matches(interactiveSelector));
type ComposedPathEvent = Pick<Event, "composedPath">;
type SelectableKeyboardEvent = ComposedPathEvent & Pick<KeyboardEvent, "key" | "preventDefault">;
export function isFromInteractiveElement(event: ComposedPathEvent): boolean {
return event.composedPath().some((target) => targetMatches(target, interactiveSelector));
}
function isElementLike(target: EventTarget): target is Element {
if (typeof Element !== "undefined") return target instanceof Element;
return typeof (target as Partial<Element>).matches === "function";
function targetMatches(target: EventTarget, selector: string): boolean {
if (typeof Element !== "undefined" && target instanceof Element) return target.matches(selector);
if (!("matches" in target)) return false;
const { matches } = target;
return typeof matches === "function" && matches.call(target, selector) === true;
}
export function activateSelectableRow(event: MouseEvent, action: () => void): void {
export function activateSelectableRow(event: ComposedPathEvent, action: () => void): void {
if (isFromInteractiveElement(event)) return;
action();
}
export function activateSelectableRowFromKeyboard(event: KeyboardEvent, action: () => void): void {
export function activateSelectableRowFromKeyboard(event: SelectableKeyboardEvent, action: () => void): void {
if (event.key !== "Enter" && event.key !== " ") return;
if (isFromInteractiveElement(event)) return;
event.preventDefault();