feat: add authenticated list reading
@@ -102,6 +102,8 @@ const server = createServer(async (request, response) => {
|
||||
'UserTweets',
|
||||
'TweetDetail',
|
||||
'SearchTimeline',
|
||||
'ListsManagementPageTimeline',
|
||||
'ListLatestTweetsTimeline',
|
||||
].find((name) => url.pathname.endsWith(`/${name}`))
|
||||
|
||||
if (!operation) {
|
||||
@@ -178,6 +180,94 @@ const server = createServer(async (request, response) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (operation === 'ListsManagementPageTimeline') {
|
||||
if (variables.count !== 100) {
|
||||
fail(response, 400, 'invalid lists management variables')
|
||||
return
|
||||
}
|
||||
const features = url.searchParams.get('features')
|
||||
try {
|
||||
if (!features || !isRecord(JSON.parse(features))) throw new Error()
|
||||
} catch {
|
||||
fail(response, 400, 'invalid lists management feature lock')
|
||||
return
|
||||
}
|
||||
send(response, {
|
||||
data: {
|
||||
viewer: {
|
||||
list_management_timeline: {
|
||||
timeline: {
|
||||
instructions: [
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
content: {
|
||||
clientEventInfo: {
|
||||
component: 'suggest_owned_subscribed_list',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
item: {
|
||||
itemContent: {
|
||||
list: {
|
||||
id_str: '1234567890',
|
||||
name: 'My E2E List',
|
||||
description: 'Accounts selected for testing',
|
||||
member_count: 10,
|
||||
mode: 'Public',
|
||||
created_at: 1_704_067_200_000,
|
||||
user_results: {
|
||||
result: {
|
||||
rest_id: '42',
|
||||
core: {
|
||||
screen_name: 'yuta',
|
||||
name: 'Yuta',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: [{ message: 'optional banner field failed' }],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (operation === 'ListLatestTweetsTimeline') {
|
||||
const expectedCursor =
|
||||
variables.cursor === undefined || variables.cursor === 'list-next'
|
||||
if (
|
||||
typeof variables.listId !== 'string' ||
|
||||
!/^\d+$/.test(variables.listId) ||
|
||||
variables.count !== 20 ||
|
||||
!expectedCursor
|
||||
) {
|
||||
fail(response, 400, 'invalid list timeline variables')
|
||||
return
|
||||
}
|
||||
const secondPage = variables.cursor === 'list-next'
|
||||
const entries = secondPage
|
||||
? [tweet('3010', 'list page 2', 'listed')]
|
||||
: [tweet('3001', 'list page 1', 'listed'), cursor('list-next')]
|
||||
send(response, {
|
||||
data: {
|
||||
list: { tweets_timeline: { timeline: timeline(entries) } },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (operation === 'TweetDetail') {
|
||||
const focalTweetId = variables.focalTweetId
|
||||
if (typeof focalTweetId !== 'string' || !/^\d+$/.test(focalTweetId)) {
|
||||
|
||||
@@ -62,6 +62,20 @@ test('requires intent and infinitely loads a user timeline', async ({
|
||||
})
|
||||
})
|
||||
|
||||
test('selects an authenticated list and infinitely loads its timeline', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openReader(page, '/list?target=')
|
||||
await expect(page.locator('article')).toHaveCount(0)
|
||||
|
||||
await page.getByRole('link', { name: /My E2E List.*10 メンバー/ }).click()
|
||||
|
||||
await expect(page).toHaveURL(/\/list\?target=(?:%22)?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,
|
||||
}) => {
|
||||
@@ -151,6 +165,7 @@ test('keeps keyboard focus intentional and discovery or mutation controls absent
|
||||
|
||||
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()
|
||||
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |