[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.

Replace hasText test selectors

Grace Kind (Jul 19, 2026, 9:22 PM -0500) e84694dc 3c24d548

+63 -65
+1 -3
src/js/components/component.js
··· 1 - function kebabCase(str) { 2 - return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 3 - } 1 + import { kebabCase } from "/js/utils.js"; 4 2 5 3 export class Component extends HTMLElement { 6 4 static register() {
+1
src/js/components/edit-profile-dialog.js
··· 134 134 <div class="edit-profile-dialog-header"> 135 135 <button 136 136 class="edit-profile-dialog-header-button" 137 + data-testid="edit-profile-crop-cancel-button" 137 138 @click=${() => { 138 139 this._croppingTarget = null; 139 140 this._croppingImageSrc = null;
+16 -3
src/js/components/report-dialog.js
··· 1 1 import { html, render } from "/js/lib/lit-html.js"; 2 2 import { Component } from "/js/components/component.js"; 3 3 import { ScrollLock } from "/js/scrollLock.js"; 4 - import { enableDragToDismiss, resetScrollOnBlur } from "/js/utils.js"; 4 + import { 5 + enableDragToDismiss, 6 + kebabCase, 7 + resetScrollOnBlur, 8 + } from "/js/utils.js"; 5 9 import { avatarTemplate } from "/js/templates/avatar.template.js"; 6 10 import { checkIconTemplate } from "/js/templates/icons/checkIcon.template.js"; 7 11 import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; ··· 314 318 315 319 function categoryCardTemplate({ category, onClick }) { 316 320 return html` 317 - <button class="report-option-card" @click=${onClick}> 321 + <button 322 + class="report-option-card" 323 + data-testid="report-category-${kebabCase(category.key)}" 324 + @click=${onClick} 325 + > 318 326 <div class="report-option-title">${category.title}</div> 319 327 <div class="report-option-description">${category.description}</div> 320 328 </button> ··· 322 330 } 323 331 324 332 function reasonTypeCardTemplate({ reasonType, onClick }) { 333 + const reasonSlug = kebabCase(reasonType.reasonType.split("#reason")[1]); 325 334 return html` 326 - <button class="report-option-card" @click=${onClick}> 335 + <button 336 + class="report-option-card" 337 + data-testid="report-reason-${reasonSlug}" 338 + @click=${onClick} 339 + > 327 340 <div class="report-option-title">${reasonType.title}</div> 328 341 </button> 329 342 `;
+3
src/js/templates/labelerSettings.template.js
··· 46 46 active: value === "ignore", 47 47 })} 48 48 data-testid="label-pref-button" 49 + data-teststate="off" 49 50 @click=${() => onClick("ignore")} 50 51 > 51 52 Off ··· 57 58 active: value === "warn", 58 59 })} 59 60 data-testid="label-pref-button" 61 + data-teststate="warn" 60 62 @click=${() => onClick("warn")} 61 63 > 62 64 ${getWarnLabel(labelDefinition)} ··· 68 70 active: value === "hide", 69 71 })} 70 72 data-testid="label-pref-button" 73 + data-teststate="hide" 71 74 @click=${() => onClick("hide")} 72 75 > 73 76 Hide
+1
src/js/templates/postActionBar.template.js
··· 399 399 <div class="post-action"> 400 400 <button 401 401 class="post-action-button text-button" 402 + data-testid="post-action-more" 402 403 @click=${(e) => { 403 404 e.stopPropagation(); 404 405 openPostContextMenu(e, {
+4
src/js/utils.js
··· 2 2 3 3 export function noop() {} 4 4 5 + export function kebabCase(str) { 6 + return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 7 + } 8 + 5 9 export function unique(array, { by: keyOrFn } = {}) { 6 10 let getKey = (i) => i; 7 11 if (keyOrFn) {
+3 -1
src/js/views/home.view.js
··· 189 189 An issue occurred when contacting the feed server.<br /> 190 190 Please let the feed owner know about this issue.<br /> 191 191 ${feedGenerator.creator 192 - ? html`<a href=${linkToProfile(feedGenerator.creator)} 192 + ? html`<a 193 + href=${linkToProfile(feedGenerator.creator)} 194 + data-testid="feed-error-view-profile" 193 195 >View profile</a 194 196 ><br />` 195 197 : ""}
+3 -1
src/js/views/postThread.view.js
··· 402 402 <div class="load-more-spacer"> 403 403 <div class="reply-context-ellipsis"></div> 404 404 </div> 405 - <a href=${linkToPostFromUri(replyParent.uri)} 405 + <a 406 + href=${linkToPostFromUri(replyParent.uri)} 407 + data-testid="post-thread-load-parent" 406 408 >Load parent post</a 407 409 > 408 410 </div>`
+2 -6
tests/e2e/specs/concerns/postComposerEdgeCases.test.js
··· 145 145 await altDialog 146 146 .locator(".image-alt-text-dialog-textarea") 147 147 .fill("A sunset photo"); 148 - await altDialog 149 - .locator(".rounded-button-primary", { hasText: "Save" }) 150 - .click(); 148 + await altDialog.locator('[data-testid="alt-text-save"]').click(); 151 149 152 150 // Verify has-alt indicator 153 151 await expect(composer.locator(".alt-indicator.has-alt")).toBeVisible(); ··· 169 167 await altDialog2 170 168 .locator(".image-alt-text-dialog-textarea") 171 169 .fill("A beautiful sunset over the ocean"); 172 - await altDialog2 173 - .locator(".rounded-button-primary", { hasText: "Save" }) 174 - .click(); 170 + await altDialog2.locator('[data-testid="alt-text-save"]').click(); 175 171 176 172 // Verify still has-alt 177 173 await expect(composer.locator(".alt-indicator.has-alt")).toBeVisible();
+1 -3
tests/e2e/specs/flows/createPost.test.js
··· 108 108 .fill("A beautiful sunset over the mountains"); 109 109 110 110 // Save alt text 111 - await altDialog 112 - .locator(".rounded-button-primary", { hasText: "Save" }) 113 - .click(); 111 + await altDialog.locator('[data-testid="alt-text-save"]').click(); 114 112 115 113 // Verify alt indicator now shows has-alt 116 114 await expect(composer.locator(".alt-indicator.has-alt")).toBeVisible();
+2 -4
tests/e2e/specs/flows/editProfile.test.js
··· 183 183 ).toBeVisible({ timeout: 5000 }); 184 184 185 185 // Click Cancel 186 - await dialog 187 - .locator(".edit-profile-dialog-header-button", { hasText: "Cancel" }) 188 - .click(); 186 + await dialog.locator('[data-testid="edit-profile-cancel-button"]').click(); 189 187 190 188 // Dialog should be closed 191 189 await expect(dialog.locator(".edit-profile-dialog[open]")).toHaveCount(0, { ··· 446 444 447 445 // Click Cancel on the cropper 448 446 await dialog 449 - .locator(".edit-profile-dialog-header-button", { hasText: "Cancel" }) 447 + .locator('[data-testid="edit-profile-crop-cancel-button"]') 450 448 .click(); 451 449 452 450 // The edit dialog should still be visible with the original fields
+9 -9
tests/e2e/specs/flows/labelPreference.test.js
··· 102 102 await expect(labelRow).toBeVisible(); 103 103 104 104 // The default is "Warn", so the Warn button should be active 105 - const warnButton = labelRow 106 - .locator('[data-testid="label-pref-button"]') 107 - .filter({ hasText: "Warn" }); 105 + const warnButton = labelRow.locator( 106 + '[data-testid="label-pref-button"][data-teststate="warn"]', 107 + ); 108 108 await expect(warnButton).toHaveClass(/active/); 109 109 110 110 // Change from "Warn" to "Hide" 111 - const hideButton = labelRow 112 - .locator('[data-testid="label-pref-button"]') 113 - .filter({ hasText: "Hide" }); 111 + const hideButton = labelRow.locator( 112 + '[data-testid="label-pref-button"][data-teststate="hide"]', 113 + ); 114 114 await hideButton.click(); 115 115 await expect(hideButton).toHaveClass(/active/); 116 116 ··· 134 134 '[data-testid="label-preference-row"]', 135 135 { hasText: "Custom Warning" }, 136 136 ); 137 - const offButton = labelRow2 138 - .locator('[data-testid="label-pref-button"]') 139 - .filter({ hasText: "Off" }); 137 + const offButton = labelRow2.locator( 138 + '[data-testid="label-pref-button"][data-teststate="off"]', 139 + ); 140 140 await offButton.click(); 141 141 await expect(offButton).toHaveClass(/active/); 142 142
+2 -2
tests/e2e/specs/flows/reportPost.test.js
··· 39 39 40 40 // Step 1: Select category "Misleading" 41 41 await reportDialog 42 - .locator(".report-option-card", { hasText: "Misleading" }) 42 + .locator('[data-testid="report-category-misleading"]') 43 43 .click(); 44 44 45 45 // Step 2: Select reason "Spam" 46 46 await reportDialog 47 - .locator(".report-option-card", { hasText: "Spam" }) 47 + .locator('[data-testid="report-reason-misleading-spam"]') 48 48 .click(); 49 49 50 50 // Step 3: Select labeler "Bluesky Moderation"
+2 -2
tests/e2e/specs/flows/reportProfile.test.js
··· 42 42 43 43 // Step 1: Select category "Harassment or hate" 44 44 await reportDialog 45 - .locator(".report-option-card", { hasText: "Harassment or hate" }) 45 + .locator('[data-testid="report-category-harassment-hate"]') 46 46 .click(); 47 47 48 48 // Step 2: Select reason "Trolling" 49 49 await reportDialog 50 - .locator(".report-option-card", { hasText: "Trolling" }) 50 + .locator('[data-testid="report-reason-harassment-troll"]') 51 51 .click(); 52 52 53 53 // Step 3: Select labeler "Bluesky Moderation"
+1 -1
tests/e2e/specs/views/home.view.test.js
··· 461 461 { timeout: 10000 }, 462 462 ); 463 463 await expect( 464 - errorState.locator("a", { hasText: "View profile" }), 464 + errorState.locator('[data-testid="feed-error-view-profile"]'), 465 465 ).toHaveAttribute("href", "/profile/creator1.bsky.social"); 466 466 }); 467 467
+12 -30
tests/e2e/specs/views/postThread.view.test.js
··· 309 309 timeout: 10000, 310 310 }); 311 311 312 - const loadParentLink = view.locator(".load-more-link a", { 313 - hasText: "Load parent post", 314 - }); 312 + const loadParentLink = view.locator( 313 + '[data-testid="post-thread-load-parent"]', 314 + ); 315 315 await expect(loadParentLink).toBeVisible(); 316 316 await expect(loadParentLink).toHaveAttribute( 317 317 "href", ··· 1544 1544 const largePost = view.locator('[data-testid="large-post"]'); 1545 1545 await expect(largePost).toBeVisible({ timeout: 10000 }); 1546 1546 1547 - await largePost 1548 - .locator(".post-action-button", { hasText: "..." }) 1549 - .click(); 1547 + await largePost.locator('[data-testid="post-action-more"]').click(); 1550 1548 1551 1549 const translateItem = page.locator( 1552 1550 '[data-testid="menu-action-post-translate"]', ··· 1585 1583 const largePost = view.locator('[data-testid="large-post"]'); 1586 1584 await expect(largePost).toBeVisible({ timeout: 10000 }); 1587 1585 1588 - await largePost 1589 - .locator(".post-action-button", { hasText: "..." }) 1590 - .click(); 1586 + await largePost.locator('[data-testid="post-action-more"]').click(); 1591 1587 1592 1588 await expect( 1593 1589 page.locator('[data-testid="menu-action-post-translate"]'), ··· 1611 1607 await expect(largePost).toBeVisible({ timeout: 10000 }); 1612 1608 1613 1609 // Open context menu 1614 - await largePost 1615 - .locator(".post-action-button", { hasText: "..." }) 1616 - .click(); 1610 + await largePost.locator('[data-testid="post-action-more"]').click(); 1617 1611 await expect( 1618 1612 page.locator('[data-testid="menu-action-post-hide"]'), 1619 1613 ).toBeVisible(); ··· 1633 1627 const largePost = view.locator('[data-testid="large-post"]'); 1634 1628 await expect(largePost).toBeVisible({ timeout: 10000 }); 1635 1629 1636 - await largePost 1637 - .locator(".post-action-button", { hasText: "..." }) 1638 - .click(); 1630 + await largePost.locator('[data-testid="post-action-more"]').click(); 1639 1631 await expect( 1640 1632 page.locator( 1641 1633 '[data-testid="menu-action-post-mute"][data-teststate="unmuted"]', ··· 1663 1655 const largePost = view.locator('[data-testid="large-post"]'); 1664 1656 await expect(largePost).toBeVisible({ timeout: 10000 }); 1665 1657 1666 - await largePost 1667 - .locator(".post-action-button", { hasText: "..." }) 1668 - .click(); 1658 + await largePost.locator('[data-testid="post-action-more"]').click(); 1669 1659 await expect( 1670 1660 page.locator( 1671 1661 '[data-testid="menu-action-post-mute"][data-teststate="muted"]', ··· 1687 1677 const largePost = view.locator('[data-testid="large-post"]'); 1688 1678 await expect(largePost).toBeVisible({ timeout: 10000 }); 1689 1679 1690 - await largePost 1691 - .locator(".post-action-button", { hasText: "..." }) 1692 - .click(); 1680 + await largePost.locator('[data-testid="post-action-more"]').click(); 1693 1681 await expect( 1694 1682 page.locator( 1695 1683 '[data-testid="menu-action-post-block"][data-teststate="not-blocking"]', ··· 1720 1708 const largePost = view.locator('[data-testid="large-post"]'); 1721 1709 await expect(largePost).toBeVisible({ timeout: 10000 }); 1722 1710 1723 - await largePost 1724 - .locator(".post-action-button", { hasText: "..." }) 1725 - .click(); 1711 + await largePost.locator('[data-testid="post-action-more"]').click(); 1726 1712 await expect( 1727 1713 page.locator( 1728 1714 '[data-testid="menu-action-post-block"][data-teststate="blocking"]', ··· 1742 1728 const largePost = view.locator('[data-testid="large-post"]'); 1743 1729 await expect(largePost).toBeVisible({ timeout: 10000 }); 1744 1730 1745 - await largePost 1746 - .locator(".post-action-button", { hasText: "..." }) 1747 - .click(); 1731 + await largePost.locator('[data-testid="post-action-more"]').click(); 1748 1732 await expect( 1749 1733 page.locator('[data-testid="menu-action-post-report"]'), 1750 1734 ).toBeVisible(); ··· 1771 1755 const largePost = view.locator('[data-testid="large-post"]'); 1772 1756 await expect(largePost).toBeVisible({ timeout: 10000 }); 1773 1757 1774 - await largePost 1775 - .locator(".post-action-button", { hasText: "..." }) 1776 - .click(); 1758 + await largePost.locator('[data-testid="post-action-more"]').click(); 1777 1759 1778 1760 // Moderation actions should not be present on own post 1779 1761 await expect(