diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue
index 8b707c23cd..13e7bdb82d 100644
--- a/src/components/Sing/ScoreSequencer.vue
+++ b/src/components/Sing/ScoreSequencer.vue
@@ -204,7 +204,7 @@ import {
PreviewMode,
} from "@/sing/viewHelper";
import SequencerGrid from "@/components/Sing/SequencerGrid/Container.vue";
-import SequencerRuler from "@/components/Sing/SequencerRuler.vue";
+import SequencerRuler from "@/components/Sing/SequencerRuler/Container.vue";
import SequencerKeys from "@/components/Sing/SequencerKeys.vue";
import SequencerNote from "@/components/Sing/SequencerNote.vue";
import SequencerShadowNote from "@/components/Sing/SequencerShadowNote.vue";
diff --git a/src/components/Sing/SequencerRuler/Container.vue b/src/components/Sing/SequencerRuler/Container.vue
new file mode 100644
index 0000000000..e30f5db34b
--- /dev/null
+++ b/src/components/Sing/SequencerRuler/Container.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
diff --git a/src/components/Sing/SequencerRuler.vue b/src/components/Sing/SequencerRuler/Presentation.vue
similarity index 74%
rename from src/components/Sing/SequencerRuler.vue
rename to src/components/Sing/SequencerRuler/Presentation.vue
index 9ddae01849..8c078fb1c0 100644
--- a/src/components/Sing/SequencerRuler.vue
+++ b/src/components/Sing/SequencerRuler/Presentation.vue
@@ -62,60 +62,57 @@
diff --git a/src/components/Sing/SequencerRuler/index.stories.ts b/src/components/Sing/SequencerRuler/index.stories.ts
new file mode 100644
index 0000000000..345415e841
--- /dev/null
+++ b/src/components/Sing/SequencerRuler/index.stories.ts
@@ -0,0 +1,73 @@
+import type { Meta, StoryObj } from "@storybook/vue3";
+import { fn, expect, Mock } from "@storybook/test";
+import { ref } from "vue";
+
+import Presentation from "./Presentation.vue";
+import { UnreachableError } from "@/type/utility";
+
+const meta: Meta = {
+ component: Presentation,
+ args: {
+ timeSignatures: [
+ {
+ beats: 4,
+ beatType: 4,
+ measureNumber: 1,
+ },
+ ],
+ sequencerZoomX: 0.25,
+ tpqn: 480,
+ offset: 0,
+ numMeasures: 32,
+ "onUpdate:playheadTicks": fn<(value: number) => void>(),
+ onDeselectAllNotes: fn(),
+ },
+ render: (args) => ({
+ components: { Presentation },
+ setup() {
+ const playheadTicks = ref(0);
+ return { args, playheadTicks };
+ },
+ template: ``,
+ }),
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const MovePlayhead: Story = {
+ name: "再生位置を移動",
+
+ play: async ({ canvasElement, args }) => {
+ const ruler =
+ canvasElement.querySelector(".sequencer-ruler");
+
+ if (!ruler) {
+ throw new UnreachableError("ruler is not found");
+ }
+
+ // userEvent.pointerは座標指定が上手くいかないので、MouseEventを使って手動でクリックをエミュレートする
+ const rect = ruler.getBoundingClientRect();
+ const width = rect.width;
+ const event = new MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ clientX: rect.left + width / 2,
+ clientY: rect.top + rect.height,
+ });
+
+ ruler.dispatchEvent(event);
+
+ await expect(args["onUpdate:playheadTicks"]).toHaveBeenCalled();
+
+ const onUpdateCallback = args["onUpdate:playheadTicks"] as Mock<
+ (value: number) => void
+ >;
+ const newTick = onUpdateCallback.mock.calls[0][0];
+
+ await expect(newTick).toBeGreaterThan(0);
+ await expect(args["onDeselectAllNotes"]).toHaveBeenCalled();
+ },
+};
diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-storybook-win32.png"
new file mode 100644
index 0000000000..38d7351162
Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-storybook-win32.png" differ