From 756d44ef1439e7f28d138215a0669452e0329692 Mon Sep 17 00:00:00 2001 From: Romot Date: Sat, 23 Nov 2024 19:05:39 +0900 Subject: [PATCH] =?UTF-8?q?zod=E3=81=A7=E5=9E=8B=E3=82=92=E5=AE=9A?= =?UTF-8?q?=E7=BE=A9=E3=81=8A=E3=82=88=E3=81=B3useCursorState=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E3=82=84=E3=83=86=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useCursorState.ts | 53 +-- src/composables/useEditMode.ts | 102 ++--- src/sing/viewHelper.ts | 28 ++ src/store/type.ts | 19 +- tests/unit/composable/useCursorState.spec.ts | 164 +++++++++ tests/unit/composable/useEditMode.spec.ts | 368 ++++++++++++++----- 6 files changed, 550 insertions(+), 184 deletions(-) create mode 100644 tests/unit/composable/useCursorState.spec.ts diff --git a/src/composables/useCursorState.ts b/src/composables/useCursorState.ts index 437882e7e9..da8595cfba 100644 --- a/src/composables/useCursorState.ts +++ b/src/composables/useCursorState.ts @@ -1,6 +1,6 @@ import { computed, watch, Ref } from "vue"; import { SequencerEditTarget, NoteEditTool, PitchEditTool } from "@/store/type"; -import { PreviewMode } from "@/sing/viewHelper"; +import { PreviewMode, CursorState } from "@/sing/viewHelper"; // カーソル状態の外部コンテキスト export interface CursorStateContext { @@ -13,19 +13,27 @@ export interface CursorStateContext { readonly previewMode: Ref; } -export function useCursorState(context: CursorStateContext) { +export function useCursorState(cursorStateContext: CursorStateContext) { + const { + ctrlKey, + shiftKey, + nowPreviewing, + editTarget, + selectedNoteTool, + selectedPitchTool, + previewMode, + } = cursorStateContext; + // カーソルの状態 - const cursorState = computed(() => { - return resolveCursorBehavior(); - }); + const cursorState: Ref = computed(() => resolveCursorBehavior()); /** * カーソルの状態を関連するコンテキストから取得する */ - const resolveCursorBehavior = () => { + const resolveCursorBehavior = (): CursorState => { // プレビューの場合 - if (context.nowPreviewing.value && context.previewMode.value !== "IDLE") { - switch (context.previewMode.value) { + if (nowPreviewing.value && previewMode.value !== "IDLE") { + switch (previewMode.value) { case "ADD_NOTE": return "DRAW"; case "MOVE_NOTE": @@ -43,13 +51,13 @@ export function useCursorState(context: CursorStateContext) { } // ノート編集の場合 - if (context.editTarget.value === "NOTE") { + if (editTarget.value === "NOTE") { // シフトキーが押されていたら常に十字カーソル - if (context.shiftKey.value) { + if (shiftKey.value) { return "CROSSHAIR"; } // ノート編集ツールが選択されていたら描画カーソル - if (context.selectedNoteTool.value === "EDIT_FIRST") { + if (selectedNoteTool.value === "EDIT_FIRST") { return "DRAW"; } // それ以外は未設定 @@ -57,17 +65,14 @@ export function useCursorState(context: CursorStateContext) { } // ピッチ編集の場合 - if (context.editTarget.value === "PITCH") { + if (editTarget.value === "PITCH") { // Ctrlキーが押されていたもしくは削除ツールが選択されていたら消しゴムカーソル - if ( - context.ctrlKey.value || - context.selectedPitchTool.value === "ERASE" - ) { + if (ctrlKey.value || selectedPitchTool.value === "ERASE") { return "ERASE"; } // 描画ツールが選択されていたら描画カーソル - if (context.selectedPitchTool.value === "DRAW") { + if (selectedPitchTool.value === "DRAW") { return "DRAW"; } } @@ -95,13 +100,13 @@ export function useCursorState(context: CursorStateContext) { // カーソルに関連するコンテキストが更新されたらカーソルの状態を変更 watch( [ - context.ctrlKey, - context.shiftKey, - context.nowPreviewing, - context.editTarget, - context.selectedNoteTool, - context.selectedPitchTool, - context.previewMode, + ctrlKey, + shiftKey, + nowPreviewing, + editTarget, + selectedNoteTool, + selectedPitchTool, + previewMode, ], () => {}, { immediate: true }, diff --git a/src/composables/useEditMode.ts b/src/composables/useEditMode.ts index ddba7e1f67..aec13db6e3 100644 --- a/src/composables/useEditMode.ts +++ b/src/composables/useEditMode.ts @@ -1,19 +1,11 @@ -import { watch, Ref } from "vue"; +import { watch, ref, Ref } from "vue"; import { NoteId } from "@/type/preload"; import { NoteEditTool, PitchEditTool, SequencerEditTarget } from "@/store/type"; -import { MouseButton } from "@/sing/viewHelper"; - -// マウスダウン時の振る舞い -export type MouseDownBehavior = - | "IGNORE" - | "DESELECT_ALL" - | "ADD_NOTE" - | "START_RECT_SELECT" - | "DRAW_PITCH" - | "ERASE_PITCH"; - -// ダブルクリック時の振る舞い -export type MouseDoubleClickBehavior = "IGNORE" | "ADD_NOTE"; +import { + MouseButton, + MouseDownBehavior, + MouseDoubleClickBehavior, +} from "@/sing/viewHelper"; // 編集モードの外部コンテキスト export interface EditModeContext { @@ -33,6 +25,15 @@ export interface MouseDownBehaviorContext { } export function useEditMode(editModeContext: EditModeContext) { + const { + ctrlKey, + shiftKey, + nowPreviewing, + editTarget, + selectedNoteTool, + selectedPitchTool, + } = editModeContext; + /** * マウスダウン時の振る舞いを判定する * 条件の判定のみを行い、実際の処理は呼び出し側で行う @@ -40,37 +41,35 @@ export function useEditMode(editModeContext: EditModeContext) { const resolveMouseDownBehavior = ( mouseDownContext: MouseDownBehaviorContext, ): MouseDownBehavior => { - // マウスダウン時のコンテキストも使う - const context = { - ...editModeContext, - ...mouseDownContext, - }; + const { isSelfEventTarget, mouseButton, editingLyricNoteId } = + mouseDownContext; + // プレビュー中は無視 - if (context.nowPreviewing.value) return "IGNORE"; + if (nowPreviewing.value) return "IGNORE"; // ノート編集の場合 - if (context.editTarget.value === "NOTE") { + if (editTarget.value === "NOTE") { // イベントが来ていない場合は無視 - if (!context.isSelfEventTarget) return "IGNORE"; + if (!isSelfEventTarget) return "IGNORE"; // 歌詞編集中は無視 - if (context.editingLyricNoteId != undefined) return "IGNORE"; + if (editingLyricNoteId != undefined) return "IGNORE"; // 左クリックの場合 - if (context.mouseButton === "LEFT_BUTTON") { + if (mouseButton === "LEFT_BUTTON") { // シフトキーが押されている場合は常に矩形選択開始 - if (context.shiftKey.value) return "START_RECT_SELECT"; + if (shiftKey.value) return "START_RECT_SELECT"; // 編集優先ツールの場合 - if (context.selectedNoteTool.value === "EDIT_FIRST") { + if (selectedNoteTool.value === "EDIT_FIRST") { // コントロールキーが押されている場合は全選択解除 - if (context.ctrlKey.value) { + if (ctrlKey.value) { return "DESELECT_ALL"; } return "ADD_NOTE"; } // 選択優先ツールの場合 - if (context.selectedNoteTool.value === "SELECT_FIRST") { + if (selectedNoteTool.value === "SELECT_FIRST") { // 矩形選択開始 return "START_RECT_SELECT"; } @@ -80,19 +79,16 @@ export function useEditMode(editModeContext: EditModeContext) { } // ピッチ編集の場合 - if (context.editTarget.value === "PITCH") { + if (editTarget.value === "PITCH") { // 左クリック以外は無視 - if (context.mouseButton !== "LEFT_BUTTON") return "IGNORE"; + if (mouseButton !== "LEFT_BUTTON") return "IGNORE"; // ピッチ削除ツールが選択されているかコントロールキーが押されている場合はピッチ削除 - if ( - context.selectedPitchTool.value === "ERASE" || - context.ctrlKey.value - ) { + if (selectedPitchTool.value === "ERASE" || ctrlKey.value) { return "ERASE_PITCH"; } - // それ以外はピッチ描画 + // それ以外はピッチ編集 return "DRAW_PITCH"; } @@ -103,16 +99,13 @@ export function useEditMode(editModeContext: EditModeContext) { * ダブルクリック時の振る舞いを判定する */ const resolveDoubleClickBehavior = (): MouseDoubleClickBehavior => { - const context = { - ...editModeContext, - }; // プレビュー中は無視 - if (context.nowPreviewing.value) return "IGNORE"; + if (nowPreviewing.value) return "IGNORE"; // ノート編集の選択優先ツールではノート追加 if ( - context.editTarget.value === "NOTE" && - context.selectedNoteTool.value === "SELECT_FIRST" + editTarget.value === "NOTE" && + selectedNoteTool.value === "SELECT_FIRST" ) { return "ADD_NOTE"; } @@ -120,20 +113,33 @@ export function useEditMode(editModeContext: EditModeContext) { return "IGNORE"; }; + // Ctrlキーが押されたときにピッチツールを変更したかどうか + const toolChangedByCtrl = ref(false); + // ピッチ編集モードにおいてCtrlキーが押されたときにピッチツールを消しゴムツールにする watch( - [editModeContext.ctrlKey], + [ctrlKey], () => { // ピッチ編集モードでない場合は無視 - if (editModeContext.editTarget.value !== "PITCH") { + if (editTarget.value !== "PITCH") { return; } - // Ctrlキーが押されたとき - if (editModeContext.ctrlKey.value) { - // ピッチ描画ツールの場合はピッチ削除ツールに変更 - if (editModeContext.selectedPitchTool.value === "DRAW") { - editModeContext.selectedPitchTool.value = "ERASE"; + // 現在のツールがピッチ描画ツールの場合 + if (selectedPitchTool.value === "DRAW") { + // Ctrlキーが押されたときはピッチ削除ツールに変更 + if (ctrlKey.value) { + selectedPitchTool.value = "ERASE"; + toolChangedByCtrl.value = true; + } + } + + // 現在のツールがピッチ削除ツールかつCtrlキーが離されたとき + if (selectedPitchTool.value === "ERASE" && toolChangedByCtrl.value) { + // ピッチ描画ツールに戻す + if (!ctrlKey.value) { + selectedPitchTool.value = "DRAW"; + toolChangedByCtrl.value = false; } } }, diff --git a/src/sing/viewHelper.ts b/src/sing/viewHelper.ts index 0f86b0d1ed..fade4738cb 100644 --- a/src/sing/viewHelper.ts +++ b/src/sing/viewHelper.ts @@ -144,6 +144,23 @@ export type PreviewMode = | "DRAW_PITCH" | "ERASE_PITCH"; +// マウスダウン時の振る舞い +export const mouseDownBehaviorSchema = z.enum([ + "IGNORE", + "DESELECT_ALL", + "ADD_NOTE", + "START_RECT_SELECT", + "DRAW_PITCH", + "ERASE_PITCH", +]); +export type MouseDownBehavior = z.infer; + +// ダブルクリック時の振る舞い +export const mouseDoubleClickBehaviorSchema = z.enum(["IGNORE", "ADD_NOTE"]); +export type MouseDoubleClickBehavior = z.infer< + typeof mouseDoubleClickBehaviorSchema +>; + export function getButton(event: MouseEvent): MouseButton { // macOSの場合、Ctrl+クリックは右クリック if (isMac && event.button === 0 && event.ctrlKey) { @@ -157,3 +174,14 @@ export function getButton(event: MouseEvent): MouseButton { return "OTHER_BUTTON"; } } + +// カーソルの状態 +export const cursorStateSchema = z.enum([ + "UNSET", + "DRAW", + "MOVE", + "EW_RESIZE", + "CROSSHAIR", + "ERASE", +]); +export type CursorState = z.infer; diff --git a/src/store/type.ts b/src/store/type.ts index 1ac31760d5..936bb75a50 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -840,20 +840,15 @@ export const PhraseKey = (id: string): PhraseKey => phraseKeySchema.parse(id); // 編集対象 ノート or ピッチ // ボリュームを足すのであれば"VOLUME"を追加する -export type SequencerEditTarget = "NOTE" | "PITCH"; +export const sequencerEditTargetSchema = z.enum(["NOTE", "PITCH"]); +export type SequencerEditTarget = z.infer; + // ノート編集ツール -export type NoteEditTool = "SELECT_FIRST" | "EDIT_FIRST"; +export const noteEditToolSchema = z.enum(["SELECT_FIRST", "EDIT_FIRST"]); +export type NoteEditTool = z.infer; // ピッチ編集ツール -export type PitchEditTool = "DRAW" | "ERASE"; - -// カーソルの状態 -export type CursorState = - | "UNSET" - | "DRAW" - | "MOVE" - | "EW_RESIZE" - | "CROSSHAIR" - | "ERASE"; +export const pitchEditToolSchema = z.enum(["DRAW", "ERASE"]); +export type PitchEditTool = z.infer; export type TrackParameters = { gain: boolean; diff --git a/tests/unit/composable/useCursorState.spec.ts b/tests/unit/composable/useCursorState.spec.ts new file mode 100644 index 0000000000..2f542e25b3 --- /dev/null +++ b/tests/unit/composable/useCursorState.spec.ts @@ -0,0 +1,164 @@ +import { ref, nextTick } from "vue"; +import { + useCursorState, + CursorStateContext, +} from "@/composables/useCursorState"; +import { SequencerEditTarget, NoteEditTool, PitchEditTool } from "@/store/type"; +import { PreviewMode } from "@/sing/viewHelper"; + +describe("useCursorState", () => { + let context: CursorStateContext; + + beforeEach(() => { + // モックコンテキスト + context = { + ctrlKey: ref(false), + shiftKey: ref(false), + nowPreviewing: ref(false), + editTarget: ref("NOTE"), + selectedNoteTool: ref("EDIT_FIRST"), + selectedPitchTool: ref("DRAW"), + previewMode: ref("IDLE"), + }; + }); + + describe("プレビュー中の動作", () => { + it("ノート追加の場合はDRAW", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "ADD_NOTE"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("DRAW"); + }); + + it("ノート移動の場合はMOVE", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "MOVE_NOTE"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("MOVE"); + }); + + it("リサイズ右の場合はEW_RESIZE", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "RESIZE_NOTE_RIGHT"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("EW_RESIZE"); + }); + + it("リサイズ左の場合はEW_RESIZE", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "RESIZE_NOTE_LEFT"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("EW_RESIZE"); + }); + + it("ピッチ編集の場合はDRAW", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "DRAW_PITCH"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("DRAW"); + }); + + it("ピッチ削除の場合はERASE", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "ERASE_PITCH"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("ERASE"); + }); + + it("その他の場合はUNSET", () => { + context.nowPreviewing.value = true; + context.previewMode.value = "UNKNOWN_MODE" as PreviewMode; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("UNSET"); + }); + }); + + describe("ノート編集中の動作", () => { + beforeEach(() => { + context.editTarget.value = "NOTE"; + }); + + it("編集優先ツール選択時はDRAW", () => { + context.selectedNoteTool.value = "EDIT_FIRST"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("DRAW"); + }); + + it("編集ツール選択時でもSHIFTキー押下時はCROSSHAIR", () => { + context.selectedNoteTool.value = "EDIT_FIRST"; + context.shiftKey.value = true; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("CROSSHAIR"); + }); + + it("他のツール選択時はUNSET", () => { + context.selectedNoteTool.value = "UNKNOWN_TOOL" as NoteEditTool; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("UNSET"); + }); + }); + + describe("ピッチ編集中の動作", () => { + beforeEach(() => { + context.editTarget.value = "PITCH"; + }); + + it("ピッチ編集ツール選択時はDRAW", () => { + context.selectedPitchTool.value = "DRAW"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("DRAW"); + }); + + it("ピッチ削除ツール選択時はERASE", () => { + context.selectedPitchTool.value = "ERASE"; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("ERASE"); + }); + + it("他のツール選択時はUNSET", () => { + context.selectedPitchTool.value = "UNKNOWN_TOOL" as PitchEditTool; + + const { cursorState } = useCursorState(context); + expect(cursorState.value).toBe("UNSET"); + }); + }); + + describe("watcherの動作", () => { + it("Shiftキー押下時はCROSSHAIR", async () => { + const { cursorState } = useCursorState(context); + + context.shiftKey.value = true; + await nextTick(); + expect(cursorState.value).toBe("CROSSHAIR"); + }); + + it("CtrlキーとShiftキーが同時に押された場合はCROSSHAIR", async () => { + const { cursorState } = useCursorState(context); + context.ctrlKey.value = true; + context.shiftKey.value = true; + await nextTick(); + expect(cursorState.value).toBe("CROSSHAIR"); // Shift優先 + }); + + it("プレビュー中のモード変更時にDRAWに更新される", async () => { + const { cursorState } = useCursorState(context); + + context.nowPreviewing.value = true; + context.previewMode.value = "ADD_NOTE"; + await nextTick(); + expect(cursorState.value).toBe("DRAW"); + }); + }); +}); diff --git a/tests/unit/composable/useEditMode.spec.ts b/tests/unit/composable/useEditMode.spec.ts index cb77354103..24eb31c604 100644 --- a/tests/unit/composable/useEditMode.spec.ts +++ b/tests/unit/composable/useEditMode.spec.ts @@ -1,128 +1,296 @@ -import { ref } from "vue"; -import { describe, it, expect } from "vitest"; -import { useEditMode, EditModeState } from "@/composables/useEditMode"; +import { ref, nextTick } from "vue"; +import { + useEditMode, + EditModeContext, + MouseDownBehaviorContext, +} from "@/composables/useEditMode"; import { SequencerEditTarget, NoteEditTool, PitchEditTool } from "@/store/type"; +import { NoteId } from "@/type/preload"; describe("useEditMode", () => { - // モックステート - const createMockState = ( - overrides: Partial = {}, - ): EditModeState => { - return { - editTarget: ref("NOTE" as SequencerEditTarget), - selectedNoteTool: ref("EDIT_FIRST" as NoteEditTool), - selectedPitchTool: ref("DRAW" as PitchEditTool), - ...overrides, - }; - }; + let context: EditModeContext; - // 公開メソッドをテスト - it("初期状態ではeditTargetが'NOTE'", () => { - const state = createMockState(); - const { editTarget } = useEditMode(state); - expect(editTarget.value).toBe("NOTE"); + beforeEach(() => { + // モックコンテキスト + context = { + ctrlKey: ref(false), + shiftKey: ref(false), + nowPreviewing: ref(false), + editTarget: ref("NOTE"), + selectedNoteTool: ref("EDIT_FIRST"), + selectedPitchTool: ref("DRAW"), + }; }); - it("setEditTargetが正しく更新される", () => { - const state = createMockState(); - const { editTarget, setEditTarget } = useEditMode(state); - setEditTarget("PITCH"); - expect(editTarget.value).toBe("PITCH"); - }); + // マウスダウンの振る舞い + describe("resolveMouseDownBehavior", () => { + let mouseDownContext: MouseDownBehaviorContext; - it("editTargetの変更に応じてisNoteEditTargetが変化する", () => { - const state = createMockState(); - const { isNoteEditTarget, setEditTarget } = useEditMode(state); - expect(isNoteEditTarget.value).toBe(true); - setEditTarget("PITCH"); - expect(isNoteEditTarget.value).toBe(false); - }); + beforeEach(() => { + mouseDownContext = { + isSelfEventTarget: true, + mouseButton: "LEFT_BUTTON", + editingLyricNoteId: undefined, + } as MouseDownBehaviorContext; + }); - it("editTargetの変更に応じてisPitchEditTargetが変化する", () => { - const state = createMockState(); - const { isPitchEditTarget, setEditTarget } = useEditMode(state); - expect(isPitchEditTarget.value).toBe(false); - setEditTarget("PITCH"); - expect(isPitchEditTarget.value).toBe(true); - }); + describe("プレビュー中の動作", () => { + it("モード関係なくプレビュー中は無視", () => { + context.nowPreviewing.value = true; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("IGNORE"); + }); + }); - it("selectedNoteToolの初期値は'EDIT_FIRST'", () => { - const state = createMockState(); - const { selectedNoteTool } = useEditMode(state); - expect(selectedNoteTool.value).toBe("EDIT_FIRST"); - }); + describe("ノート編集モードでの動作", () => { + beforeEach(() => { + context.editTarget.value = "NOTE"; + }); - it("setSelectedNoteToolでツールが正しく更新される", () => { - const state = createMockState(); - const { selectedNoteTool, setSelectedNoteTool } = useEditMode(state); - setSelectedNoteTool("SELECT_FIRST"); - expect(selectedNoteTool.value).toBe("SELECT_FIRST"); - }); + it("自分からイベントがきていない場合は無視", () => { + mouseDownContext = { + ...mouseDownContext, + isSelfEventTarget: false, + } as MouseDownBehaviorContext; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("IGNORE"); + }); - it("selectedPitchToolの初期値は'DRAW'", () => { - const state = createMockState(); - const { selectedPitchTool } = useEditMode(state); - expect(selectedPitchTool.value).toBe("DRAW"); - }); + it("歌詞編集中は無視", () => { + mouseDownContext = { + ...mouseDownContext, + editingLyricNoteId: NoteId("some-id"), + } as MouseDownBehaviorContext; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("IGNORE"); + }); - it("setSelectedPitchToolでツールが正しく更新される", () => { - const state = createMockState(); - const { selectedPitchTool, setSelectedPitchTool } = useEditMode(state); - setSelectedPitchTool("ERASE"); - expect(selectedPitchTool.value).toBe("ERASE"); - }); + describe("マウスの左ボタンが押された場合", () => { + beforeEach(() => { + mouseDownContext = { + ...mouseDownContext, + mouseButton: "LEFT_BUTTON", + } as MouseDownBehaviorContext; + }); + + it("Shiftが押されている場合、常に矩形選択開始", () => { + context.shiftKey.value = true; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("START_RECT_SELECT"); + }); + + describe("選択ツールが編集優先の場合", () => { + beforeEach(() => { + context.selectedNoteTool.value = "EDIT_FIRST"; + }); - it("editTargetが'NOTE'でselectedNoteToolが'SELECT_FIRST'の場合、isNoteSelectFirstToolがtrueである", () => { - const state = createMockState({ - editTarget: ref("NOTE"), - selectedNoteTool: ref("SELECT_FIRST"), + it("ctrlが押されている場合、DESELECT_ALL を返す", () => { + context.ctrlKey.value = true; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("DESELECT_ALL"); + }); + + it("ctrlが押されていない場合、ノート追加", () => { + context.ctrlKey.value = false; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("ADD_NOTE"); + }); + }); + + describe("選択ツールが選択優先の場合", () => { + beforeEach(() => { + context.selectedNoteTool.value = "SELECT_FIRST"; + }); + + it("矩形選択開始", () => { + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("START_RECT_SELECT"); + }); + }); + + it("その他の場合、全選択解除", () => { + context.selectedNoteTool.value = "UNKNOWN_TOOL" as NoteEditTool; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("DESELECT_ALL"); + }); + }); + + it("マウスの左ボタン以外が押された場合(メニュー表示右ボタンなど)、全選択解除", () => { + mouseDownContext = { + ...mouseDownContext, + mouseButton: "RIGHT_BUTTON", + } as MouseDownBehaviorContext; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("DESELECT_ALL"); + }); }); - const { isNoteSelectFirstTool } = useEditMode(state); - expect(isNoteSelectFirstTool.value).toBe(true); - }); - it("editTargetが'NOTE'でselectedNoteToolが'EDIT_FIRST'の場合、isNoteEditFirstToolがtrueである", () => { - const state = createMockState({ - editTarget: ref("NOTE"), - selectedNoteTool: ref("EDIT_FIRST"), + describe("ピッチ編集モードでの動作", () => { + beforeEach(() => { + context.editTarget.value = "PITCH"; + }); + + it("マウスの左ボタン以外が押された場合、無視", () => { + mouseDownContext = { + ...mouseDownContext, + mouseButton: "RIGHT_BUTTON", + } as MouseDownBehaviorContext; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("IGNORE"); + }); + + describe("マウスの左ボタンが押された場合", () => { + beforeEach(() => { + mouseDownContext = { + ...mouseDownContext, + mouseButton: "LEFT_BUTTON", + } as MouseDownBehaviorContext; + }); + + it("削除ツールの場合、削除", () => { + context.selectedPitchTool.value = "ERASE"; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("ERASE_PITCH"); + }); + + it("編集ツールが選択されていてCtrlが押されていない場合、編集ツール", () => { + context.selectedPitchTool.value = "DRAW"; + context.ctrlKey.value = false; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("DRAW_PITCH"); + }); + + it("ctrlが押されている場合、削除", () => { + context.selectedPitchTool.value = "DRAW"; + context.ctrlKey.value = true; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("ERASE_PITCH"); + }); + }); }); - const { isNoteEditFirstTool } = useEditMode(state); - expect(isNoteEditFirstTool.value).toBe(true); - }); - it("editTargetが'PITCH'でselectedNoteToolが'EDIT_FIRST'の場合、isNoteEditFirstToolはfalseである", () => { - const state = createMockState({ - editTarget: ref("PITCH"), - selectedNoteTool: ref("EDIT_FIRST"), + it("いずれでもない場合は無視", () => { + context.editTarget.value = "UNKNOWN_TARGET" as SequencerEditTarget; + const { resolveMouseDownBehavior } = useEditMode(context); + const behavior = resolveMouseDownBehavior(mouseDownContext); + expect(behavior).toBe("IGNORE"); }); - const { isNoteEditFirstTool } = useEditMode(state); - expect(isNoteEditFirstTool.value).toBe(false); }); - it("editTargetが'PITCH'でselectedPitchToolが'DRAW'の場合、isPitchDrawToolがtrueである", () => { - const state = createMockState({ - editTarget: ref("PITCH"), - selectedPitchTool: ref("DRAW"), + // ダブルクリックの振る舞い + describe("resolveDoubleClickBehavior", () => { + it("プレビュー中は無視", () => { + context.nowPreviewing.value = true; + const { resolveDoubleClickBehavior } = useEditMode(context); + const behavior = resolveDoubleClickBehavior(); + expect(behavior).toBe("IGNORE"); }); - const { isPitchDrawTool } = useEditMode(state); - expect(isPitchDrawTool.value).toBe(true); - }); - it("editTargetが'PITCH'でselectedPitchToolが'ERASE'の場合、isPitchEraseToolがtrueである", () => { - const state = createMockState({ - editTarget: ref("PITCH"), - selectedPitchTool: ref("ERASE"), + it("ノート編集モードで選択ツールが選択優先の場合、ノート追加", () => { + context.editTarget.value = "NOTE"; + context.selectedNoteTool.value = "SELECT_FIRST"; + const { resolveDoubleClickBehavior } = useEditMode(context); + const behavior = resolveDoubleClickBehavior(); + expect(behavior).toBe("ADD_NOTE"); + }); + + it("ノート編集モードで選択ツールが編集優先の場合、無視", () => { + context.editTarget.value = "NOTE"; + context.selectedNoteTool.value = "EDIT_FIRST"; + const { resolveDoubleClickBehavior } = useEditMode(context); + const behavior = resolveDoubleClickBehavior(); + expect(behavior).toBe("IGNORE"); + }); + + it("ピッチ編集モードでは無視", () => { + context.editTarget.value = "PITCH"; + const { resolveDoubleClickBehavior } = useEditMode(context); + const behavior = resolveDoubleClickBehavior(); + expect(behavior).toBe("IGNORE"); }); - const { isPitchEraseTool } = useEditMode(state); - expect(isPitchEraseTool.value).toBe(true); }); - it("editTargetが'NOTE'でselectedPitchToolが'DRAW'の場合、isPitchDrawToolはfalseである", () => { - const state = createMockState({ - editTarget: ref("NOTE"), - selectedPitchTool: ref("DRAW"), + describe("watcherの動作", () => { + it("ピッチ編集モードでctrlKeyが押されたとき削除ツールに変更される", async () => { + context.editTarget.value = "PITCH"; + context.selectedPitchTool.value = "DRAW"; + context.ctrlKey.value = false; + + useEditMode(context); + + context.ctrlKey.value = true; + await nextTick(); + + expect(context.selectedPitchTool.value).toBe("ERASE"); + }); + + it("ピッチ編集モードでない場合、描画ツールは変更されない", async () => { + context.editTarget.value = "NOTE"; + context.selectedPitchTool.value = "ERASE"; + context.ctrlKey.value = false; + + useEditMode(context); + + context.ctrlKey.value = true; + await nextTick(); + + expect(context.selectedPitchTool.value).toBe("ERASE"); + }); + + it("ピッチ編集モードで編集ツールでないときctrlKeyが押されたときはなにもしない", async () => { + context.editTarget.value = "PITCH"; + context.selectedPitchTool.value = "ERASE"; + context.ctrlKey.value = false; + + useEditMode(context); + + context.ctrlKey.value = true; + await nextTick(); + + expect(context.selectedPitchTool.value).toBe("ERASE"); + }); + + it("ピッチ編集モードでCtrlキーで削除ツールにしていた場合、Ctrlキーが離されたとき描画ツールに戻る", async () => { + context.editTarget.value = "PITCH"; + context.selectedPitchTool.value = "DRAW"; + context.ctrlKey.value = false; + + useEditMode(context); + + // Ctrlキーを押す + context.ctrlKey.value = true; + await nextTick(); + expect(context.selectedPitchTool.value).toBe("ERASE"); + + // Ctrlキーを離す + context.ctrlKey.value = false; + await nextTick(); + expect(context.selectedPitchTool.value).toBe("DRAW"); + }); + it("ピッチ編集モードで削除ツール選択中だがCtrlキーによる変更でない場合、Ctrlキーが離されても描画ツールに戻らない", async () => { + context.editTarget.value = "PITCH"; + context.selectedPitchTool.value = "ERASE"; // 手動で消しゴムツールに設定した場合 + context.ctrlKey.value = true; + + useEditMode(context); + + // Ctrlキーを離す + context.ctrlKey.value = false; + await nextTick(); + expect(context.selectedPitchTool.value).toBe("ERASE"); }); - const { isPitchDrawTool } = useEditMode(state); - expect(isPitchDrawTool.value).toBe(false); }); });