kopia lustrzana https://github.com/cloudflare/wildebeest
show account posts in account page
rodzic
e88a55a7e0
commit
37bddfbe19
|
@ -6,6 +6,7 @@ import type { Handle } from 'wildebeest/backend/src/utils/parse'
|
|||
import { queryAcct } from 'wildebeest/backend/src/webfinger/index'
|
||||
import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
||||
import { MastodonAccount } from '../types'
|
||||
import { adjustLocalHostDomain } from '../utils/adjustLocalHostDomain'
|
||||
|
||||
export async function getAccount(domain: string, accountId: string, db: D1Database): Promise<MastodonAccount | null> {
|
||||
const handle = parseHandle(accountId)
|
||||
|
@ -44,16 +45,3 @@ async function getLocalAccount(domain: string, db: D1Database, handle: Handle):
|
|||
|
||||
return await loadLocalMastodonAccount(db, actor)
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if a domain is a localhost one ('localhost' or '127.x.x.x') and
|
||||
* in that case replaces it with '0.0.0.0' (which is what we use for our local data)
|
||||
*
|
||||
* Note: only needed for local development
|
||||
*
|
||||
* @param domain the potentially localhost domain
|
||||
* @returns the adjusted domain if it was a localhost one, the original domain otherwise
|
||||
*/
|
||||
function adjustLocalHostDomain(domain: string) {
|
||||
return domain.replace(/^localhost$|^127(\.(?:\d){1,3}){3}$/, '0.0.0.0')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MastodonAccount } from 'wildebeest/backend/src/types/account'
|
||||
import { unwrapPrivateKey } from 'wildebeest/backend/src/utils/key-ops'
|
||||
import type { Actor } from '../activitypub/actors'
|
||||
import { Actor } from '../activitypub/actors'
|
||||
import { defaultImages } from 'wildebeest/config/accounts'
|
||||
import * as apOutbox from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||
import * as apFollow from 'wildebeest/backend/src/activitypub/actors/follow'
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* checks if a domain is a localhost one ('localhost' or '127.x.x.x') and
|
||||
* in that case replaces it with '0.0.0.0' (which is what we use for our local data)
|
||||
*
|
||||
* Note: only needed for local development
|
||||
*
|
||||
* @param domain the potentially localhost domain
|
||||
* @returns the adjusted domain if it was a localhost one, the original domain otherwise
|
||||
*/
|
||||
export function adjustLocalHostDomain(domain: string) {
|
||||
return domain.replace(/^localhost$|^127(\.(?:\d){1,3}){3}$/, '0.0.0.0')
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { $, component$, useClientEffect$, useSignal, type QRL } from '@builder.io/qwik'
|
||||
import { type MastodonStatus } from '~/types'
|
||||
import Status from '../Status'
|
||||
|
||||
type Props = {
|
||||
initialStatuses: MastodonStatus[]
|
||||
fetchMoreStatuses: QRL<(numOfCurrentStatuses: number) => Promise<MastodonStatus[]>>
|
||||
}
|
||||
|
||||
export const StatusesPanel = component$(({ initialStatuses, fetchMoreStatuses: fetchMoreStatusesFn }: Props) => {
|
||||
const fetchingMoreStatuses = useSignal(false)
|
||||
const noMoreStatusesAvailable = useSignal(false)
|
||||
const lastStatusRef = useSignal<HTMLDivElement>()
|
||||
const statuses = useSignal<MastodonStatus[]>(initialStatuses)
|
||||
|
||||
const fetchMoreStatuses = $(async () => {
|
||||
if (fetchingMoreStatuses.value || noMoreStatusesAvailable.value) {
|
||||
return
|
||||
}
|
||||
fetchingMoreStatuses.value = true
|
||||
const newStatuses = await fetchMoreStatusesFn(statuses.value.length)
|
||||
fetchingMoreStatuses.value = false
|
||||
noMoreStatusesAvailable.value = newStatuses.length === 0
|
||||
})
|
||||
|
||||
useClientEffect$(({ track }) => {
|
||||
track(() => lastStatusRef.value)
|
||||
if (lastStatusRef.value) {
|
||||
const observer = new IntersectionObserver(
|
||||
async ([lastStatus]) => {
|
||||
if (lastStatus.isIntersecting) {
|
||||
await fetchMoreStatuses()
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ rootMargin: '250px' }
|
||||
)
|
||||
observer.observe(lastStatusRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{statuses.value.length > 0 ? (
|
||||
statuses.value.map((status, i) => {
|
||||
const isLastStatus = i === statuses.value.length - 1
|
||||
const divProps = isLastStatus ? { ref: lastStatusRef } : {}
|
||||
return (
|
||||
<div key={status.id} {...divProps}>
|
||||
<Status status={status} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div class="flex-1 grid place-items-center bg-wildebeest-600 text-center">
|
||||
<p>Nothing to see right now. Check back later!</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -1126,6 +1126,116 @@ const mastodonRawStatuses: MastodonStatus[] = [
|
|||
},
|
||||
poll: null,
|
||||
},
|
||||
{
|
||||
id: '109261798394272672',
|
||||
created_at: '2022-10-31T07:52:14.290Z',
|
||||
in_reply_to_id: null,
|
||||
in_reply_to_account_id: null,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
visibility: 'public',
|
||||
language: 'en',
|
||||
uri: 'https://mastodon.design/users/rafa/statuses/109261798394272672',
|
||||
url: 'https://mastodon.design/@rafa/109261798394272672',
|
||||
replies_count: 5,
|
||||
reblogs_count: 10,
|
||||
favourites_count: 42,
|
||||
edited_at: null,
|
||||
content:
|
||||
'<p>Time for an updated <a href="https://mastodon.design/tags/Introduction" class="mention hashtag" rel="tag">#<span>Introduction</span></a>:</p><p>I'm Rafa, a designer and app developer currently living in Amsterdam ☀️</p><p>I make Hand Mirror for macOS, Booby Track for iOS/watchOS, and co-host a design podcast with my friend <span class="h-card"><a href="https://mastodon.design/@kevin" class="u-url mention">@<span>kevin</span></a></span> called Layout.</p><p>Been loving the sense of community and the good vibes you're all giving in this platform, say hi 👋</p><p>Here's some topics I feel like I like to participate in, I hear hashtags can help with discovery so here it goes: <a href="https://mastodon.design/tags/Design" class="mention hashtag" rel="tag">#<span>Design</span></a> <a href="https://mastodon.design/tags/Tech" class="mention hashtag" rel="tag">#<span>Tech</span></a> <a href="https://mastodon.design/tags/AppDevelopment" class="mention hashtag" rel="tag">#<span>AppDevelopment</span></a> <a href="https://mastodon.design/tags/Parenthood" class="mention hashtag" rel="tag">#<span>Parenthood</span></a> <a href="https://mastodon.design/tags/Therapy" class="mention hashtag" rel="tag">#<span>Therapy</span></a></p>',
|
||||
reblog: null,
|
||||
application: {
|
||||
name: 'Web',
|
||||
website: null,
|
||||
},
|
||||
account: {
|
||||
id: '11932',
|
||||
username: 'rafa',
|
||||
acct: 'rafa',
|
||||
display_name: 'Rafa',
|
||||
locked: false,
|
||||
bot: false,
|
||||
discoverable: true,
|
||||
group: false,
|
||||
created_at: '2018-08-21T00:00:00.000Z',
|
||||
note: '<p>I’m a designer and app developer, currently working on Sketch, and Hand Mirror for Mac</p>',
|
||||
url: 'https://mastodon.design/@rafa',
|
||||
avatar: 'https://cdn.masto.host/mastodondesign/accounts/avatars/000/011/932/original/8f601be03c98b2e8.png',
|
||||
avatar_static: 'https://cdn.masto.host/mastodondesign/accounts/avatars/000/011/932/original/8f601be03c98b2e8.png',
|
||||
header: 'https://cdn.masto.host/mastodondesign/accounts/headers/000/011/932/original/668f6d32abb54252.jpeg',
|
||||
header_static:
|
||||
'https://cdn.masto.host/mastodondesign/accounts/headers/000/011/932/original/668f6d32abb54252.jpeg',
|
||||
followers_count: 3135,
|
||||
following_count: 453,
|
||||
statuses_count: 1289,
|
||||
last_status_at: '2023-02-12',
|
||||
noindex: false,
|
||||
emojis: [],
|
||||
fields: [
|
||||
{
|
||||
name: 'Website',
|
||||
value:
|
||||
'<a href="https://rafa.design" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://</span><span class="">rafa.design</span><span class="invisible"></span></a>',
|
||||
verified_at: '2022-11-06T16:49:24.339+00:00',
|
||||
},
|
||||
{
|
||||
name: 'App',
|
||||
value:
|
||||
'<a href="http://handmirror.app" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">http://</span><span class="">handmirror.app</span><span class="invisible"></span></a>',
|
||||
verified_at: null,
|
||||
},
|
||||
{
|
||||
name: 'Podcast',
|
||||
value:
|
||||
'<a href="http://layout.fm" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">http://</span><span class="">layout.fm</span><span class="invisible"></span></a>',
|
||||
verified_at: null,
|
||||
},
|
||||
{
|
||||
name: 'Pronouns',
|
||||
value: 'He/Them',
|
||||
verified_at: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
media_attachments: [],
|
||||
mentions: [
|
||||
{
|
||||
id: '12179',
|
||||
username: 'kevin',
|
||||
url: 'https://mastodon.design/@kevin',
|
||||
acct: 'kevin',
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: 'therapy',
|
||||
url: 'https://mastodon.design/tags/therapy',
|
||||
},
|
||||
{
|
||||
name: 'parenthood',
|
||||
url: 'https://mastodon.design/tags/parenthood',
|
||||
},
|
||||
{
|
||||
name: 'appdevelopment',
|
||||
url: 'https://mastodon.design/tags/appdevelopment',
|
||||
},
|
||||
{
|
||||
name: 'tech',
|
||||
url: 'https://mastodon.design/tags/tech',
|
||||
},
|
||||
{
|
||||
name: 'design',
|
||||
url: 'https://mastodon.design/tags/design',
|
||||
},
|
||||
{
|
||||
name: 'introduction',
|
||||
url: 'https://mastodon.design/tags/introduction',
|
||||
},
|
||||
],
|
||||
emojis: [],
|
||||
card: null,
|
||||
poll: null,
|
||||
},
|
||||
]
|
||||
|
||||
export const statuses: MastodonStatus[] = mastodonRawStatuses.map((rawStatus) => ({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { component$, useStyles$ } from '@builder.io/qwik'
|
||||
import { $, component$, useStyles$ } from '@builder.io/qwik'
|
||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||
import { MastodonAccount } from 'wildebeest/backend/src/types'
|
||||
import StickyHeader from '~/components/StickyHeader/StickyHeader'
|
||||
|
@ -9,18 +9,27 @@ import { getAccount } from 'wildebeest/backend/src/accounts/getAccount'
|
|||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||
import type { MastodonStatus } from '~/types'
|
||||
import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel'
|
||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
||||
import { getLocalStatuses } from 'wildebeest/functions/api/v1/accounts/[id]/statuses'
|
||||
|
||||
export const accountLoader = loader$<
|
||||
{ DATABASE: D1Database },
|
||||
Promise<{ account: MastodonAccount; accountHandle: string }>
|
||||
Promise<{ account: MastodonAccount; accountHandle: string; statuses: MastodonStatus[] }>
|
||||
>(async ({ platform, request, html }) => {
|
||||
let account: MastodonAccount | null = null
|
||||
let statuses: MastodonStatus[] = []
|
||||
try {
|
||||
const url = new URL(request.url)
|
||||
const domain = url.hostname
|
||||
const accountId = url.pathname.split('/')[1]
|
||||
|
||||
account = await getAccount(domain, accountId, platform.DATABASE)
|
||||
|
||||
const handle = parseHandle(accountId)
|
||||
const response = await getLocalStatuses(request as Request, platform.DATABASE, handle)
|
||||
statuses = await response.json<Array<MastodonStatus>>()
|
||||
} catch {
|
||||
throw html(
|
||||
500,
|
||||
|
@ -36,7 +45,7 @@ export const accountLoader = loader$<
|
|||
|
||||
const accountHandle = `@${account.acct}${accountDomain ? `@${accountDomain}` : ''}`
|
||||
|
||||
return { account, accountHandle }
|
||||
return { account, accountHandle, statuses: JSON.parse(JSON.stringify(statuses)) }
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
|
@ -70,38 +79,56 @@ export default component$(() => {
|
|||
return (
|
||||
<div>
|
||||
<StickyHeader withBackButton />
|
||||
<div class="relative mb-16">
|
||||
<img
|
||||
src={accountDetails.account.header}
|
||||
alt={`Header of ${accountDetails.account.display_name}`}
|
||||
class="w-full h-40 object-cover bg-wildebeest-500"
|
||||
/>
|
||||
<img
|
||||
class="rounded h-24 w-24 absolute bottom-[-3rem] left-5 border-2 border-wildebeest-600"
|
||||
src={accountDetails.account.avatar}
|
||||
alt={`Avatar of ${accountDetails.account.display_name}`}
|
||||
/>
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<h2 class="font-bold">{accountDetails.account.display_name}</h2>
|
||||
<span class="block my-1 text-wildebeest-400">{accountDetails.accountHandle}</span>
|
||||
<div class="inner-html-content my-5" dangerouslySetInnerHTML={accountDetails.account.note} />
|
||||
<dl class="mb-6 flex flex-col bg-wildebeest-800 border border-wildebeest-600 rounded-md">
|
||||
{fields.map(({ name, value }) => (
|
||||
<div class="border-b border-wildebeest-600 p-3 text-sm" key={name}>
|
||||
<dt class="uppercase font-semibold text-wildebeest-500 opacity-80 mb-1">{name}</dt>
|
||||
<dd class="inner-html-content opacity-80 text-wildebeest-200" dangerouslySetInnerHTML={value}></dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<div data-testid="stats" class="pb-4 flex flex-wrap gap-5">
|
||||
{stats.map(({ name, value }) => (
|
||||
<div class="flex gap-1" key={name}>
|
||||
<span class="font-semibold">{value}</span>
|
||||
<span class="text-wildebeest-500">{name}</span>
|
||||
</div>
|
||||
))}
|
||||
<div data-testid="account-info">
|
||||
<div class="relative mb-16">
|
||||
<img
|
||||
src={accountDetails.account.header}
|
||||
alt={`Header of ${accountDetails.account.display_name}`}
|
||||
class="w-full h-40 object-cover bg-wildebeest-500"
|
||||
/>
|
||||
<img
|
||||
class="rounded h-24 w-24 absolute bottom-[-3rem] left-5 border-2 border-wildebeest-600"
|
||||
src={accountDetails.account.avatar}
|
||||
alt={`Avatar of ${accountDetails.account.display_name}`}
|
||||
/>
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<h2 class="font-bold">{accountDetails.account.display_name}</h2>
|
||||
<span class="block my-1 text-wildebeest-400">{accountDetails.accountHandle}</span>
|
||||
<div class="inner-html-content my-5" dangerouslySetInnerHTML={accountDetails.account.note} />
|
||||
<dl class="mb-6 flex flex-col bg-wildebeest-800 border border-wildebeest-600 rounded-md">
|
||||
{fields.map(({ name, value }) => (
|
||||
<div class="border-b border-wildebeest-600 p-3 text-sm" key={name}>
|
||||
<dt class="uppercase font-semibold text-wildebeest-500 opacity-80 mb-1">{name}</dt>
|
||||
<dd class="inner-html-content opacity-80 text-wildebeest-200" dangerouslySetInnerHTML={value}></dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<div data-testid="stats" class="pb-4 flex flex-wrap gap-5">
|
||||
{stats.map(({ name, value }) => (
|
||||
<div class="flex gap-1" key={name}>
|
||||
<span class="font-semibold">{value}</span>
|
||||
<span class="text-wildebeest-500">{name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-wildebeest-800 flex justify-around mt-6">
|
||||
<span class="my-3 text-wildebeest-200">
|
||||
<span>Posts</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="account-statuses">
|
||||
{accountDetails.statuses.length > 0 && (
|
||||
<StatusesPanel
|
||||
initialStatuses={accountDetails.statuses}
|
||||
fetchMoreStatuses={$(async () => {
|
||||
// TODO-DARIO: implement this function
|
||||
return []
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { $, component$, useClientEffect$, useSignal } from '@builder.io/qwik'
|
||||
import { $, component$ } from '@builder.io/qwik'
|
||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||
import { RequestContext } from '@builder.io/qwik-city/middleware/request-handler'
|
||||
import * as timelines from 'wildebeest/functions/api/v1/timelines/public'
|
||||
import Status from '~/components/Status'
|
||||
import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel'
|
||||
import type { MastodonStatus } from '~/types'
|
||||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||
|
@ -25,67 +25,23 @@ export const statusesLoader = loader$<{ DATABASE: D1Database; domain: string },
|
|||
|
||||
export default component$(() => {
|
||||
const statuses = statusesLoader.use()
|
||||
return <StatusesPanel initialStatuses={statuses.value} />
|
||||
})
|
||||
|
||||
type StatusesPanelProps = { initialStatuses: MastodonStatus[] }
|
||||
|
||||
export const StatusesPanel = component$(({ initialStatuses }: StatusesPanelProps) => {
|
||||
const fetchingMoreStatuses = useSignal(false)
|
||||
const noMoreStatusesAvailable = useSignal(false)
|
||||
const lastStatusRef = useSignal<HTMLDivElement>()
|
||||
const statuses = useSignal<MastodonStatus[]>(initialStatuses)
|
||||
|
||||
const fetchMoreStatuses = $(async () => {
|
||||
if (fetchingMoreStatuses.value || noMoreStatusesAvailable.value) {
|
||||
return
|
||||
}
|
||||
fetchingMoreStatuses.value = true
|
||||
const response = await fetch(`/api/v1/timelines/public?offset=${statuses.value.length}`)
|
||||
fetchingMoreStatuses.value = false
|
||||
if (response.ok) {
|
||||
const results = await response.text()
|
||||
const newStatuses: MastodonStatus[] = JSON.parse(results)
|
||||
noMoreStatusesAvailable.value = newStatuses.length === 0
|
||||
statuses.value = statuses.value.concat(newStatuses)
|
||||
}
|
||||
fetchingMoreStatuses.value = false
|
||||
})
|
||||
|
||||
useClientEffect$(({ track }) => {
|
||||
track(() => lastStatusRef.value)
|
||||
if (lastStatusRef.value) {
|
||||
const observer = new IntersectionObserver(
|
||||
async ([lastStatus]) => {
|
||||
if (lastStatus.isIntersecting) {
|
||||
await fetchMoreStatuses()
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ rootMargin: '250px' }
|
||||
)
|
||||
observer.observe(lastStatusRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{statuses.value.length > 0 ? (
|
||||
statuses.value.map((status, i) => {
|
||||
const isLastStatus = i === statuses.value.length - 1
|
||||
const divProps = isLastStatus ? { ref: lastStatusRef } : {}
|
||||
return (
|
||||
<div key={status.id} {...divProps}>
|
||||
<Status status={status} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div class="flex-1 grid place-items-center bg-wildebeest-600 text-center">
|
||||
<p>Nothing to see right now. Check back later!</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<StatusesPanel
|
||||
initialStatuses={statuses.value}
|
||||
fetchMoreStatuses={$(async (numOfCurrentStatuses: number) => {
|
||||
let statuses: MastodonStatus[] = []
|
||||
try {
|
||||
const response = await fetch(`/api/v1/timelines/public?offset=${numOfCurrentStatuses}`)
|
||||
if (response.ok) {
|
||||
const results = await response.text()
|
||||
statuses = JSON.parse(results)
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
return statuses
|
||||
})}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import * as webfinger from 'wildebeest/backend/src/webfinger'
|
|||
import * as outbox from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
||||
import { toMastodonStatusFromRow } from 'wildebeest/backend/src/mastodon/status'
|
||||
import { adjustLocalHostDomain } from 'wildebeest/backend/src/utils/adjustLocalHostDomain'
|
||||
|
||||
const headers = {
|
||||
...cors(),
|
||||
|
@ -112,9 +113,9 @@ async function getRemoteStatuses(request: Request, handle: Handle, db: D1Databas
|
|||
return new Response(JSON.stringify(statuses), { headers })
|
||||
}
|
||||
|
||||
async function getLocalStatuses(request: Request, db: D1Database, handle: Handle): Promise<Response> {
|
||||
export async function getLocalStatuses(request: Request, db: D1Database, handle: Handle): Promise<Response> {
|
||||
const domain = new URL(request.url).hostname
|
||||
const actorId = actorURL(domain, handle.localPart)
|
||||
const actorId = actorURL(adjustLocalHostDomain(domain), handle.localPart)
|
||||
|
||||
const QUERY = `
|
||||
SELECT objects.*,
|
||||
|
|
|
@ -1,14 +1,41 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Navigation to and view of an account', async ({ page }) => {
|
||||
test('Navigation to and view of an account (with 1 post)', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:8788/explore')
|
||||
await page.getByRole('article').getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
||||
await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
||||
await expect(page.getByRole('img', { name: 'Header of Ben Rosengart' })).toBeVisible()
|
||||
await expect(page.getByRole('img', { name: 'Avatar of Ben Rosengart' })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Ben Rosengart' })).toBeVisible()
|
||||
await expect(page.getByText('Joined')).toBeVisible()
|
||||
await expect(page.getByTestId('stats')).toHaveText('1Posts0Following0Followers')
|
||||
|
||||
await expect(page.getByTestId('account-info').getByRole('img', { name: 'Header of Ben Rosengart' })).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByRole('img', { name: 'Avatar of Ben Rosengart' })).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByRole('heading', { name: 'Ben Rosengart' })).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByText('Joined')).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByTestId('stats')).toHaveText('1Posts0Following0Followers')
|
||||
|
||||
expect(await page.getByTestId('account-statuses').getByRole('article').count()).toBe(1)
|
||||
await expect(
|
||||
page.getByTestId('account-statuses').getByRole('article').getByRole('img', { name: 'Avatar of Ben Rosengart' })
|
||||
).toBeVisible()
|
||||
await expect(page.getByTestId('account-statuses').getByRole('article')).toContainText('What fresh hell is this?')
|
||||
})
|
||||
|
||||
test('Navigation to and view of an account (with 2 posts)', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:8788/explore')
|
||||
await page.locator('article').filter({ hasText: "I'm Rafa, a designer and app" }).locator('i.fa-globe + span').click()
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.getByRole('link', { name: 'Rafa', exact: true }).click()
|
||||
|
||||
await expect(page.getByTestId('account-info').getByRole('img', { name: 'Header of Rafa' })).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByRole('img', { name: 'Avatar of Rafa' })).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByRole('heading', { name: 'Rafa' })).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByText('Joined')).toBeVisible()
|
||||
await expect(page.getByTestId('account-info').getByTestId('stats')).toHaveText('2Posts0Following0Followers')
|
||||
|
||||
expect(await page.getByTestId('account-statuses').getByRole('article').count()).toBe(2)
|
||||
const [post1Locator, post2Locator] = await page.getByTestId('account-statuses').getByRole('article').all()
|
||||
await expect(post1Locator.getByRole('img', { name: 'Avatar of Rafa' })).toBeVisible()
|
||||
await expect(post1Locator).toContainText("I'm Rafa, a designer and app developer currently living in Amsterdam")
|
||||
await expect(post2Locator.getByRole('img', { name: 'Avatar of Rafa' })).toBeVisible()
|
||||
await expect(post2Locator).toContainText('Hi, meet HiDock!')
|
||||
})
|
||||
|
|
Ładowanie…
Reference in New Issue