From 349168118bbbeffec8f9b152f3928ee1b9a79ddc Mon Sep 17 00:00:00 2001 From: Jang Date: Thu, 10 Sep 2026 20:23:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?test:=20=EB=B9=88=20=EB=81=9D=20=EC=B9=B8?= =?UTF-8?q?=EC=9D=98=20=EC=95=88=EB=82=B4=20=EA=B8=80=EC=94=A8=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=EB=A5=BC=20=EB=A8=BC=EC=A0=80=20=EC=9E=A0=EA=B7=BC?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CLN2x7BatxttSEsxhfLhqA --- tests/e2e/endPlaceholder.spec.js | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/e2e/endPlaceholder.spec.js diff --git a/tests/e2e/endPlaceholder.spec.js b/tests/e2e/endPlaceholder.spec.js new file mode 100644 index 0000000..8975557 --- /dev/null +++ b/tests/e2e/endPlaceholder.spec.js @@ -0,0 +1,86 @@ +import { test, expect } from '../fixtures/extensionContext.js' + +// 둘째 곡은 뒤에 트랙이 있고 셋째 곡은 마지막 트랙이다. 안내 글씨가 갈리는 두 경우가 한 목록에 있다. +const TIMELINE_COMMENT = ['00:00 첫 곡', '05:00 둘째 곡', '10:00 셋째 곡'].join('\n') + +const PANEL = '#timeline-skip-panel' +const END_INPUT = `${PANEL} .timeline-skip-end-input` + +test.describe('끝 칸의 안내 글씨', () => { + test('끝을 정하지 않은 트랙은 끝 칸의 안내 글씨가 다음 트랙 시작 시각이다', async ({ openWatchPage }) => { + const page = await openTimeline(openWatchPage) + + await openEditRow(page, '둘째 곡') + + await expect(page.locator(END_INPUT)).toHaveAttribute('placeholder', '10:00') + }) + + test('끝을 정해 둔 트랙도 안내 글씨는 다음 트랙 시작 시각이다', async ({ openWatchPage }) => { + const page = await openTimeline(openWatchPage) + await openEditRow(page, '첫 곡') + await page.locator(END_INPUT).fill('3:00') + await page.locator(`${PANEL} button[aria-label="저장"]`).click() + await expect(page.locator(`${PANEL} .timeline-skip-time`).first()).toHaveText('00:00 ~ 03:00') + + await openEditRow(page, '첫 곡') + + await expect(page.locator(END_INPUT)).toHaveAttribute('placeholder', '05:00') + }) + + test('마지막 트랙은 끝 칸의 안내 글씨가 영상 끝 시각이다', async ({ openWatchPage }) => { + const page = await openTimeline(openWatchPage) + await waitForDuration(page) + + await openEditRow(page, '셋째 곡') + + await expect(page.locator(END_INPUT)).toHaveAttribute('placeholder', '30:00') + }) + + test('영상 길이를 모르면 마지막 트랙의 안내 글씨는 끝까지다', async ({ openWatchPage }) => { + const page = await openTimeline(openWatchPage, { videoSourceUrl: null }) + + await openEditRow(page, '셋째 곡') + + await expect(page.locator(END_INPUT)).toHaveAttribute('placeholder', '끝까지') + }) + + test('추가 행의 끝 칸은 재생 위치 다음 트랙의 시작 시각을 안내한다', async ({ openWatchPage }) => { + const page = await openTimeline(openWatchPage) + await pauseAt(page, 120) + + await page.locator(`${PANEL} .timeline-skip-add`).click() + + await expect(page.locator(END_INPUT)).toHaveAttribute('placeholder', '05:00') + }) +}) + +async function openTimeline(openWatchPage, fixture = {}) { + const { page } = await openWatchPage({ commentTexts: [TIMELINE_COMMENT], ...fixture }) + await page.locator('.timeline-skip-load-button').first().click() + await expect(page.locator('.timeline-skip-row').first()).toBeVisible() + + return page +} + +async function openEditRow(page, title) { + await page.locator(`${PANEL} button[aria-label="${title} 수정"]`).click() + await expect(page.locator(END_INPUT)).toBeVisible() +} + +// 안내 글씨는 편집을 열 때 정해진다. 영상 길이를 읽기 전에 열면 마지막 트랙은 끝을 모른다. +async function waitForDuration(page) { + await expect.poll(() => page.evaluate(() => document.querySelector('video').duration)).toBe(1800) +} + +// 재생 중이면 초가 흘러 추가 행이 잡는 시각이 흔들린다. 멈춘 채로 옮겨 둔다. +async function pauseAt(page, timestampSeconds) { + await expect + .poll(() => page.evaluate(() => document.querySelector('video').readyState)) + .toBeGreaterThanOrEqual(1) + + await page.evaluate((seconds) => { + const video = document.querySelector('video') + video.pause() + video.currentTime = seconds + }, timestampSeconds) +} From 982ca108e9449dd2808acdd389b5f622d13a51f6 Mon Sep 17 00:00:00 2001 From: Jang Date: Thu, 10 Sep 2026 20:25:27 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=B9=88=20=EB=81=9D=20=EC=B9=B8?= =?UTF-8?q?=EC=97=90=20=EC=8B=A4=EC=A0=9C=EB=A1=9C=20=EB=81=9D=EB=82=98?= =?UTF-8?q?=EB=8A=94=20=EC=8B=9C=EA=B0=81=EC=9D=84=20=ED=9D=90=EB=A6=AC?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=B4=EC=97=AC=EC=A4=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CLN2x7BatxttSEsxhfLhqA --- src/ui/panel.css | 5 +++++ src/ui/parts/timeStepper.js | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ui/panel.css b/src/ui/panel.css index 861e19d..09e55a2 100644 --- a/src/ui/panel.css +++ b/src/ui/panel.css @@ -205,6 +205,11 @@ min-width: 0; } +/* 흐린 글씨는 정해 둔 값이 아니라, 비워 두면 끝나는 시각이다. 둘은 색으로 갈린다. */ +.timeline-skip-end-input::placeholder { + color: var(--yt-spec-text-secondary, #909090); +} + .timeline-skip-stepper { padding: 0 8px 2px; } diff --git a/src/ui/parts/timeStepper.js b/src/ui/parts/timeStepper.js index b62663c..8a048ed 100644 --- a/src/ui/parts/timeStepper.js +++ b/src/ui/parts/timeStepper.js @@ -18,7 +18,7 @@ const STEPS = [ // 시작·끝 칸과 그 옆의 조정 버튼. 마우스만으로 편집을 끝낼 수 있게 하려는 것이라 직접 입력도 그대로 받는다. // 칸 값은 저장할 때 편집 폼이 읽으므로 칸도 함께 돌려준다. export function createTimeStepper(draft, view) { - const context = { draft, view, startInput: createStartInput(draft), endInput: createEndInput(draft) } + const context = { draft, view, startInput: createStartInput(draft), endInput: createEndInput(draft, view) } const element = document.createElement('div') element.className = 'timeline-skip-stepper' @@ -116,14 +116,20 @@ function createStartInput(draft) { // 파생된 끝(다음 트랙까지)은 비워둔 채로 연다. 미리 채우면 제목만 고쳐 저장해도 그때의 // 파생값이 끝으로 굳어, 나중에 이웃 트랙을 옮겼을 때 없던 빈 구간이 생긴다. -function createEndInput(draft) { +// 대신 그 시각을 안내 글씨로 보여준다. 안내 글씨는 값이 아니라서 저장되지 않는다. +function createEndInput(draft, view) { const input = createInput({ className: 'timeline-skip-end-input', value: Number.isFinite(draft.trimmedEndSeconds) ? formatTimestamp(draft.trimmedEndSeconds) : '' }) - input.placeholder = '다음 트랙까지' + input.placeholder = toEndHint(findRange({ draft, view }, draft.startSeconds).toSeconds) input.title = END_HINT input.setAttribute('aria-label', '끝 시각') return input } + +// 영상 길이를 모르는 마지막 트랙은 끝나는 시각이 없다. 영상이 끝날 때까지 재생된다. +function toEndHint(toSeconds) { + return toSeconds === null ? '끝까지' : formatTimestamp(toSeconds) +}