v1.12.30 긴 접속 시간 표시와 중단 후 새 방문 처리 개선

This commit is contained in:
2026-10-21 18:45:44 +09:00
parent 51d5de148f
commit 7b6fc45bf3
19 changed files with 310 additions and 68 deletions

View File

@@ -16,7 +16,7 @@ test('대시보드 숫자·섹션 순서와 한 줄 접속자 페이지 목록
sources: [], devices: [], topPosts: [], topPages: []
} }))
const session = {
activeKey: 'test-visitor', isLoggedIn: false, ipAddress: '198.51.100.1', durationSeconds: 1800, pageCount: 6,
activeKey: 'test-visitor', isLoggedIn: false, ipAddress: '198.51.100.1', durationSeconds: 4920, pageCount: 6,
pages: [
{ key: 'old', path: '/post/old', postTitle: '이전 페이지', lastSeenAt: '2026-10-21T01:00:00Z' },
{ key: 'new', path: '/post/new', postTitle: '가장 최근 페이지', lastSeenAt: '2026-10-21T06:00:00Z' },
@@ -42,7 +42,8 @@ test('대시보드 숫자·섹션 순서와 한 줄 접속자 페이지 목록
await expect(visitor.locator('.admin-realtime-pages__extra')).toHaveCSS('color', 'rgb(37, 99, 235)')
await expect(visitor.locator('.admin-realtime-pages__extra-number')).toHaveText('4')
await expect(visitor.locator('.admin-realtime-pages__extra-number')).toHaveCSS('font-weight', '700')
await expect(visitor.locator('.admin-dashboard__live-duration')).toHaveAttribute('title', /백그라운드 시간을 포함하며 최대 30분/)
await expect(visitor.locator('.admin-dashboard__live-duration')).toHaveText('1시간 22분')
await expect(visitor.locator('.admin-dashboard__live-duration')).toHaveAttribute('title', /30분 이상 중단되면 새 방문으로 시작/)
expect((await visitor.boundingBox()).height).toBeLessThan(45)
const trigger = visitor.getByRole('button', { name: '열린 페이지 목록', exact: true })
await trigger.hover()
@@ -65,3 +66,58 @@ test('대시보드 숫자·섹션 순서와 한 줄 접속자 페이지 목록
await expect(singleVisitor.getByRole('link', { name: '한 페이지만 보는 방문자' })).toHaveAttribute('href', '/post/single')
await expect(popover).toHaveCount(0)
})
test('공개 페이지는 짧은 실행 중단을 제외하고 긴 중단 후 새 방문으로 시작한다', async ({ page }) => {
const login = await page.request.post('/admin/api/auth/login', {
data: { email: 'playwright-admin@example.com', password: 'playwright-admin-password' }
})
expect(login.ok()).toBeTruthy()
const created = await page.request.post('/admin/api/posts', {
data: { title: '접속 시간 검증', slug: 'duration-ui-test', content: '시간 측정 본문', status: 'published', tags: [], tagNames: {} }
})
expect(created.ok()).toBeTruthy()
const post = await created.json()
try {
const heartbeats = []
const pageviews = []
await page.clock.install({ time: new Date('2026-10-21T00:00:00Z') })
await page.route('**/api/analytics/heartbeat', async (route) => {
heartbeats.push(route.request().postDataJSON())
await route.fulfill({ json: { ok: true } })
})
await page.route('**/api/analytics/pageview', async (route) => {
pageviews.push(route.request().postDataJSON())
await route.fulfill({ json: { ok: true } })
})
await page.goto('/post/duration-ui-test')
await expect.poll(() => heartbeats.length).toBeGreaterThan(0)
await page.clock.pauseAt(new Date('2026-10-21T00:01:00Z'))
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight))
await page.evaluate(() => document.dispatchEvent(new Event('visibilitychange')))
await expect.poll(() => heartbeats.at(-1)?.durationSeconds).toBeGreaterThan(0)
const initialDuration = heartbeats.at(-1).durationSeconds
await page.clock.runFor(40000)
await expect.poll(() => heartbeats.at(-1)?.durationSeconds).toBe(initialDuration + 40)
const initialContentSession = heartbeats.at(-1).contentSessionId
const beforeSleepCount = heartbeats.length
await page.clock.fastForward(120000)
await expect.poll(() => heartbeats.length).toBeGreaterThan(beforeSleepCount)
expect(heartbeats.at(-1).durationSeconds).toBe(initialDuration + 40)
expect(heartbeats.at(-1).contentSessionId).toBe(initialContentSession)
await page.clock.runFor(20000)
await expect.poll(() => heartbeats.at(-1)?.durationSeconds).toBe(initialDuration + 60)
const beforeRestartViews = pageviews.filter((entry) => !entry.read).length
await page.clock.fastForward(2 * 86400000)
await expect.poll(() => heartbeats.at(-1)?.durationSeconds).toBe(0)
expect(heartbeats.at(-1).contentSessionId).not.toBe(initialContentSession)
await expect.poll(() => pageviews.filter((entry) => !entry.read).length).toBe(beforeRestartViews + 1)
expect(pageviews.at(-1).contentSessionId).toBe(heartbeats.at(-1).contentSessionId)
const restartedContentSession = heartbeats.at(-1).contentSessionId
expect(pageviews.filter((entry) => entry.read && entry.contentSessionId === restartedContentSession)).toHaveLength(0)
await page.clock.runFor(20000)
await expect.poll(() => heartbeats.at(-1)?.durationSeconds).toBe(20)
await expect.poll(() => pageviews.filter((entry) => entry.read && entry.contentSessionId === restartedContentSession).length).toBe(1)
} finally {
await page.request.delete(`/admin/api/posts/${post.id}`)
}
})