edit.test.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import { describe, expect, test } from "bun:test"
  2. import { replace } from "../../src/tool/edit"
  3. interface TestCase {
  4. content: string
  5. find: string
  6. replace: string
  7. all?: boolean
  8. fail?: boolean
  9. }
  10. const testCases: TestCase[] = [
  11. // SimpleReplacer cases
  12. {
  13. content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
  14. find: 'console.log("world");',
  15. replace: 'console.log("universe");',
  16. },
  17. {
  18. content: [
  19. "if (condition) {",
  20. " doSomething();",
  21. " doSomethingElse();",
  22. "}",
  23. ].join("\n"),
  24. find: [" doSomething();", " doSomethingElse();"].join("\n"),
  25. replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
  26. },
  27. // LineTrimmedReplacer cases
  28. {
  29. content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
  30. find: 'console.log("hello");',
  31. replace: 'console.log("goodbye");',
  32. },
  33. {
  34. content: ["const x = 5; ", "const y = 10;"].join("\n"),
  35. find: "const x = 5;",
  36. replace: "const x = 15;",
  37. },
  38. {
  39. content: [" if (true) {", " return false;", " }"].join("\n"),
  40. find: ["if (true) {", "return false;", "}"].join("\n"),
  41. replace: ["if (false) {", "return true;", "}"].join("\n"),
  42. },
  43. // BlockAnchorReplacer cases
  44. {
  45. content: [
  46. "function calculate(a, b) {",
  47. " const temp = a + b;",
  48. " const result = temp * 2;",
  49. " return result;",
  50. "}",
  51. ].join("\n"),
  52. find: [
  53. "function calculate(a, b) {",
  54. " // different middle content",
  55. " return result;",
  56. "}",
  57. ].join("\n"),
  58. replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
  59. "\n",
  60. ),
  61. },
  62. {
  63. content: [
  64. "class MyClass {",
  65. " constructor() {",
  66. " this.value = 0;",
  67. " }",
  68. " ",
  69. " getValue() {",
  70. " return this.value;",
  71. " }",
  72. "}",
  73. ].join("\n"),
  74. find: ["class MyClass {", " // different implementation", "}"].join("\n"),
  75. replace: [
  76. "class MyClass {",
  77. " constructor() {",
  78. " this.value = 42;",
  79. " }",
  80. "}",
  81. ].join("\n"),
  82. },
  83. // WhitespaceNormalizedReplacer cases
  84. {
  85. content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
  86. find: ' console.log("hello");',
  87. replace: ' console.log("world");',
  88. },
  89. {
  90. content: "const x = 5;",
  91. find: "const x = 5;",
  92. replace: "const x = 10;",
  93. },
  94. {
  95. content: "if\t( condition\t) {",
  96. find: "if ( condition ) {",
  97. replace: "if (newCondition) {",
  98. },
  99. // IndentationFlexibleReplacer cases
  100. {
  101. content: [
  102. " function nested() {",
  103. ' console.log("deeply nested");',
  104. " return true;",
  105. " }",
  106. ].join("\n"),
  107. find: [
  108. "function nested() {",
  109. ' console.log("deeply nested");',
  110. " return true;",
  111. "}",
  112. ].join("\n"),
  113. replace: [
  114. "function nested() {",
  115. ' console.log("updated");',
  116. " return false;",
  117. "}",
  118. ].join("\n"),
  119. },
  120. {
  121. content: [
  122. " if (true) {",
  123. ' console.log("level 1");',
  124. ' console.log("level 2");',
  125. " }",
  126. ].join("\n"),
  127. find: [
  128. "if (true) {",
  129. 'console.log("level 1");',
  130. ' console.log("level 2");',
  131. "}",
  132. ].join("\n"),
  133. replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
  134. },
  135. // replaceAll option cases
  136. {
  137. content: [
  138. 'console.log("test");',
  139. 'console.log("test");',
  140. 'console.log("test");',
  141. ].join("\n"),
  142. find: 'console.log("test");',
  143. replace: 'console.log("updated");',
  144. all: true,
  145. },
  146. {
  147. content: ['console.log("test");', 'console.log("test");'].join("\n"),
  148. find: 'console.log("test");',
  149. replace: 'console.log("updated");',
  150. all: false,
  151. },
  152. // Error cases
  153. {
  154. content: 'console.log("hello");',
  155. find: "nonexistent string",
  156. replace: "updated",
  157. fail: true,
  158. },
  159. {
  160. content: ["test", "test", "different content", "test"].join("\n"),
  161. find: "test",
  162. replace: "updated",
  163. all: false,
  164. fail: true,
  165. },
  166. // Edge cases
  167. {
  168. content: "",
  169. find: "",
  170. replace: "new content",
  171. },
  172. {
  173. content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
  174. find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
  175. replace: "/\\\\w+/g",
  176. },
  177. {
  178. content: 'const message = "Hello 世界! 🌍";',
  179. find: "Hello 世界! 🌍",
  180. replace: "Hello World! 🌎",
  181. },
  182. // EscapeNormalizedReplacer cases
  183. {
  184. content: 'console.log("Hello\nWorld");',
  185. find: 'console.log("Hello\\nWorld");',
  186. replace: 'console.log("Hello\nUniverse");',
  187. },
  188. {
  189. content: "const str = 'It's working';",
  190. find: "const str = 'It\\'s working';",
  191. replace: "const str = 'It's fixed';",
  192. },
  193. {
  194. content: "const template = `Hello ${name}`;",
  195. find: "const template = `Hello \\${name}`;",
  196. replace: "const template = `Hi ${name}`;",
  197. },
  198. {
  199. content: "const path = 'C:\\Users\\test';",
  200. find: "const path = 'C:\\\\Users\\\\test';",
  201. replace: "const path = 'C:\\Users\\admin';",
  202. },
  203. // MultiOccurrenceReplacer cases (with replaceAll)
  204. {
  205. content: ["debug('start');", "debug('middle');", "debug('end');"].join(
  206. "\n",
  207. ),
  208. find: "debug",
  209. replace: "log",
  210. all: true,
  211. },
  212. {
  213. content: "const x = 1; const y = 1; const z = 1;",
  214. find: "1",
  215. replace: "2",
  216. all: true,
  217. },
  218. // TrimmedBoundaryReplacer cases
  219. {
  220. content: [" function test() {", " return true;", " }"].join("\n"),
  221. find: ["function test() {", " return true;", "}"].join("\n"),
  222. replace: ["function test() {", " return false;", "}"].join("\n"),
  223. },
  224. {
  225. content: "\n const value = 42; \n",
  226. find: "const value = 42;",
  227. replace: "const value = 24;",
  228. },
  229. {
  230. content: ["", " if (condition) {", " doSomething();", " }", ""].join(
  231. "\n",
  232. ),
  233. find: ["if (condition) {", " doSomething();", "}"].join("\n"),
  234. replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
  235. },
  236. // ContextAwareReplacer cases
  237. {
  238. content: [
  239. "function calculate(a, b) {",
  240. " const temp = a + b;",
  241. " const result = temp * 2;",
  242. " return result;",
  243. "}",
  244. ].join("\n"),
  245. find: [
  246. "function calculate(a, b) {",
  247. " // some different content here",
  248. " // more different content",
  249. " return result;",
  250. "}",
  251. ].join("\n"),
  252. replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
  253. "\n",
  254. ),
  255. },
  256. {
  257. content: [
  258. "class TestClass {",
  259. " constructor() {",
  260. " this.value = 0;",
  261. " }",
  262. " ",
  263. " method() {",
  264. " return this.value;",
  265. " }",
  266. "}",
  267. ].join("\n"),
  268. find: [
  269. "class TestClass {",
  270. " // different implementation",
  271. " // with multiple lines",
  272. "}",
  273. ].join("\n"),
  274. replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
  275. "\n",
  276. ),
  277. },
  278. // Combined edge cases for new replacers
  279. {
  280. content: '\tconsole.log("test");\t',
  281. find: 'console.log("test");',
  282. replace: 'console.log("updated");',
  283. },
  284. {
  285. content: [" ", "function test() {", " return 'value';", "}", " "].join(
  286. "\n",
  287. ),
  288. find: ["function test() {", "return 'value';", "}"].join("\n"),
  289. replace: ["function test() {", "return 'new value';", "}"].join("\n"),
  290. },
  291. // Test for same oldString and newString (should fail)
  292. {
  293. content: 'console.log("test");',
  294. find: 'console.log("test");',
  295. replace: 'console.log("test");',
  296. fail: true,
  297. },
  298. // Additional tests for fixes made
  299. // WhitespaceNormalizedReplacer - test regex special characters that could cause errors
  300. {
  301. content: 'const pattern = "test[123]";',
  302. find: "test[123]",
  303. replace: "test[456]",
  304. },
  305. {
  306. content: 'const regex = "^start.*end$";',
  307. find: "^start.*end$",
  308. replace: "^begin.*finish$",
  309. },
  310. // EscapeNormalizedReplacer - test single backslash vs double backslash
  311. {
  312. content: 'const path = "C:\\Users";',
  313. find: 'const path = "C:\\Users";',
  314. replace: 'const path = "D:\\Users";',
  315. },
  316. {
  317. content: 'console.log("Line1\\nLine2");',
  318. find: 'console.log("Line1\\nLine2");',
  319. replace: 'console.log("First\\nSecond");',
  320. },
  321. // BlockAnchorReplacer - test edge case with exact newline boundaries
  322. {
  323. content: ["function test() {", " return true;", "}"].join("\n"),
  324. find: ["function test() {", " // middle", "}"].join("\n"),
  325. replace: ["function test() {", " return false;", "}"].join("\n"),
  326. },
  327. // ContextAwareReplacer - test with trailing newline in find string
  328. {
  329. content: [
  330. "class Test {",
  331. " method1() {",
  332. " return 1;",
  333. " }",
  334. "}",
  335. ].join("\n"),
  336. find: [
  337. "class Test {",
  338. " // different content",
  339. "}",
  340. "", // trailing empty line
  341. ].join("\n"),
  342. replace: ["class Test {", " method2() { return 2; }", "}"].join("\n"),
  343. },
  344. // Test validation for empty strings with same oldString and newString
  345. {
  346. content: "",
  347. find: "",
  348. replace: "",
  349. fail: true,
  350. },
  351. // Test multiple occurrences with replaceAll=false (should fail)
  352. {
  353. content: ["const a = 1;", "const b = 1;", "const c = 1;"].join("\n"),
  354. find: "= 1",
  355. replace: "= 2",
  356. all: false,
  357. fail: true,
  358. },
  359. // Test whitespace normalization with multiple spaces and tabs mixed
  360. {
  361. content: "if\t \t( \tcondition\t )\t{",
  362. find: "if ( condition ) {",
  363. replace: "if (newCondition) {",
  364. },
  365. // Test escape sequences in template literals
  366. {
  367. content: "const msg = `Hello\\tWorld`;",
  368. find: "const msg = `Hello\\tWorld`;",
  369. replace: "const msg = `Hi\\tWorld`;",
  370. },
  371. ]
  372. describe("EditTool Replacers", () => {
  373. test.each(testCases)("case %#", (testCase) => {
  374. if (testCase.fail) {
  375. expect(() => {
  376. replace(testCase.content, testCase.find, testCase.replace, testCase.all)
  377. }).toThrow()
  378. } else {
  379. const result = replace(
  380. testCase.content,
  381. testCase.find,
  382. testCase.replace,
  383. testCase.all,
  384. )
  385. expect(result).toContain(testCase.replace)
  386. }
  387. })
  388. })