this repo has no description
32
fork

Configure Feed

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

Refactor scenario phases and verification helpers

alice bc633561 6133cd31

+235 -213
+31 -41
src/browser/lib/dual-actions/profile.mjs
··· 121 121 }; 122 122 }; 123 123 124 + const readProfileCountsSnapshot = async (page, viewerAccount, profileHandle) => { 125 + await gotoProfile(page, profileHandle); 126 + await waitForProfileHandle(page, profileHandle); 127 + const rendered = await readRenderedProfileCounts(page); 128 + const apiResult = await xrpcJson('app.bsky.actor.getProfile', { 129 + token: viewerAccount?.accessJwt, 130 + pdsUrl: viewerAccount?.pdsUrl, 131 + params: { actor: profileHandle }, 132 + timeoutMs: 15000, 133 + }); 134 + if (!apiResult.ok) { 135 + throw new Error(`failed to read profile counts for ${profileHandle}`); 136 + } 137 + return { 138 + rendered, 139 + api: { 140 + followersCount: apiResult.json?.followersCount, 141 + followsCount: apiResult.json?.followsCount, 142 + }, 143 + }; 144 + }; 145 + 124 146 const verifyProfileCountsAfterReload = async (page, viewerAccount, profileHandle, expected, timeoutMs = 30000) => { 125 147 const started = Date.now(); 126 - let lastRendered; 127 - let lastApi; 148 + let snapshot; 128 149 while (Date.now() - started < timeoutMs) { 129 - await gotoProfile(page, profileHandle); 130 - await waitForProfileHandle(page, profileHandle); 131 - lastRendered = await readRenderedProfileCounts(page); 132 - const apiResult = await xrpcJson('app.bsky.actor.getProfile', { 133 - token: viewerAccount?.accessJwt, 134 - pdsUrl: viewerAccount?.pdsUrl, 135 - params: { actor: profileHandle }, 136 - timeoutMs: 15000, 137 - }); 138 - if (apiResult.ok) { 139 - lastApi = { 140 - followersCount: apiResult.json?.followersCount, 141 - followsCount: apiResult.json?.followsCount, 142 - }; 150 + try { 151 + snapshot = await readProfileCountsSnapshot(page, viewerAccount, profileHandle); 143 152 const matches = Object.entries(expected).every(([key, value]) => 144 - lastRendered?.[key] === value && lastApi?.[key] === value); 153 + snapshot?.rendered?.[key] === value && snapshot?.api?.[key] === value); 145 154 if (matches) { 146 - return { 147 - rendered: lastRendered, 148 - api: lastApi, 149 - }; 155 + return snapshot; 150 156 } 157 + } catch { 158 + // Retry until the timeout expires. 151 159 } 152 160 await wait(page, 2000); 153 161 } 154 162 155 163 throw new Error( 156 - `profile counts for ${profileHandle} did not converge; expected=${JSON.stringify(expected)} rendered=${JSON.stringify(lastRendered)} api=${JSON.stringify(lastApi)}`, 164 + `profile counts for ${profileHandle} did not converge; expected=${JSON.stringify(expected)} rendered=${JSON.stringify(snapshot?.rendered)} api=${JSON.stringify(snapshot?.api)}`, 157 165 ); 158 166 }; 159 167 ··· 162 170 let lastError; 163 171 while (Date.now() - started < timeoutMs) { 164 172 try { 165 - await gotoProfile(page, profileHandle); 166 - await waitForProfileHandle(page, profileHandle); 167 - const rendered = await readRenderedProfileCounts(page); 168 - const apiResult = await xrpcJson('app.bsky.actor.getProfile', { 169 - token: viewerAccount?.accessJwt, 170 - pdsUrl: viewerAccount?.pdsUrl, 171 - params: { actor: profileHandle }, 172 - timeoutMs: 15000, 173 - }); 174 - if (!apiResult.ok) { 175 - throw new Error(`failed to read profile counts for ${profileHandle}`); 176 - } 177 - return { 178 - rendered, 179 - api: { 180 - followersCount: apiResult.json?.followersCount, 181 - followsCount: apiResult.json?.followsCount, 182 - }, 183 - }; 173 + return await readProfileCountsSnapshot(page, viewerAccount, profileHandle); 184 174 } catch (error) { 185 175 lastError = error; 186 176 await wait(page, 2000);
+108 -100
src/browser/lib/dual-scenario/phases.mjs
··· 3 3 return match ? decodeURIComponent(match[1]) : undefined; 4 4 }; 5 5 6 - export const runDualSetupPhase = async ({ 7 - config, 8 - step, 9 - primaryPage, 10 - secondaryPage, 11 - primary, 12 - secondary, 13 - login, 14 - completeAgeAssuranceIfNeeded, 15 - createSession, 16 - cleanupStaleSmokeArtifacts, 17 - composePost, 18 - waitForOwnPostRecord, 19 - gotoProfile, 20 - waitForProfileHandle, 21 - findRowByPrimaryText, 22 - composePostWithImage, 23 - editProfile, 24 - verifyLocalProfileAfterEdit, 25 - verifyPublicProfileAfterEdit, 26 - readProfileCountsAfterReload, 27 - createList, 28 - waitForOwnListRecord, 29 - recordRkey, 30 - openListPage, 31 - editCurrentList, 32 - addUserToCurrentList, 33 - waitForOwnListItemRecord, 34 - removeUserFromCurrentList, 35 - waitForNoOwnRecord, 36 - deleteCurrentList, 37 - maybeUnfollow, 38 - }) => { 6 + export const runDualSetupPhase = async (ctx) => { 7 + const { 8 + config, 9 + step, 10 + primaryPage, 11 + secondaryPage, 12 + primary, 13 + secondary, 14 + login, 15 + completeAgeAssuranceIfNeeded, 16 + createSession, 17 + cleanupStaleSmokeArtifacts, 18 + composePost, 19 + waitForOwnPostRecord, 20 + gotoProfile, 21 + waitForProfileHandle, 22 + findRowByPrimaryText, 23 + composePostWithImage, 24 + editProfile, 25 + verifyLocalProfileAfterEdit, 26 + verifyPublicProfileAfterEdit, 27 + readProfileCountsAfterReload, 28 + createList, 29 + waitForOwnListRecord, 30 + recordRkey, 31 + openListPage, 32 + editCurrentList, 33 + addUserToCurrentList, 34 + waitForOwnListItemRecord, 35 + removeUserFromCurrentList, 36 + waitForNoOwnRecord, 37 + deleteCurrentList, 38 + maybeUnfollow, 39 + } = ctx; 40 + 39 41 await step('primary-login', () => login(primaryPage, primary), { pageNames: ['primary'] }); 40 42 await step('primary-age-assurance', () => completeAgeAssuranceIfNeeded(primaryPage, primary), { 41 43 optional: true, ··· 238 240 }); 239 241 }; 240 242 241 - export const runDualPrimaryWavePhase = async ({ 242 - config, 243 - step, 244 - primaryPage, 245 - secondaryPage, 246 - primary, 247 - secondary, 248 - gotoProfile, 249 - waitForProfileHandle, 250 - maybeUnfollow, 251 - maybeFollow, 252 - waitForFollowRecord, 253 - verifyProfileCountsAfterReload, 254 - findRowByPrimaryText, 255 - ensureLiked, 256 - ensureBookmarked, 257 - openSavedPosts, 258 - ensureReposted, 259 - clickQuote, 260 - clickReply, 261 - waitForOwnPostRecord, 262 - pollNotifications, 263 - openNotifications, 264 - waitForNotificationsFeed, 265 - }) => { 243 + export const runDualPrimaryWavePhase = async (ctx) => { 244 + const { 245 + config, 246 + step, 247 + primaryPage, 248 + secondaryPage, 249 + primary, 250 + secondary, 251 + gotoProfile, 252 + waitForProfileHandle, 253 + maybeUnfollow, 254 + maybeFollow, 255 + waitForFollowRecord, 256 + verifyProfileCountsAfterReload, 257 + findRowByPrimaryText, 258 + ensureLiked, 259 + ensureBookmarked, 260 + openSavedPosts, 261 + ensureReposted, 262 + clickQuote, 263 + clickReply, 264 + waitForOwnPostRecord, 265 + pollNotifications, 266 + openNotifications, 267 + waitForNotificationsFeed, 268 + } = ctx; 269 + 266 270 const primaryWaveStarted = Date.now() - 1000; 267 271 await step('primary-open-secondary-profile', async () => { 268 272 await gotoProfile(primaryPage, secondary.handle); ··· 399 403 }, { pageNames: ['secondary'] }); 400 404 }; 401 405 402 - export const runDualSecondaryWaveAndSettingsPhase = async ({ 403 - step, 404 - primaryPage, 405 - secondaryPage, 406 - primary, 407 - secondary, 408 - gotoProfile, 409 - waitForProfileHandle, 410 - maybeUnfollow, 411 - maybeFollow, 412 - waitForFollowRecord, 413 - verifyProfileCountsAfterReload, 414 - pollNotifications, 415 - openNotifications, 416 - waitForNotificationsFeed, 417 - ensureProfileMuted, 418 - ensureProfileUnmuted, 419 - findRowByPrimaryText, 420 - openReportPostDraft, 421 - blockProfile, 422 - unblockProfile, 423 - setRadioSetting, 424 - setCheckboxSetting, 425 - }) => { 406 + export const runDualSecondaryWaveAndSettingsPhase = async (ctx) => { 407 + const { 408 + step, 409 + primaryPage, 410 + secondaryPage, 411 + primary, 412 + secondary, 413 + gotoProfile, 414 + waitForProfileHandle, 415 + maybeUnfollow, 416 + maybeFollow, 417 + waitForFollowRecord, 418 + verifyProfileCountsAfterReload, 419 + pollNotifications, 420 + openNotifications, 421 + waitForNotificationsFeed, 422 + ensureProfileMuted, 423 + ensureProfileUnmuted, 424 + findRowByPrimaryText, 425 + openReportPostDraft, 426 + blockProfile, 427 + unblockProfile, 428 + setRadioSetting, 429 + setCheckboxSetting, 430 + } = ctx; 431 + 426 432 const secondaryWaveStarted = Date.now() - 1000; 427 433 await step('secondary-open-primary-profile', async () => { 428 434 await gotoProfile(secondaryPage, primary.handle); ··· 559 565 }, { pageNames: ['primary'] }); 560 566 }; 561 567 562 - export const runDualCleanupPhase = async ({ 563 - config, 564 - step, 565 - primaryPage, 566 - secondaryPage, 567 - primary, 568 - secondary, 569 - gotoProfile, 570 - findRowByPrimaryText, 571 - ensureNotLiked, 572 - ensureNotBookmarked, 573 - ensureNotReposted, 574 - maybeUnfollow, 575 - verifyProfileCountsAfterReload, 576 - waitForProfileHandle, 577 - openProfileTab, 578 - maybeDeleteOwnPostByText, 579 - }) => { 568 + export const runDualCleanupPhase = async (ctx) => { 569 + const { 570 + config, 571 + step, 572 + primaryPage, 573 + secondaryPage, 574 + primary, 575 + secondary, 576 + gotoProfile, 577 + findRowByPrimaryText, 578 + ensureNotLiked, 579 + ensureNotBookmarked, 580 + ensureNotReposted, 581 + maybeUnfollow, 582 + verifyProfileCountsAfterReload, 583 + waitForProfileHandle, 584 + openProfileTab, 585 + maybeDeleteOwnPostByText, 586 + } = ctx; 587 + 580 588 await step('primary-cleanup-unlike-secondary-post', async () => { 581 589 await gotoProfile(primaryPage, secondary.handle); 582 590 const row = await findRowByPrimaryText(primaryPage, secondary.postText, 60000);
+35 -19
src/browser/lib/settings.mjs
··· 15 15 return (await locator.getAttribute('aria-checked')) === 'true'; 16 16 }; 17 17 18 - const setCheckboxSetting = async (page, route, name, desired) => { 18 + const setPersistedSetting = async ({ 19 + page, 20 + route, 21 + role, 22 + name, 23 + desired, 24 + verifyError, 25 + result, 26 + }) => { 19 27 await openSettingRoute(page, route); 20 - const locator = roleSetting(page, 'checkbox', name); 21 - const current = await settingState(page, 'checkbox', name); 28 + const locator = roleSetting(page, role, name); 29 + const current = await settingState(page, role, name); 22 30 if (current !== desired) { 23 31 await locator.click({ noWaitAfter: true }); 24 32 await wait(page, 2000); 25 33 } 26 34 await openSettingRoute(page, route); 27 - const verified = await settingState(page, 'checkbox', name); 35 + const verified = await settingState(page, role, name); 28 36 if (verified !== desired) { 29 - throw new Error(`checkbox setting ${name} on ${route} expected ${desired} but saw ${verified}`); 37 + throw new Error(verifyError(verified)); 30 38 } 31 - return { desired, verified }; 39 + return result(verified); 40 + }; 41 + 42 + const setCheckboxSetting = async (page, route, name, desired) => { 43 + return await setPersistedSetting({ 44 + page, 45 + route, 46 + role: 'checkbox', 47 + name, 48 + desired, 49 + verifyError: (verified) => `checkbox setting ${name} on ${route} expected ${desired} but saw ${verified}`, 50 + result: (verified) => ({ desired, verified }), 51 + }); 32 52 }; 33 53 34 54 const setRadioSetting = async (page, route, name) => { 35 - await openSettingRoute(page, route); 36 - const locator = roleSetting(page, 'radio', name); 37 - const current = await settingState(page, 'radio', name); 38 - if (!current) { 39 - await locator.click({ noWaitAfter: true }); 40 - await wait(page, 2000); 41 - } 42 - await openSettingRoute(page, route); 43 - const verified = await settingState(page, 'radio', name); 44 - if (!verified) { 45 - throw new Error(`radio setting ${name} on ${route} did not persist`); 46 - } 47 - return { selected: name }; 55 + return await setPersistedSetting({ 56 + page, 57 + route, 58 + role: 'radio', 59 + name, 60 + desired: true, 61 + verifyError: () => `radio setting ${name} on ${route} did not persist`, 62 + result: () => ({ selected: name }), 63 + }); 48 64 }; 49 65 50 66 return {
+61 -53
src/browser/lib/single-scenario/phases.mjs
··· 1 - export const runSingleBootstrapPhase = async ({ 2 - step, 3 - config, 4 - login, 5 - completeAgeAssuranceIfNeeded, 6 - composePost, 7 - verifyPublicHandleResolution, 8 - verifyPublicProfile, 9 - verifyPublicAuthorFeed, 10 - gotoProfile, 11 - page, 12 - findRowByPrimaryText, 13 - ensureLiked, 14 - ensureReposted, 15 - clickQuote, 16 - clickReply, 17 - ensureNotLiked, 18 - ensureNotReposted, 19 - }) => { 1 + export const runSingleBootstrapPhase = async (ctx) => { 2 + const { 3 + step, 4 + config, 5 + login, 6 + completeAgeAssuranceIfNeeded, 7 + composePost, 8 + verifyPublicHandleResolution, 9 + verifyPublicProfile, 10 + verifyPublicAuthorFeed, 11 + gotoProfile, 12 + page, 13 + findRowByPrimaryText, 14 + ensureLiked, 15 + ensureReposted, 16 + clickQuote, 17 + clickReply, 18 + ensureNotLiked, 19 + ensureNotReposted, 20 + } = ctx; 21 + 20 22 await step('login', login); 21 23 await step('age-assurance', completeAgeAssuranceIfNeeded, { optional: true }); 22 24 await step('compose-own-post', () => composePost(config.postText)); ··· 60 62 }, { optional: true }); 61 63 }; 62 64 63 - export const runSingleTargetInteractionPhase = async ({ 64 - step, 65 - config, 66 - gotoProfile, 67 - maybeFollowTarget, 68 - findFirstFeedItem, 69 - ensureBookmarked, 70 - openSavedPosts, 71 - page, 72 - ensureLiked, 73 - ensureReposted, 74 - clickQuote, 75 - clickReply, 76 - ensureNotLiked, 77 - ensureNotReposted, 78 - ensureNotBookmarked, 79 - maybeUnfollowTarget, 80 - openNotifications, 81 - }) => { 65 + export const runSingleTargetInteractionPhase = async (ctx) => { 66 + const { 67 + step, 68 + config, 69 + gotoProfile, 70 + maybeFollowTarget, 71 + findFirstFeedItem, 72 + ensureBookmarked, 73 + openSavedPosts, 74 + page, 75 + ensureLiked, 76 + ensureReposted, 77 + clickQuote, 78 + clickReply, 79 + ensureNotLiked, 80 + ensureNotReposted, 81 + ensureNotBookmarked, 82 + maybeUnfollowTarget, 83 + openNotifications, 84 + } = ctx; 85 + 82 86 await step('target-profile', async () => { 83 87 await gotoProfile(config.targetHandle); 84 88 }); ··· 166 170 }, { optional: true }); 167 171 }; 168 172 169 - export const runSingleProfilePhase = async ({ 170 - step, 171 - config, 172 - gotoProfile, 173 - editProfile, 174 - verifyLocalProfileAfterEdit, 175 - verifyPublicProfileAfterEdit, 176 - }) => { 173 + export const runSingleProfilePhase = async (ctx) => { 174 + const { 175 + step, 176 + config, 177 + gotoProfile, 178 + editProfile, 179 + verifyLocalProfileAfterEdit, 180 + verifyPublicProfileAfterEdit, 181 + } = ctx; 182 + 177 183 if (!config.editProfile) { 178 184 return; 179 185 } ··· 188 194 } 189 195 }; 190 196 191 - export const runSingleCleanupPhase = async ({ 192 - step, 193 - config, 194 - gotoProfile, 195 - openProfileTab, 196 - maybeDeleteOwnPostByText, 197 - }) => { 197 + export const runSingleCleanupPhase = async (ctx) => { 198 + const { 199 + step, 200 + config, 201 + gotoProfile, 202 + openProfileTab, 203 + maybeDeleteOwnPostByText, 204 + } = ctx; 205 + 198 206 await step('cleanup-own-posts-tab', async () => { 199 207 await gotoProfile(config.handle); 200 208 await openProfileTab('Posts');