diff --git "a/tests/e2e/browser/song/\343\202\275\343\203\263\343\202\260.spec.ts" "b/tests/e2e/browser/song/\343\202\275\343\203\263\343\202\260.spec.ts" index 06f346400b..f1e46456ba 100644 --- "a/tests/e2e/browser/song/\343\202\275\343\203\263\343\202\260.spec.ts" +++ "b/tests/e2e/browser/song/\343\202\275\343\203\263\343\202\260.spec.ts" @@ -1,15 +1,10 @@ import { test, expect, Page } from "@playwright/test"; -import { gotoHome, navigateToMain } from "../../navigators"; +import { gotoHome, navigateToSong } from "../../navigators"; +import { ensureNotNullish } from "@/helpers/errorHelper"; test.beforeEach(gotoHome); -async function navigateToSong(page: Page) { - await navigateToMain(page); - await expect(page.getByText("ソング")).toBeVisible(); - await page.getByText("ソング").click(); -} - async function getCurrentPlayhead(page: Page) { const boundingBox = await page .getByTestId("sequencer-playhead") @@ -24,7 +19,7 @@ test("再生ボタンを押して再生できる", async ({ page }) => { const sequencer = page.getByLabel("シーケンサ"); - await sequencer.click({ position: { x: 107, y: 171 } }); // ノートを追加 + await sequencer.click({ position: { x: 100, y: 171 } }); // ノートを追加 const beforePosition = await getCurrentPlayhead(page); // 再生ヘッドの初期位置 await page.getByText("play_arrow").click(); // 再生ボタンを押す await page.waitForTimeout(3000); @@ -42,56 +37,49 @@ test("ノートを追加・削除できる", async ({ page }) => { const getCurrentNoteCount = async () => await sequencer.locator(".note").count(); - // ノートの追加 - expect(await getCurrentNoteCount()).toBe(0); - await sequencer.click({ position: { x: 107, y: 171 } }); - expect(await getCurrentNoteCount()).toBe(1); - await sequencer.click({ position: { x: 200, y: 171 } }); - expect(await getCurrentNoteCount()).toBe(2); - - // ノートの削除 - expect(await getCurrentNoteCount()).toBe(2); - await sequencer.click({ position: { x: 107, y: 171 } }); - await page.keyboard.press("Delete"); - expect(await getCurrentNoteCount()).toBe(1); - await sequencer.click({ position: { x: 200, y: 171 } }); - await page.keyboard.press("Delete"); - expect(await getCurrentNoteCount()).toBe(0); + await test.step("ノートの追加", async () => { + expect(await getCurrentNoteCount()).toBe(0); + await sequencer.click({ position: { x: 100, y: 171 } }); + expect(await getCurrentNoteCount()).toBe(1); + await sequencer.click({ position: { x: 200, y: 171 } }); + expect(await getCurrentNoteCount()).toBe(2); + }); + + await test.step("ノートの削除", async () => { + expect(await getCurrentNoteCount()).toBe(2); + await sequencer.click({ position: { x: 100, y: 171 } }); + await page.keyboard.press("Delete"); + expect(await getCurrentNoteCount()).toBe(1); + await sequencer.click({ position: { x: 200, y: 171 } }); + await page.keyboard.press("Delete"); + expect(await getCurrentNoteCount()).toBe(0); + }); }); -test("ダブルクリックで歌詞を編集できる", async ({ page }) => { +test("ドラッグで長いノートを追加できる", async ({ page }) => { await navigateToSong(page); const sequencer = page.getByLabel("シーケンサ"); - const getCurrentNoteLyric = async () => - await sequencer.locator(".note-lyric").first().textContent(); - - // ノートを追加し、表示されるまで待つ - await sequencer.click({ position: { x: 107, y: 171 } }); - await page.waitForSelector(".note"); - - // ノートの歌詞を取得 - const note = sequencer.locator(".note").first(); - const beforeLyric = await getCurrentNoteLyric(); + await test.step("クリックで短いノートを追加", async () => { + await sequencer.click({ position: { x: 100, y: 171 } }); + }); - // ノートをダブルクリックし、入力フィールドが表示されるまで待つ - await note.dblclick(); - await page.waitForSelector(".lyric-input"); + await test.step("ドラッグで長いノートを追加", async () => { + const startPos = { x: 200, y: 171 }; + const endPos = { x: 400, y: 171 }; + await sequencer.hover({ position: startPos }); + await page.mouse.down(); + await page.mouse.move(endPos.x, endPos.y); + await page.mouse.up(); + }); - // 歌詞を入力し、Enterキーを押す - const lyricInput = sequencer.locator(".lyric-input"); - await lyricInput.fill("あ"); - await lyricInput.press("Enter"); + await test.step("ノートが2つ表示されるのを待ち、2つ目のノートが長いことを確認", async () => { + const notes = sequencer.locator(".note"); + await expect(notes).toHaveCount(2); - // 変更が反映されるまで待つ - await page.waitForFunction(() => { - const lyricElement = document.querySelector(".note-lyric"); - return lyricElement && lyricElement.textContent === "あ"; + const firstNoteBox = ensureNotNullish(await notes.nth(0).boundingBox()); + const secondNoteBox = ensureNotNullish(await notes.nth(1).boundingBox()); + expect(secondNoteBox.width).toBeGreaterThanOrEqual(firstNoteBox.width * 2); }); - - // 歌詞が変更されたことを確認 - const afterLyric = await getCurrentNoteLyric(); - expect(afterLyric).not.toEqual(beforeLyric); - expect(afterLyric).toEqual("あ"); }); diff --git "a/tests/e2e/browser/song/\346\255\214\350\251\236.spec.ts" "b/tests/e2e/browser/song/\346\255\214\350\251\236.spec.ts" new file mode 100644 index 0000000000..7e36664e50 --- /dev/null +++ "b/tests/e2e/browser/song/\346\255\214\350\251\236.spec.ts" @@ -0,0 +1,100 @@ +import { test, expect, Page, Locator } from "@playwright/test"; + +import { gotoHome, navigateToSong } from "../../navigators"; +import { ensureNotNullish } from "@/helpers/errorHelper"; + +test.beforeEach(gotoHome); + +function getSequencer(page: Page) { + return page.getByLabel("シーケンサ"); +} + +async function addNotes(page: Page, count: number) { + await test.step(`ノートを ${count} つ追加`, async () => { + const sequencer = getSequencer(page); + for (let i = 0; i < count; i++) { + await sequencer.click({ position: { x: (i + 1) * 100, y: 171 } }); + } + const notes = sequencer.locator(".note"); + await expect(notes).toHaveCount(count); + }); +} + +async function toSortedLocator(locators: Locator[]): Promise { + const locatorsWithPosition = await Promise.all( + locators.map(async (locator) => ({ + locator, + x: ensureNotNullish(await locator.boundingBox()).x, + })), + ); + locatorsWithPosition.sort((a, b) => a.x - b.x); + return locatorsWithPosition.map(({ locator }) => locator); +} + +async function getSortedNotes(page: Page): Promise { + return await test.step("ノートをソートして取得", async () => { + const sequencer = getSequencer(page); + const notes = await sequencer.locator(".note").all(); + return toSortedLocator(notes); + }); +} + +async function getSortedNoteLylics(page: Page): Promise { + return await test.step("ノートをソートして歌詞を取得", async () => { + const sequencer = getSequencer(page); + const lyrics = await sequencer.locator(".note-lyric").all(); + const sortedLyrics = await toSortedLocator(lyrics); + return Promise.all( + sortedLyrics.map(async (lyric) => + ensureNotNullish(await lyric.textContent()), + ), + ); + }); +} + +async function editNoteLyric(page: Page, note: Locator, lyric: string) { + await test.step("ノートをダブルクリックして歌詞を入力", async () => { + await note.dblclick(); + + const sequencer = getSequencer(page); + const lyricInput = sequencer.locator(".lyric-input"); + await expect(lyricInput).toBeVisible(); + await lyricInput.fill(lyric); + await lyricInput.press("Enter"); + await expect(lyricInput).not.toBeVisible(); + }); +} + +test("ダブルクリックで歌詞を編集できる", async ({ page }) => { + await navigateToSong(page); + + await addNotes(page, 1); + const note = (await getSortedNotes(page))[0]; + const beforeLyric = (await getSortedNoteLylics(page))[0]; + + await editNoteLyric(page, note, "あ"); + + await test.step("歌詞が変更されていることを確認", async () => { + const afterLyric = await getSortedNoteLylics(page); + expect(afterLyric[0]).not.toEqual(beforeLyric); + expect(afterLyric[0]).toEqual("あ"); + }); +}); + +test("複数ノートの歌詞を一度に編集できる", async ({ page }) => { + await navigateToSong(page); + + await addNotes(page, 3); + + await editNoteLyric(page, (await getSortedNotes(page))[0], "あいう"); + await test.step("全てのノートの歌詞が変更されていることを確認", async () => { + const afterLyrics = await getSortedNoteLylics(page); + expect(afterLyrics).toEqual(["あ", "い", "う"]); + }); + + await editNoteLyric(page, (await getSortedNotes(page))[0], "かきくけこ"); + await test.step("最後のノートに残りの文字が入力されていることを確認", async () => { + const afterLyrics = await getSortedNoteLylics(page); + expect(afterLyrics).toEqual(["か", "き", "くけこ"]); + }); +}); diff --git a/tests/e2e/navigators.ts b/tests/e2e/navigators.ts index 614a16e612..20a0fbbe1a 100644 --- a/tests/e2e/navigators.ts +++ b/tests/e2e/navigators.ts @@ -1,66 +1,73 @@ -import { expect, Page } from "@playwright/test"; +import { expect, Locator, Page, test } from "@playwright/test"; import { getNewestQuasarDialog, getQuasarMenu } from "./locators"; -/** - * 最初の画面に移動 - */ export async function gotoHome({ page }: { page: Page }) { - const BASE_URL = "http://localhost:7357/"; - await page.setViewportSize({ width: 1024, height: 630 }); - await page.goto(BASE_URL); + await test.step("最初の画面に移動", async () => { + const BASE_URL = "http://localhost:7357/"; + await page.setViewportSize({ width: 1024, height: 630 }); + await page.goto(BASE_URL); + }); } -/** - * 初回起動時の確認を完了してメイン画面に移動 - */ export async function navigateToMain(page: Page) { - await expect(page.getByText("利用規約に関するお知らせ")).toBeVisible({ - timeout: 90 * 1000, + await test.step("初回起動時の確認を完了してメイン画面に移動", async () => { + await expect(page.getByText("利用規約に関するお知らせ")).toBeVisible({ + timeout: 90 * 1000, + }); + await page.waitForTimeout(100); + await page.getByRole("button", { name: "同意して使用開始" }).click(); + await page.waitForTimeout(100); + await page.getByRole("button", { name: "完了" }).click(); + await page.waitForTimeout(100); + await page.getByRole("button", { name: "許可" }).click(); + await page.waitForTimeout(100); }); - await page.waitForTimeout(100); - await page.getByRole("button", { name: "同意して使用開始" }).click(); - await page.waitForTimeout(100); - await page.getByRole("button", { name: "完了" }).click(); - await page.waitForTimeout(100); - await page.getByRole("button", { name: "許可" }).click(); - await page.waitForTimeout(100); } -/** - * 特定の設定をトグルする - */ export async function toggleSetting(page: Page, settingName: string) { - await page.getByRole("button", { name: "設定" }).click(); - await page.waitForTimeout(100); - await page.getByText("オプション").click(); - await page.waitForTimeout(100); - await page - .locator(".row-card", { - has: page.getByText(settingName), - }) - .click(); - await page.waitForTimeout(100); - await page.getByRole("button", { name: "設定を閉じる" }).click(); + await test.step(`設定 ${settingName} をトグルする`, async () => { + await page.getByRole("button", { name: "設定" }).click(); + await page.waitForTimeout(100); + await page.getByText("オプション").click(); + await page.waitForTimeout(100); + await page + .locator(".row-card", { + has: page.getByText(settingName), + }) + .click(); + await page.waitForTimeout(100); + await page.getByRole("button", { name: "設定を閉じる" }).click(); + }); await page.waitForTimeout(500); } -/** - * ヘルプダイアログの表示まで移動 - */ -export async function navigateToHelpDialog(page: Page) { - await navigateToMain(page); - await page.waitForTimeout(100); - await page.getByRole("button", { name: "ヘルプ" }).click(); - return getNewestQuasarDialog(page); +export async function navigateToHelpDialog(page: Page): Promise { + return await test.step("ヘルプダイアログの表示まで移動", async () => { + await navigateToMain(page); + await page.waitForTimeout(100); + await page.getByRole("button", { name: "ヘルプ" }).click(); + return getNewestQuasarDialog(page); + }); +} + +export async function navigateToSettingDialog(page: Page): Promise { + return await test.step("設定ダイアログの表示まで移動", async () => { + await navigateToMain(page); + await page.waitForTimeout(100); + await page.getByRole("button", { name: "設定" }).click(); + await getQuasarMenu(page, "オプション").click(); + return getNewestQuasarDialog(page); + }); } -/** - * 設定ダイアログの表示まで移動 - */ -export async function navigateToSettingDialog(page: Page) { - await navigateToMain(page); - await page.waitForTimeout(100); - await page.getByRole("button", { name: "設定" }).click(); - await getQuasarMenu(page, "オプション").click(); - return getNewestQuasarDialog(page); +export async function navigateToSong(page: Page) { + await test.step("ソング画面に移動", async () => { + await navigateToMain(page); + await expect(page.getByText("ソング")).toBeVisible(); + await page.getByText("ソング").click(); + + // 見やすいようにスナップを1/8に変更 + await page.getByLabel("スナップ").click(); + await page.getByRole("option", { name: "1/8", exact: true }).click(); + }); }