Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/ui/panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
12 changes: 9 additions & 3 deletions src/ui/parts/timeStepper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
86 changes: 86 additions & 0 deletions tests/e2e/endPlaceholder.spec.js
Original file line number Diff line number Diff line change
@@ -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)
}
Loading