step8-stop-cleanup.test.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. const assert = require('assert');
  2. const fs = require('fs');
  3. const helperSource = fs.readFileSync('background.js', 'utf8');
  4. const step8ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
  5. function extractFunction(source, name) {
  6. const markers = [`async function ${name}(`, `function ${name}(`];
  7. const start = markers
  8. .map(marker => source.indexOf(marker))
  9. .find(index => index >= 0);
  10. if (start < 0) {
  11. throw new Error(`missing function ${name}`);
  12. }
  13. let parenDepth = 0;
  14. let signatureEnded = false;
  15. let braceStart = -1;
  16. for (let i = start; i < source.length; i++) {
  17. const ch = source[i];
  18. if (ch === '(') {
  19. parenDepth += 1;
  20. } else if (ch === ')') {
  21. parenDepth -= 1;
  22. if (parenDepth === 0) {
  23. signatureEnded = true;
  24. }
  25. } else if (ch === '{' && signatureEnded) {
  26. braceStart = i;
  27. break;
  28. }
  29. }
  30. if (braceStart < 0) {
  31. throw new Error(`missing body for function ${name}`);
  32. }
  33. let depth = 0;
  34. let end = braceStart;
  35. for (; end < source.length; end++) {
  36. const ch = source[end];
  37. if (ch === '{') depth += 1;
  38. if (ch === '}') {
  39. depth -= 1;
  40. if (depth === 0) {
  41. end += 1;
  42. break;
  43. }
  44. }
  45. }
  46. return source.slice(start, end);
  47. }
  48. const helperBundle = [
  49. extractFunction(helperSource, 'normalizeAutoRunSessionId'),
  50. extractFunction(helperSource, 'clearCurrentAutoRunSessionId'),
  51. extractFunction(helperSource, 'throwIfStopped'),
  52. extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
  53. extractFunction(helperSource, 'rejectPendingStep8'),
  54. extractFunction(helperSource, 'throwIfStep8SettledOrStopped'),
  55. extractFunction(helperSource, 'requestStop'),
  56. ].join('\n');
  57. const api = new Function('step8ModuleSource', `
  58. const self = {};
  59. let stopRequested = false;
  60. const STOP_ERROR_MESSAGE = '流程已被用户停止。';
  61. let webNavListener = null;
  62. let webNavCommittedListener = null;
  63. let step8TabUpdatedListener = null;
  64. let step8PendingReject = null;
  65. let autoRunActive = true;
  66. let autoRunCurrentRun = 2;
  67. let autoRunTotalRuns = 3;
  68. let autoRunAttemptRun = 4;
  69. let autoRunSessionId = 99;
  70. const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
  71. const STEP8_CLICK_RETRY_DELAY_MS = 500;
  72. const STEP8_MAX_ROUNDS = 5;
  73. const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
  74. const STEP8_STRATEGIES = [
  75. { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
  76. { mode: 'debugger', label: 'debugger click' },
  77. ];
  78. const added = {
  79. beforeNavigate: 0,
  80. committed: 0,
  81. tabUpdated: 0,
  82. };
  83. const removed = {
  84. beforeNavigate: 0,
  85. committed: 0,
  86. tabUpdated: 0,
  87. };
  88. const sentMessages = [];
  89. let clickCount = 0;
  90. let resolveTabId = null;
  91. const chrome = {
  92. webNavigation: {
  93. onBeforeNavigate: {
  94. addListener() {
  95. added.beforeNavigate += 1;
  96. },
  97. removeListener() {
  98. removed.beforeNavigate += 1;
  99. },
  100. },
  101. onCommitted: {
  102. addListener() {
  103. added.committed += 1;
  104. },
  105. removeListener() {
  106. removed.committed += 1;
  107. },
  108. },
  109. },
  110. tabs: {
  111. onUpdated: {
  112. addListener() {
  113. added.tabUpdated += 1;
  114. },
  115. removeListener() {
  116. removed.tabUpdated += 1;
  117. },
  118. },
  119. async update() {},
  120. },
  121. };
  122. const stepWaiters = new Map();
  123. let resumeWaiter = null;
  124. function cancelPendingCommands() {}
  125. async function addLog() {}
  126. async function broadcastStopToContentScripts() {}
  127. async function markRunningStepsStopped() {}
  128. async function broadcastAutoRunStatus() {}
  129. async function getState() {
  130. return { autoRunning: false };
  131. }
  132. function getPendingAutoRunTimerPlan() {
  133. return null;
  134. }
  135. function isAutoRunScheduledState() {
  136. return false;
  137. }
  138. function getStep8CallbackUrlFromNavigation() { return ''; }
  139. function getStep8CallbackUrlFromTabUpdate() { return ''; }
  140. function getStep8EffectLabel() { return 'no_effect'; }
  141. async function completeStepFromBackground() {}
  142. async function getTabId() {
  143. return await new Promise((resolve) => {
  144. resolveTabId = resolve;
  145. });
  146. }
  147. async function reuseOrCreateTab() {
  148. return 999;
  149. }
  150. async function isTabAlive() {
  151. return true;
  152. }
  153. async function ensureStep8SignupPageReady() {}
  154. async function prepareStep8DebuggerClick() {
  155. sentMessages.push({ source: 'signup-page', type: 'STEP8_FIND_AND_CLICK' });
  156. return { rect: { centerX: 10, centerY: 20 } };
  157. }
  158. async function triggerStep8ContentStrategy() {
  159. sentMessages.push({ source: 'signup-page', type: 'STEP8_TRIGGER_CONTINUE' });
  160. }
  161. async function waitForStep8ClickEffect() {
  162. return { progressed: false, reason: 'no_effect' };
  163. }
  164. async function waitForStep8Ready() {
  165. return { consentReady: true, url: 'https://example.com/consent' };
  166. }
  167. async function reloadStep8ConsentPage() {}
  168. async function sleepWithStop() {}
  169. async function clickWithDebugger() {
  170. clickCount += 1;
  171. }
  172. function setWebNavListener(listener) {
  173. webNavListener = listener;
  174. }
  175. function getWebNavListener() {
  176. return webNavListener;
  177. }
  178. function setWebNavCommittedListener(listener) {
  179. webNavCommittedListener = listener;
  180. }
  181. function getWebNavCommittedListener() {
  182. return webNavCommittedListener;
  183. }
  184. function setStep8TabUpdatedListener(listener) {
  185. step8TabUpdatedListener = listener;
  186. }
  187. function getStep8TabUpdatedListener() {
  188. return step8TabUpdatedListener;
  189. }
  190. function setStep8PendingReject(handler) {
  191. step8PendingReject = handler;
  192. }
  193. ${helperBundle}
  194. ${step8ModuleSource}
  195. const executor = self.MultiPageBackgroundStep9.createStep9Executor({
  196. addLog,
  197. chrome,
  198. cleanupStep8NavigationListeners,
  199. clickWithDebugger,
  200. completeStepFromBackground,
  201. ensureStep8SignupPageReady,
  202. getStep8CallbackUrlFromNavigation,
  203. getStep8CallbackUrlFromTabUpdate,
  204. getStep8EffectLabel,
  205. getTabId,
  206. getWebNavCommittedListener,
  207. getWebNavListener,
  208. getStep8TabUpdatedListener,
  209. isTabAlive,
  210. prepareStep8DebuggerClick,
  211. reloadStep8ConsentPage,
  212. reuseOrCreateTab,
  213. setStep8PendingReject,
  214. setStep8TabUpdatedListener,
  215. setWebNavCommittedListener,
  216. setWebNavListener,
  217. sleepWithStop,
  218. STEP8_CLICK_RETRY_DELAY_MS,
  219. STEP8_MAX_ROUNDS,
  220. STEP8_READY_WAIT_TIMEOUT_MS,
  221. STEP8_STRATEGIES,
  222. throwIfStep8SettledOrStopped,
  223. triggerStep8ContentStrategy,
  224. waitForStep8ClickEffect,
  225. waitForStep8Ready,
  226. });
  227. return {
  228. executeStep9: executor.executeStep9,
  229. requestStop,
  230. resolveTabId(tabId) {
  231. if (!resolveTabId) {
  232. throw new Error('resolveTabId is not ready');
  233. }
  234. resolveTabId(tabId);
  235. },
  236. snapshot() {
  237. return {
  238. stopRequested,
  239. webNavListener,
  240. webNavCommittedListener,
  241. step8TabUpdatedListener,
  242. step8PendingReject,
  243. added,
  244. removed,
  245. sentMessages,
  246. clickCount,
  247. autoRunActive,
  248. autoRunSessionId,
  249. };
  250. },
  251. };
  252. `)(step8ModuleSource);
  253. (async () => {
  254. const step8Promise = api.executeStep9({ oauthUrl: 'https://example.com/oauth' });
  255. const settledStep8Promise = step8Promise.catch((err) => err);
  256. await new Promise((resolve) => setImmediate(resolve));
  257. await api.requestStop();
  258. await new Promise((resolve) => setImmediate(resolve));
  259. api.resolveTabId(123);
  260. await new Promise((resolve) => setImmediate(resolve));
  261. await new Promise((resolve) => setImmediate(resolve));
  262. const error = await settledStep8Promise;
  263. const state = api.snapshot();
  264. assert.strictEqual(error?.message, '流程已被用户停止。', 'Stop 后 Step 8 promise 应被拒绝为停止错误');
  265. assert.deepStrictEqual(
  266. state.added,
  267. { beforeNavigate: 0, committed: 0, tabUpdated: 0 },
  268. 'Stop 先发生时,不应再注册 Step 8 监听'
  269. );
  270. assert.strictEqual(state.sentMessages.length, 0, 'Stop 后不应再发送 Step 8 执行动作');
  271. assert.strictEqual(state.clickCount, 0, 'Stop 后不应再触发 debugger 点击');
  272. assert.strictEqual(state.webNavListener, null, 'Stop 后 onBeforeNavigate 引用应为空');
  273. assert.strictEqual(state.webNavCommittedListener, null, 'Stop 后 onCommitted 引用应为空');
  274. assert.strictEqual(state.step8TabUpdatedListener, null, 'Stop 后 tabs.onUpdated 引用应为空');
  275. assert.strictEqual(state.step8PendingReject, null, 'Stop 后不应保留 Step 8 挂起 reject');
  276. assert.strictEqual(state.autoRunSessionId, 0, 'Stop 后自动运行 session 应失效');
  277. console.log('step8 stop cleanup tests passed');
  278. })().catch((error) => {
  279. console.error(error);
  280. process.exit(1);
  281. });