this repo has no description
32
fork

Configure Feed

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

Share browser summary and step runner plumbing

alice 6133cd31 390d216d

+142 -503
+11 -46
src/browser/lib/dual-browser.mjs
··· 4 4 attachPageLogging, 5 5 buttonText, 6 6 closeBrowserSafely, 7 + createStepRunner, 7 8 createProgressEmitter, 8 9 finalizeSummary, 9 10 launchBrowserWithFallback, 10 11 normalizeText, 12 + recordStep, 11 13 sleep, 12 14 } from './runtime-utils.mjs'; 13 15 import { ··· 53 55 return file; 54 56 }; 55 57 56 - const recordStep = (name, status, extra = {}) => { 57 - summary.steps.push({ 58 - name, 59 - status, 60 - at: new Date().toISOString(), 61 - ...extra, 62 - }); 63 - }; 64 - 65 - const step = async (name, fn, { optional = false, pageNames = [], timeoutMs } = {}) => { 66 - const effectiveTimeoutMs = Number(timeoutMs || stepTimeoutMs); 67 - emitProgress('start', name); 68 - let timeoutId; 69 - try { 70 - const result = await Promise.race([ 71 - fn(), 72 - new Promise((_, reject) => { 73 - timeoutId = setTimeout(() => { 74 - reject(new Error(`step timed out after ${effectiveTimeoutMs}ms`)); 75 - }, effectiveTimeoutMs); 76 - }), 77 - ]); 58 + const step = createStepRunner({ 59 + summary, 60 + emitProgress, 61 + defaultTimeoutMs: stepTimeoutMs, 62 + captureArtifacts: async ({ name, pageNames, failed }) => { 78 63 const screenshots = {}; 79 64 for (const pageName of pageNames) { 80 - screenshots[pageName] = await screenshot(pageName, name); 81 - } 82 - recordStep(name, 'ok', { screenshots, ...(result ?? {}) }); 83 - emitProgress('ok', name); 84 - return result; 85 - } catch (error) { 86 - const screenshots = {}; 87 - for (const pageName of pageNames) { 88 - screenshots[pageName] = await screenshot(pageName, `${name}-error`).catch(() => undefined); 89 - } 90 - recordStep(name, optional ? 'skipped' : 'failed', { 91 - screenshots, 92 - error: String(error?.message ?? error), 93 - }); 94 - emitProgress(optional ? 'skip' : 'fail', name, String(error?.message ?? error)); 95 - if (!optional) { 96 - throw error; 97 - } 98 - return null; 99 - } finally { 100 - if (timeoutId) { 101 - clearTimeout(timeoutId); 65 + screenshots[pageName] = await screenshot(pageName, failed ? `${name}-error` : name).catch(() => undefined); 102 66 } 103 - } 104 - }; 67 + return { screenshots }; 68 + }, 69 + }); 105 70 106 71 const wait = async (page, ms) => { 107 72 await page.waitForTimeout(ms);
+5 -160
src/browser/lib/dual-scenario.mjs
··· 5 5 runDualSetupPhase, 6 6 } from './dual-scenario/phases.mjs'; 7 7 8 - export const runDualScenario = async ({ 9 - config, 10 - step, 11 - primaryPage, 12 - secondaryPage, 13 - primary, 14 - secondary, 15 - login, 16 - completeAgeAssuranceIfNeeded, 17 - createSession, 18 - cleanupStaleSmokeArtifacts, 19 - composePost, 20 - waitForOwnPostRecord, 21 - gotoProfile, 22 - waitForProfileHandle, 23 - verifyProfileCountsAfterReload, 24 - readProfileCountsAfterReload, 25 - findRowByPrimaryText, 26 - composePostWithImage, 27 - editProfile, 28 - verifyLocalProfileAfterEdit, 29 - verifyPublicProfileAfterEdit, 30 - createList, 31 - waitForOwnListRecord, 32 - recordRkey, 33 - openListPage, 34 - editCurrentList, 35 - addUserToCurrentList, 36 - waitForOwnListItemRecord, 37 - removeUserFromCurrentList, 38 - waitForNoOwnRecord, 39 - deleteCurrentList, 40 - maybeUnfollow, 41 - maybeFollow, 42 - waitForFollowRecord, 43 - ensureLiked, 44 - ensureBookmarked, 45 - openSavedPosts, 46 - ensureReposted, 47 - clickQuote, 48 - clickReply, 49 - pollNotifications, 50 - openNotifications, 51 - waitForNotificationsFeed, 52 - ensureProfileMuted, 53 - ensureProfileUnmuted, 54 - openReportPostDraft, 55 - blockProfile, 56 - unblockProfile, 57 - setRadioSetting, 58 - setCheckboxSetting, 59 - ensureNotLiked, 60 - ensureNotBookmarked, 61 - ensureNotReposted, 62 - openProfileTab, 63 - maybeDeleteOwnPostByText, 64 - }) => { 65 - await runDualSetupPhase({ 66 - config, 67 - step, 68 - primaryPage, 69 - secondaryPage, 70 - primary, 71 - secondary, 72 - login, 73 - completeAgeAssuranceIfNeeded, 74 - createSession, 75 - cleanupStaleSmokeArtifacts, 76 - composePost, 77 - waitForOwnPostRecord, 78 - gotoProfile, 79 - waitForProfileHandle, 80 - findRowByPrimaryText, 81 - composePostWithImage, 82 - editProfile, 83 - verifyLocalProfileAfterEdit, 84 - verifyPublicProfileAfterEdit, 85 - readProfileCountsAfterReload, 86 - createList, 87 - waitForOwnListRecord, 88 - recordRkey, 89 - openListPage, 90 - editCurrentList, 91 - addUserToCurrentList, 92 - waitForOwnListItemRecord, 93 - removeUserFromCurrentList, 94 - waitForNoOwnRecord, 95 - deleteCurrentList, 96 - maybeUnfollow, 97 - }); 98 - 99 - await runDualPrimaryWavePhase({ 100 - config, 101 - step, 102 - primaryPage, 103 - secondaryPage, 104 - primary, 105 - secondary, 106 - gotoProfile, 107 - waitForProfileHandle, 108 - maybeUnfollow, 109 - maybeFollow, 110 - waitForFollowRecord, 111 - verifyProfileCountsAfterReload, 112 - findRowByPrimaryText, 113 - ensureLiked, 114 - ensureBookmarked, 115 - openSavedPosts, 116 - ensureReposted, 117 - clickQuote, 118 - clickReply, 119 - waitForOwnPostRecord, 120 - pollNotifications, 121 - openNotifications, 122 - waitForNotificationsFeed, 123 - }); 124 - 125 - await runDualSecondaryWaveAndSettingsPhase({ 126 - step, 127 - primaryPage, 128 - secondaryPage, 129 - primary, 130 - secondary, 131 - gotoProfile, 132 - waitForProfileHandle, 133 - maybeUnfollow, 134 - maybeFollow, 135 - waitForFollowRecord, 136 - verifyProfileCountsAfterReload, 137 - pollNotifications, 138 - openNotifications, 139 - waitForNotificationsFeed, 140 - ensureProfileMuted, 141 - ensureProfileUnmuted, 142 - findRowByPrimaryText, 143 - openReportPostDraft, 144 - blockProfile, 145 - unblockProfile, 146 - setRadioSetting, 147 - setCheckboxSetting, 148 - }); 149 - 150 - await runDualCleanupPhase({ 151 - config, 152 - step, 153 - primaryPage, 154 - secondaryPage, 155 - primary, 156 - secondary, 157 - gotoProfile, 158 - findRowByPrimaryText, 159 - ensureNotLiked, 160 - ensureNotBookmarked, 161 - ensureNotReposted, 162 - maybeUnfollow, 163 - verifyProfileCountsAfterReload, 164 - waitForProfileHandle, 165 - openProfileTab, 166 - maybeDeleteOwnPostByText, 167 - }); 8 + export const runDualScenario = async (ctx) => { 9 + await runDualSetupPhase(ctx); 10 + await runDualPrimaryWavePhase(ctx); 11 + await runDualSecondaryWaveAndSettingsPhase(ctx); 12 + await runDualCleanupPhase(ctx); 168 13 };
+65
src/browser/lib/runtime-utils.mjs
··· 9 9 10 10 export const normalizeText = (text) => (text || '').replace(/\s+/g, ' ').trim(); 11 11 12 + export const createBaseSummary = (fields = {}) => ({ 13 + startedAt: new Date().toISOString(), 14 + steps: [], 15 + console: [], 16 + pageErrors: [], 17 + requestFailures: [], 18 + httpFailures: [], 19 + xrpc: [], 20 + notes: [], 21 + ...fields, 22 + }); 23 + 24 + export const recordStep = (summary, name, status, extra = {}) => { 25 + summary.steps.push({ 26 + name, 27 + status, 28 + at: new Date().toISOString(), 29 + ...extra, 30 + }); 31 + }; 32 + 33 + export const createStepRunner = ({ 34 + summary, 35 + emitProgress, 36 + captureArtifacts, 37 + defaultTimeoutMs, 38 + }) => { 39 + return async (name, fn, { optional = false, timeoutMs, pageNames = [] } = {}) => { 40 + const effectiveTimeoutMs = Number(timeoutMs || defaultTimeoutMs || 0); 41 + emitProgress('start', name); 42 + let timeoutId; 43 + try { 44 + const result = effectiveTimeoutMs > 0 45 + ? await Promise.race([ 46 + fn(), 47 + new Promise((_, reject) => { 48 + timeoutId = setTimeout(() => { 49 + reject(new Error(`step timed out after ${effectiveTimeoutMs}ms`)); 50 + }, effectiveTimeoutMs); 51 + }), 52 + ]) 53 + : await fn(); 54 + const artifacts = await captureArtifacts({ name, pageNames, failed: false }); 55 + recordStep(summary, name, 'ok', { ...artifacts, ...(result ?? {}) }); 56 + emitProgress('ok', name); 57 + return result; 58 + } catch (error) { 59 + const artifacts = await captureArtifacts({ name, pageNames, failed: true }); 60 + recordStep(summary, name, optional ? 'skipped' : 'failed', { 61 + ...artifacts, 62 + error: String(error?.message ?? error), 63 + }); 64 + emitProgress(optional ? 'skip' : 'fail', name, String(error?.message ?? error)); 65 + if (!optional) { 66 + throw error; 67 + } 68 + return null; 69 + } finally { 70 + if (timeoutId) { 71 + clearTimeout(timeoutId); 72 + } 73 + } 74 + }; 75 + }; 76 + 12 77 export const buildBrowserLaunchCandidates = async (config) => { 13 78 const base = { 14 79 headless: config.headless !== false,
+5 -87
src/browser/lib/single-scenario.mjs
··· 5 5 runSingleTargetInteractionPhase, 6 6 } from './single-scenario/phases.mjs'; 7 7 8 - export const runSingleScenario = async ({ 9 - step, 10 - config, 11 - login, 12 - completeAgeAssuranceIfNeeded, 13 - composePost, 14 - verifyPublicHandleResolution, 15 - verifyPublicProfile, 16 - verifyPublicAuthorFeed, 17 - gotoProfile, 18 - page, 19 - findRowByPrimaryText, 20 - ensureLiked, 21 - ensureReposted, 22 - clickQuote, 23 - clickReply, 24 - ensureNotLiked, 25 - ensureNotReposted, 26 - maybeFollowTarget, 27 - findFirstFeedItem, 28 - ensureBookmarked, 29 - openSavedPosts, 30 - ensureNotBookmarked, 31 - maybeUnfollowTarget, 32 - openNotifications, 33 - editProfile, 34 - verifyLocalProfileAfterEdit, 35 - verifyPublicProfileAfterEdit, 36 - openProfileTab, 37 - maybeDeleteOwnPostByText, 38 - }) => { 39 - await runSingleBootstrapPhase({ 40 - step, 41 - config, 42 - login, 43 - completeAgeAssuranceIfNeeded, 44 - composePost, 45 - verifyPublicHandleResolution, 46 - verifyPublicProfile, 47 - verifyPublicAuthorFeed, 48 - gotoProfile, 49 - page, 50 - findRowByPrimaryText, 51 - ensureLiked, 52 - ensureReposted, 53 - clickQuote, 54 - clickReply, 55 - ensureNotLiked, 56 - ensureNotReposted, 57 - }); 58 - 59 - await runSingleTargetInteractionPhase({ 60 - step, 61 - config, 62 - gotoProfile, 63 - maybeFollowTarget, 64 - findFirstFeedItem, 65 - ensureBookmarked, 66 - openSavedPosts, 67 - page, 68 - ensureLiked, 69 - ensureReposted, 70 - clickQuote, 71 - clickReply, 72 - ensureNotLiked, 73 - ensureNotReposted, 74 - ensureNotBookmarked, 75 - maybeUnfollowTarget, 76 - openNotifications, 77 - }); 78 - 79 - await runSingleProfilePhase({ 80 - step, 81 - config, 82 - gotoProfile, 83 - editProfile, 84 - verifyLocalProfileAfterEdit, 85 - verifyPublicProfileAfterEdit, 86 - }); 87 - 88 - await runSingleCleanupPhase({ 89 - step, 90 - config, 91 - gotoProfile, 92 - openProfileTab, 93 - maybeDeleteOwnPostByText, 94 - }); 8 + export const runSingleScenario = async (ctx) => { 9 + await runSingleBootstrapPhase(ctx); 10 + await runSingleTargetInteractionPhase(ctx); 11 + await runSingleProfilePhase(ctx); 12 + await runSingleCleanupPhase(ctx); 95 13 };
+28 -100
src/browser/run-dual.mjs
··· 7 7 import { createSettingsHelpers } from './lib/settings.mjs'; 8 8 import { runDualScenario } from './lib/dual-scenario.mjs'; 9 9 import { createDualActions } from './lib/dual-actions.mjs'; 10 - import { AVATAR_PNG_BASE64, sleep } from './lib/runtime-utils.mjs'; 10 + import { AVATAR_PNG_BASE64, createBaseSummary, sleep } from './lib/runtime-utils.mjs'; 11 11 12 12 export const runDualFromConfig = async (config) => { 13 13 await fs.mkdir(config.artifactsDir, { recursive: true }); 14 14 const appBaseUrl = config.appUrl.replace(/\/$/, ''); 15 15 16 - const summary = { 17 - startedAt: new Date().toISOString(), 16 + const summary = createBaseSummary({ 18 17 appUrl: config.appUrl, 19 18 pdsUrl: config.pdsUrl, 20 19 primaryPdsUrl: config.primary?.pdsUrl || config.pdsUrl, ··· 24 23 remoteReplyPostUrl: config.remoteReplyPostUrl, 25 24 primaryHandle: config.primary?.handle, 26 25 secondaryHandle: config.secondary?.handle, 27 - steps: [], 28 - console: [], 29 - pageErrors: [], 30 - requestFailures: [], 31 - httpFailures: [], 32 - xrpc: [], 33 - notes: [], 34 - }; 26 + }); 35 27 36 28 if (config.accountSource) { 37 29 summary.notes.push(`account source: ${config.accountSource}`); ··· 82 74 startedAt: summary.startedAt, 83 75 }); 84 76 85 - const { 86 - login, 87 - completeAgeAssuranceIfNeeded, 88 - gotoProfile, 89 - waitForProfileHandle, 90 - verifyProfileCountsAfterReload, 91 - readProfileCountsAfterReload, 92 - composePost, 93 - composePostWithImage, 94 - editProfile, 95 - verifyLocalProfileAfterEdit, 96 - verifyPublicProfileAfterEdit, 97 - findRowByPrimaryText, 98 - ensureLiked, 99 - ensureNotLiked, 100 - ensureReposted, 101 - ensureNotReposted, 102 - ensureBookmarked, 103 - ensureNotBookmarked, 104 - clickQuote, 105 - clickReply, 106 - maybeFollow, 107 - maybeUnfollow, 108 - openNotifications, 109 - openSavedPosts, 110 - waitForNotificationsFeed, 111 - ensureProfileMuted, 112 - ensureProfileUnmuted, 113 - blockProfile, 114 - unblockProfile, 115 - openReportPostDraft, 116 - openProfileTab, 117 - maybeDeleteOwnPostByText, 118 - } = createDualActions({ 77 + const actions = createDualActions({ 119 78 config, 120 79 summary, 121 80 appBaseUrl, ··· 131 90 132 91 try { 133 92 await runDualScenario({ 134 - config, 135 - step, 136 - primaryPage, 137 - secondaryPage, 138 - primary, 139 - secondary, 140 - login, 141 - completeAgeAssuranceIfNeeded, 142 - createSession, 143 - cleanupStaleSmokeArtifacts, 144 - composePost, 145 - waitForOwnPostRecord, 146 - gotoProfile, 147 - waitForProfileHandle, 148 - verifyProfileCountsAfterReload, 149 - readProfileCountsAfterReload, 150 - findRowByPrimaryText, 151 - composePostWithImage, 152 - editProfile, 153 - verifyLocalProfileAfterEdit, 154 - verifyPublicProfileAfterEdit, 155 - createList, 156 - waitForOwnListRecord, 157 - recordRkey, 158 - openListPage, 159 - editCurrentList, 160 - addUserToCurrentList, 161 - waitForOwnListItemRecord, 162 - removeUserFromCurrentList, 163 - waitForNoOwnRecord, 164 - deleteCurrentList, 165 - maybeUnfollow, 166 - maybeFollow, 167 - waitForFollowRecord, 168 - ensureLiked, 169 - ensureBookmarked, 170 - openSavedPosts, 171 - ensureReposted, 172 - clickQuote, 173 - clickReply, 174 - pollNotifications, 175 - openNotifications, 176 - waitForNotificationsFeed, 177 - ensureProfileMuted, 178 - ensureProfileUnmuted, 179 - openReportPostDraft, 180 - blockProfile, 181 - unblockProfile, 182 - setRadioSetting, 183 - setCheckboxSetting, 184 - ensureNotLiked, 185 - ensureNotBookmarked, 186 - ensureNotReposted, 187 - openProfileTab, 188 - maybeDeleteOwnPostByText, 93 + config, 94 + step, 95 + primaryPage, 96 + secondaryPage, 97 + primary, 98 + secondary, 99 + createSession, 100 + cleanupStaleSmokeArtifacts, 101 + waitForOwnPostRecord, 102 + waitForFollowRecord, 103 + waitForNoOwnRecord, 104 + waitForOwnListRecord, 105 + waitForOwnListItemRecord, 106 + recordRkey, 107 + pollNotifications, 108 + createList, 109 + openListPage, 110 + editCurrentList, 111 + addUserToCurrentList, 112 + removeUserFromCurrentList, 113 + deleteCurrentList, 114 + setRadioSetting, 115 + setCheckboxSetting, 116 + ...actions, 189 117 }); 190 118 } catch (error) { 191 119 summary.fatal = String(error?.message ?? error);
+28 -110
src/browser/run-single.mjs
··· 7 7 attachPageLogging, 8 8 buttonText, 9 9 closeBrowserSafely, 10 + createBaseSummary, 10 11 createProgressEmitter, 12 + createStepRunner, 11 13 fetchJsonWithTimeout, 12 14 fetchStatusWithTimeout, 13 15 finalizeSummary, ··· 28 30 await fs.mkdir(config.artifactsDir, { recursive: true }); 29 31 const appBaseUrl = config.appUrl.replace(/\/$/, ''); 30 32 31 - const summary = { 32 - startedAt: new Date().toISOString(), 33 + const summary = createBaseSummary({ 33 34 appUrl: config.appUrl, 34 35 pdsUrl: config.pdsUrl, 35 36 publicApiUrl: config.publicApiUrl, 36 37 handle: config.handle, 37 38 loginIdentifier: config.loginIdentifier, 38 39 targetHandle: config.targetHandle, 39 - steps: [], 40 - console: [], 41 - pageErrors: [], 42 - requestFailures: [], 43 - httpFailures: [], 44 - xrpc: [], 45 - notes: [], 46 - }; 40 + }); 47 41 48 42 const progressEnabled = config.progress !== false; 49 43 const emitProgress = createProgressEmitter({ enabled: progressEnabled }); ··· 66 60 return file; 67 61 }; 68 62 69 - const recordStep = (name, status, extra = {}) => { 70 - summary.steps.push({ 71 - name, 72 - status, 73 - at: new Date().toISOString(), 74 - ...extra, 75 - }); 76 - }; 77 - 78 - const step = async (name, fn, { optional = false } = {}) => { 79 - emitProgress('start', name); 80 - try { 81 - const result = await fn(); 82 - const shot = await screenshot(name); 83 - recordStep(name, 'ok', { screenshot: shot, ...(result ?? {}) }); 84 - emitProgress('ok', name); 85 - return result; 86 - } catch (error) { 87 - const shot = await screenshot(`${name}-error`).catch(() => undefined); 88 - recordStep(name, optional ? 'skipped' : 'failed', { 89 - screenshot: shot, 90 - error: String(error?.message ?? error), 91 - }); 92 - emitProgress(optional ? 'skip' : 'fail', name, String(error?.message ?? error)); 93 - if (!optional) { 94 - throw error; 95 - } 96 - return null; 97 - } 98 - }; 63 + const step = createStepRunner({ 64 + summary, 65 + emitProgress, 66 + captureArtifacts: async ({ name, failed }) => ({ 67 + screenshot: await screenshot(failed ? `${name}-error` : name).catch(() => undefined), 68 + }), 69 + }); 99 70 100 71 const wait = (ms) => page.waitForTimeout(ms); 101 72 const fetchJson = async (url, options = {}) => ··· 118 89 fetchJson, 119 90 }); 120 91 121 - const { 122 - login, 123 - completeAgeAssuranceIfNeeded, 124 - gotoProfile, 125 - maybeFollowTarget, 126 - composePost, 127 - waitForProfileHandle, 128 - findRowByPrimaryText, 129 - findFirstFeedItem, 130 - clickQuote, 131 - clickReply, 132 - ensureBookmarked, 133 - ensureNotBookmarked, 134 - ensureLiked, 135 - ensureNotLiked, 136 - ensureReposted, 137 - ensureNotReposted, 138 - openProfileTab, 139 - maybeUnfollowTarget, 140 - maybeDeleteOwnPostByText, 141 - openNotifications, 142 - openSavedPosts, 143 - verifyPublicHandleResolution, 144 - verifyPublicAuthorFeed, 145 - verifyPublicProfile, 146 - verifyPublicProfileAfterEdit, 147 - verifyLocalProfileAfterEdit, 148 - editProfile, 149 - } = createSingleActions({ 150 - config, 151 - summary, 152 - page, 153 - appBaseUrl, 154 - wait, 155 - sleep, 156 - normalizeText, 157 - buttonText, 158 - fetchStatus, 159 - pollJson, 160 - avatarPngBase64: AVATAR_PNG_BASE64, 161 - }); 92 + const actions = createSingleActions({ 93 + config, 94 + summary, 95 + page, 96 + appBaseUrl, 97 + wait, 98 + sleep, 99 + normalizeText, 100 + buttonText, 101 + fetchStatus, 102 + pollJson, 103 + avatarPngBase64: AVATAR_PNG_BASE64, 104 + }); 162 105 163 106 try { 164 107 await runSingleScenario({ 165 - step, 166 - config, 167 - login, 168 - completeAgeAssuranceIfNeeded, 169 - composePost, 170 - verifyPublicHandleResolution, 171 - verifyPublicProfile, 172 - verifyPublicAuthorFeed, 173 - gotoProfile, 174 - page, 175 - findRowByPrimaryText, 176 - ensureLiked, 177 - ensureReposted, 178 - clickQuote, 179 - clickReply, 180 - ensureNotLiked, 181 - ensureNotReposted, 182 - maybeFollowTarget, 183 - findFirstFeedItem, 184 - ensureBookmarked, 185 - openSavedPosts, 186 - ensureNotBookmarked, 187 - maybeUnfollowTarget, 188 - openNotifications, 189 - editProfile, 190 - verifyLocalProfileAfterEdit, 191 - verifyPublicProfileAfterEdit, 192 - openProfileTab, 193 - maybeDeleteOwnPostByText, 108 + step, 109 + config, 110 + page, 111 + ...actions, 194 112 }); 195 113 } catch (error) { 196 114 summary.fatal = String(error?.message ?? error);