From 49a375d1e2574925ed90abecd1a8699a5429ee59 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:18:31 +0100 Subject: [PATCH 1/8] #792 Remove dead code: packages/transaction/lib/bridge.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file was unused: - No imports found in the codebase - Not exported from packages/transaction/index.ts - Functionality duplicated in bridge-api.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/transaction/lib/bridge.ts | 204 ----------------------------- 1 file changed, 204 deletions(-) delete mode 100644 packages/transaction/lib/bridge.ts diff --git a/packages/transaction/lib/bridge.ts b/packages/transaction/lib/bridge.ts deleted file mode 100644 index 1c0f38af1..000000000 --- a/packages/transaction/lib/bridge.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { hiveChainService } from './hive-chain-service'; -import { getLogger } from '@ui/lib/logging'; -import { IGetPostHeader, IFollowList, IAccountRelationship, Entry, IUnreadNotifications, Community, IAccountNotification, FollowListType } from './extended-hive.chain'; -import {commonVariables} from'@ui/lib/common-variables'; - -const logger = getLogger('app'); - -const chain = await hiveChainService.getHiveChain(); - -export type Subscription = Array; - -export const DATA_LIMIT = 20; - -const resolvePost = (post: Entry, observer: string): Promise => { - const { json_metadata: json } = post; - - if (json.original_author && json.original_permlink && json.tags && json.tags[0] === 'cross-post') { - return getPost(json.original_author, json.original_permlink, observer) - .then((resp) => { - if (resp) { - return { - ...post, - original_entry: resp - }; - } - - return post; - }) - .catch(() => { - return post; - }); - } - - return new Promise((resolve) => { - resolve(post); - }); -}; - -const resolvePosts = (posts: Entry[], observer: string): Promise => { - const promises = posts.map((p) => resolvePost(p, observer)); - - return Promise.all(promises); -}; - -export const getPostsRanked = async ( - sort: string, - tag: string = '', - start_author: string = '', - start_permlink: string = '', - observer: string, - limit: number = DATA_LIMIT -): Promise => { - return chain - .api.bridge.get_ranked_posts({ - sort, - start_author, - start_permlink, - limit, - tag, - observer - }) - .then((resp) => { - // logger.info('getPostsRanked result: %o', resp); - if (resp) { - return resolvePosts(resp, observer); - } - - return resp; - }); -}; - -export const getAccountPosts = async ( - sort: string, - account: string, - observer: string, - start_author: string = '', - start_permlink: string = '', - limit: number = DATA_LIMIT -): Promise => { - return chain - .api.bridge.get_account_posts({ - sort, - account, - start_author, - start_permlink, - limit, - observer - }) - .then((resp) => { - if (resp) { - return resolvePosts(resp, observer); - } - - return resp; - }); -}; - -export const getPost = async ( - author: string = '', - permlink: string = '', - observer: string = '' -): Promise => { - return chain - .api.bridge.get_post({ - author, - permlink, - observer - }) - .then((resp) => { - if (resp) { - return resolvePost(resp, observer); - } - - return resp; - }); -}; - -// I have problem with this func, I pass good account name but RPC call it with empty string -export const getAccountNotifications = async ( - account: string, - lastId: number | null = null, - limit = 50 -): Promise => { - const params: { account: string; last_id?: number; limit: number } = { - account, - limit - }; - - if (lastId) { - params.last_id = lastId; - } - return chain.api.bridge.account_notifications(params); -}; - -export const getDiscussion = async ( - author: string, - permlink: string, - observer?: string -): Promise | null> => { - return chain.api.bridge.get_discussion({ - author, - permlink, - observer - }); -}; - -export const getCommunity = async ( - name: string, - observer: string | undefined = '' -): Promise => { - return chain.api.bridge.get_community({ name, observer }); -}; - -export const getListCommunityRoles = async (community: string): Promise => { - return chain.api.bridge.list_community_roles({ community }); -}; - -export const getCommunities = async ( - sort: string, - query?: string | null, - // last: string = '', - // limit: number = 100, - observer: string = 'hive.blog' -): Promise => { - return chain.api.bridge.list_communities({ - // limit, - query, - sort, - observer: observer !== '' ? observer : commonVariables.defaultObserver, - }); -}; - -export const normalizePost = async (post: Entry): Promise => { - return chain.api.bridge.normalize_post({ - post - }); -}; - -export const getRelationshipBetweenAccounts = async ( - follower: string, - following: string -): Promise => { - return chain.api.bridge.get_relationship_between_accounts([follower, following]); -}; - -export type TwitterInfo = { - twitter_username: string; - twitter_profile: string; -}; - -export const getTwitterInfo = async (username: string) => { - const response = await fetch(`https://hiveposh.com/api/v0/twitter/${username}`); - if (!response.ok) { - throw new Error(`Posh API Error: ${response.status}`); - } - - const data = await response.json(); - const { error } = data; - if (error) { - throw new Error(`Posh API Error: ${error}`); - } - - return data; -}; -- GitLab From a8ac80bdfcf62d53d053f07c21ecc0251c91ce0a Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:18:42 +0100 Subject: [PATCH 2/8] For non-logged-in users, the SSR prefetch was fetching subscriptions for the default observer which was never used because the client-side query is disabled for non-logged-in users. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This saves 1 API call per feed page visit by anonymous users. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../features/layouts/sorts/server-side-layout.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/blog/features/layouts/sorts/server-side-layout.tsx b/apps/blog/features/layouts/sorts/server-side-layout.tsx index 4b5e2b8ae..bec6c07ef 100644 --- a/apps/blog/features/layouts/sorts/server-side-layout.tsx +++ b/apps/blog/features/layouts/sorts/server-side-layout.tsx @@ -1,7 +1,8 @@ import { getObserverFromCookies } from '@/blog/lib/auth-utils'; import { getQueryClient } from '@/blog/lib/react-query'; +import { DEFAULT_OBSERVER } from '@/blog/lib/utils'; import { dehydrate, Hydrate } from '@tanstack/react-query'; -import { getCommunities } from '@transaction/lib/bridge-api'; +import { getCommunities, getSubscriptions } from '@transaction/lib/bridge-api'; import { ReactNode } from 'react'; import { getLogger } from '@ui/lib/logging'; @@ -18,6 +19,14 @@ const ServerSideLayout = async ({ children }: { children: ReactNode }) => { queryKey: ['communitiesList', sort], queryFn: () => getCommunities(sort, query, observer) }); + // Only prefetch subscriptions if logged in (observer is actual username, not default) + // For non-logged-in users, client-side query is disabled anyway + if (observer !== DEFAULT_OBSERVER) { + await queryClient.prefetchQuery({ + queryKey: ['subscriptions', observer], + queryFn: () => getSubscriptions(observer) + }); + } } catch (error) { logger.error(error, 'Error in ServerSideLayout:'); } -- GitLab From f0c242c13b745b6781df0a8f8b7152fdf8d014e1 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:18:54 +0100 Subject: [PATCH 3/8] #793 Remove duplicate community data prefetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate prefetches from sort-page.tsx (layout handles it) - Remove getSubscribers and getAccountNotifications from SSR prefetch (not critical for initial render, client fetches for sidebar) - Keep only getCommunity in SSR (needed for page metadata) This reduces redundant API calls per community page visit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../features/community-profile/sort-page.tsx | 23 ++----------------- .../layouts/community/prefetch-component.tsx | 19 ++++----------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/apps/blog/features/community-profile/sort-page.tsx b/apps/blog/features/community-profile/sort-page.tsx index f0aa08d7b..5b7c00dc1 100644 --- a/apps/blog/features/community-profile/sort-page.tsx +++ b/apps/blog/features/community-profile/sort-page.tsx @@ -2,16 +2,10 @@ import { getObserverFromCookies } from '@/blog/lib/auth-utils'; import { getQueryClient } from '@/blog/lib/react-query'; import { SortTypes } from '@/blog/lib/utils'; import { dehydrate, Hydrate } from '@tanstack/react-query'; -import { - getAccountNotifications, - getCommunity, - getPostsRanked, - getSubscribers -} from '@transaction/lib/bridge-api'; +import { getPostsRanked } from '@transaction/lib/bridge-api'; import { Entry } from '@transaction/lib/extended-hive.chain'; import { ReactNode } from 'react'; import { getLogger } from '@ui/lib/logging'; -import { isCommunity } from '@ui/lib/utils'; const logger = getLogger('app'); @@ -27,20 +21,7 @@ const SortPage = async ({ const queryClient = getQueryClient(); try { const observer = getObserverFromCookies(); - if (isCommunity(tag)) { - await queryClient.prefetchQuery({ - queryKey: ['community', tag], - queryFn: async () => await getCommunity(tag, observer) - }); - await queryClient.prefetchQuery({ - queryKey: ['subscribers', tag], - queryFn: async () => await getSubscribers(tag) - }); - await queryClient.prefetchQuery({ - queryKey: ['AccountNotification', tag], - queryFn: async () => await getAccountNotifications(tag) - }); - } + // Community data (getCommunity) is already prefetched in the layout's PrefetchComponent await queryClient.prefetchInfiniteQuery({ queryKey: ['entriesInfinite', sort, tag], queryFn: async ({ pageParam }) => { diff --git a/apps/blog/features/layouts/community/prefetch-component.tsx b/apps/blog/features/layouts/community/prefetch-component.tsx index 67f8aa4fc..bde69dcdd 100644 --- a/apps/blog/features/layouts/community/prefetch-component.tsx +++ b/apps/blog/features/layouts/community/prefetch-component.tsx @@ -1,12 +1,7 @@ import { ReactNode } from 'react'; import { getQueryClient } from '@/blog/lib/react-query'; import { dehydrate, Hydrate } from '@tanstack/react-query'; -import { - getAccountNotifications, - getCommunities, - getCommunity, - getSubscribers -} from '@transaction/lib/bridge-api'; +import { getCommunities, getCommunity } from '@transaction/lib/bridge-api'; import CommunityLayout from './community-layout'; import { getObserverFromCookies } from '@/blog/lib/auth-utils'; import { getLogger } from '@ui/lib/logging'; @@ -26,17 +21,11 @@ const PrefetchComponent = async ({ children, community }: { children: ReactNode; queryFn: () => getCommunities(sort, query, observer) }); if (isCommunity(community)) { + // Only prefetch what's needed for page metadata and initial render + // Subscribers and notifications are not critical for SSR - let client fetch await queryClient.prefetchQuery({ queryKey: ['community', community], - queryFn: async () => await getCommunity(community, observer) - }); - await queryClient.prefetchQuery({ - queryKey: ['subscribers', community], - queryFn: async () => await getSubscribers(community) - }); - await queryClient.prefetchQuery({ - queryKey: ['AccountNotification', community], - queryFn: async () => await getAccountNotifications(community) + queryFn: () => getCommunity(community, observer) }); } } catch (error) { -- GitLab From 7ba2b35e6148339f86fd6474743b3d06a8fc481f Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:19:12 +0100 Subject: [PATCH 4/8] #794 Deduplicate getAccountFull calls in profile page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use React cache() to deduplicate getAccountFull calls between generateMetadata and Layout prefetch within the same request. - Add cached-api.ts with getAccountFullCached wrapper - Update profile layout to use cached version in both places This eliminates duplicate API calls for the same user data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/blog/app/[param]/(user-profile)/layout.tsx | 13 ++++++------- apps/blog/lib/cached-api.ts | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 apps/blog/lib/cached-api.ts diff --git a/apps/blog/app/[param]/(user-profile)/layout.tsx b/apps/blog/app/[param]/(user-profile)/layout.tsx index 581bcf3a9..2c09e0963 100644 --- a/apps/blog/app/[param]/(user-profile)/layout.tsx +++ b/apps/blog/app/[param]/(user-profile)/layout.tsx @@ -3,7 +3,8 @@ import { ReactNode } from 'react'; import { Metadata } from 'next'; import { dehydrate, Hydrate } from '@tanstack/react-query'; import { getQueryClient } from '@/blog/lib/react-query'; -import { getAccountFull, getAccountReputations, getDynamicGlobalProperties } from '@transaction/lib/hive-api'; +import { getAccountFullCached } from '@/blog/lib/cached-api'; +import { getAccountReputations, getDynamicGlobalProperties } from '@transaction/lib/hive-api'; import { getTwitterInfo } from '@transaction/lib/custom-api'; import { isUsernameValid } from '@/blog/utils/validate-links'; import { notFound } from 'next/navigation'; @@ -21,12 +22,9 @@ export async function generateMetadata({ params }: { params: { param: string } } }; } const username = raw.startsWith('%40') ? raw.replace('%40', '') : raw.replace('@', ''); - const queryClient = getQueryClient(); try { - const account = await queryClient.fetchQuery({ - queryKey: ['profileData', username], - queryFn: () => getAccountFull(username) - }); + // Use cached version - deduplicated with Layout's prefetch within the same request + const account = await getAccountFullCached(username); const image = account?.profile?.profile_image || 'https://hive.blog/images/hive-blog-share.png'; const about = account?.profile?.about || `Profile of @${username} on Hive.`; const title = `Blog ${username}`; @@ -78,9 +76,10 @@ const Layout = async ({ children, params }: { children: ReactNode; params: { par } try { + // Use cached version - deduplicated with generateMetadata within the same request await queryClient.prefetchQuery({ queryKey: ['profileData', username], - queryFn: () => getAccountFull(username) + queryFn: () => getAccountFullCached(username) }); await queryClient.prefetchQuery({ queryKey: ['accountReputationData', username], diff --git a/apps/blog/lib/cached-api.ts b/apps/blog/lib/cached-api.ts new file mode 100644 index 000000000..aad9535cd --- /dev/null +++ b/apps/blog/lib/cached-api.ts @@ -0,0 +1,15 @@ +import { cache } from 'react'; +import { getAccountFull } from '@transaction/lib/hive-api'; +import { getPost } from '@transaction/lib/bridge-api'; + +/** + * Request-level cached version of getAccountFull. + * Deduplicates calls within the same request (e.g., generateMetadata + layout). + */ +export const getAccountFullCached = cache(getAccountFull); + +/** + * Request-level cached version of getPost. + * Deduplicates calls within the same request (e.g., generateMetadata + page). + */ +export const getPostCached = cache(getPost); -- GitLab From 06acba0492ff515426b5ec899241a98e4f131829 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:19:23 +0100 Subject: [PATCH 5/8] #795 Deduplicate getPost calls in post page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use React cache() to deduplicate getPost calls between generateMetadata and page prefetch within the same request. - Add getPostCached to cached-api.ts - Update post layout and page to use cached version This eliminates duplicate API calls for the same post data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/blog/app/[param]/[p2]/[permlink]/layout.tsx | 10 +++------- apps/blog/app/[param]/[p2]/[permlink]/page.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/blog/app/[param]/[p2]/[permlink]/layout.tsx b/apps/blog/app/[param]/[p2]/[permlink]/layout.tsx index 68101be81..d86d2a6e4 100644 --- a/apps/blog/app/[param]/[p2]/[permlink]/layout.tsx +++ b/apps/blog/app/[param]/[p2]/[permlink]/layout.tsx @@ -1,7 +1,6 @@ import { Metadata } from 'next'; import React, { PropsWithChildren } from 'react'; -import { getPost } from '@transaction/lib/bridge-api'; -import { getQueryClient } from '@/blog/lib/react-query'; +import { getPostCached } from '@/blog/lib/cached-api'; import { getObserverFromCookies } from '@/blog/lib/auth-utils'; import { getLogger } from '@ui/lib/logging'; @@ -23,12 +22,9 @@ export async function generateMetadata({ const permlink = params?.permlink; const observer = getObserverFromCookies(); - const queryClient = getQueryClient(); try { - const post = await queryClient.fetchQuery({ - queryKey: ['postData', author, permlink], - queryFn: () => getPost(author, permlink, observer) - }); + // Use cached version - deduplicated with page's prefetch within the same request + const post = await getPostCached(author, permlink, observer); const title = post?.title ? `${post.title} ` : 'Hive Blog'; const description = diff --git a/apps/blog/app/[param]/[p2]/[permlink]/page.tsx b/apps/blog/app/[param]/[p2]/[permlink]/page.tsx index a7d16afeb..efc9354f7 100644 --- a/apps/blog/app/[param]/[p2]/[permlink]/page.tsx +++ b/apps/blog/app/[param]/[p2]/[permlink]/page.tsx @@ -2,7 +2,8 @@ import { Suspense } from 'react'; import { getQueryClient } from '@/blog/lib/react-query'; import { dehydrate, Hydrate } from '@tanstack/react-query'; import PostContent from './content'; -import { getCommunity, getListCommunityRoles, getPost } from '@transaction/lib/bridge-api'; +import { getPostCached } from '@/blog/lib/cached-api'; +import { getCommunity, getListCommunityRoles } from '@transaction/lib/bridge-api'; import { getDiscussion } from '@transaction/lib/bridge-api'; import { getActiveVotes } from '@transaction/lib/hive-api'; import { getObserverFromCookies } from '@/blog/lib/auth-utils'; @@ -33,9 +34,10 @@ const PostPage = async ({ if (!isPermlinkValid(permlink)) notFound(); try { const observer = getObserverFromCookies(); + // Use cached version - deduplicated with layout's generateMetadata within the same request await queryClient.prefetchQuery({ queryKey: ['postData', username, permlink], - queryFn: () => getPost(username, permlink, observer) + queryFn: () => getPostCached(username, permlink, observer) }); await queryClient.prefetchQuery({ -- GitLab From b76335c56fede2a3369deb1c8690ee39e3056087 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:19:38 +0100 Subject: [PATCH 6/8] #706 Fix back navigation from post to hivesense search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: After clicking a search result and pressing back, users saw an empty page instead of search results. Fix: - ai-result.tsx: Use useMemo for displayedPosts derived from React Query cache (fullPosts) + loaded stubs. This ensures data persists on back navigation. - use-search.ts: Add useEffect to sync input state with URL params when navigating back. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/blog/features/search/ai-result.tsx | 28 ++++++++++++------------- packages/ui/hooks/use-search.ts | 12 +++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/blog/features/search/ai-result.tsx b/apps/blog/features/search/ai-result.tsx index cdd07f887..73b63d612 100644 --- a/apps/blog/features/search/ai-result.tsx +++ b/apps/blog/features/search/ai-result.tsx @@ -21,8 +21,8 @@ const AIResult = ({ query, nsfwPreferences }: { query: string; nsfwPreferences: const { t } = useTranslation('common_blog'); const observer = user.isLoggedIn ? user.username : DEFAULT_OBSERVER; - const [currentPage, setCurrentPage] = useState(0); - const [displayedPosts, setDisplayedPosts] = useState([]); + const [loadedStubPosts, setLoadedStubPosts] = useState([]); + const [currentPage, setCurrentPage] = useState(1); const [isLoadingMore, setIsLoadingMore] = useState(false); // Fetch all results in a single call @@ -48,7 +48,7 @@ const AIResult = ({ query, nsfwPreferences }: { query: string; nsfwPreferences: staleTime: 5 * 60 * 1000 // Cache for 5 minutes }); - // Separate full posts and stubs + // Separate full posts and stubs from search results const { fullPosts, stubPosts } = useMemo(() => { if (!searchResults) return { fullPosts: [], stubPosts: [] }; @@ -72,13 +72,11 @@ const AIResult = ({ query, nsfwPreferences }: { query: string; nsfwPreferences: return { fullPosts: full, stubPosts: stubs }; }, [searchResults]); - // Initialize displayed posts with full posts from initial response - useEffect(() => { - if (fullPosts.length > 0 && displayedPosts.length === 0) { - setDisplayedPosts(fullPosts); - setCurrentPage(1); - } - }, [fullPosts]); + // Combine initial full posts with additionally loaded stub posts + // This ensures data persists when navigating back (fullPosts comes from React Query cache) + const displayedPosts = useMemo(() => { + return [...fullPosts, ...loadedStubPosts]; + }, [fullPosts, loadedStubPosts]); // Calculate if there are more posts to load const hasNextPage = useMemo(() => { @@ -110,10 +108,10 @@ const AIResult = ({ query, nsfwPreferences }: { query: string; nsfwPreferences: }); if (fullPostData) { - // Filter out null or invalid posts before adding to displayed posts + // Filter out null or invalid posts before adding to loaded posts const validPosts = fullPostData.filter((post) => post && post.post_id); if (validPosts.length > 0) { - setDisplayedPosts((prev) => [...prev, ...validPosts]); + setLoadedStubPosts((prev) => [...prev, ...validPosts]); } setCurrentPage((prev) => prev + 1); } @@ -131,10 +129,10 @@ const AIResult = ({ query, nsfwPreferences }: { query: string; nsfwPreferences: } }, [inView, hasNextPage, isLoadingMore]); - // Reset on query change + // Reset loaded stub posts on query change useEffect(() => { - setCurrentPage(0); - setDisplayedPosts([]); + setCurrentPage(1); + setLoadedStubPosts([]); }, [query]); if (!query) return null; diff --git a/packages/ui/hooks/use-search.ts b/packages/ui/hooks/use-search.ts index f4ddbc0cd..7853104ed 100644 --- a/packages/ui/hooks/use-search.ts +++ b/packages/ui/hooks/use-search.ts @@ -29,6 +29,18 @@ export function useSearch() { const [inputValue, setInputValue] = useState(query ?? aiQuery ?? topicQuery ?? ''); const [mode, setMode] = useState(currentMode ?? 'ai'); const [secondInputValue, setSecondInputValue] = useState(userTopicQuery ?? ''); + + // Sync state with URL params (e.g., when navigating back) + useEffect(() => { + const newMode = getMode(query, aiQuery, userTopicQuery); + if (newMode) { + setMode(newMode); + } + // Always sync with URL - use empty string as fallback for reset + setInputValue(aiQuery ?? query ?? topicQuery ?? ''); + setSecondInputValue(userTopicQuery ?? ''); + }, [query, aiQuery, userTopicQuery, topicQuery]); + useEffect(() => { if (inputValue.startsWith('/')) { setMode('userTopic'); -- GitLab From f987bd9f21b929d71ef55736af60f45749ee26f8 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Wed, 7 Jan 2026 12:35:17 +0100 Subject: [PATCH 7/8] Fix flaky e2e test for vote tooltips on replies page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test was failing intermittently because it expected exact tooltip text "UpvoteUpvote" / "DownvoteDownvote", but posts after payout show additional warning text. Fix: Accept both variants (with and without payout warning), matching the approach already used in profileBlogPage.spec.ts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../tests/e2e/profileRepliesPage.spec.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts b/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts index 5c14daa27..b65544c12 100644 --- a/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts +++ b/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts @@ -238,8 +238,13 @@ test.describe('Replies Tab in Profile page of @gtg', () => { // Hover upvote button await profilePage.postUpvoteButton.first().hover(); await profilePage.page.waitForTimeout(1000); - // Validate the tooltip message - expect(await profilePage.postUpvoteTooltip.textContent()).toBe('UpvoteUpvote'); + // Validate the tooltip message (may include payout warning for old posts) + const upvoteTooltipText = await profilePage.postUpvoteTooltip.textContent(); + expect( + upvoteTooltipText === 'UpvoteUpvote' || + upvoteTooltipText === + 'UpvoteVoting on Content after their payout does not generate any new rewardsUpvoteVoting on Content after their payout does not generate any new rewards' + ).toBeTruthy(); // Upvote icon color expect( await profilePage.getElementCssPropertyValue( @@ -270,8 +275,13 @@ test.describe('Replies Tab in Profile page of @gtg', () => { // Hover Downvote button await profilePage.postDownvoteButton.first().hover(); await profilePage.page.waitForTimeout(1000); - // Validate the tooltip message - expect(await profilePage.postDownvoteTooltip.textContent()).toBe('DownvoteDownvote'); + // Validate the tooltip message (may include payout warning for old posts) + const downvoteTooltipText = await profilePage.postDownvoteTooltip.textContent(); + expect( + downvoteTooltipText === 'DownvoteDownvote' || + downvoteTooltipText === + 'DownvoteVoting on Content after their payout does not generate any new rewardsDownvoteVoting on Content after their payout does not generate any new rewards' + ).toBeTruthy(); // Upvote icon color expect( await profilePage.getElementCssPropertyValue( -- GitLab From b28f7feddbdbdc707b9aeb3defb1a97709d4d8b6 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Wed, 7 Jan 2026 20:03:56 +0100 Subject: [PATCH 8/8] Simplify tooltip assertions and extend fix to mainTimeline tests Improve kkocot's fix by using toContain() instead of explicit string matching - more resilient if tooltip message text changes in the future. Also apply the same fix to mainTimeline.spec.ts (4 more flaky assertions). --- .../playwright/tests/e2e/mainTimeline.spec.ts | 16 ++++++++-------- .../tests/e2e/profileRepliesPage.spec.ts | 14 ++------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/apps/blog/playwright/tests/e2e/mainTimeline.spec.ts b/apps/blog/playwright/tests/e2e/mainTimeline.spec.ts index c64cfa9b8..434e5cef0 100644 --- a/apps/blog/playwright/tests/e2e/mainTimeline.spec.ts +++ b/apps/blog/playwright/tests/e2e/mainTimeline.spec.ts @@ -756,8 +756,8 @@ test.describe('Home page tests', () => { await homePage.getFirstPostUpvoteButton.hover(); // Wait for tooltip to be visible instead of fixed timeout await expect(homePage.getFirstPostUpvoteButtonTooltip).toBeVisible({ timeout: 15000 }); - // Validate the tooltip message - expect(await homePage.getFirstPostUpvoteButtonTooltip.textContent()).toBe('UpvoteUpvote'); + // Validate the tooltip message (may include payout warning for old posts) + expect(await homePage.getFirstPostUpvoteButtonTooltip.textContent()).toContain('Upvote'); // Upvote icon color expect( await homePage.getElementCssPropertyValue( @@ -803,8 +803,8 @@ test.describe('Home page tests', () => { await homePage.getFirstPostUpvoteButton.hover(); // Wait for tooltip to be visible instead of fixed timeout await expect(homePage.getFirstPostUpvoteButtonTooltip).toBeVisible({ timeout: 15000 }); - // Validate the tooltip message - expect(await homePage.getFirstPostUpvoteButtonTooltip.textContent()).toBe('UpvoteUpvote'); + // Validate the tooltip message (may include payout warning for old posts) + expect(await homePage.getFirstPostUpvoteButtonTooltip.textContent()).toContain('Upvote'); // Upvote icon color expect( await homePage.getElementCssPropertyValue( @@ -857,8 +857,8 @@ test.describe('Home page tests', () => { await homePage.getFirstPostDownvoteButton.hover(); // Wait for tooltip to be visible instead of fixed timeout await expect(homePage.getFirstPostDownvoteButtonTooltip).toBeVisible({ timeout: 15000 }); - // Validate the tooltip message - expect(await homePage.getFirstPostDownvoteButtonTooltip.textContent()).toBe('DownvoteDownvote'); + // Validate the tooltip message (may include payout warning for old posts) + expect(await homePage.getFirstPostDownvoteButtonTooltip.textContent()).toContain('Downvote'); // Downvote icon color expect( await homePage.getElementCssPropertyValue( @@ -904,8 +904,8 @@ test.describe('Home page tests', () => { await homePage.getFirstPostDownvoteButton.hover(); // Wait for tooltip to be visible instead of fixed timeout await expect(homePage.getFirstPostDownvoteButtonTooltip).toBeVisible({ timeout: 15000 }); - // Validate the tooltip message - expect(await homePage.getFirstPostDownvoteButtonTooltip.textContent()).toBe('DownvoteDownvote'); + // Validate the tooltip message (may include payout warning for old posts) + expect(await homePage.getFirstPostDownvoteButtonTooltip.textContent()).toContain('Downvote'); // Downvote icon color expect( await homePage.getElementCssPropertyValue( diff --git a/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts b/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts index b65544c12..72be98af8 100644 --- a/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts +++ b/apps/blog/playwright/tests/e2e/profileRepliesPage.spec.ts @@ -239,12 +239,7 @@ test.describe('Replies Tab in Profile page of @gtg', () => { await profilePage.postUpvoteButton.first().hover(); await profilePage.page.waitForTimeout(1000); // Validate the tooltip message (may include payout warning for old posts) - const upvoteTooltipText = await profilePage.postUpvoteTooltip.textContent(); - expect( - upvoteTooltipText === 'UpvoteUpvote' || - upvoteTooltipText === - 'UpvoteVoting on Content after their payout does not generate any new rewardsUpvoteVoting on Content after their payout does not generate any new rewards' - ).toBeTruthy(); + expect(await profilePage.postUpvoteTooltip.textContent()).toContain('Upvote'); // Upvote icon color expect( await profilePage.getElementCssPropertyValue( @@ -276,12 +271,7 @@ test.describe('Replies Tab in Profile page of @gtg', () => { await profilePage.postDownvoteButton.first().hover(); await profilePage.page.waitForTimeout(1000); // Validate the tooltip message (may include payout warning for old posts) - const downvoteTooltipText = await profilePage.postDownvoteTooltip.textContent(); - expect( - downvoteTooltipText === 'DownvoteDownvote' || - downvoteTooltipText === - 'DownvoteVoting on Content after their payout does not generate any new rewardsDownvoteVoting on Content after their payout does not generate any new rewards' - ).toBeTruthy(); + expect(await profilePage.postDownvoteTooltip.textContent()).toContain('Downvote'); // Upvote icon color expect( await profilePage.getElementCssPropertyValue( -- GitLab