[READ-ONLY] Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
6

Configure Feed

Select the types of activity you want to include in your feed.

Add more test selectors

Grace Kind (May 15, 2026, 11:42 PM -0500) fe0f7ad5 09546a8b

+582 -439
+1 -1
package.json
··· 1 1 { 2 2 "name": "impro", 3 - "version": "0.14.32", 3 + "version": "0.14.33", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "rm -rf build && NODE_ENV=development eleventy --serve",
+4
src/js/components/edit-profile-dialog.js
··· 232 232 <context-menu class="edit-profile-banner-menu"> 233 233 <context-menu-item-group> 234 234 <context-menu-item 235 + data-testid="menu-action-banner-upload" 235 236 @click=${() => this._pickImage("banner")} 236 237 > 237 238 Upload from Files ··· 240 241 ${bannerSrc 241 242 ? html`<context-menu-item-group> 242 243 <context-menu-item 244 + data-testid="menu-action-banner-remove" 243 245 @click=${() => { 244 246 this._newBannerDataUrl = null; 245 247 this._removeBanner = true; ··· 255 257 <context-menu class="edit-profile-avatar-menu"> 256 258 <context-menu-item-group> 257 259 <context-menu-item 260 + data-testid="menu-action-avatar-upload" 258 261 @click=${() => this._pickImage("avatar")} 259 262 > 260 263 Upload from Files ··· 263 266 ${avatarSrc 264 267 ? html`<context-menu-item-group> 265 268 <context-menu-item 269 + data-testid="menu-action-avatar-remove" 266 270 @click=${() => { 267 271 this._newAvatarDataUrl = null; 268 272 this._removeAvatar = true;
+2
src/js/components/image-alt-text-dialog.js
··· 89 89 <div class="image-alt-text-dialog-footer-buttons"> 90 90 <button 91 91 class="rounded-button rounded-button-secondary" 92 + data-testid="alt-text-cancel" 92 93 @click=${() => this.close()} 93 94 > 94 95 Cancel 95 96 </button> 96 97 <button 97 98 class="rounded-button rounded-button-primary" 99 + data-testid="alt-text-save" 98 100 @click=${() => this.save()} 99 101 .disabled=${isAboveCharLimit} 100 102 >
+41 -10
src/js/modals.js
··· 9 9 render( 10 10 html` 11 11 <div class="modal-dialog-content"> 12 - <h2 class="modal-dialog-title modal-dialog-title-large">Sign in</h2> 13 - <p class="modal-dialog-message">Sign in to join the conversation!</p> 12 + <h2 13 + class="modal-dialog-title modal-dialog-title-large" 14 + data-testid="modal-title" 15 + > 16 + Sign in 17 + </h2> 18 + <p class="modal-dialog-message" data-testid="modal-message"> 19 + Sign in to join the conversation! 20 + </p> 14 21 <a 15 22 href=${linkToLogin()} 16 23 class="modal-dialog-button primary-button full-width" 24 + data-testid="modal-primary-button" 17 25 @click=${() => { 18 26 dialog.close(); 19 27 dialog.remove(); ··· 45 53 render( 46 54 html` 47 55 <div class="modal-dialog-content"> 48 - <h2 class="modal-dialog-title">${title}</h2> 49 - <p class="modal-dialog-message">${message}</p> 56 + <h2 class="modal-dialog-title" data-testid="modal-title">${title}</h2> 57 + <p class="modal-dialog-message" data-testid="modal-message"> 58 + ${message} 59 + </p> 50 60 <div class="modal-dialog-buttons"> 51 - <button class="modal-dialog-button primary-button"> 61 + <button 62 + class="modal-dialog-button primary-button" 63 + data-testid="modal-primary-button" 64 + > 52 65 ${confirmButtonText} 53 66 </button> 54 67 </div> ··· 98 111 render( 99 112 html` 100 113 <div class="modal-dialog-content"> 101 - ${title ? html`<h2 class="modal-dialog-title">${title}</h2>` : null} 102 - <p class="modal-dialog-message">${message}</p> 114 + ${title 115 + ? html`<h2 class="modal-dialog-title" data-testid="modal-title"> 116 + ${title} 117 + </h2>` 118 + : null} 119 + <p class="modal-dialog-message" data-testid="modal-message"> 120 + ${message} 121 + </p> 103 122 <div class="modal-dialog-buttons"> 104 - <button class="modal-dialog-button cancel-button">Cancel</button> 123 + <button 124 + class="modal-dialog-button cancel-button" 125 + data-testid="modal-cancel-button" 126 + > 127 + Cancel 128 + </button> 105 129 <button 106 130 class="modal-dialog-button confirm-button ${confirmButtonStyle}-button" 131 + data-testid="modal-confirm-button" 107 132 > 108 133 ${confirmButtonText} 109 134 </button> ··· 211 236 render( 212 237 html` 213 238 <div class="modal-dialog-content"> 214 - <h2 class="modal-dialog-title">Who can interact with this post?</h2> 239 + <h2 class="modal-dialog-title" data-testid="modal-title"> 240 + Who can interact with this post? 241 + </h2> 215 242 <div class="modal-dialog-message who-can-reply-body"> 216 243 <span>${threadgateRuleTemplate({ post })}</span> 217 244 ${embeddingDisabled ··· 219 246 : ""} 220 247 </div> 221 248 <div class="modal-dialog-buttons"> 222 - <button class="modal-dialog-button primary-button" @click=${dismiss}> 249 + <button 250 + class="modal-dialog-button primary-button" 251 + data-testid="modal-primary-button" 252 + @click=${dismiss} 253 + > 223 254 OK 224 255 </button> 225 256 </div>
+1 -1
src/js/templates/footer.template.js
··· 25 25 class="footer-nav logged-out-footer" 26 26 data-testid="logged-out-footer" 27 27 > 28 - <a href="/"><h2>IMPRO</h2></a> 28 + <a href="/"><h2 data-testid="brand-title">IMPRO</h2></a> 29 29 <a 30 30 href=${linkToLogin()} 31 31 class="square-button primary-button login-button"
+35 -4
src/js/templates/postActionBar.template.js
··· 40 40 return html` 41 41 <context-menu-item-group> 42 42 <context-menu-item 43 + data-testid="menu-action-post-open-in-bsky" 43 44 @click=${() => { 44 45 window.open(getBlueskyLinkForPost(post), "_blank"); 45 46 }} ··· 47 48 Open in bsky.app 48 49 </context-menu-item> 49 50 <context-menu-item 51 + data-testid="menu-action-post-copy-link" 50 52 @click=${() => { 51 53 navigator.clipboard.writeText(getPermalinkForPost(post)); 52 54 showToast("Link copied to clipboard", { style: "success" }); ··· 57 59 ${post.record?.text 58 60 ? html` 59 61 <context-menu-item 62 + data-testid="menu-action-post-copy-text" 60 63 @click=${() => { 61 64 navigator.clipboard.writeText( 62 65 richTextToString(post.record.text, post.record.facets), ··· 76 79 ${enableFeedFeedback 77 80 ? html` 78 81 <context-menu-item-group> 79 - <context-menu-item @click=${() => onClickShowMore(post)}> 82 + <context-menu-item 83 + data-testid="menu-action-post-show-more" 84 + @click=${() => onClickShowMore(post)} 85 + > 80 86 Show more like this 81 87 </context-menu-item> 82 - <context-menu-item @click=${() => onClickShowLess(post)}> 88 + <context-menu-item 89 + data-testid="menu-action-post-show-less" 90 + @click=${() => onClickShowLess(post)} 91 + > 83 92 Show less like this 84 93 </context-menu-item> 85 94 </context-menu-item-group> ··· 91 100 ? html` 92 101 <context-menu-item-group> 93 102 <context-menu-item 103 + data-testid="menu-action-post-hide" 94 104 @click=${() => onClickHidePost(post)} 95 105 > 96 106 Hide ${post.record?.reply ? "reply" : "post"} for me ··· 100 110 : null} 101 111 <context-menu-item-group> 102 112 <context-menu-item 113 + data-testid="menu-action-post-mute" 114 + data-teststate=${post.author.viewer?.muted 115 + ? "muted" 116 + : "unmuted"} 103 117 @click=${() => 104 118 onClickMute(post.author, !post.author.viewer?.muted)} 105 119 > ··· 108 122 : "Mute account"} 109 123 </context-menu-item> 110 124 <context-menu-item 125 + data-testid="menu-action-post-block" 126 + data-teststate=${post.author.viewer?.blocking 127 + ? "blocking" 128 + : "not-blocking"} 111 129 @click=${() => 112 130 onClickBlock(post.author, !post.author.viewer?.blocking)} 113 131 > ··· 115 133 ? "Unblock account" 116 134 : "Block account"} 117 135 </context-menu-item> 118 - <context-menu-item @click=${() => onClickReport(post)}> 136 + <context-menu-item 137 + data-testid="menu-action-post-report" 138 + @click=${() => onClickReport(post)} 139 + > 119 140 Report post 120 141 </context-menu-item> 121 142 </context-menu-item-group> ··· 127 148 ${canPin 128 149 ? html` 129 150 <context-menu-item 151 + data-testid="menu-action-post-pin" 152 + data-teststate=${isPinnedToProfile 153 + ? "pinned" 154 + : "unpinned"} 130 155 @click=${() => onClickPin(post, !isPinnedToProfile)} 131 156 > 132 157 ${isPinnedToProfile ··· 135 160 </context-menu-item> 136 161 ` 137 162 : null} 138 - <context-menu-item @click=${() => onClickDelete(post)}> 163 + <context-menu-item 164 + data-testid="menu-action-post-delete" 165 + @click=${() => onClickDelete(post)} 166 + > 139 167 Delete post 140 168 </context-menu-item> 141 169 </context-menu-item-group> ··· 256 284 </button> 257 285 <context-menu> 258 286 <context-menu-item 287 + data-testid="menu-action-repost" 288 + data-teststate=${isReposted ? "reposted" : "not-reposted"} 259 289 @click=${() => { 260 290 if (!isAuthenticated) { 261 291 showSignInModal(); ··· 267 297 ${isReposted ? "Undo repost" : "Repost"} 268 298 </context-menu-item> 269 299 <context-menu-item 300 + data-testid="menu-action-quote-post" 270 301 ?disabled=${!canQuotePost || !currentUser} 271 302 @click=${() => { 272 303 if (!isAuthenticated) {
+18 -1
src/js/templates/profileCard.template.js
··· 102 102 return html` 103 103 <context-menu-item-group> 104 104 <context-menu-item 105 + data-testid="menu-action-profile-open-in-bsky" 105 106 @click=${() => { 106 107 window.open(getBlueskyLinkForProfile(profile), "_blank"); 107 108 }} ··· 109 110 Open in bsky.app 110 111 </context-menu-item> 111 112 <context-menu-item 113 + data-testid="menu-action-profile-copy-link" 112 114 @click=${() => { 113 115 navigator.clipboard.writeText(getPermalinkForProfile(profile)); 114 116 showToast("Link copied to clipboard", { style: "success" }); ··· 120 122 ${isAuthenticated 121 123 ? html` 122 124 <context-menu-item 125 + data-testid="menu-action-profile-search-posts" 123 126 @click=${() => { 124 127 router.go(linkToSearchPostsByProfile(profile)); 125 128 }} ··· 133 136 ${isLabeler 134 137 ? html` 135 138 <context-menu-item 136 - data-testid="context-menu-follow" 139 + data-testid="menu-action-profile-follow" 140 + data-teststate=${isFollowing ? "following" : "not-following"} 137 141 @click=${() => { 138 142 onClickFollow(profile, !isFollowing); 139 143 }} ··· 144 148 : null} 145 149 <context-menu-item-group> 146 150 <context-menu-item 151 + data-testid="menu-action-profile-mute" 152 + data-teststate=${profile.viewer?.muted ? "muted" : "unmuted"} 147 153 @click=${() => { 148 154 onClickMute(profile, !profile.viewer?.muted); 149 155 }} ··· 151 157 ${profile.viewer?.muted ? "Unmute Account" : "Mute Account"} 152 158 </context-menu-item> 153 159 <context-menu-item 160 + data-testid="menu-action-profile-block" 161 + data-teststate=${profile.viewer?.blocking 162 + ? "blocking" 163 + : "not-blocking"} 154 164 @click=${() => { 155 165 onClickBlock(profile, !profile.viewer?.blocking); 156 166 }} ··· 158 168 ${profile.viewer?.blocking ? "Unblock Account" : "Block Account"} 159 169 </context-menu-item> 160 170 <context-menu-item 171 + data-testid="menu-action-profile-report" 161 172 @click=${() => { 162 173 onClickReport(profile); 163 174 }} ··· 316 327 "rounded-button-primary": !isSubscribed, 317 328 })} 318 329 data-testid="subscribe-button" 330 + data-teststate=${isSubscribed ? "subscribed" : "not-subscribed"} 319 331 > 320 332 ${isSubscribed ? "Subscribed" : "+ Subscribe"} 321 333 </button>`; ··· 334 346 "rounded-button-primary": !isFollowing, 335 347 })} 336 348 data-testid="follow-button" 349 + data-teststate=${isFollowing 350 + ? "following" 351 + : isFollowedBy 352 + ? "follow-back" 353 + : "follow"} 337 354 > 338 355 ${isFollowing 339 356 ? "Following"
+1
src/js/templates/tabBar.template.js
··· 10 10 class=${classnames("tab-bar-button", { 11 11 active: activeTab === tab.value, 12 12 })} 13 + data-testid="tab-${tab.value}" 13 14 @click=${() => onTabClick(tab.value)} 14 15 > 15 16 <span class="tab-bar-button-label">${tab.label}</span>
+1
src/js/toasts.js
··· 78 78 ) { 79 79 const toast = document.createElement("div"); 80 80 toast.classList.add("toast", style); 81 + toast.dataset.testid = "toast"; 81 82 const resolvedIconTemplate = 82 83 iconTemplate ?? STYLE_ICONS[style] ?? STYLE_ICONS.default; 83 84 render(
+2
src/js/views/feedDetail.view.js
··· 103 103 </button> 104 104 <context-menu> 105 105 <context-menu-item 106 + data-testid="menu-action-feed-open-in-bsky" 106 107 @click=${() => { 107 108 window.open(feedLink, "_blank"); 108 109 }} ··· 110 111 Open in bsky.app 111 112 </context-menu-item> 112 113 <context-menu-item 114 + data-testid="menu-action-feed-copy-link" 113 115 @click=${() => { 114 116 navigator.clipboard.writeText(feedLink); 115 117 showToast("Link copied to clipboard", {
+2 -2
src/js/views/postThread.view.js
··· 63 63 error.status === 400 && 64 64 error.data?.error === "NotFound" 65 65 ) { 66 - return html`<div class="error-state"> 66 + return html`<div class="error-state" data-testid="post-not-found"> 67 67 <div>Post not found</div> 68 68 <button @click=${() => window.location.reload()}>Try again</button> 69 69 </div>`; 70 70 } else { 71 71 console.error(error); 72 - return html`<div class="error-state"> 72 + return html`<div class="error-state" data-testid="thread-error"> 73 73 <div>Error loading thread</div> 74 74 <button @click=${() => window.location.reload()}>Try again</button> 75 75 </div>`;
+16 -3
src/js/views/search.view.js
··· 179 179 </div>`; 180 180 } 181 181 if (!postSearchResults || postSearchResults.length === 0) { 182 - return html`<div class="search-status-message">No posts found.</div>`; 182 + return html`<div 183 + class="search-status-message" 184 + data-testid="empty-state" 185 + > 186 + No posts found. 187 + </div>`; 183 188 } 184 189 return html`<infinite-scroll-container 185 190 lookahead="2500px" ··· 233 238 </div>`; 234 239 } 235 240 if (!profileSearchResults || profileSearchResults.length === 0) { 236 - return html`<div class="search-status-message"> 241 + return html`<div 242 + class="search-status-message" 243 + data-testid="empty-state" 244 + > 237 245 No profiles found. 238 246 </div>`; 239 247 } ··· 276 284 </div>`; 277 285 } 278 286 if (!feedSearchResults || feedSearchResults.length === 0) { 279 - return html`<div class="search-status-message">No feeds found.</div>`; 287 + return html`<div 288 + class="search-status-message" 289 + data-testid="empty-state" 290 + > 291 + No feeds found. 292 + </div>`; 280 293 } 281 294 return html`<infinite-scroll-container 282 295 lookahead="2500px"
+24 -4
src/js/views/settings.view.js
··· 28 28 29 29 const menuItems = [ 30 30 { 31 + key: "appearance", 31 32 icon: eyeIconTemplate, 32 33 label: "Appearance", 33 34 url: "/settings/appearance", 34 35 enabled: true, 35 36 }, 36 37 { 38 + key: "muted-words", 37 39 icon: mutedWordIconTemplate, 38 40 label: "Muted words", 39 41 url: "/settings/muted-words", 40 42 enabled: true, 41 43 }, 42 44 { 45 + key: "muted-accounts", 43 46 icon: eyeSlashIconTemplate, 44 47 label: "Muted accounts", 45 48 url: "/settings/muted-accounts", 46 49 enabled: true, 47 50 }, 48 51 { 52 + key: "blocked-accounts", 49 53 icon: restrictedIconTemplate, 50 54 label: "Blocked accounts", 51 55 url: "/settings/blocked-accounts", ··· 54 58 ...(window.env.environment === "development" 55 59 ? [ 56 60 { 61 + key: "plugins", 57 62 icon: boxIconTemplate, 58 63 label: "Plugins (beta)", 59 64 url: "/settings/plugins", ··· 62 67 ] 63 68 : []), 64 69 { 70 + key: "advanced", 65 71 icon: codeIconTemplate, 66 72 label: "Advanced", 67 73 url: "/settings/advanced", ··· 98 104 class=${classnames("vertical-nav-item", { 99 105 disabled: !item.enabled, 100 106 })} 107 + data-testid="settings-nav-${item.key}" 101 108 > 102 109 <span class="vertical-nav-icon">${item.icon()}</span> 103 110 <span class="vertical-nav-label">${item.label}</span> ··· 110 117 <hr /> 111 118 <button 112 119 class="vertical-nav-item danger-button" 120 + data-testid="settings-sign-out" 113 121 @click=${async () => { 114 122 if ( 115 123 !(await confirm("Are you sure you want to sign out?", { ··· 127 135 Sign out 128 136 </button> 129 137 </nav> 130 - <div class="version-info"> 138 + <div class="version-info" data-testid="version-info"> 131 139 Impro v${window.env.version} - ${window.env.gitCommit} 132 140 </div> 133 141 <div class="settings-footer-links"> 134 - <a href="/tos.html" data-external="true">Terms</a> 142 + <a 143 + href="/tos.html" 144 + data-testid="footer-link-terms" 145 + data-external="true" 146 + >Terms</a 147 + > 135 148 <span class="settings-footer-separator">·</span> 136 - <a href="/privacy.html" data-external="true" 149 + <a 150 + href="/privacy.html" 151 + data-testid="footer-link-privacy" 152 + data-external="true" 137 153 >Privacy Policy</a 138 154 > 139 155 <span class="settings-footer-separator">·</span> 140 - <a href="https://github.com/improsocial/impro">GitHub</a> 156 + <a 157 + href="https://github.com/improsocial/impro" 158 + data-testid="footer-link-github" 159 + >GitHub</a 160 + > 141 161 </div> 142 162 </main>`, 143 163 })}
+12 -3
src/js/views/settings/appearance.view.js
··· 65 65 onClickBackButton: () => window.router.go("/settings"), 66 66 })} 67 67 <main> 68 - <section class="settings-section"> 68 + <section 69 + class="settings-section" 70 + data-testid="settings-section-color-scheme" 71 + > 69 72 <h2>Color scheme</h2> 70 73 <p>Choose between light and dark mode.</p> 71 74 <select ··· 95 98 </option> 96 99 </select> 97 100 </section> 98 - <section class="settings-section"> 101 + <section 102 + class="settings-section" 103 + data-testid="settings-section-highlight-color" 104 + > 99 105 <h2>Highlight color</h2> 100 106 <p>Choose the highlight color for buttons and links.</p> 101 107 <div class="settings-color-picker"> ··· 116 122 </button> 117 123 </div> 118 124 </section> 119 - <section class="settings-section"> 125 + <section 126 + class="settings-section" 127 + data-testid="settings-section-like-color" 128 + > 120 129 <h2>Like color</h2> 121 130 <p>Choose the color for liked posts.</p> 122 131 <div class="settings-color-picker">
+4 -1
src/js/views/settings/blockedAccounts.view.js
··· 95 95 onClickBackButton: () => window.router.go("/settings"), 96 96 })} 97 97 <main> 98 - <p class="blocked-account-description"> 98 + <p 99 + class="blocked-account-description" 100 + data-testid="page-description" 101 + > 99 102 Blocked accounts cannot reply to your posts, mention you, or 100 103 interact with you. You won't see their content. 101 104 </p>
+4 -1
src/js/views/settings/mutedAccounts.view.js
··· 93 93 onClickBackButton: () => window.router.go("/settings"), 94 94 })} 95 95 <main> 96 - <p class="muted-account-description"> 96 + <p 97 + class="muted-account-description" 98 + data-testid="page-description" 99 + > 97 100 Muted accounts have their posts removed from your feed and 98 101 from your notifications. Mutes are completely private. 99 102 </p>
+6 -2
src/js/views/settings/mutedWords.view.js
··· 263 263 onClickBackButton: () => window.router.go("/settings"), 264 264 })} 265 265 <main> 266 - <form class="muted-word-form" @submit=${(e) => handleSubmit(e)}> 266 + <form 267 + class="muted-word-form" 268 + data-testid="muted-word-form" 269 + @submit=${(e) => handleSubmit(e)} 270 + > 267 271 <h2>Add muted words and tags</h2> 268 - <p> 272 + <p data-testid="page-description"> 269 273 Posts can be muted based on their text, their tags, or both. 270 274 </p> 271 275 <input
+1 -3
tests/e2e/specs/concerns/dragToDismiss.test.js
··· 94 94 await page 95 95 .locator('#home-view [data-testid="feed-item"] .text-button') 96 96 .click(); 97 - await page 98 - .locator("context-menu-item", { hasText: "Report post" }) 99 - .click(); 97 + await page.locator('[data-testid="menu-action-post-report"]').click(); 100 98 const reportDialog = page.locator("report-dialog .report-dialog"); 101 99 await expect(reportDialog).toBeVisible({ timeout: 5000 }); 102 100 return reportDialog;
+8 -8
tests/e2e/specs/concerns/postComposerEdgeCases.test.js
··· 269 269 .click(); 270 270 271 271 // Verify error toast appears 272 - const errorToast = page.locator(".toast.error"); 272 + const errorToast = page.locator('[data-testid="toast"].error'); 273 273 await expect(errorToast).toBeVisible({ timeout: 10000 }); 274 - await expect(errorToast).toContainText("Failed to send post"); 275 274 276 275 // Verify composer remains open (not closed on error) 277 276 await expect(composer).toBeVisible(); ··· 495 494 const videoInput = composer.locator(".media-picker-input"); 496 495 await videoInput.setInputFiles(fakeVideoFile()); 497 496 498 - await expect(page.locator(".toast")).toContainText( 499 - "Could not read video file", 500 - { timeout: 5000 }, 501 - ); 497 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 498 + timeout: 5000, 499 + }); 502 500 await expect( 503 501 composer.locator(".post-composer-video-preview"), 504 502 ).toHaveCount(0); ··· 529 527 await expect(preview).toContainText("Daily video limit reached", { 530 528 timeout: 10000, 531 529 }); 532 - await expect(page.locator(".toast")).toContainText( 530 + await expect(page.locator('[data-testid="toast"]')).toContainText( 533 531 "Daily video limit reached", 534 532 ); 535 533 }); ··· 554 552 const preview = composer.locator(".post-composer-video-preview"); 555 553 await expect(preview).toBeVisible({ timeout: 5000 }); 556 554 await expect(preview).toContainText("mock failure", { timeout: 10000 }); 557 - await expect(page.locator(".toast")).toContainText("mock failure"); 555 + await expect(page.locator('[data-testid="toast"]')).toContainText( 556 + "mock failure", 557 + ); 558 558 559 559 // Remove button still works after the error. 560 560 await preview.locator(".image-preview-remove-button").click();
+4 -10
tests/e2e/specs/flows/blockUser.test.js
··· 44 44 ).toContainText("Other User", { timeout: 10000 }); 45 45 46 46 await profileView.locator(".ellipsis-button").click(); 47 - await page 48 - .locator("context-menu-item", { hasText: "Block Account" }) 49 - .click(); 47 + await page.locator('[data-testid="menu-action-profile-block"]').click(); 50 48 51 49 // Confirm the block in the confirmation dialog 52 50 const confirmButton = page.locator("button.confirm-button"); ··· 107 105 108 106 // Open the post's context menu and block the user 109 107 await feedItem.locator(".text-button").click(); 110 - await page 111 - .locator("context-menu-item", { hasText: "Block Account" }) 112 - .click(); 108 + await page.locator('[data-testid="menu-action-post-block"]').click(); 113 109 114 110 // Confirm the block in the confirmation dialog 115 111 const confirmButton = page.locator("button.confirm-button"); ··· 117 113 await confirmButton.click(); 118 114 119 115 // Verify a toast confirms the block action 120 - await expect(page.locator(".toast")).toContainText("Account blocked", { 116 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 121 117 timeout: 5000, 122 118 }); 123 119 ··· 168 164 ).toContainText("Other User", { timeout: 10000 }); 169 165 170 166 await profileView.locator(".ellipsis-button").click(); 171 - await page 172 - .locator("context-menu-item", { hasText: "Block Account" }) 173 - .click(); 167 + await page.locator('[data-testid="menu-action-profile-block"]').click(); 174 168 175 169 // Cancel the confirmation dialog 176 170 const cancelButton = page.locator("button.cancel-button");
+1 -1
tests/e2e/specs/flows/createPost.test.js
··· 380 380 await expect(composer.locator(".post-composer-video-preview")).toHaveCount( 381 381 0, 382 382 ); 383 - await expect(page.locator(".toast")).toContainText("Unsupported", { 383 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 384 384 timeout: 5000, 385 385 }); 386 386 });
+3 -3
tests/e2e/specs/flows/deletePost.test.js
··· 43 43 44 44 // Open the post action menu and click "Delete post" 45 45 await feedItem.locator(".text-button").click(); 46 - await page.locator("context-menu-item", { hasText: "Delete post" }).click(); 46 + await page.locator('[data-testid="menu-action-post-delete"]').click(); 47 47 48 48 // Confirm deletion if a confirmation dialog appears 49 49 const confirmButton = page.locator("button.confirm-button"); ··· 89 89 90 90 // Open the post action menu and click "Delete post" 91 91 await largePost.locator(".text-button").click(); 92 - await page.locator("context-menu-item", { hasText: "Delete post" }).click(); 92 + await page.locator('[data-testid="menu-action-post-delete"]').click(); 93 93 94 94 // Confirm deletion if a confirmation dialog appears 95 95 const confirmButton = page.locator("button.confirm-button"); ··· 147 147 const feedItem = profileView.locator('[data-testid="feed-item"]'); 148 148 await expect(feedItem).toHaveCount(1, { timeout: 10000 }); 149 149 await feedItem.locator(".text-button").click(); 150 - await page.locator("context-menu-item", { hasText: "Delete post" }).click(); 150 + await page.locator('[data-testid="menu-action-post-delete"]').click(); 151 151 const confirmButton = page.locator("button.confirm-button"); 152 152 await expect(confirmButton).toBeVisible({ timeout: 5000 }); 153 153 await confirmButton.click();
+10 -16
tests/e2e/specs/flows/editProfile.test.js
··· 60 60 await dialog.locator('[data-testid="edit-profile-save-button"]').click(); 61 61 62 62 // Verify the toast shows success 63 - await expect(page.locator(".toast")).toContainText("Profile updated", { 63 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 64 64 timeout: 5000, 65 65 }); 66 66 ··· 233 233 // Context menu should appear with "Upload from Files" and "Remove Avatar" 234 234 const menu = dialog.locator(".edit-profile-avatar-menu"); 235 235 await expect( 236 - menu.locator("context-menu-item", { hasText: "Upload from Files" }), 236 + menu.locator('[data-testid="menu-action-avatar-upload"]'), 237 237 ).toBeVisible({ timeout: 5000 }); 238 238 await expect( 239 - menu.locator("context-menu-item", { hasText: "Remove Avatar" }), 239 + menu.locator('[data-testid="menu-action-avatar-remove"]'), 240 240 ).toBeVisible(); 241 241 }); 242 242 ··· 274 274 // Context menu should only have "Upload from Files" 275 275 const menu = dialog.locator(".edit-profile-avatar-menu"); 276 276 await expect( 277 - menu.locator("context-menu-item", { hasText: "Upload from Files" }), 277 + menu.locator('[data-testid="menu-action-avatar-upload"]'), 278 278 ).toBeVisible({ timeout: 5000 }); 279 279 await expect( 280 - menu.locator("context-menu-item", { hasText: "Remove Avatar" }), 280 + menu.locator('[data-testid="menu-action-avatar-remove"]'), 281 281 ).toHaveCount(0); 282 282 }); 283 283 ··· 320 320 321 321 // Click "Remove Avatar" 322 322 const menu = dialog.locator(".edit-profile-avatar-menu"); 323 - await menu 324 - .locator("context-menu-item", { hasText: "Remove Avatar" }) 325 - .click(); 323 + await menu.locator('[data-testid="menu-action-avatar-remove"]').click(); 326 324 327 325 // Avatar image should be removed from the preview (placeholder may still exist) 328 326 await expect( ··· 334 332 // Save 335 333 await dialog.locator('[data-testid="edit-profile-save-button"]').click(); 336 334 337 - await expect(page.locator(".toast")).toContainText("Profile updated", { 335 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 338 336 timeout: 5000, 339 337 }); 340 338 }); ··· 376 374 377 375 // Click "Remove Banner" 378 376 const menu = dialog.locator(".edit-profile-banner-menu"); 379 - await menu 380 - .locator("context-menu-item", { hasText: "Remove Banner" }) 381 - .click(); 377 + await menu.locator('[data-testid="menu-action-banner-remove"]').click(); 382 378 383 379 // Banner image should be removed from the preview 384 380 await expect( ··· 388 384 // Save 389 385 await dialog.locator('[data-testid="edit-profile-save-button"]').click(); 390 386 391 - await expect(page.locator(".toast")).toContainText("Profile updated", { 387 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 392 388 timeout: 5000, 393 389 }); 394 390 }); ··· 431 427 // Set a file on the hidden file input to trigger the cropper 432 428 const [fileChooser] = await Promise.all([ 433 429 page.waitForEvent("filechooser"), 434 - menu 435 - .locator("context-menu-item", { hasText: "Upload from Files" }) 436 - .click(), 430 + menu.locator('[data-testid="menu-action-avatar-upload"]').click(), 437 431 ]); 438 432 439 433 await fileChooser.setFiles({
+9 -5
tests/e2e/specs/flows/followUser.test.js
··· 36 36 // Click the follow button 37 37 await profileView.locator('[data-testid="follow-button"]').click(); 38 38 39 - // Verify the button changes to "Following" 39 + // Verify the button changes to following state 40 40 await expect( 41 - profileView.locator('[data-testid="follow-button"]'), 42 - ).toContainText("Following", { timeout: 10000 }); 41 + profileView.locator( 42 + '[data-testid="follow-button"][data-teststate="following"]', 43 + ), 44 + ).toBeVisible({ timeout: 10000 }); 43 45 44 46 // Verify the follower count incremented 45 47 await expect( ··· 91 93 92 94 const profileView = page.locator("#profile-view"); 93 95 await expect( 94 - profileView.locator('[data-testid="follow-button"]'), 95 - ).toContainText("Following", { timeout: 10000 }); 96 + profileView.locator( 97 + '[data-testid="follow-button"][data-teststate="following"]', 98 + ), 99 + ).toBeVisible({ timeout: 10000 }); 96 100 97 101 // Click the follow button to unfollow 98 102 await profileView.locator('[data-testid="follow-button"]').click();
+3 -7
tests/e2e/specs/flows/hidePost.test.js
··· 34 34 35 35 // Open the post's context menu and click "Hide post for me" 36 36 await feedItem.locator(".text-button").click(); 37 - await page 38 - .locator("context-menu-item", { hasText: "Hide post for me" }) 39 - .click(); 37 + await page.locator('[data-testid="menu-action-post-hide"]').click(); 40 38 41 39 // Confirm the hide action in the dialog 42 40 const confirmButton = page.locator("button.confirm-button"); ··· 44 42 await confirmButton.click(); 45 43 46 44 // The post should immediately be hidden from the feed 47 - await expect(page.locator(".toast")).toContainText("Post hidden", { 45 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 48 46 timeout: 5000, 49 47 }); 50 48 await expect(feedItem).toHaveCount(0, { timeout: 10000 }); ··· 106 104 // Open the reply's context menu and click "Hide reply for me" 107 105 const replyPost = view.locator('[data-testid="small-post"]'); 108 106 await replyPost.locator(".text-button").click(); 109 - await page 110 - .locator("context-menu-item", { hasText: "Hide reply for me" }) 111 - .click(); 107 + await page.locator('[data-testid="menu-action-post-hide"]').click(); 112 108 113 109 // Confirm the hide action in the dialog 114 110 const confirmButton = page.locator("button.confirm-button");
+26 -20
tests/e2e/specs/flows/labelPreference.test.js
··· 176 176 profileView.locator('[data-testid="profile-name"]'), 177 177 ).toContainText("Test Labeler", { timeout: 10000 }); 178 178 179 - // Should show "+ Subscribe" button 179 + // Should show not-subscribed state 180 180 await expect( 181 - profileView.locator('[data-testid="subscribe-button"]'), 182 - ).toContainText("+ Subscribe"); 181 + profileView.locator( 182 + '[data-testid="subscribe-button"][data-teststate="not-subscribed"]', 183 + ), 184 + ).toBeVisible(); 183 185 184 186 // Label preference buttons should NOT be visible (not subscribed) 185 187 await expect( ··· 189 191 // Click Subscribe 190 192 await profileView.locator('[data-testid="subscribe-button"]').click(); 191 193 192 - // Button should change to "Subscribed" 194 + // Button should change to subscribed state 193 195 await expect( 194 - profileView.locator('[data-testid="subscribe-button"]'), 195 - ).toContainText("Subscribed", { timeout: 10000 }); 196 + profileView.locator( 197 + '[data-testid="subscribe-button"][data-teststate="subscribed"]', 198 + ), 199 + ).toBeVisible({ timeout: 10000 }); 196 200 197 201 // Toast should confirm subscription 198 - await expect(page.locator(".toast")).toContainText( 199 - "Subscribed to labeler", 200 - { timeout: 5000 }, 201 - ); 202 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 203 + timeout: 5000, 204 + }); 202 205 203 206 // Label preference buttons should now be visible 204 207 await expect( ··· 241 244 profileView.locator('[data-testid="profile-name"]'), 242 245 ).toContainText("Test Labeler", { timeout: 10000 }); 243 246 244 - // Should show "Subscribed" button 247 + // Should show subscribed state 245 248 await expect( 246 - profileView.locator('[data-testid="subscribe-button"]'), 247 - ).toContainText("Subscribed"); 249 + profileView.locator( 250 + '[data-testid="subscribe-button"][data-teststate="subscribed"]', 251 + ), 252 + ).toBeVisible(); 248 253 249 254 // Label preference buttons should be visible (subscribed) 250 255 await expect( ··· 254 259 // Click "Subscribed" to unsubscribe 255 260 await profileView.locator('[data-testid="subscribe-button"]').click(); 256 261 257 - // Button should change to "+ Subscribe" 262 + // Button should change to not-subscribed state 258 263 await expect( 259 - profileView.locator('[data-testid="subscribe-button"]'), 260 - ).toContainText("+ Subscribe", { timeout: 10000 }); 264 + profileView.locator( 265 + '[data-testid="subscribe-button"][data-teststate="not-subscribed"]', 266 + ), 267 + ).toBeVisible({ timeout: 10000 }); 261 268 262 269 // Toast should confirm unsubscription 263 - await expect(page.locator(".toast")).toContainText( 264 - "Unsubscribed from labeler", 265 - { timeout: 5000 }, 266 - ); 270 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 271 + timeout: 5000, 272 + }); 267 273 268 274 // Label preference buttons should no longer be visible 269 275 await expect(
+12 -12
tests/e2e/specs/flows/likePost.test.js
··· 39 39 40 40 const profileView = page.locator("#profile-view"); 41 41 const tabBar = profileView.locator(".tab-bar"); 42 - await expect( 43 - tabBar.locator(".tab-bar-button", { hasText: "Likes" }), 44 - ).toBeVisible({ timeout: 10000 }); 42 + await expect(tabBar.locator('[data-testid="tab-likes"]')).toBeVisible({ 43 + timeout: 10000, 44 + }); 45 45 46 - await tabBar.locator(".tab-bar-button", { hasText: "Likes" }).click(); 46 + await tabBar.locator('[data-testid="tab-likes"]').click(); 47 47 48 48 await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 49 49 1, ··· 76 76 77 77 const profileView = page.locator("#profile-view"); 78 78 const tabBar = profileView.locator(".tab-bar"); 79 - await expect( 80 - tabBar.locator(".tab-bar-button", { hasText: "Likes" }), 81 - ).toBeVisible({ timeout: 10000 }); 79 + await expect(tabBar.locator('[data-testid="tab-likes"]')).toBeVisible({ 80 + timeout: 10000, 81 + }); 82 82 83 - await tabBar.locator(".tab-bar-button", { hasText: "Likes" }).click(); 83 + await tabBar.locator('[data-testid="tab-likes"]').click(); 84 84 85 85 await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 86 86 1, ··· 105 105 // Navigate back to own profile Likes tab 106 106 await page.goto(`/profile/${userProfile.did}`); 107 107 108 - await expect( 109 - tabBar.locator(".tab-bar-button", { hasText: "Likes" }), 110 - ).toBeVisible({ timeout: 10000 }); 108 + await expect(tabBar.locator('[data-testid="tab-likes"]')).toBeVisible({ 109 + timeout: 10000, 110 + }); 111 111 112 - await tabBar.locator(".tab-bar-button", { hasText: "Likes" }).click(); 112 + await tabBar.locator('[data-testid="tab-likes"]').click(); 113 113 114 114 await expect( 115 115 profileView.locator(
+12 -14
tests/e2e/specs/flows/muteUser.test.js
··· 44 44 ).toContainText("Other User", { timeout: 10000 }); 45 45 46 46 await profileView.locator(".ellipsis-button").click(); 47 - await page 48 - .locator("context-menu-item", { hasText: "Mute Account" }) 49 - .click(); 47 + await page.locator('[data-testid="menu-action-profile-mute"]').click(); 50 48 51 - // Verify context menu now shows "Unmute Account" 49 + // Verify context menu now shows muted state 52 50 await profileView.locator(".ellipsis-button").click(); 53 51 await expect( 54 - page.locator("context-menu-item", { hasText: "Unmute Account" }), 52 + page.locator( 53 + '[data-testid="menu-action-profile-mute"][data-teststate="muted"]', 54 + ), 55 55 ).toBeVisible({ timeout: 5000 }); 56 56 // Close the menu by pressing Escape 57 57 await page.keyboard.press("Escape"); ··· 96 96 97 97 // Open the post's context menu and mute the user 98 98 await feedItem.locator(".text-button").click(); 99 - await page 100 - .locator("context-menu-item", { hasText: "Mute Account" }) 101 - .click(); 99 + await page.locator('[data-testid="menu-action-post-mute"]').click(); 102 100 103 101 // Verify a toast confirms the mute action 104 - await expect(page.locator(".toast")).toContainText("Account muted", { 102 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 105 103 timeout: 5000, 106 104 }); 107 105 ··· 117 115 profileView.locator('[data-testid="profile-name"]'), 118 116 ).toContainText("Other User", { timeout: 10000 }); 119 117 120 - // Verify context menu shows "Unmute Account" 118 + // Verify context menu shows muted state 121 119 await profileView.locator(".ellipsis-button").click(); 122 120 await expect( 123 - page.locator("context-menu-item", { hasText: "Unmute Account" }), 121 + page.locator( 122 + '[data-testid="menu-action-profile-mute"][data-teststate="muted"]', 123 + ), 124 124 ).toBeVisible({ timeout: 5000 }); 125 125 }); 126 126 ··· 162 162 ).toContainText("Other User", { timeout: 10000 }); 163 163 164 164 await profileView.locator(".ellipsis-button").click(); 165 - await page 166 - .locator("context-menu-item", { hasText: "Unmute Account" }) 167 - .click(); 165 + await page.locator('[data-testid="menu-action-profile-mute"]').click(); 168 166 169 167 // Navigate to home and verify posts reappear 170 168 await page.goto("/");
+7 -7
tests/e2e/specs/flows/pinPost.test.js
··· 39 39 0, 40 40 ); 41 41 42 - // Open the post action menu and click "Pin to your profile" 42 + // Open the post action menu and click the pin item 43 43 await feedItem.locator(".text-button").click(); 44 - await page 45 - .locator("context-menu-item", { hasText: "Pin to your profile" }) 46 - .click(); 44 + await page.locator('[data-testid="menu-action-post-pin"]').click(); 47 45 48 46 // The pinned label should appear optimistically 49 47 await expect( ··· 58 56 cid: post.cid, 59 57 }); 60 58 61 - // Re-opening the menu now shows "Unpin from your profile" 59 + // Re-opening the menu now shows pinned state 62 60 await profileView 63 61 .locator('[data-testid="feed-item"]') 64 62 .first() 65 63 .locator(".text-button") 66 64 .click(); 67 65 await expect( 68 - page.locator("context-menu-item", { hasText: "Unpin from your profile" }), 66 + page.locator( 67 + '[data-testid="menu-action-post-pin"][data-teststate="pinned"]', 68 + ), 69 69 ).toBeVisible(); 70 70 }); 71 71 ··· 93 93 await feedItem.locator(".text-button").click(); 94 94 95 95 await expect( 96 - page.locator("context-menu-item", { hasText: "Pin to your profile" }), 96 + page.locator('[data-testid="menu-action-post-pin"]'), 97 97 ).toHaveCount(0); 98 98 }); 99 99 });
+2 -2
tests/e2e/specs/flows/quotePost.test.js
··· 28 28 await expect(feedItem).toHaveCount(1, { timeout: 10000 }); 29 29 30 30 await feedItem.locator('[data-testid="repost-button"]').click(); 31 - await page.locator("context-menu-item", { hasText: "Quote Post" }).click(); 31 + await page.locator('[data-testid="menu-action-quote-post"]').click(); 32 32 33 33 // Wait for the composer to open with the quoted post 34 34 const composer = page.locator("post-composer .post-composer"); ··· 83 83 await expect(feedItem).toHaveCount(1, { timeout: 10000 }); 84 84 85 85 await feedItem.locator('[data-testid="repost-button"]').click(); 86 - await page.locator("context-menu-item", { hasText: "Quote Post" }).click(); 86 + await page.locator('[data-testid="menu-action-quote-post"]').click(); 87 87 88 88 const composer = page.locator("post-composer .post-composer"); 89 89 await expect(composer).toBeVisible({ timeout: 10000 });
+2 -2
tests/e2e/specs/flows/reportPost.test.js
··· 29 29 await feedItem.locator(".text-button").click(); 30 30 31 31 // Click "Report post" 32 - await page.locator("context-menu-item", { hasText: "Report post" }).click(); 32 + await page.locator('[data-testid="menu-action-post-report"]').click(); 33 33 34 34 // Report dialog should open 35 35 const reportDialog = page.locator("report-dialog"); ··· 56 56 await reportDialog.locator(".report-submit-button").click(); 57 57 58 58 // Verify the toast shows success 59 - await expect(page.locator(".toast")).toContainText("Report submitted", { 59 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 60 60 timeout: 5000, 61 61 }); 62 62
+2 -4
tests/e2e/specs/flows/reportProfile.test.js
··· 32 32 await profileView.locator(".ellipsis-button").click(); 33 33 34 34 // Click "Report account" 35 - await page 36 - .locator("context-menu-item", { hasText: "Report account" }) 37 - .click(); 35 + await page.locator('[data-testid="menu-action-profile-report"]').click(); 38 36 39 37 // Report dialog should open 40 38 const reportDialog = page.locator("report-dialog"); ··· 61 59 await reportDialog.locator(".report-submit-button").click(); 62 60 63 61 // Verify the toast shows success 64 - await expect(page.locator(".toast")).toContainText("Report submitted", { 62 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 65 63 timeout: 5000, 66 64 }); 67 65
+2 -2
tests/e2e/specs/flows/repostPost.test.js
··· 28 28 await expect(feedItem).toHaveCount(1, { timeout: 10000 }); 29 29 30 30 await feedItem.locator('[data-testid="repost-button"]').click(); 31 - await page.locator("context-menu-item", { hasText: "Repost" }).click(); 31 + await page.locator('[data-testid="menu-action-repost"]').click(); 32 32 33 33 await expect( 34 34 feedItem.locator('[data-testid="repost-button"].reposted'), ··· 83 83 await expect(feedItem).toHaveCount(1, { timeout: 10000 }); 84 84 85 85 await feedItem.locator('[data-testid="repost-button"].reposted').click(); 86 - await page.locator("context-menu-item", { hasText: "Undo repost" }).click(); 86 + await page.locator('[data-testid="menu-action-repost"]').click(); 87 87 88 88 await expect( 89 89 feedItem.locator('[data-testid="repost-button"].reposted'),
+8 -14
tests/e2e/specs/views/feedDetail.view.test.js
··· 118 118 await expect(view.locator(".pin-feed-button.pinned")).toBeVisible({ 119 119 timeout: 10000, 120 120 }); 121 - await expect(page.locator(".toast")).toContainText("Feed pinned"); 121 + await expect(page.locator('[data-testid="toast"]')).toBeVisible(); 122 122 }); 123 123 124 124 test("should pin a feed that is not already saved", async ({ page }) => { ··· 144 144 await expect(view.locator(".pin-feed-button.pinned")).toBeVisible({ 145 145 timeout: 10000, 146 146 }); 147 - await expect(page.locator(".toast")).toContainText("Feed pinned"); 147 + await expect(page.locator('[data-testid="toast"]')).toBeVisible(); 148 148 }); 149 149 150 150 test("should unpin a pinned feed when pin button is clicked", async ({ ··· 170 170 await pinButton.click(); 171 171 172 172 await expect(view.locator(".pin-feed-button.pinned")).toHaveCount(0); 173 - await expect(page.locator(".toast")).toContainText("Feed unpinned"); 173 + await expect(page.locator('[data-testid="toast"]')).toBeVisible(); 174 174 }); 175 175 176 176 test("should display empty state when feed has no posts", async ({ ··· 224 224 timeout: 5000, 225 225 }); 226 226 await expect( 227 - menu.locator("context-menu-item", { hasText: "Open in bsky.app" }), 227 + menu.locator('[data-testid="menu-action-feed-open-in-bsky"]'), 228 228 ).toBeVisible(); 229 229 await expect( 230 - menu.locator("context-menu-item", { hasText: "Copy link to feed" }), 230 + menu.locator('[data-testid="menu-action-feed-copy-link"]'), 231 231 ).toBeVisible(); 232 232 }); 233 233 ··· 253 253 254 254 const popupPromise = page.waitForEvent("popup"); 255 255 await view.locator(".feed-menu-button").click(); 256 - await view 257 - .locator("context-menu-item", { hasText: "Open in bsky.app" }) 258 - .click(); 256 + await view.locator('[data-testid="menu-action-feed-open-in-bsky"]').click(); 259 257 260 258 const popup = await popupPromise; 261 259 expect(popup.url()).toBe( ··· 284 282 }); 285 283 286 284 await view.locator(".feed-menu-button").click(); 287 - await view 288 - .locator("context-menu-item", { hasText: "Copy link to feed" }) 289 - .click(); 285 + await view.locator('[data-testid="menu-action-feed-copy-link"]').click(); 290 286 291 - await expect(page.locator(".toast")).toContainText( 292 - "Link copied to clipboard", 293 - ); 287 + await expect(page.locator('[data-testid="toast"]')).toBeVisible(); 294 288 }); 295 289 296 290 test.describe("Logged-out behavior", () => {
+6 -11
tests/e2e/specs/views/home.view.test.js
··· 230 230 await expect(feedItem).toHaveCount(1, { timeout: 10000 }); 231 231 232 232 await feedItem.locator('[data-testid="repost-button"]').click(); 233 - await page.locator("context-menu-item", { hasText: "Repost" }).click(); 233 + await page.locator('[data-testid="menu-action-repost"]').click(); 234 234 235 235 await expect( 236 236 feedItem.locator('[data-testid="repost-button"].reposted'), ··· 1152 1152 req.url().includes("app.bsky.feed.sendInteractions"), 1153 1153 ); 1154 1154 await feedItem.locator(".text-button").click(); 1155 - await page 1156 - .locator("context-menu-item", { hasText: "Show less like this" }) 1157 - .click(); 1155 + await page.locator('[data-testid="menu-action-post-show-less"]').click(); 1158 1156 1159 1157 await sendInteractionsRequest; 1160 1158 await expect( ··· 1204 1202 req.url().includes("app.bsky.feed.sendInteractions"), 1205 1203 ); 1206 1204 await feedItem.locator(".text-button").click(); 1207 - await page 1208 - .locator("context-menu-item", { hasText: "Show more like this" }) 1209 - .click(); 1205 + await page.locator('[data-testid="menu-action-post-show-more"]').click(); 1210 1206 1211 1207 await sendInteractionsRequest; 1212 - await expect(page.locator(".toast")).toContainText( 1213 - "Feedback sent to feed operator", 1214 - { timeout: 10000 }, 1215 - ); 1208 + await expect(page.locator('[data-testid="toast"]')).toBeVisible({ 1209 + timeout: 10000, 1210 + }); 1216 1211 }); 1217 1212 }); 1218 1213 });
+23 -16
tests/e2e/specs/views/postThread.view.test.js
··· 284 284 await expect(largePost).toBeVisible({ timeout: 10000 }); 285 285 286 286 await largePost.locator('[data-testid="repost-button"]').click(); 287 - await page.locator("context-menu-item", { hasText: "Repost" }).click(); 287 + await page.locator('[data-testid="menu-action-repost"]').click(); 288 288 289 289 await expect( 290 290 largePost.locator('[data-testid="repost-button"].reposted'), ··· 430 430 await page.goto("/profile/author1.bsky.social/post/abc123"); 431 431 432 432 const view = page.locator("#post-detail-view"); 433 - await expect(view.locator(".error-state")).toContainText("Post not found", { 433 + await expect(view.locator('[data-testid="post-not-found"]')).toBeVisible({ 434 434 timeout: 10000, 435 435 }); 436 436 }); ··· 455 455 await page.goto("/profile/author1.bsky.social/post/abc123"); 456 456 457 457 const view = page.locator("#post-detail-view"); 458 - await expect(view.locator(".error-state")).toContainText( 459 - "Error loading thread", 460 - { timeout: 10000 }, 461 - ); 458 + await expect(view.locator('[data-testid="thread-error"]')).toBeVisible({ 459 + timeout: 10000, 460 + }); 462 461 }); 463 462 464 463 test.describe("Logged-out behavior", () => { ··· 1360 1359 .locator(".post-action-button", { hasText: "..." }) 1361 1360 .click(); 1362 1361 await expect( 1363 - page.locator("context-menu-item", { hasText: "Hide post for me" }), 1362 + page.locator('[data-testid="menu-action-post-hide"]'), 1364 1363 ).toBeVisible(); 1365 1364 }); 1366 1365 ··· 1382 1381 .locator(".post-action-button", { hasText: "..." }) 1383 1382 .click(); 1384 1383 await expect( 1385 - page.locator("context-menu-item", { hasText: "Mute account" }), 1384 + page.locator( 1385 + '[data-testid="menu-action-post-mute"][data-teststate="unmuted"]', 1386 + ), 1386 1387 ).toBeVisible(); 1387 1388 }); 1388 1389 ··· 1410 1411 .locator(".post-action-button", { hasText: "..." }) 1411 1412 .click(); 1412 1413 await expect( 1413 - page.locator("context-menu-item", { hasText: "Unmute account" }), 1414 + page.locator( 1415 + '[data-testid="menu-action-post-mute"][data-teststate="muted"]', 1416 + ), 1414 1417 ).toBeVisible(); 1415 1418 }); 1416 1419 ··· 1432 1435 .locator(".post-action-button", { hasText: "..." }) 1433 1436 .click(); 1434 1437 await expect( 1435 - page.locator("context-menu-item", { hasText: "Block account" }), 1438 + page.locator( 1439 + '[data-testid="menu-action-post-block"][data-teststate="not-blocking"]', 1440 + ), 1436 1441 ).toBeVisible(); 1437 1442 }); 1438 1443 ··· 1463 1468 .locator(".post-action-button", { hasText: "..." }) 1464 1469 .click(); 1465 1470 await expect( 1466 - page.locator("context-menu-item", { hasText: "Unblock account" }), 1471 + page.locator( 1472 + '[data-testid="menu-action-post-block"][data-teststate="blocking"]', 1473 + ), 1467 1474 ).toBeVisible(); 1468 1475 }); 1469 1476 ··· 1483 1490 .locator(".post-action-button", { hasText: "..." }) 1484 1491 .click(); 1485 1492 await expect( 1486 - page.locator("context-menu-item", { hasText: "Report post" }), 1493 + page.locator('[data-testid="menu-action-post-report"]'), 1487 1494 ).toBeVisible(); 1488 1495 }); 1489 1496 ··· 1514 1521 1515 1522 // Moderation actions should not be present on own post 1516 1523 await expect( 1517 - page.locator("context-menu-item", { hasText: "Mute account" }), 1524 + page.locator('[data-testid="menu-action-post-mute"]'), 1518 1525 ).not.toBeAttached(); 1519 1526 await expect( 1520 - page.locator("context-menu-item", { hasText: "Block account" }), 1527 + page.locator('[data-testid="menu-action-post-block"]'), 1521 1528 ).not.toBeAttached(); 1522 1529 await expect( 1523 - page.locator("context-menu-item", { hasText: "Report post" }), 1530 + page.locator('[data-testid="menu-action-post-report"]'), 1524 1531 ).not.toBeAttached(); 1525 1532 1526 1533 // Delete post should be available on own post 1527 1534 await expect( 1528 - page.locator("context-menu-item", { hasText: "Delete post" }), 1535 + page.locator('[data-testid="menu-action-post-delete"]'), 1529 1536 ).toBeVisible(); 1530 1537 }); 1531 1538 });
+66 -55
tests/e2e/specs/views/profile.view.test.js
··· 177 177 await page.goto(`/profile/${followedUser.did}`); 178 178 179 179 const view = page.locator("#profile-view"); 180 - await expect(view.locator('[data-testid="follow-button"]')).toContainText( 181 - "Following", 182 - { timeout: 10000 }, 183 - ); 180 + await expect( 181 + view.locator('[data-testid="follow-button"][data-teststate="following"]'), 182 + ).toBeVisible({ timeout: 10000 }); 184 183 }); 185 184 186 185 test("should display posts in the author feed", async ({ page }) => { ··· 269 268 const view = page.locator("#profile-view"); 270 269 await expect( 271 270 view.locator('[data-testid="feed-end-message"]').first(), 272 - ).toContainText("Feed is empty.", { timeout: 10000 }); 271 + ).toBeVisible({ timeout: 10000 }); 273 272 }); 274 273 275 274 test("should show Posts, Replies, and Media tabs", async ({ page }) => { ··· 308 307 const tabBar = view.locator(".tab-bar"); 309 308 310 309 // Posts tab should be active by default 311 - await expect(tabBar.locator(".tab-bar-button.active")).toContainText( 312 - "Posts", 310 + await expect(tabBar.locator('[data-testid="tab-posts"]')).toHaveClass( 311 + /active/, 313 312 { timeout: 10000 }, 314 313 ); 315 314 316 315 // Click Replies tab 317 - await tabBar.locator(".tab-bar-button", { hasText: "Replies" }).click(); 318 - await expect(tabBar.locator(".tab-bar-button.active")).toContainText( 319 - "Replies", 316 + await tabBar.locator('[data-testid="tab-replies"]').click(); 317 + await expect(tabBar.locator('[data-testid="tab-replies"]')).toHaveClass( 318 + /active/, 320 319 ); 321 320 322 321 // Click Media tab 323 - await tabBar.locator(".tab-bar-button", { hasText: "Media" }).click(); 324 - await expect(tabBar.locator(".tab-bar-button.active")).toContainText( 325 - "Media", 322 + await tabBar.locator('[data-testid="tab-media"]').click(); 323 + await expect(tabBar.locator('[data-testid="tab-media"]')).toHaveClass( 324 + /active/, 326 325 ); 327 326 328 327 // Click back to Posts tab 329 - await tabBar.locator(".tab-bar-button", { hasText: "Posts" }).click(); 330 - await expect(tabBar.locator(".tab-bar-button.active")).toContainText( 331 - "Posts", 328 + await tabBar.locator('[data-testid="tab-posts"]').click(); 329 + await expect(tabBar.locator('[data-testid="tab-posts"]')).toHaveClass( 330 + /active/, 332 331 ); 333 332 }); 334 333 ··· 628 627 timeout: 5000, 629 628 }); 630 629 await expect( 631 - menu.locator("context-menu-item", { hasText: "Open in bsky.app" }), 630 + menu.locator('[data-testid="menu-action-profile-open-in-bsky"]'), 632 631 ).toBeVisible(); 633 632 await expect( 634 - menu.locator("context-menu-item", { hasText: "Copy link to profile" }), 633 + menu.locator('[data-testid="menu-action-profile-copy-link"]'), 635 634 ).toBeVisible(); 636 635 await expect( 637 - menu.locator("context-menu-item", { hasText: "Search posts" }), 636 + menu.locator('[data-testid="menu-action-profile-search-posts"]'), 638 637 ).toBeVisible(); 639 638 await expect( 640 - menu.locator("context-menu-item", { hasText: "Mute Account" }), 639 + menu.locator('[data-testid="menu-action-profile-mute"]'), 641 640 ).toBeVisible(); 642 641 await expect( 643 - menu.locator("context-menu-item", { hasText: "Block Account" }), 642 + menu.locator('[data-testid="menu-action-profile-block"]'), 644 643 ).toBeVisible(); 645 644 await expect( 646 - menu.locator("context-menu-item", { hasText: "Report account" }), 645 + menu.locator('[data-testid="menu-action-profile-report"]'), 647 646 ).toBeVisible(); 648 647 }); 649 648 ··· 674 673 675 674 const menu = page.locator(".profile-context-menu"); 676 675 await expect( 677 - menu.locator("context-menu-item", { hasText: "Unmute Account" }), 676 + menu.locator( 677 + '[data-testid="menu-action-profile-mute"][data-teststate="muted"]', 678 + ), 678 679 ).toBeVisible({ timeout: 5000 }); 679 680 await expect( 680 - menu.locator("context-menu-item", { hasText: /^Mute Account$/ }), 681 + menu.locator( 682 + '[data-testid="menu-action-profile-mute"][data-teststate="unmuted"]', 683 + ), 681 684 ).not.toBeVisible(); 682 685 }); 683 686 ··· 707 710 708 711 const menu = page.locator(".profile-context-menu"); 709 712 await expect( 710 - menu.locator("context-menu-item", { hasText: "Unblock Account" }), 713 + menu.locator( 714 + '[data-testid="menu-action-profile-block"][data-teststate="blocking"]', 715 + ), 711 716 ).toBeVisible({ timeout: 5000 }); 712 717 await expect( 713 - menu.locator("context-menu-item", { hasText: /^Block Account$/ }), 718 + menu.locator( 719 + '[data-testid="menu-action-profile-block"][data-teststate="not-blocking"]', 720 + ), 714 721 ).not.toBeVisible(); 715 722 }); 716 723 ··· 743 750 timeout: 5000, 744 751 }); 745 752 await expect( 746 - menu.locator("context-menu-item", { hasText: "Search posts" }), 753 + menu.locator('[data-testid="menu-action-profile-search-posts"]'), 747 754 ).toBeVisible(); 748 755 await expect( 749 - menu.locator("context-menu-item", { hasText: "Mute Account" }), 756 + menu.locator('[data-testid="menu-action-profile-mute"]'), 750 757 ).not.toBeVisible(); 751 758 await expect( 752 - menu.locator("context-menu-item", { hasText: "Block Account" }), 759 + menu.locator('[data-testid="menu-action-profile-block"]'), 753 760 ).not.toBeVisible(); 754 761 await expect( 755 - menu.locator("context-menu-item", { hasText: "Report account" }), 762 + menu.locator('[data-testid="menu-action-profile-report"]'), 756 763 ).not.toBeVisible(); 757 764 }); 758 765 ··· 775 782 776 783 const menu = page.locator(".profile-context-menu"); 777 784 await menu 778 - .locator("context-menu-item", { hasText: "Search posts" }) 785 + .locator('[data-testid="menu-action-profile-search-posts"]') 779 786 .click(); 780 787 781 788 await expect(page).toHaveURL( ··· 810 817 811 818 const menu = page.locator(".profile-context-menu"); 812 819 await menu 813 - .locator("context-menu-item", { hasText: "Search posts" }) 820 + .locator('[data-testid="menu-action-profile-search-posts"]') 814 821 .click(); 815 822 816 823 await expect(page).toHaveURL( ··· 1322 1329 1323 1330 const view = page.locator("#profile-view"); 1324 1331 await expect( 1325 - view.locator('[data-testid="subscribe-button"]'), 1326 - ).toContainText("+ Subscribe", { timeout: 10000 }); 1332 + view.locator( 1333 + '[data-testid="subscribe-button"][data-teststate="not-subscribed"]', 1334 + ), 1335 + ).toBeVisible({ timeout: 10000 }); 1327 1336 }); 1328 1337 1329 1338 test("should show 'Labels' tab and 'Subscribed' button when subscribed to a labeler", async ({ ··· 1378 1387 const view = page.locator("#profile-view"); 1379 1388 const tabBar = view.locator(".tab-bar"); 1380 1389 await expect( 1381 - tabBar.locator(".tab-bar-button", { hasText: "Labels" }), 1390 + tabBar.locator('[data-testid="tab-labeler-settings"]'), 1382 1391 ).toBeVisible({ timeout: 10000 }); 1383 1392 1384 1393 await expect( 1385 - view.locator('[data-testid="subscribe-button"]'), 1386 - ).toContainText("Subscribed"); 1394 + view.locator( 1395 + '[data-testid="subscribe-button"][data-teststate="subscribed"]', 1396 + ), 1397 + ).toBeVisible(); 1387 1398 }); 1388 1399 1389 1400 test("should list configurable labels in the labeler settings tab", async ({ ··· 1692 1703 timeout: 5000, 1693 1704 }); 1694 1705 await expect( 1695 - menu.locator("context-menu-item", { hasText: "Open in bsky.app" }), 1706 + menu.locator('[data-testid="menu-action-profile-open-in-bsky"]'), 1696 1707 ).toBeVisible(); 1697 1708 await expect( 1698 - menu.locator("context-menu-item", { hasText: "Copy link to profile" }), 1709 + menu.locator('[data-testid="menu-action-profile-copy-link"]'), 1699 1710 ).toBeVisible(); 1700 1711 }); 1701 1712 ··· 1812 1823 1813 1824 const view = page.locator("#profile-view"); 1814 1825 const tabBar = view.locator(".tab-bar"); 1815 - await expect( 1816 - tabBar.locator(".tab-bar-button", { hasText: "Feeds" }), 1817 - ).toBeVisible({ timeout: 10000 }); 1826 + await expect(tabBar.locator('[data-testid="tab-feeds"]')).toBeVisible({ 1827 + timeout: 10000, 1828 + }); 1818 1829 }); 1819 1830 1820 1831 test("should not show Feeds tab when profile has no feed generators", async ({ ··· 1832 1843 timeout: 10000, 1833 1844 }); 1834 1845 await expect( 1835 - tabBar.locator(".tab-bar-button", { hasText: "Feeds" }), 1846 + tabBar.locator('[data-testid="tab-feeds"]'), 1836 1847 ).not.toBeVisible(); 1837 1848 }); 1838 1849 ··· 1848 1859 1849 1860 const view = page.locator("#profile-view"); 1850 1861 const tabBar = view.locator(".tab-bar"); 1851 - await expect( 1852 - tabBar.locator(".tab-bar-button", { hasText: "Feeds" }), 1853 - ).toBeVisible({ timeout: 10000 }); 1862 + await expect(tabBar.locator('[data-testid="tab-feeds"]')).toBeVisible({ 1863 + timeout: 10000, 1864 + }); 1854 1865 1855 - await tabBar.locator(".tab-bar-button", { hasText: "Feeds" }).click(); 1866 + await tabBar.locator('[data-testid="tab-feeds"]').click(); 1856 1867 await expect(tabBar.locator(".tab-bar-button.active")).toContainText( 1857 1868 "Feeds", 1858 1869 ); ··· 1879 1890 1880 1891 const view = page.locator("#profile-view"); 1881 1892 const tabBar = view.locator(".tab-bar"); 1882 - await expect( 1883 - tabBar.locator(".tab-bar-button", { hasText: "Feeds" }), 1884 - ).toBeVisible({ timeout: 10000 }); 1893 + await expect(tabBar.locator('[data-testid="tab-feeds"]')).toBeVisible({ 1894 + timeout: 10000, 1895 + }); 1885 1896 1886 - await tabBar.locator(".tab-bar-button", { hasText: "Feeds" }).click(); 1897 + await tabBar.locator('[data-testid="tab-feeds"]').click(); 1887 1898 1888 1899 const feedsList = view.locator(".feeds-list"); 1889 1900 await expect(feedsList.locator(".feeds-list-item")).toHaveCount(1, { ··· 1927 1938 1928 1939 const view = page.locator("#profile-view"); 1929 1940 const tabBar = view.locator(".tab-bar"); 1930 - await expect( 1931 - tabBar.locator(".tab-bar-button", { hasText: "Feeds" }), 1932 - ).toBeVisible({ timeout: 10000 }); 1941 + await expect(tabBar.locator('[data-testid="tab-feeds"]')).toBeVisible({ 1942 + timeout: 10000, 1943 + }); 1933 1944 }); 1934 1945 1935 1946 test("should not show Feeds tab on own profile when user has no feed generators", async ({ ··· 1946 1957 timeout: 10000, 1947 1958 }); 1948 1959 await expect( 1949 - tabBar.locator(".tab-bar-button", { hasText: "Feeds" }), 1960 + tabBar.locator('[data-testid="tab-feeds"]'), 1950 1961 ).not.toBeVisible(); 1951 1962 }); 1952 1963 });
+23 -29
tests/e2e/specs/views/search.view.test.js
··· 77 77 78 78 const view = page.locator("#search-view"); 79 79 // Click the Posts tab 80 - await view.locator(".tab-bar-button", { hasText: "Posts" }).click(); 80 + await view.locator('[data-testid="tab-posts"]').click(); 81 81 82 82 await expect(view.locator("[data-post-uri]")).toHaveCount(2, { 83 83 timeout: 10000, ··· 102 102 103 103 const view = page.locator("#search-view"); 104 104 await expect( 105 - view.locator(".tab-bar-button.active", { hasText: "Profiles" }), 105 + view.locator('[data-testid="tab-profiles"].active'), 106 106 ).toBeVisible({ timeout: 10000 }); 107 107 }); 108 108 ··· 117 117 const profilesPanel = view.locator( 118 118 ".search-tab-panel:not([hidden]) .search-results-panel", 119 119 ); 120 - await expect(profilesPanel.locator(".search-status-message")).toContainText( 121 - "No profiles found.", 122 - { timeout: 10000 }, 123 - ); 120 + await expect( 121 + profilesPanel.locator('[data-testid="empty-state"]'), 122 + ).toBeVisible({ timeout: 10000 }); 124 123 }); 125 124 126 125 test("should show empty state when no posts match", async ({ page }) => { ··· 132 131 133 132 const view = page.locator("#search-view"); 134 133 await expect( 135 - view.locator(".search-post-results .search-status-message"), 136 - ).toContainText("No posts found.", { timeout: 10000 }); 134 + view.locator('.search-post-results [data-testid="empty-state"]'), 135 + ).toBeVisible({ timeout: 10000 }); 137 136 }); 138 137 139 138 test("should switch between Profiles, Posts, and Feeds tabs", async ({ ··· 175 174 }); 176 175 177 176 // Switch to Posts tab 178 - await view.locator(".tab-bar-button", { hasText: "Posts" }).click(); 177 + await view.locator('[data-testid="tab-posts"]').click(); 179 178 await expect( 180 - view.locator(".tab-bar-button.active", { hasText: "Posts" }), 179 + view.locator('[data-testid="tab-posts"].active'), 181 180 ).toBeVisible(); 182 181 await expect(view.locator("[data-post-uri]")).toHaveCount(1, { 183 182 timeout: 10000, ··· 185 184 await expect(view).toContainText("A matching post"); 186 185 187 186 // Switch to Feeds tab 188 - await view.locator(".tab-bar-button", { hasText: "Feeds" }).click(); 187 + await view.locator('[data-testid="tab-feeds"]').click(); 189 188 await expect( 190 - view.locator(".tab-bar-button.active", { hasText: "Feeds" }), 189 + view.locator('[data-testid="tab-feeds"].active'), 191 190 ).toBeVisible(); 192 191 await expect(view.locator(".feeds-list-item")).toHaveCount(1, { 193 192 timeout: 10000, ··· 195 194 await expect(view).toContainText("My Custom Feed"); 196 195 197 196 // Switch back to Profiles tab 198 - await view.locator(".tab-bar-button", { hasText: "Profiles" }).click(); 197 + await view.locator('[data-testid="tab-profiles"]').click(); 199 198 await expect( 200 - view.locator(".tab-bar-button.active", { hasText: "Profiles" }), 199 + view.locator('[data-testid="tab-profiles"].active'), 201 200 ).toBeVisible(); 202 201 await expect(view.locator(".profile-list-item")).toHaveCount(1); 203 202 }); ··· 294 293 await page.goto("/search?q=test&tab=posts"); 295 294 296 295 const view = page.locator("#search-view"); 297 - await expect( 298 - view.locator(".tab-bar-button.active", { hasText: "Posts" }), 299 - ).toBeVisible({ timeout: 10000 }); 296 + await expect(view.locator('[data-testid="tab-posts"].active')).toBeVisible({ 297 + timeout: 10000, 298 + }); 300 299 await expect(view.locator("[data-post-uri]")).toHaveCount(1, { 301 300 timeout: 10000, 302 301 }); ··· 357 356 await page.goto("/search?q=feed"); 358 357 359 358 const view = page.locator("#search-view"); 360 - await view.locator(".tab-bar-button", { hasText: "Feeds" }).click(); 359 + await view.locator('[data-testid="tab-feeds"]').click(); 361 360 362 361 await expect(view.locator(".feeds-list-item")).toHaveCount(2, { 363 362 timeout: 10000, ··· 381 380 const feedsPanel = view.locator( 382 381 ".search-tab-panel:not([hidden]) .search-results-panel", 383 382 ); 384 - await expect(feedsPanel.locator(".search-status-message")).toContainText( 385 - "No feeds found.", 383 + await expect(feedsPanel.locator('[data-testid="empty-state"]')).toBeVisible( 386 384 { timeout: 10000 }, 387 385 ); 388 386 }); ··· 430 428 await page.goto("/search?q=test&tab=feeds"); 431 429 432 430 const view = page.locator("#search-view"); 433 - await expect( 434 - view.locator(".tab-bar-button.active", { hasText: "Feeds" }), 435 - ).toBeVisible({ timeout: 10000 }); 431 + await expect(view.locator('[data-testid="tab-feeds"].active')).toBeVisible({ 432 + timeout: 10000, 433 + }); 436 434 await expect(view.locator(".feeds-list-item")).toHaveCount(1, { 437 435 timeout: 10000, 438 436 }); ··· 649 647 await expect(view).toContainText("Alicia"); 650 648 651 649 // Posts and Feeds tabs should be hidden for logged-out users 652 - await expect( 653 - view.locator(".tab-bar-button", { hasText: "Posts" }), 654 - ).not.toBeVisible(); 655 - await expect( 656 - view.locator(".tab-bar-button", { hasText: "Feeds" }), 657 - ).not.toBeVisible(); 650 + await expect(view.locator('[data-testid="tab-posts"]')).not.toBeVisible(); 651 + await expect(view.locator('[data-testid="tab-feeds"]')).not.toBeVisible(); 658 652 }); 659 653 }); 660 654 });
+39 -34
tests/e2e/specs/views/settings.view.test.js
··· 17 17 ); 18 18 19 19 const nav = view.locator(".vertical-nav"); 20 - await expect(nav.locator(".vertical-nav-item")).toHaveCount(6, { 20 + await expect(nav.locator(".vertical-nav-item")).toHaveCount(7, { 21 21 timeout: 10000, 22 22 }); 23 - await expect(nav.locator(".vertical-nav-label").first()).toContainText( 24 - "Appearance", 25 - ); 26 - await expect(nav.locator(".vertical-nav-label").nth(1)).toContainText( 27 - "Muted words", 28 - ); 29 - await expect(nav.locator(".vertical-nav-label").nth(2)).toContainText( 30 - "Blocked accounts", 31 - ); 32 - await expect(nav.locator(".vertical-nav-label").nth(3)).toContainText( 33 - "Plugins", 34 - ); 35 - await expect(nav.locator(".vertical-nav-label").nth(4)).toContainText( 36 - "Advanced", 37 - ); 23 + await expect( 24 + nav.locator('[data-testid="settings-nav-appearance"]'), 25 + ).toBeVisible(); 26 + await expect( 27 + nav.locator('[data-testid="settings-nav-muted-words"]'), 28 + ).toBeVisible(); 29 + await expect( 30 + nav.locator('[data-testid="settings-nav-muted-accounts"]'), 31 + ).toBeVisible(); 32 + await expect( 33 + nav.locator('[data-testid="settings-nav-blocked-accounts"]'), 34 + ).toBeVisible(); 35 + await expect( 36 + nav.locator('[data-testid="settings-nav-plugins"]'), 37 + ).toBeVisible(); 38 + await expect( 39 + nav.locator('[data-testid="settings-nav-advanced"]'), 40 + ).toBeVisible(); 38 41 }); 39 42 40 43 test("should navigate to blocked accounts settings", async ({ page }) => { ··· 45 48 await page.goto("/settings"); 46 49 47 50 const view = page.locator("#settings-view"); 48 - await view 49 - .locator(".vertical-nav-item", { hasText: "Blocked accounts" }) 50 - .click(); 51 + await view.locator('[data-testid="settings-nav-blocked-accounts"]').click(); 51 52 52 53 await expect(page).toHaveURL("/settings/blocked-accounts", { 53 54 timeout: 10000, ··· 64 65 await page.goto("/settings"); 65 66 66 67 const view = page.locator("#settings-view"); 67 - await expect(view.locator(".vertical-nav-label").first()).toContainText( 68 - "Appearance", 69 - { timeout: 10000 }, 70 - ); 68 + await expect( 69 + view.locator('[data-testid="settings-nav-appearance"]'), 70 + ).toBeVisible({ timeout: 10000 }); 71 71 72 - await view.locator(".vertical-nav-item", { hasText: "Appearance" }).click(); 72 + await view.locator('[data-testid="settings-nav-appearance"]').click(); 73 73 74 74 await expect(page).toHaveURL("/settings/appearance", { timeout: 10000 }); 75 75 }); ··· 82 82 await page.goto("/settings"); 83 83 84 84 const view = page.locator("#settings-view"); 85 - await expect(view.locator(".danger-button")).toContainText("Sign out", { 86 - timeout: 10000, 87 - }); 85 + await expect(view.locator('[data-testid="settings-sign-out"]')).toBeVisible( 86 + { timeout: 10000 }, 87 + ); 88 88 }); 89 89 90 90 test("should display version info", async ({ page }) => { ··· 95 95 await page.goto("/settings"); 96 96 97 97 const view = page.locator("#settings-view"); 98 - await expect(view.locator(".version-info")).toBeVisible({ timeout: 10000 }); 99 - await expect(view.locator(".version-info")).toContainText("Impro v"); 98 + await expect(view.locator('[data-testid="version-info"]')).toBeVisible({ 99 + timeout: 10000, 100 + }); 100 101 }); 101 102 102 103 test("should display footer links", async ({ page }) => { ··· 107 108 await page.goto("/settings"); 108 109 109 110 const view = page.locator("#settings-view"); 110 - const footer = view.locator(".settings-footer-links"); 111 - await expect(footer).toBeVisible({ timeout: 10000 }); 112 - await expect(footer).toContainText("Terms"); 113 - await expect(footer).toContainText("Privacy Policy"); 114 - await expect(footer).toContainText("GitHub"); 111 + await expect(view.locator('[data-testid="footer-link-terms"]')).toBeVisible( 112 + { timeout: 10000 }, 113 + ); 114 + await expect( 115 + view.locator('[data-testid="footer-link-privacy"]'), 116 + ).toBeVisible(); 117 + await expect( 118 + view.locator('[data-testid="footer-link-github"]'), 119 + ).toBeVisible(); 115 120 }); 116 121 117 122 test.describe("Logged-out behavior", () => {
+26 -22
tests/e2e/specs/views/settings/appearance.view.test.js
··· 11 11 await page.goto("/settings/appearance"); 12 12 13 13 const view = page.locator("#settings-appearance-view"); 14 - await expect(view.locator('[data-testid="header-title"]')).toContainText( 15 - "Appearance", 16 - { timeout: 10000 }, 17 - ); 14 + await expect(view.locator('[data-testid="header-title"]')).toBeVisible({ 15 + timeout: 10000, 16 + }); 18 17 19 - const sections = view.locator(".settings-section"); 20 - await expect(sections).toHaveCount(3, { timeout: 10000 }); 21 - 22 - await expect(view).toContainText("Color scheme"); 23 - await expect(view).toContainText("Highlight color"); 24 - await expect(view).toContainText("Like color"); 18 + await expect( 19 + view.locator('[data-testid="settings-section-color-scheme"]'), 20 + ).toBeVisible(); 21 + await expect( 22 + view.locator('[data-testid="settings-section-highlight-color"]'), 23 + ).toBeVisible(); 24 + await expect( 25 + view.locator('[data-testid="settings-section-like-color"]'), 26 + ).toBeVisible(); 25 27 }); 26 28 27 29 test("should display color scheme dropdown with three options", async ({ ··· 39 41 40 42 const options = select.locator("option"); 41 43 await expect(options).toHaveCount(3); 42 - await expect(options.nth(0)).toContainText("System"); 43 - await expect(options.nth(1)).toContainText("Light"); 44 - await expect(options.nth(2)).toContainText("Dark"); 44 + await expect(options.nth(0)).toHaveAttribute("value", "system"); 45 + await expect(options.nth(1)).toHaveAttribute("value", "light"); 46 + await expect(options.nth(2)).toHaveAttribute("value", "dark"); 45 47 }); 46 48 47 49 test("should display color pickers with reset buttons", async ({ page }) => { ··· 60 62 ).toBeVisible(); 61 63 await expect( 62 64 colorPickers.nth(0).locator(".settings-color-picker-reset"), 63 - ).toContainText("Reset"); 65 + ).toBeVisible(); 64 66 65 67 await expect( 66 68 colorPickers.nth(1).locator('input[type="color"]'), 67 69 ).toBeVisible(); 68 70 await expect( 69 71 colorPickers.nth(1).locator(".settings-color-picker-reset"), 70 - ).toContainText("Reset"); 72 + ).toBeVisible(); 71 73 }); 72 74 73 75 test("should change color scheme when selecting a different option", async ({ ··· 101 103 await page.goto("/settings/appearance"); 102 104 103 105 const view = page.locator("#settings-appearance-view"); 104 - await expect(view).toContainText("Choose between light and dark mode.", { 105 - timeout: 10000, 106 - }); 107 - await expect(view).toContainText( 108 - "Choose the highlight color for buttons and links.", 109 - ); 110 - await expect(view).toContainText("Choose the color for liked posts."); 106 + for (const sectionTestid of [ 107 + "settings-section-color-scheme", 108 + "settings-section-highlight-color", 109 + "settings-section-like-color", 110 + ]) { 111 + await expect( 112 + view.locator(`[data-testid="${sectionTestid}"] p`), 113 + ).toBeVisible({ timeout: 10000 }); 114 + } 111 115 }); 112 116 });
+7 -6
tests/e2e/specs/views/settings/blockedAccounts.view.test.js
··· 26 26 await page.goto("/settings/blocked-accounts"); 27 27 28 28 const view = page.locator("#settings-blocked-accounts-view"); 29 - await expect(view.locator('[data-testid="header-title"]')).toContainText( 30 - "Blocked accounts", 31 - { timeout: 10000 }, 32 - ); 33 - await expect(view).toContainText("Blocked accounts cannot reply"); 29 + await expect(view.locator('[data-testid="header-title"]')).toBeVisible({ 30 + timeout: 10000, 31 + }); 32 + await expect( 33 + view.locator('[data-testid="page-description"]'), 34 + ).toBeVisible(); 34 35 }); 35 36 36 37 test("should display empty state when no accounts are blocked", async ({ ··· 45 46 const view = page.locator("#settings-blocked-accounts-view"); 46 47 await expect( 47 48 view.locator('[data-testid="blocked-account-empty"]'), 48 - ).toContainText("You haven't blocked any accounts.", { timeout: 10000 }); 49 + ).toBeVisible({ timeout: 10000 }); 49 50 }); 50 51 51 52 test("should list blocked accounts", async ({ page }) => {
+7 -8
tests/e2e/specs/views/settings/mutedAccounts.view.test.js
··· 26 26 await page.goto("/settings/muted-accounts"); 27 27 28 28 const view = page.locator("#settings-muted-accounts-view"); 29 - await expect(view.locator('[data-testid="header-title"]')).toContainText( 30 - "Muted accounts", 31 - { timeout: 10000 }, 32 - ); 33 - await expect(view).toContainText("Muted accounts have their posts removed"); 29 + await expect(view.locator('[data-testid="header-title"]')).toBeVisible({ 30 + timeout: 10000, 31 + }); 32 + await expect( 33 + view.locator('[data-testid="page-description"]'), 34 + ).toBeVisible(); 34 35 }); 35 36 36 37 test("should display empty state when no accounts are muted", async ({ ··· 45 46 const view = page.locator("#settings-muted-accounts-view"); 46 47 await expect( 47 48 view.locator('[data-testid="muted-account-empty"]'), 48 - ).toContainText("You have not muted any accounts yet.", { 49 - timeout: 10000, 50 - }); 49 + ).toBeVisible({ timeout: 10000 }); 51 50 }); 52 51 53 52 test("should list muted accounts", async ({ page }) => {
+12 -15
tests/e2e/specs/views/settings/mutedWords.view.test.js
··· 11 11 await page.goto("/settings/muted-words"); 12 12 13 13 const view = page.locator("#settings-muted-words-view"); 14 - await expect(view.locator('[data-testid="header-title"]')).toContainText( 15 - "Muted words", 16 - { timeout: 10000 }, 17 - ); 14 + await expect(view.locator('[data-testid="header-title"]')).toBeVisible({ 15 + timeout: 10000, 16 + }); 18 17 19 - await expect(view).toContainText("Add muted words and tags"); 20 - await expect(view).toContainText( 21 - "Posts can be muted based on their text, their tags, or both.", 22 - ); 18 + await expect(view.locator('[data-testid="muted-word-form"]')).toBeVisible(); 19 + await expect( 20 + view.locator('[data-testid="page-description"]'), 21 + ).toBeVisible(); 23 22 24 23 await expect( 25 24 view.locator('[data-testid="muted-word-input"]'), ··· 42 41 await page.goto("/settings/muted-words"); 43 42 44 43 const view = page.locator("#settings-muted-words-view"); 45 - await expect( 46 - view.locator('[data-testid="muted-word-empty"]'), 47 - ).toContainText("You haven't muted any words or tags yet", { 44 + await expect(view.locator('[data-testid="muted-word-empty"]')).toBeVisible({ 48 45 timeout: 10000, 49 46 }); 50 47 }); ··· 134 131 // Confirm dialog should appear 135 132 const dialog = page.locator("dialog.modal-dialog"); 136 133 await expect(dialog).toBeVisible({ timeout: 5000 }); 137 - await expect(dialog).toContainText("Are you sure?"); 138 - await expect(dialog).toContainText( 139 - 'This will delete "removeme" from your muted words', 134 + // The interpolated word name is the SUT — verify it flows into the dialog. 135 + await expect(dialog.locator('[data-testid="modal-message"]')).toContainText( 136 + "removeme", 140 137 ); 141 138 142 139 // Click Remove to confirm 143 - await dialog.locator(".confirm-button").click(); 140 + await dialog.locator('[data-testid="modal-confirm-button"]').click(); 144 141 145 142 await expect(view.locator('[data-testid="muted-word-item"]')).toHaveCount( 146 143 0,
+7 -7
tests/e2e/specs/views/settings/plugins.view.test.js
··· 118 118 ); 119 119 await dialog.locator(".confirm-button").click(); 120 120 121 - await expect(page.locator(".toast")).toContainText("Uninstalled"); 121 + await expect(page.locator('[data-testid="toast"]')).toBeVisible(); 122 122 await expect(page.locator(".plugin-list-item")).toHaveCount(0); 123 123 await expect(page.locator(".plugins-empty-state")).toBeVisible(); 124 124 }); ··· 164 164 }); 165 165 await headerButton.click(); 166 166 167 - await expect(page.locator(".toast")).toContainText( 168 - "All plugins are up to date", 169 - ); 167 + await expect(page.locator('[data-testid="toast"]')).toBeVisible(); 170 168 await expect(headerButton).toContainText("Check for updates"); 171 169 await expect(view.locator(".plugin-update-button")).toHaveCount(0); 172 170 }); ··· 191 189 }); 192 190 await headerButton.click(); 193 191 194 - await expect(page.locator(".toast")).toContainText("1 update available"); 192 + await expect(page.locator('[data-testid="toast"]')).toContainText( 193 + "1 update available", 194 + ); 195 195 await expect(headerButton).toContainText("Update all"); 196 196 197 197 const sampleItem = view.locator(".plugin-list-item", { ··· 201 201 await expect(updateButton).toBeVisible(); 202 202 await updateButton.click(); 203 203 204 - await expect(page.locator(".toast").last()).toContainText( 204 + await expect(page.locator('[data-testid="toast"]').last()).toContainText( 205 205 "Updated Remote Themes to v1.0.1", 206 206 ); 207 207 await expect(sampleItem.locator(".plugin-update-button")).toHaveCount(0); ··· 229 229 230 230 await headerButton.click(); 231 231 232 - await expect(page.locator(".toast").last()).toContainText( 232 + await expect(page.locator('[data-testid="toast"]').last()).toContainText( 233 233 "Updated 1 plugin", 234 234 ); 235 235 await expect(view.locator(".plugin-update-button")).toHaveCount(0);
+4 -4
tests/unit/specs/components/image-alt-text-dialog.test.js
··· 43 43 it("should render cancel button", () => { 44 44 const element = document.createElement("image-alt-text-dialog"); 45 45 connectElement(element); 46 - const cancelButton = element.querySelector(".rounded-button-secondary"); 46 + const cancelButton = element.querySelector( 47 + '[data-testid="alt-text-cancel"]', 48 + ); 47 49 assert(cancelButton !== null); 48 - assertEquals(cancelButton.textContent.trim(), "Cancel"); 49 50 }); 50 51 51 52 it("should render save button", () => { 52 53 const element = document.createElement("image-alt-text-dialog"); 53 54 connectElement(element); 54 - const saveButton = element.querySelector(".rounded-button-primary"); 55 + const saveButton = element.querySelector('[data-testid="alt-text-save"]'); 55 56 assert(saveButton !== null); 56 - assertEquals(saveButton.textContent.trim(), "Save"); 57 57 }); 58 58 59 59 it("should render character count", () => {
-1
tests/unit/specs/components/post-composer.test.js
··· 40 40 connectElement(element); 41 41 const cancelButton = element.querySelector(".post-composer-cancel-button"); 42 42 assert(cancelButton !== null); 43 - assertEquals(cancelButton.textContent.trim(), "Cancel"); 44 43 }); 45 44 46 45 it("should render post button", () => {
+49 -37
tests/unit/specs/modals.test.js
··· 35 35 it("should render sign in content", () => { 36 36 clearDOM(); 37 37 showSignInModal(); 38 - const title = document.querySelector(".modal-dialog-title"); 39 - assertEquals(title.textContent, "Sign in"); 40 - const message = document.querySelector(".modal-dialog-message"); 41 - assertEquals(message.textContent, "Sign in to join the conversation!"); 42 - const link = document.querySelector("a.primary-button"); 38 + assert(document.querySelector('[data-testid="modal-title"]') !== null); 39 + assert(document.querySelector('[data-testid="modal-message"]') !== null); 40 + const link = document.querySelector('[data-testid="modal-primary-button"]'); 43 41 assert(link !== null); 44 42 assert(link.getAttribute("href").startsWith("/login")); 45 - assertEquals(link.textContent.trim(), "Sign in"); 46 43 }); 47 44 48 45 it("should close and remove on backdrop click", () => { ··· 56 53 it("should close and remove on link click", () => { 57 54 clearDOM(); 58 55 showSignInModal(); 59 - const link = document.querySelector("a.primary-button"); 56 + const link = document.querySelector('[data-testid="modal-primary-button"]'); 60 57 link.click(); 61 58 assert(document.querySelector("dialog") === null); 62 59 }); ··· 76 73 it("should render the provided title", () => { 77 74 clearDOM(); 78 75 showInfoModal({ title: "My Title", message: "Some message" }); 79 - const title = document.querySelector(".modal-dialog-title"); 80 - assertEquals(title.textContent, "My Title"); 76 + const title = document.querySelector('[data-testid="modal-title"]'); 77 + assertEquals(title.textContent.trim(), "My Title"); 81 78 }); 82 79 83 80 it("should render the provided message", () => { 84 81 clearDOM(); 85 82 showInfoModal({ title: "Title", message: "Custom message" }); 86 - const message = document.querySelector(".modal-dialog-message"); 87 - assertEquals(message.textContent, "Custom message"); 83 + const message = document.querySelector('[data-testid="modal-message"]'); 84 + assertEquals(message.textContent.trim(), "Custom message"); 88 85 }); 89 86 90 - it("should use OK as default button text", () => { 87 + it("should render a primary button by default", () => { 91 88 clearDOM(); 92 89 showInfoModal({ title: "Title", message: "Msg" }); 93 - const button = document.querySelector(".primary-button"); 94 - assertEquals(button.textContent.trim(), "OK"); 90 + const button = document.querySelector( 91 + '[data-testid="modal-primary-button"]', 92 + ); 93 + assert(button !== null); 95 94 }); 96 95 97 96 it("should use custom button text when provided", () => { 98 97 clearDOM(); 99 98 showInfoModal({ title: "T", message: "M", confirmButtonText: "Got it" }); 100 - const button = document.querySelector(".primary-button"); 99 + const button = document.querySelector( 100 + '[data-testid="modal-primary-button"]', 101 + ); 101 102 assertEquals(button.textContent.trim(), "Got it"); 102 103 }); 103 104 ··· 111 112 it("should close and remove on button click", () => { 112 113 clearDOM(); 113 114 showInfoModal({ title: "T", message: "M" }); 114 - const button = document.querySelector(".primary-button"); 115 + const button = document.querySelector( 116 + '[data-testid="modal-primary-button"]', 117 + ); 115 118 button.click(); 116 119 assert(document.querySelector("dialog") === null); 117 120 }); ··· 146 149 it("should render the message", () => { 147 150 clearDOM(); 148 151 confirm("Delete this?"); 149 - const message = document.querySelector(".modal-dialog-message"); 150 - assertEquals(message.textContent, "Delete this?"); 152 + const message = document.querySelector('[data-testid="modal-message"]'); 153 + assertEquals(message.textContent.trim(), "Delete this?"); 151 154 }); 152 155 153 156 it("should render cancel and confirm buttons", () => { 154 157 clearDOM(); 155 158 confirm("Sure?"); 156 - const cancelButton = document.querySelector(".cancel-button"); 157 - const confirmButton = document.querySelector(".confirm-button"); 159 + const cancelButton = document.querySelector( 160 + '[data-testid="modal-cancel-button"]', 161 + ); 162 + const confirmButton = document.querySelector( 163 + '[data-testid="modal-confirm-button"]', 164 + ); 158 165 assert(cancelButton !== null); 159 166 assert(confirmButton !== null); 160 - assertEquals(cancelButton.textContent.trim(), "Cancel"); 161 - assertEquals(confirmButton.textContent.trim(), "Confirm"); 162 167 }); 163 168 164 169 it("should use custom confirm button text", () => { 165 170 clearDOM(); 166 171 confirm("Sure?", { confirmButtonText: "Delete" }); 167 - const confirmButton = document.querySelector(".confirm-button"); 172 + const confirmButton = document.querySelector( 173 + '[data-testid="modal-confirm-button"]', 174 + ); 168 175 assertEquals(confirmButton.textContent.trim(), "Delete"); 169 176 }); 170 177 171 178 it("should apply custom confirm button style", () => { 172 179 clearDOM(); 173 180 confirm("Sure?", { confirmButtonStyle: "danger" }); 174 - const confirmButton = document.querySelector(".confirm-button"); 181 + const confirmButton = document.querySelector( 182 + '[data-testid="modal-confirm-button"]', 183 + ); 175 184 assert(confirmButton.classList.contains("danger-button")); 176 185 }); 177 186 178 187 it("should apply primary button style by default", () => { 179 188 clearDOM(); 180 189 confirm("Sure?"); 181 - const confirmButton = document.querySelector(".confirm-button"); 190 + const confirmButton = document.querySelector( 191 + '[data-testid="modal-confirm-button"]', 192 + ); 182 193 assert(confirmButton.classList.contains("primary-button")); 183 194 }); 184 195 185 196 it("should render title when provided", () => { 186 197 clearDOM(); 187 198 confirm("Body text", { title: "Warning" }); 188 - const title = document.querySelector(".modal-dialog-title"); 199 + const title = document.querySelector('[data-testid="modal-title"]'); 189 200 assert(title !== null); 190 - assertEquals(title.textContent, "Warning"); 201 + assertEquals(title.textContent.trim(), "Warning"); 191 202 }); 192 203 193 204 it("should not render title when not provided", () => { 194 205 clearDOM(); 195 206 confirm("Body text"); 196 - const title = document.querySelector(".modal-dialog-title"); 207 + const title = document.querySelector('[data-testid="modal-title"]'); 197 208 assert(title === null); 198 209 }); 199 210 ··· 207 218 it("should resolve true when confirm is clicked", async () => { 208 219 clearDOM(); 209 220 const result = confirm("Sure?"); 210 - document.querySelector(".confirm-button").click(); 221 + document.querySelector('[data-testid="modal-confirm-button"]').click(); 211 222 assertEquals(await result, true); 212 223 }); 213 224 214 225 it("should resolve false when cancel is clicked", async () => { 215 226 clearDOM(); 216 227 const result = confirm("Sure?"); 217 - document.querySelector(".cancel-button").click(); 228 + document.querySelector('[data-testid="modal-cancel-button"]').click(); 218 229 assertEquals(await result, false); 219 230 }); 220 231 ··· 239 250 it("should remove dialog from DOM after confirm", async () => { 240 251 clearDOM(); 241 252 const result = confirm("Sure?"); 242 - document.querySelector(".confirm-button").click(); 253 + document.querySelector('[data-testid="modal-confirm-button"]').click(); 243 254 await result; 244 255 assert(document.querySelector("dialog") === null); 245 256 }); ··· 247 258 it("should remove dialog from DOM after cancel", async () => { 248 259 clearDOM(); 249 260 const result = confirm("Sure?"); 250 - document.querySelector(".cancel-button").click(); 261 + document.querySelector('[data-testid="modal-cancel-button"]').click(); 251 262 await result; 252 263 assert(document.querySelector("dialog") === null); 253 264 }); ··· 292 303 it("should render the title", () => { 293 304 clearDOM(); 294 305 showWhoCanReplyModal({ post: everybodyPost }); 295 - const title = document.querySelector(".modal-dialog-title"); 296 - assertEquals(title.textContent, "Who can interact with this post?"); 306 + const title = document.querySelector('[data-testid="modal-title"]'); 307 + assert(title !== null); 297 308 }); 298 309 299 310 it("should render everybody message when no threadgate", () => { ··· 347 358 ); 348 359 }); 349 360 350 - it("should close and remove on OK button click", () => { 361 + it("should close and remove on primary button click", () => { 351 362 clearDOM(); 352 363 showWhoCanReplyModal({ post: everybodyPost }); 353 - const button = document.querySelector(".primary-button"); 354 - assertEquals(button.textContent.trim(), "OK"); 364 + const button = document.querySelector( 365 + '[data-testid="modal-primary-button"]', 366 + ); 355 367 button.click(); 356 368 assert(document.querySelector("dialog") === null); 357 369 });
+2 -4
tests/unit/specs/templates/footer.template.test.js
··· 34 34 render(result, container); 35 35 const loginButton = container.querySelector("[data-testid='login-button']"); 36 36 assert(loginButton !== null); 37 - assert(loginButton.textContent.includes("Sign in")); 38 37 }); 39 38 40 - it("should render IMPRO title when not authenticated", () => { 39 + it("should render brand title when not authenticated", () => { 41 40 const result = footerTemplate({ 42 41 isAuthenticated: false, 43 42 currentUser: null, 44 43 }); 45 44 const container = document.createElement("div"); 46 45 render(result, container); 47 - const title = container.querySelector("h2"); 46 + const title = container.querySelector("[data-testid='brand-title']"); 48 47 assert(title !== null); 49 - assert(title.textContent.includes("IMPRO")); 50 48 }); 51 49 }); 52 50
+25 -27
tests/unit/specs/templates/profileCard.template.test.js
··· 49 49 assertEquals( 50 50 container 51 51 .querySelector("[data-testid='follow-button']") 52 - .textContent.trim(), 53 - "+ Follow", 52 + .getAttribute("data-teststate"), 53 + "follow", 54 54 ); 55 55 }); 56 56 ··· 68 68 assertEquals( 69 69 container 70 70 .querySelector("[data-testid='follow-button']") 71 - .textContent.trim(), 72 - "Following", 71 + .getAttribute("data-teststate"), 72 + "following", 73 73 ); 74 74 }); 75 75 76 - it("should render + Follow back when followed by but not following", () => { 76 + it("should render follow-back state when followed by but not following", () => { 77 77 const profile = { 78 78 ...mockProfile, 79 79 viewer: { following: false, followedBy: true }, ··· 87 87 assertEquals( 88 88 container 89 89 .querySelector("[data-testid='follow-button']") 90 - .textContent.trim(), 91 - "+ Follow back", 90 + .getAttribute("data-teststate"), 91 + "follow-back", 92 92 ); 93 93 }); 94 94 95 - it("should render Following (not Follow back) when mutuals", () => { 95 + it("should render following (not follow-back) state when mutuals", () => { 96 96 const profile = { 97 97 ...mockProfile, 98 98 viewer: { following: true, followedBy: true }, ··· 106 106 assertEquals( 107 107 container 108 108 .querySelector("[data-testid='follow-button']") 109 - .textContent.trim(), 110 - "Following", 109 + .getAttribute("data-teststate"), 110 + "following", 111 111 ); 112 112 }); 113 113 ··· 389 389 assertEquals( 390 390 container 391 391 .querySelector("[data-testid='subscribe-button']") 392 - .textContent.trim(), 393 - "+ Subscribe", 392 + .getAttribute("data-teststate"), 393 + "not-subscribed", 394 394 ); 395 395 }); 396 396 ··· 412 412 assertEquals( 413 413 container 414 414 .querySelector("[data-testid='subscribe-button']") 415 - .textContent.trim(), 416 - "Subscribed", 415 + .getAttribute("data-teststate"), 416 + "subscribed", 417 417 ); 418 418 }); 419 419 ··· 438 438 container.querySelector(".ellipsis-button").click(); 439 439 await new Promise((resolve) => setTimeout(resolve, 0)); 440 440 const item = document.body.querySelector( 441 - "[data-testid='context-menu-follow']", 441 + "[data-testid='menu-action-profile-follow']", 442 442 ); 443 443 assert(item !== null); 444 444 document.body.querySelector(".profile-context-menu")?.remove(); ··· 465 465 render(result, container); 466 466 container.querySelector(".ellipsis-button").click(); 467 467 await new Promise((resolve) => setTimeout(resolve, 0)); 468 - assertEquals( 469 - document.body 470 - .querySelector("[data-testid='context-menu-follow']") 471 - .textContent.trim(), 472 - "Unfollow account", 468 + const item = document.body.querySelector( 469 + "[data-testid='menu-action-profile-follow']", 473 470 ); 471 + assert(item !== null); 472 + assertEquals(item.getAttribute("data-teststate"), "following"); 474 473 document.body.querySelector(".profile-context-menu")?.remove(); 475 474 container.remove(); 476 475 }); ··· 613 612 "[data-testid='edit-profile-button']", 614 613 ); 615 614 assert(editButton !== null); 616 - assertEquals(editButton.textContent.trim(), "Edit Profile"); 617 615 }); 618 616 619 617 it("should not render edit profile button for other users", () => { ··· 780 778 assertEquals( 781 779 container 782 780 .querySelector("[data-testid='subscribe-button']") 783 - .textContent.trim(), 784 - "Subscribed", 781 + .getAttribute("data-teststate"), 782 + "subscribed", 785 783 ); 786 784 }); 787 785 ··· 804 802 assertEquals( 805 803 container 806 804 .querySelector("[data-testid='subscribe-button']") 807 - .textContent.trim(), 808 - "+ Subscribe", 805 + .getAttribute("data-teststate"), 806 + "not-subscribed", 809 807 ); 810 808 }); 811 809 ··· 853 851 assertEquals( 854 852 container 855 853 .querySelector("[data-testid='subscribe-button']") 856 - .textContent.trim(), 857 - "Subscribed", 854 + .getAttribute("data-teststate"), 855 + "subscribed", 858 856 ); 859 857 }); 860 858 });