Files
twitter-lite/tests/e2e/reader.spec.ts
T

207 lines
7.6 KiB
TypeScript

import { expect, type Locator, test } from '@playwright/test'
let consoleErrors: string[]
let pageErrors: string[]
test.beforeEach(async ({ page }) => {
consoleErrors = []
pageErrors = []
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text())
})
page.on('pageerror', (error) => pageErrors.push(error.message))
})
test.afterEach(() => {
expect(consoleErrors, 'browser console errors').toEqual([])
expect(pageErrors, 'uncaught browser errors').toEqual([])
})
const ambientLinkNames = /ホーム|おすすめ|トレンド|通知|履歴/
const mutationControlNames = /投稿する|返信|いいね|再投稿|フォロー|ブックマーク/
async function submitSearch(
page: import('@playwright/test').Page,
query: string,
) {
await openReader(page, '/search?q=&product=Latest&following=false')
await expect(page.locator('article')).toHaveCount(0)
await page.getByLabel('検索語').fill(query)
await page.getByRole('button', { name: '検索' }).click()
}
async function openReader(page: import('@playwright/test').Page, path: string) {
await page.goto(path)
await page.waitForLoadState('networkidle')
}
async function tabTo(page: import('@playwright/test').Page, target: Locator) {
await page.keyboard.press('Tab')
await expect(target).toBeFocused()
}
test('requires intent and infinitely loads a user timeline', async ({
page,
}) => {
await openReader(page, '/')
await expect(page).toHaveURL(/\/$/)
await expect(page.locator('article')).toHaveCount(0)
await expect(page.getByRole('link', { name: ambientLinkNames })).toHaveCount(
0,
)
await page.getByLabel('ハンドルまたはプロフィール URL').fill('@yuta')
await page.getByRole('button', { name: '表示' }).click()
await expect(page).toHaveURL(/\/yuta$/)
await expect(page.getByText('user page 1')).toBeVisible()
await expect(page.getByText('user page 2')).toBeVisible()
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
await expect(page).toHaveScreenshot('mist-user.png', {
animations: 'disabled',
fullPage: true,
})
})
test('selects an authenticated list and infinitely loads its timeline', async ({
page,
}) => {
await openReader(page, '/i/lists')
await expect(page.locator('article')).toHaveCount(0)
await page.getByRole('link', { name: /My E2E List.*10 メンバー/ }).click()
await expect(page).toHaveURL(/\/i\/lists\/1234567890$/)
await expect(page.getByText('list page 1')).toBeVisible()
await expect(page.getByText('list page 2')).toBeVisible()
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
})
test('opens a card detail and infinitely loads its conversation', async ({
page,
}) => {
await openReader(page, '/yuta')
const sourceCard = page
.getByRole('article')
.filter({ hasText: 'user page 1' })
await sourceCard.getByRole('link', { name: '詳細・スレッド' }).click()
await expect(page).toHaveURL(/\/yuta\/status\/1001$/)
await expect(page.getByRole('heading', { name: '会話' })).toBeVisible()
await expect(page.getByText('表示中の投稿')).toBeVisible()
await expect(page.getByText('user page 1')).toHaveCount(1)
await expect(page.getByText('thread root')).toBeVisible()
await expect(page.getByText('thread page 1')).toBeVisible()
await expect(page.getByText('thread page 2')).toBeVisible()
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
await expect(page).toHaveScreenshot('mist-thread.png', {
animations: 'disabled',
fullPage: true,
})
})
test('resolves a reply root and retries only the failed continuation', async ({
page,
}) => {
await openReader(page, '/yuta/status/2001')
await expect(page.getByText('retry focal')).toHaveCount(1)
await expect(page.getByText('thread root')).toBeVisible()
await expect(page.getByText('thread page 1')).toBeVisible()
const alert = page
.getByRole('alert')
.filter({ hasText: '続きの投稿を取得できませんでした。' })
await expect(alert).toHaveCount(1)
await expect(page.getByText('retry focal')).toHaveCount(1)
await alert.getByRole('button', { name: '再試行' }).click()
await expect(page.getByText('thread page 2')).toBeVisible()
await expect(alert).toHaveCount(0)
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
await expect(
page.getByRole('button', { name: mutationControlNames }),
).toHaveCount(0)
})
test('searches Top posts from followed accounts', async ({ page }) => {
await openReader(page, '/search?q=&product=Latest&following=false')
await expect(page.locator('article')).toHaveCount(0)
await page.getByLabel('検索語').fill('AI lang:ja')
await page.getByLabel('人気順').check()
await page.getByLabel('フォロー中のみ').check()
await expect(page.getByLabel('フォロー中のみ')).toBeChecked()
await page.getByRole('button', { name: '検索' }).click()
await expect(page).toHaveURL(/following=true/)
await expect(page.getByText('Top · follows page 1')).toBeVisible()
await expect(page.getByText('Top · follows page 2')).toBeVisible()
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
})
test('retains the first page through a pagination error and retries explicitly', async ({
page,
}, testInfo) => {
await submitSearch(page, `retry-${testInfo.project.name}`)
await expect(page.getByText('Latest · all page 1')).toBeVisible()
const laterPageAlert = page
.getByRole('alert')
.filter({ hasText: '続きの投稿を取得できませんでした。' })
await expect(laterPageAlert).toHaveCount(1)
await expect(page.getByText('Latest · all page 1')).toBeVisible()
await expect(page.getByText('Latest · all page 2')).toHaveCount(0)
await laterPageAlert.getByRole('button', { name: '再試行' }).click()
await expect(page.getByText('Latest · all page 2')).toBeVisible()
await expect(laterPageAlert).toHaveCount(0)
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
})
test('keeps keyboard focus intentional and discovery or mutation controls absent', async ({
page,
}) => {
await openReader(page, '/search?q=&product=Latest&following=false')
await tabTo(page, page.getByRole('link', { name: 'ユーザー' }))
await tabTo(page, page.getByRole('link', { name: '検索' }))
await tabTo(page, page.getByRole('link', { name: 'リスト' }))
await tabTo(page, page.getByLabel('検索語'))
const ranking = page.getByLabel('最新順')
await expect(ranking).toBeChecked()
await tabTo(page, ranking)
await tabTo(page, page.getByLabel('フォロー中のみ'))
await tabTo(page, page.getByRole('button', { name: '検索' }))
await expect(page.getByRole('link', { name: ambientLinkNames })).toHaveCount(
0,
)
await expect(
page.getByRole('button', { name: mutationControlNames }),
).toHaveCount(0)
await expect(
page.getByRole('link', { name: mutationControlNames }),
).toHaveCount(0)
})
test('removes loading rail animation for reduced motion', async ({
page,
}, testInfo) => {
await page.emulateMedia({ reducedMotion: 'reduce' })
await submitSearch(page, `slow-${testInfo.project.name}`)
const rail = page.locator('.loading-rail')
await expect(rail).toBeVisible()
await expect
.poll(() =>
rail.evaluate((element) =>
getComputedStyle(element, '::after').getPropertyValue('animation-name'),
),
)
.toBe('none')
await expect(page.getByText('Latest · all page 2')).toBeVisible()
await expect(page.getByText('これ以上の投稿はありません。')).toBeVisible()
})