// --------------------------------------------------------------------------- // CSS Selectors — centralised so that UI changes only require edits here. // --------------------------------------------------------------------------- export const XHS_SELECTORS = { login: { /** QR code image on the login modal (auto-appears after a few seconds). */ qrCodeImage: 'img.qrcode-img', /** Element present only when the user is logged in (sidebar channel link). */ loggedInIndicator: '.user .link-wrapper .channel', /** The "login" button that opens the QR code modal (if not already shown). */ loginButton: '.login-btn', /** Logged-in user's avatar image in the sidebar. */ userAvatar: '.user .avatar img', /** Logged-in user's profile link in the sidebar (href contains userId). */ userLink: '.user .link-wrapper a', }, feed: { /** Container for each feed card on the explore page. */ feedCard: '.note-item', /** The cover image within a feed card. */ coverImage: '.note-item a.cover img', /** The title/footer within a feed card. */ footerTitle: '.note-item .footer .title', /** Author name within a feed card. */ authorName: '.note-item .footer .author-wrapper .name', /** Author avatar within a feed card. */ authorAvatar: '.note-item .footer .author-wrapper .author-head img', /** Like count within a feed card. */ likeCount: '.note-item .footer .like-wrapper .count', }, search: { /** Search result container. */ resultContainer: '#global-search-result-container', /** Individual search result note items. */ noteItem: '.feeds-container .note-item', /** Search result cover image. */ coverImage: '.feeds-container .note-item a.cover img', /** Search result title. */ title: '.feeds-container .note-item .footer .title', /** Search result author name. */ authorName: '.feeds-container .note-item .footer .author-wrapper .name', /** Search result author avatar. */ authorAvatar: '.feeds-container .note-item .footer .author-wrapper .author-head img', /** Search result like count. */ likeCount: '.feeds-container .note-item .footer .like-wrapper .count', }, feedDetail: { /** The main content container for a note detail page. */ noteContainer: '#noteContainer', /** The title of the note. */ title: '#detail-title', /** The description / body content of the note. */ description: '#detail-desc', /** Individual images in an image note. */ images: '.note-image-list .note-image img', /** The single hero image (some notes use this instead of a list). */ heroImage: '.note-hero img', /** Video player element. */ video: '#videoplayer video', /** Video player source. */ videoSource: '#videoplayer video source', /** Tag links within the note body. */ tags: '#detail-desc a.tag', /** Like count. */ likeCount: '.engage-bar .like-wrapper .count', /** Collect (favorite) count. */ collectCount: '.engage-bar .collect-wrapper .count', /** Comment count. */ commentCount: '.engage-bar .chat-wrapper .count', /** Share count. */ /** Publish / create time text. */ createTime: '.note-scroller .bottom-container .date', /** IP location. */ ipLocation: '.note-scroller .bottom-container .ip-location', /** Author nickname on the detail page. */ authorName: '.author-container .info .name', /** Author avatar on the detail page. */ authorAvatar: '.author-container .info .avatar img', /** Author user ID link. */ authorLink: '.author-container .info a', /** Comment list container. */ commentListContainer: '.comments-container .list-container', /** Individual top-level comment items. */ commentItem: '.comments-container .list-container > .parent-comment > .comment-item', /** Parent comment content text. */ commentContent: '.content', /** Comment author name. */ commentAuthor: '.author .name', /** Comment author avatar. */ commentAvatar: '.avatar img.avatar-item', /** Comment like count. */ commentLikeCount: '.like .count', /** Comment publish time. */ commentTime: '.date', /** Comment IP location. */ commentIpLocation: '.ip-location', /** Sub-comment (reply) items. */ subCommentItem: '.sub-comment-list .sub-comment-item', /** "Show more comments" button. */ showMoreComments: '.comments-container .show-more', /** "Load more replies" button within a comment thread. */ loadMoreReplies: '.sub-comment-list .show-more', /** Sub-comment count text element (e.g. "展开 X 条回复"). */ subCommentCountText: '.sub-comment-list .show-more, .reply-container .show-more', }, userProfile: { /** Profile header container. */ headerContainer: '.user-info', /** User nickname. */ nickname: '.user-info .user-name', /** User avatar image (the img itself carries class user-image). */ avatar: '.user-info img.user-image', /** User bio / description text. */ description: '.user-info .user-desc', /** User gender icon or text. */ gender: '.user-info .gender-icon', /** IP location. */ ipLocation: '.user-info .user-ip', /** Follower / following / interaction count elements. */ followCount: '.user-info .user-interactions > div', /** Individual feed items on the user profile. */ feedItem: '.feeds-container .note-item', }, // -- Phase 4: Publish ----------------------------------------------------- publish: { /** The file input element for uploading images on the creator publish page. */ imageFileInput: 'input[type="file"]', /** Title input field on the publish form. */ titleInput: 'input.d-text[placeholder*="标题"]', /** Content / body editor area on the publish form (contenteditable ProseMirror). */ contentEditor: '.tiptap.ProseMirror', /** The tag / topic button that opens the topic input. */ tagButton: 'button.contentBtn.topic-btn', /** Tag / topic input field for typing hashtags. */ tagInput: 'button.contentBtn.topic-btn input', /** Topic / hashtag suggestion dropdown item. */ tagSuggestionItem: '.publish-topic-item, .topic-item', /** "Publish" / submit button. */ publishButton: 'button.d-button:has-text("发布")', /** Schedule / timing selector button. */ scheduleButton: '.timing-btn, button:has-text("定时")', /** Schedule date/time input field. */ scheduleInput: '.timing-input input, .schedule-input input', /** Original content declaration checkbox. */ originalCheckbox: '.original-checkbox input, input[type="checkbox"][name="original"]', /** Visibility / permission setting button. */ visibilityButton: '.permission-btn, button:has-text("可见")', /** Visibility option for public. */ visibilityPublic: '.permission-option:has-text("公开"), .visibility-option:has-text("公开")', /** Visibility option for private. */ visibilityPrivate: '.permission-option:has-text("私密"), .visibility-option:has-text("私密")', /** Visibility option for friends only. */ visibilityFriends: '.permission-option:has-text("好友"), .visibility-option:has-text("好友")', /** Upload complete indicator (images uploaded and thumbnails visible). */ uploadedImageItem: '.img-upload-area .img-container', /** Video upload complete indicator (video thumbnail visible). */ uploadedVideoItem: '.upload-video video, .video-item video, .video-container video', /** Success indicator shown after publish completes. */ publishSuccess: '.success-panel, .publish-success, .note-success', /** URL in the address bar after successful publish (used as a fallback check). */ publishSuccessUrlPattern: /\/publish\/success/, }, // -- Phase 4: Comment / Reply --------------------------------------------- comment: { /** The comment input field / textarea on the feed detail page. */ commentInput: '#content-textarea', /** Alternative comment input (contenteditable div). */ commentInputAlt: '[contenteditable][data-placeholder]', /** Comment submit / send button. */ commentSubmitButton: '.comment-submit, button.submit, .btn-send', /** Parent comment element (used to find specific comment by ID). */ commentItem: '.comment-item, .note-comment-item, [id^="comment-"]', /** Reply button on an individual comment. */ commentReplyButton: '.reply-btn, .comment-reply', /** Reply input that appears after clicking reply. */ replyInput: '.reply-input textarea, .reply-content [contenteditable], .reply-area textarea', }, // -- Phase 4: Interaction (Like / Favorite) -------------------------------- // -- Phase 5: Notification ------------------------------------------------ notification: { /** Each notification item container. */ container: '.container', /** User avatar link (href contains userId + xsecToken). */ userAvatar: 'a.user-avatar', /** User name link. */ userName: '.user-info a', /** Interaction type hint (e.g. "评论了你的笔记"). */ interactionHint: '.interaction-hint span:first-child', /** Notification time. */ interactionTime: '.interaction-time', /** Comment content text. */ interactionContent: '.interaction-content', /** Note thumbnail image (parent link href contains feedId + xsecToken). */ extraImage: '.extra img', /** Reply button to expand inline reply. */ replyButton: '.action-reply', /** Reply textarea that appears after clicking reply. */ replyInput: 'textarea.comment-input', /** Reply submit button. */ replySubmit: 'button.submit', /** Unread badge on the explore page bottom menu. */ unreadBadge: '#global > div.main-container > div.bottom-menu > div > li.link-wrapper.bottom-channel > a > div > div', }, interaction: { /** Like button on the feed detail page. */ likeButton: '.engage-bar-style .like-wrapper', /** Like button in active/liked state. */ likeButtonActive: '.engage-bar-style .like-wrapper.like-active', /** Like count element next to the like button. */ likeCount: '.engage-bar .like-wrapper .count', /** Favorite / collect button on the feed detail page. */ favoriteButton: '.engage-bar-style .collect-wrapper', /** Favorite button in active/favorited state. */ favoriteButtonActive: '.engage-bar-style .collect-wrapper.collect-active', /** Favorite count element next to the favorite button. */ favoriteCount: '.engage-bar .collect-wrapper .count', /** Container for the interaction bar at the bottom of a feed detail. */ interactionBar: '.interact-container, .engage-bar', }, } as const;