diff --git a/src/backend/browser/fileImpl.ts b/src/backend/browser/fileImpl.ts index d60f85f30f..0dab45ac29 100644 --- a/src/backend/browser/fileImpl.ts +++ b/src/backend/browser/fileImpl.ts @@ -4,6 +4,7 @@ import { openDB } from "./browserConfig"; import { SandboxKey } from "@/type/preload"; import { failure, success } from "@/type/result"; import { createLogger } from "@/domain/frontend/log"; +import { uuid4 } from "@/helpers/random"; const log = createLogger("fileImpl"); @@ -200,7 +201,7 @@ export const showOpenFilePickerImpl = async (options: { }); const paths = []; for (const handle of handles) { - const fakePath = `-${handle.name}`; + const fakePath = `-${handle.name}`; fileHandleMap.set(fakePath, handle); paths.push(fakePath); } diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 5a8a3c6514..a3717121ab 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -193,6 +193,7 @@ import { import { applyGaussianFilter, linearInterpolation } from "@/sing/utility"; import { useLyricInput } from "@/composables/useLyricInput"; import { ExhaustiveError } from "@/type/utility"; +import { uuid4 } from "@/helpers/random"; type PreviewMode = | "ADD_NOTE" @@ -720,7 +721,7 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => { return; } note = { - id: NoteId(crypto.randomUUID()), + id: NoteId(uuid4()), position: guideLineTicks, duration: snapTicks.value, noteNumber: cursorNoteNumber, diff --git a/src/helpers/random.ts b/src/helpers/random.ts new file mode 100644 index 0000000000..62e41e1ca1 --- /dev/null +++ b/src/helpers/random.ts @@ -0,0 +1,27 @@ +/** + * 乱数値を生成する。モックに対応している。 + * モックモードでは呼ばれた回数に応じて固定の値を返す。 + */ + +let mockMode = false; +let mockCount = 0; + +/** + * モックモードにし、呼ばれた回数をリセットする。 + */ +export function resetMockMode(): void { + mockMode = true; + mockCount = 0; +} + +/** + * v4 UUID を生成する。 + */ +export function uuid4(): string { + if (!mockMode) { + return crypto.randomUUID(); + } else { + mockCount++; + return `00000000-0000-4000-0000-${mockCount.toString().padStart(12, "0")}`; + } +} diff --git a/src/sing/utaformatixProject/toVoicevox.ts b/src/sing/utaformatixProject/toVoicevox.ts index f3e2448d67..04a80a6244 100644 --- a/src/sing/utaformatixProject/toVoicevox.ts +++ b/src/sing/utaformatixProject/toVoicevox.ts @@ -4,6 +4,7 @@ import { DEFAULT_TPQN, createDefaultTrack } from "@/sing/domain"; import { getDoremiFromNoteNumber } from "@/sing/viewHelper"; import { NoteId } from "@/type/preload"; import { Note, Tempo, TimeSignature, Track } from "@/store/type"; +import { uuid4 } from "@/helpers/random"; /** UtaformatixのプロジェクトをVoicevoxの楽譜データに変換する */ export const ufProjectToVoicevox = (project: UfProject): VoicevoxScore => { @@ -72,7 +73,7 @@ export const ufProjectToVoicevox = (project: UfProject): VoicevoxScore => { const notes = trackNotes.map((value): Note => { return { - id: NoteId(crypto.randomUUID()), + id: NoteId(uuid4()), position: convertPosition(value.tickOn, projectTpqn, tpqn), duration: convertDuration( value.tickOn, diff --git a/src/store/audio.ts b/src/store/audio.ts index 343ae6e8f8..df75e2cf66 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -58,9 +58,10 @@ import { AudioQuery, AccentPhrase, Speaker, SpeakerInfo } from "@/openapi"; import { base64ImageToUri, base64ToUri } from "@/helpers/base64Helper"; import { getValueOrThrow, ResultError } from "@/type/result"; import { generateWriteErrorMessage } from "@/helpers/fileHelper"; +import { uuid4 } from "@/helpers/random"; function generateAudioKey() { - return AudioKey(crypto.randomUUID()); + return AudioKey(uuid4()); } function parseTextFile( diff --git a/src/store/command.ts b/src/store/command.ts index 9af3013e82..2842ee4185 100644 --- a/src/store/command.ts +++ b/src/store/command.ts @@ -10,6 +10,7 @@ import { MutationTree, } from "@/store/vuex"; import { CommandId, EditorType } from "@/type/preload"; +import { uuid4 } from "@/helpers/random"; enablePatches(); enableMapSet(); @@ -67,7 +68,7 @@ const recordPatches = (draft: S) => recipe(draft, payload), ); return { - id: CommandId(crypto.randomUUID()), + id: CommandId(uuid4()), redoPatches: doPatches, undoPatches: undoPatches, }; diff --git a/src/store/preset.ts b/src/store/preset.ts index 7ceaa9ccf9..a4981632f9 100644 --- a/src/store/preset.ts +++ b/src/store/preset.ts @@ -1,4 +1,5 @@ import { createPartialStore } from "./vuex"; +import { uuid4 } from "@/helpers/random"; import { PresetStoreState, PresetStoreTypes, State } from "@/store/type"; import { Preset, PresetKey, Voice, VoiceId } from "@/type/preload"; @@ -181,7 +182,7 @@ export const presetStore = createPartialStore({ ADD_PRESET: { async action(context, { presetData }: { presetData: Preset }) { - const newKey = PresetKey(crypto.randomUUID()); + const newKey = PresetKey(uuid4()); const newPresetItems = { ...context.state.presetItems, [newKey]: presetData, diff --git a/src/store/singing.ts b/src/store/singing.ts index ba738ddc3e..fbc4a2896e 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -85,6 +85,7 @@ import { createLogger } from "@/domain/frontend/log"; import { noteSchema } from "@/domain/project/schema"; import { getOrThrow } from "@/helpers/mapHelper"; import { ufProjectToVoicevox } from "@/sing/utaformatixProject/toVoicevox"; +import { uuid4 } from "@/helpers/random"; const logger = createLogger("store/singing"); @@ -1724,7 +1725,7 @@ export const singingStore = createPartialStore({ const track = tracks[trackIndex]; const notes = track.notes.map((note) => ({ ...note, - id: NoteId(crypto.randomUUID()), + id: NoteId(uuid4()), })); if (tpqn !== state.tpqn) { @@ -2144,7 +2145,7 @@ export const singingStore = createPartialStore({ const quantizedPastePos = Math.round(pasteOriginPos / snapTicks) * snapTicks; return { - id: NoteId(crypto.randomUUID()), + id: NoteId(uuid4()), position: quantizedPastePos, duration: Number(note.duration), noteNumber: Number(note.noteNumber), diff --git a/tests/unit/lib/selectPriorPhrase.spec.ts b/tests/unit/lib/selectPriorPhrase.spec.ts index 4f8b2e2580..cebfdcaca6 100644 --- a/tests/unit/lib/selectPriorPhrase.spec.ts +++ b/tests/unit/lib/selectPriorPhrase.spec.ts @@ -7,6 +7,7 @@ import { } from "@/store/type"; import { DEFAULT_TPQN, selectPriorPhrase } from "@/sing/domain"; import { NoteId } from "@/type/preload"; +import { uuid4 } from "@/helpers/random"; const createPhrase = ( firstRestDuration: number, @@ -18,7 +19,7 @@ const createPhrase = ( firstRestDuration: firstRestDuration * DEFAULT_TPQN, notes: [ { - id: NoteId(crypto.randomUUID()), + id: NoteId(uuid4()), position: start * DEFAULT_TPQN, duration: (end - start) * DEFAULT_TPQN, noteNumber: 60, diff --git a/tests/unit/lib/utaformatixProject/export.spec.ts b/tests/unit/lib/utaformatixProject/export.spec.ts index 1fad3469b5..5193905dc7 100644 --- a/tests/unit/lib/utaformatixProject/export.spec.ts +++ b/tests/unit/lib/utaformatixProject/export.spec.ts @@ -6,8 +6,9 @@ import { createDefaultTrack, } from "@/sing/domain"; import { NoteId } from "@/type/preload"; +import { uuid4 } from "@/helpers/random"; -const createNoteId = () => NoteId(crypto.randomUUID()); +const createNoteId = () => NoteId(uuid4()); it("トラックを変換できる", async () => { const track = createDefaultTrack(); diff --git a/tests/unit/store/__snapshots__/command.spec.ts.snap b/tests/unit/store/__snapshots__/command.spec.ts.snap new file mode 100644 index 0000000000..6d2a116d0c --- /dev/null +++ b/tests/unit/store/__snapshots__/command.spec.ts.snap @@ -0,0 +1,41 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`コマンド実行で履歴が作られる 1`] = ` +{ + "audioKeys": [ + "00000000-0000-4000-0000-000000000001", + ], + "redoCommands": { + "song": [], + "talk": [], + }, + "undoCommands": { + "song": [], + "talk": [ + { + "id": "00000000-0000-4000-0000-000000000002", + "redoPatches": [ + { + "op": "replace", + "path": [ + "audioKeys", + ], + "value": [ + "00000000-0000-4000-0000-000000000001", + ], + }, + ], + "undoPatches": [ + { + "op": "replace", + "path": [ + "audioKeys", + ], + "value": [], + }, + ], + }, + ], + }, +} +`; diff --git a/tests/unit/store/command.spec.ts b/tests/unit/store/command.spec.ts new file mode 100644 index 0000000000..4ee14a1068 --- /dev/null +++ b/tests/unit/store/command.spec.ts @@ -0,0 +1,19 @@ +import { toRaw } from "vue"; +import { store } from "@/store"; +import { AudioKey } from "@/type/preload"; +import { resetMockMode, uuid4 } from "@/helpers/random"; + +const initialState = structuredClone(toRaw(store.state)); +beforeEach(() => { + store.replaceState(initialState); + + resetMockMode(); +}); + +test("コマンド実行で履歴が作られる", async () => { + await store.dispatch("COMMAND_SET_AUDIO_KEYS", { + audioKeys: [AudioKey(uuid4())], + }); + const { audioKeys, redoCommands, undoCommands } = store.state; + expect({ audioKeys, redoCommands, undoCommands }).toMatchSnapshot(); +}); diff --git a/tests/unit/store/utility.spec.ts b/tests/unit/store/utility.spec.ts index c4a8907dc9..cb6430de9e 100644 --- a/tests/unit/store/utility.spec.ts +++ b/tests/unit/store/utility.spec.ts @@ -22,6 +22,7 @@ import { isOnCommandOrCtrlKeyDown, filterCharacterInfosByStyleType, } from "@/store/utility"; +import { uuid4 } from "@/helpers/random"; function createDummyMora(text: string): Mora { return { @@ -305,13 +306,13 @@ describe("filterCharacterInfosByStyleType", () => { const createCharacterInfo = ( styleTypes: (undefined | "talk" | "frame_decode" | "sing")[], ): CharacterInfo => { - const engineId = EngineId(crypto.randomUUID()); + const engineId = EngineId(uuid4()); return { portraitPath: "path/to/portrait", metas: { policy: "policy", speakerName: "speakerName", - speakerUuid: SpeakerId(crypto.randomUUID()), + speakerUuid: SpeakerId(uuid4()), styles: styleTypes.map((styleType) => ({ styleType, styleName: "styleName",