io.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. /**
  2. * Tests for input and output of various types to ensure the editors work as expected
  3. * and retain data integrity, especially when it comes to special characters.
  4. *
  5. * @author n1474335 [n1474335@gmail.com]
  6. * @copyright Crown Copyright 2023
  7. * @license Apache-2.0
  8. */
  9. const SPECIAL_CHARS = [
  10. "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000a\u000b\u000c\u000d\u000e\u000f",
  11. "\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f",
  12. "\u007f",
  13. "\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008a\u008b\u008c\u008d\u008e\u008f",
  14. "\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009a\u009b\u009c\u009d\u009e\u009f",
  15. "\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9\ufffa\ufffb\ufffc"
  16. ].join("");
  17. const ALL_BYTES = [
  18. "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
  19. "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
  20. "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f",
  21. "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f",
  22. "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f",
  23. "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f",
  24. "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f",
  25. "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f",
  26. "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f",
  27. "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f",
  28. "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf",
  29. "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf",
  30. "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf",
  31. "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf",
  32. "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef",
  33. "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff",
  34. ].join("");
  35. const PUA_CHARS = "\ue000\ue001\uf8fe\uf8ff";
  36. const MULTI_LINE_STRING =`"You know," said Arthur, "it's at times like this, when I'm trapped in a Vogon airlock with a man from Betelgeuse, and about to die of asphyxiation in deep space that I really wish I'd listened to what my mother told me when I was young."
  37. "Why, what did she tell you?"
  38. "I don't know, I didn't listen."`;
  39. const SELECTABLE_STRING = `ONE
  40. two
  41. ONE
  42. three
  43. ONE
  44. four
  45. ONE`;
  46. // Descriptions for named control characters
  47. const CONTROL_CHAR_NAMES = {
  48. 0: "null",
  49. 7: "bell",
  50. 8: "backspace",
  51. 10: "line feed",
  52. 11: "vertical tab",
  53. 13: "carriage return",
  54. 27: "escape",
  55. 8203: "zero width space",
  56. 8204: "zero width non-joiner",
  57. 8205: "zero width joiner",
  58. 8206: "left-to-right mark",
  59. 8207: "right-to-left mark",
  60. 8232: "line separator",
  61. 8237: "left-to-right override",
  62. 8238: "right-to-left override",
  63. 8294: "left-to-right isolate",
  64. 8295: "right-to-left isolate",
  65. 8297: "pop directional isolate",
  66. 8233: "paragraph separator",
  67. 65279: "zero width no-break space",
  68. 65532: "object replacement"
  69. };
  70. module.exports = {
  71. before: browser => {
  72. browser
  73. .resizeWindow(1280, 800)
  74. .url(browser.launchUrl)
  75. .useCss()
  76. .waitForElementNotPresent("#preloader", 10000)
  77. .click("#auto-bake-label");
  78. },
  79. "CodeMirror has loaded correctly": browser => {
  80. /* Editor has initialised */
  81. browser
  82. .useCss()
  83. // Input
  84. .waitForElementVisible("#input-text")
  85. .waitForElementVisible("#input-text .cm-editor")
  86. .waitForElementVisible("#input-text .cm-editor .cm-scroller")
  87. .waitForElementVisible("#input-text .cm-editor .cm-scroller .cm-content")
  88. .waitForElementVisible("#input-text .cm-editor .cm-scroller .cm-content .cm-line")
  89. // Output
  90. .waitForElementVisible("#output-text")
  91. .waitForElementVisible("#output-text .cm-editor")
  92. .waitForElementVisible("#output-text .cm-editor .cm-scroller")
  93. .waitForElementVisible("#output-text .cm-editor .cm-scroller .cm-content")
  94. .waitForElementVisible("#output-text .cm-editor .cm-scroller .cm-content .cm-line");
  95. /* Status bar is showing and has correct values */
  96. browser // Input
  97. .waitForElementVisible("#input-text .cm-status-bar")
  98. .waitForElementVisible("#input-text .cm-status-bar .stats-length-value")
  99. .expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("0");
  100. browser.waitForElementVisible("#input-text .cm-status-bar .stats-lines-value")
  101. .expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1");
  102. browser.waitForElementVisible("#input-text .cm-status-bar .chr-enc-value")
  103. .expect.element("#input-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes");
  104. browser.waitForElementVisible("#input-text .cm-status-bar .eol-value")
  105. .expect.element("#input-text .cm-status-bar .eol-value").text.to.equal("LF");
  106. browser // Output
  107. .waitForElementVisible("#output-text .cm-status-bar")
  108. .waitForElementVisible("#output-text .cm-status-bar .stats-length-value")
  109. .expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("0");
  110. browser.waitForElementVisible("#output-text .cm-status-bar .stats-lines-value")
  111. .expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1");
  112. browser.waitForElementVisible("#output-text .cm-status-bar .baking-time-info")
  113. .expect.element("#output-text .cm-status-bar .baking-time-info").text.to.contain("ms");
  114. browser.waitForElementVisible("#output-text .cm-status-bar .chr-enc-value")
  115. .expect.element("#output-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes");
  116. browser.waitForElementVisible("#output-text .cm-status-bar .eol-value")
  117. .expect.element("#output-text .cm-status-bar .eol-value").text.to.equal("LF");
  118. },
  119. "Adding content": browser => {
  120. /* Status bar updates correctly */
  121. setInput(browser, MULTI_LINE_STRING);
  122. browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301");
  123. browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("3");
  124. browser.expect.element("#input-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes");
  125. browser.expect.element("#input-text .cm-status-bar .eol-value").text.to.equal("LF");
  126. browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("0");
  127. browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1");
  128. browser.expect.element("#output-text .cm-status-bar .baking-time-info").text.to.contain("ms");
  129. browser.expect.element("#output-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes");
  130. browser.expect.element("#output-text .cm-status-bar .eol-value").text.to.equal("LF");
  131. /* Output updates correctly */
  132. bake(browser);
  133. browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301");
  134. browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("3");
  135. browser.expect.element("#output-text .cm-status-bar .baking-time-info").text.to.contain("ms");
  136. browser.expect.element("#output-text .cm-status-bar .chr-enc-value").text.to.equal("Raw Bytes");
  137. browser.expect.element("#output-text .cm-status-bar .eol-value").text.to.equal("LF");
  138. },
  139. "Special content": browser => {
  140. /* Special characters are rendered correctly */
  141. setInput(browser, SPECIAL_CHARS, false);
  142. // First line
  143. for (let i = 0x0; i <= 0x8; i++) {
  144. browser.expect.element(`#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`)
  145. .to.have.property("title").equals(`Control character ${CONTROL_CHAR_NAMES[i] || "0x" + i.toString(16)}`);
  146. browser.expect.element(`#input-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`)
  147. .text.to.equal(String.fromCharCode(0x2400 + i));
  148. }
  149. // Tab \u0009
  150. browser.expect.element(`#input-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/\u0009$/);
  151. // Line feed \u000a
  152. browser.expect.element(`#input-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/^.{10}$/);
  153. browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2");
  154. // Second line
  155. for (let i = 0x0b; i < SPECIAL_CHARS.length; i++) {
  156. const index = SPECIAL_CHARS.charCodeAt(i);
  157. const name = CONTROL_CHAR_NAMES[index] || "0x" + index.toString(16);
  158. const value = index >= 32 ? "\u2022" : String.fromCharCode(0x2400 + index);
  159. browser.expect.element(`#input-text .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`)
  160. .to.have.property("title").equals(`Control character ${name}`);
  161. browser.expect.element(`#input-text .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`)
  162. .text.to.equal(value);
  163. }
  164. /* Output renders correctly */
  165. setChrEnc(browser, "output", "UTF-8");
  166. bake(browser);
  167. // First line
  168. for (let i = 0x0; i <= 0x8; i++) {
  169. browser.expect.element(`#output-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`)
  170. .to.have.property("title").equals(`Control character ${CONTROL_CHAR_NAMES[i] || "0x" + i.toString(16)}`);
  171. browser.expect.element(`#output-text .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(${i+1})`)
  172. .text.to.equal(String.fromCharCode(0x2400 + i));
  173. }
  174. // Tab \u0009
  175. browser.expect.element(`#output-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/\u0009$/);
  176. // Line feed \u000a
  177. browser.expect.element(`#output-text .cm-line:nth-of-type(1)`).to.have.property("textContent").match(/^.{10}$/);
  178. browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2");
  179. // Second line
  180. for (let i = 0x0b; i < SPECIAL_CHARS.length; i++) {
  181. const index = SPECIAL_CHARS.charCodeAt(i);
  182. const name = CONTROL_CHAR_NAMES[index] || "0x" + index.toString(16);
  183. const value = index >= 32 ? "\u2022" : String.fromCharCode(0x2400 + index);
  184. browser.expect.element(`#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`)
  185. .to.have.property("title").equals(`Control character ${name}`);
  186. browser.expect.element(`#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar:nth-of-type(${i-10})`)
  187. .text.to.equal(value);
  188. }
  189. /* Bytes are rendered correctly */
  190. setInput(browser, ALL_BYTES, false);
  191. // Expect length to be 255, since one character is creating a newline
  192. browser.expect.element(`#input-text .cm-content`).to.have.property("textContent").match(/^.{255}$/);
  193. browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("256");
  194. browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2");
  195. /* PUA \ue000-\uf8ff */
  196. setInput(browser, PUA_CHARS, false);
  197. setChrEnc(browser, "output", "UTF-8");
  198. bake(browser);
  199. // Confirm input and output as expected
  200. /* In order to render whitespace characters as control character pictures in the output, even
  201. when they are the designated line separator, CyberChef sometimes chooses to represent them
  202. internally using the Unicode Private Use Area (https://en.wikipedia.org/wiki/Private_Use_Areas).
  203. See `Utils.escapeWhitespace()` for an example of this.
  204. Therefore, PUA characters should be rendered normally in the Input but as control character
  205. pictures in the output.
  206. */
  207. browser.expect.element(`#input-text .cm-content`).to.have.property("textContent").match(/^\ue000\ue001\uf8fe\uf8ff$/);
  208. browser.expect.element(`#output-text .cm-content`).to.have.property("textContent").match(/^\u2400\u2401\u3cfe\u3cff$/);
  209. /* Can be copied */
  210. setInput(browser, SPECIAL_CHARS, false);
  211. setChrEnc(browser, "output", "UTF-8");
  212. bake(browser);
  213. // Manual copy
  214. browser
  215. .doubleClick("#output-text .cm-content .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(1)")
  216. .waitForElementVisible("#output-text .cm-selectionBackground");
  217. copy(browser);
  218. paste(browser, "#search"); // Paste into search box as this won't mess with the values
  219. // Ensure that the values are as expected
  220. browser.expect.element("#search").to.have.value.that.equals("\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008");
  221. browser.clearValue("#search");
  222. // Raw copy
  223. browser
  224. .click("#copy-output")
  225. .pause(100);
  226. paste(browser, "#search"); // Paste into search box as this won't mess with the values
  227. // Ensure that the values are as expected
  228. browser.expect.element("#search").to.have.value.that.matches(/^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009/);
  229. browser.clearValue("#search");
  230. },
  231. "HTML output": browser => {
  232. /* Displays correctly */
  233. loadRecipe(browser, "Entropy", ALL_BYTES);
  234. bake(browser);
  235. browser
  236. .waitForElementVisible("#output-html")
  237. .waitForElementVisible("#output-html #chart-area");
  238. /* Status bar widgets are disabled */
  239. browser.expect.element("#output-text .cm-status-bar .disabled .stats-length-value").to.be.visible;
  240. browser.expect.element("#output-text .cm-status-bar .disabled .stats-lines-value").to.be.visible;
  241. browser.expect.element("#output-text .cm-status-bar .disabled .chr-enc-value").to.be.visible;
  242. browser.expect.element("#output-text .cm-status-bar .disabled .eol-value").to.be.visible;
  243. /* Displays special chars correctly */
  244. loadRecipe(browser, "To Table", ",\u0000\u0001\u0002\u0003\u0004", [",", "\\r\\n", false, "HTML"]);
  245. bake(browser);
  246. for (let i = 0x0; i <= 0x4; i++) {
  247. browser.expect.element(`#output-html .cm-specialChar:nth-of-type(${i+1})`)
  248. .to.have.property("title").equals(`Control character ${CONTROL_CHAR_NAMES[i] || "0x" + i.toString(16)}`);
  249. browser.expect.element(`#output-html .cm-specialChar:nth-of-type(${i+1})`)
  250. .text.to.equal(String.fromCharCode(0x2400 + i));
  251. }
  252. /* Can be copied */
  253. // Raw copy
  254. browser
  255. .click("#copy-output")
  256. .pause(100);
  257. paste(browser, "#search"); // Paste into search box as this won't mess with the values
  258. // Ensure that the values are as expected
  259. browser.expect.element("#search").to.have.value.that.matches(/\u0000\u0001\u0002\u0003\u0004/);
  260. browser.clearValue("#search");
  261. },
  262. "Highlighting": browser => {
  263. setInput(browser, SELECTABLE_STRING);
  264. bake(browser);
  265. /* Selecting input text also selects other instances in input and output */
  266. browser // Input
  267. .click("#auto-bake-label")
  268. .doubleClick("#input-text .cm-content .cm-line:nth-of-type(1)")
  269. .waitForElementVisible("#input-text .cm-selectionLayer .cm-selectionBackground")
  270. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch")
  271. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch")
  272. .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch")
  273. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch")
  274. .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch")
  275. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch")
  276. .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch");
  277. browser // Output
  278. .waitForElementVisible("#output-text .cm-selectionLayer .cm-selectionBackground")
  279. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch")
  280. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch")
  281. .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch")
  282. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch")
  283. .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch")
  284. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch")
  285. .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch");
  286. /* Selecting output text highlights in input */
  287. browser // Output
  288. .click("#output-text")
  289. .waitForElementNotPresent("#input-text .cm-selectionLayer .cm-selectionBackground")
  290. .waitForElementNotPresent("#output-text .cm-selectionLayer .cm-selectionBackground")
  291. .waitForElementNotPresent("#input-text .cm-content .cm-line .cm-selectionMatch")
  292. .waitForElementNotPresent("#output-text .cm-content .cm-line .cm-selectionMatch")
  293. .doubleClick("#output-text .cm-content .cm-line:nth-of-type(7)")
  294. .waitForElementVisible("#output-text .cm-selectionLayer .cm-selectionBackground")
  295. .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch")
  296. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch")
  297. .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch")
  298. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch")
  299. .waitForElementVisible("#output-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch")
  300. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch")
  301. .waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch");
  302. browser // Input
  303. .waitForElementVisible("#input-text .cm-selectionLayer .cm-selectionBackground")
  304. .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(1) .cm-selectionMatch")
  305. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2) .cm-selectionMatch")
  306. .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(3) .cm-selectionMatch")
  307. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(4) .cm-selectionMatch")
  308. .waitForElementVisible("#input-text .cm-content .cm-line:nth-of-type(5) .cm-selectionMatch")
  309. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(6) .cm-selectionMatch")
  310. .waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(7) .cm-selectionMatch");
  311. // Turn autobake off again
  312. browser.click("#auto-bake-label");
  313. },
  314. "Character encoding": browser => {
  315. const CHINESE_CHARS = "不要恐慌。";
  316. /* Dropup works */
  317. /* Selecting changes output correctly */
  318. setInput(browser, CHINESE_CHARS, false);
  319. setChrEnc(browser, "input", "UTF-8");
  320. bake(browser);
  321. expectOutput(browser, "\u00E4\u00B8\u008D\u00E8\u00A6\u0081\u00E6\u0081\u0090\u00E6\u0085\u008C\u00E3\u0080\u0082");
  322. /* Changing output to match input works as expected */
  323. setChrEnc(browser, "output", "UTF-8");
  324. bake(browser);
  325. expectOutput(browser, CHINESE_CHARS);
  326. /* Encodings appear in the URL */
  327. browser.assert.urlContains("ienc=65001");
  328. browser.assert.urlContains("oenc=65001");
  329. /* Preserved when changing tabs */
  330. browser
  331. .click("#btn-new-tab")
  332. .waitForElementVisible("#input-tabs li:nth-of-type(2).active-input-tab");
  333. browser.expect.element("#input-text .chr-enc-value").text.that.equals("Raw Bytes");
  334. browser.expect.element("#output-text .chr-enc-value").text.that.equals("Raw Bytes");
  335. setChrEnc(browser, "input", "UTF-7");
  336. setChrEnc(browser, "output", "UTF-7");
  337. browser
  338. .click("#input-tabs li:nth-of-type(1)")
  339. .waitForElementVisible("#input-tabs li:nth-of-type(1).active-input-tab");
  340. browser.expect.element("#input-text .chr-enc-value").text.that.equals("UTF-8");
  341. browser.expect.element("#output-text .chr-enc-value").text.that.equals("UTF-8");
  342. /* Try various encodings */
  343. // These are not meant to be realistic encodings for this data
  344. setInput(browser, CHINESE_CHARS, false);
  345. setChrEnc(browser, "input", "UTF-8");
  346. setChrEnc(browser, "output", "UTF-16LE");
  347. bake(browser);
  348. expectOutput(browser, "\uB8E4\uE88D\u81A6\u81E6\uE690\u8C85\u80E3");
  349. setChrEnc(browser, "output", "Simplified Chinese GBK");
  350. bake(browser);
  351. expectOutput(browser, "\u6D93\u5D88\uFDFF\u93AD\u612D\u53A1\u9286\u0000");
  352. setChrEnc(browser, "input", "UTF-7");
  353. bake(browser);
  354. expectOutput(browser, "+Tg0-+iYE-+YFA-+YUw-");
  355. setChrEnc(browser, "input", "Traditional Chinese Big5");
  356. bake(browser);
  357. expectOutput(browser, "\u3043\u74B6\uFDFF\u7A3A\uFDFF");
  358. setChrEnc(browser, "output", "Windows-1251 Cyrillic");
  359. bake(browser);
  360. expectOutput(browser, "\u00A4\u0408\u00ADn\u00AE\u0408\u00B7W\u040EC");
  361. },
  362. "Line endings": browser => {
  363. /* Dropup works */
  364. /* Selecting changes view in input */
  365. /* Adding new line ending changes output correctly */
  366. /* Other EOL characters are displayed correctly when not being used to end a line */
  367. /* Changing in output has the correct effect */
  368. /* Line endings appear in the URL */
  369. /* Preserved when changing tabs */
  370. },
  371. "File inputs": browser => {
  372. /* By button */
  373. /* By drag and drop */
  374. /* Side panel displays correct info */
  375. /* Side panel can be hidden */
  376. },
  377. "Folder inputs": browser => {
  378. /* By button */
  379. /* By drag and drop */
  380. },
  381. "Loading from URL": browser => {
  382. /* Complex deep link populates the input correctly (encoding, eol, input) */
  383. },
  384. "Replace input with output": browser => {
  385. /* Input is correctly populated */
  386. /* Special characters, encodings and line endings all as expected */
  387. },
  388. after: browser => {
  389. browser.end();
  390. }
  391. };
  392. /** @function
  393. * Clears the recipe and input
  394. *
  395. * @param {Browser} browser - Nightwatch client
  396. */
  397. function clear(browser) {
  398. browser
  399. .useCss()
  400. .click("#clr-recipe")
  401. .click("#clr-io")
  402. .waitForElementNotPresent("#rec-list li.operation")
  403. .expect.element("#input-text .cm-content").text.that.equals("");
  404. }
  405. /** @function
  406. * Sets the input to the desired string
  407. *
  408. * @param {Browser} browser - Nightwatch client
  409. * @param {string} input - The text to populate the input with
  410. * @param {boolean} [type=true] - Whether to type the characters in by using sendKeys,
  411. * or to set the value of the editor directly (useful for special characters)
  412. */
  413. function setInput(browser, input, type=true) {
  414. clear(browser);
  415. if (type) {
  416. browser
  417. .useCss()
  418. .sendKeys("#input-text .cm-content", input)
  419. .pause(100);
  420. } else {
  421. browser.execute(text => {
  422. window.app.setInput(text);
  423. }, [input]);
  424. }
  425. }
  426. /** @function
  427. * Triggers a bake
  428. *
  429. * @param {Browser} browser - Nightwatch client
  430. */
  431. function bake(browser) {
  432. browser
  433. .click("#bake")
  434. .pause(100)
  435. .waitForElementPresent("#stale-indicator.hidden", 5000);
  436. }
  437. /** @function
  438. * Sets the character encoding in the input or output
  439. *
  440. * @param {Browser} browser - Nightwatch client
  441. * @param {string} io - Either "input" or "output"
  442. * @param {string} enc - The encoding to be set
  443. */
  444. function setChrEnc(browser, io, enc) {
  445. io = `#${io}-text`;
  446. browser
  447. .useCss()
  448. .click(io + " .chr-enc-value")
  449. .waitForElementVisible(io + " .cm-status-bar-select-scroll")
  450. .click("link text", enc)
  451. .waitForElementNotVisible(io + " .cm-status-bar-select-scroll")
  452. .expect.element(io + " .chr-enc-value").text.that.equals(enc);
  453. }
  454. /** @function
  455. * Copies whatever is currently selected
  456. *
  457. * @param {Browser} browser - Nightwatch client
  458. */
  459. function copy(browser) {
  460. browser.perform(function() {
  461. const actions = this.actions({async: true});
  462. // Ctrl + Ins used as this works on Windows, Linux and Mac
  463. return actions
  464. .keyDown(browser.Keys.CONTROL)
  465. .keyDown(browser.Keys.INSERT)
  466. .keyUp(browser.Keys.INSERT)
  467. .keyUp(browser.Keys.CONTROL);
  468. });
  469. }
  470. /** @function
  471. * Pastes into the target element
  472. *
  473. * @param {Browser} browser - Nightwatch client
  474. * @param {string} el - Target element selector
  475. */
  476. function paste(browser, el) {
  477. browser
  478. .click(el)
  479. .perform(function() {
  480. const actions = this.actions({async: true});
  481. // Shift + Ins used as this works on Windows, Linux and Mac
  482. return actions
  483. .keyDown(browser.Keys.SHIFT)
  484. .keyDown(browser.Keys.INSERT)
  485. .keyUp(browser.Keys.INSERT)
  486. .keyUp(browser.Keys.SHIFT);
  487. })
  488. .pause(100);
  489. }
  490. /** @function
  491. * Loads a recipe and input
  492. *
  493. * @param {Browser} browser - Nightwatch client
  494. * @param {string|Array<string>} opName - name of operation to be loaded, array for multiple ops
  495. * @param {string} input - input text for test
  496. * @param {Array<string>|Array<Array<string>>} args - arguments, nested if multiple ops
  497. */
  498. function loadRecipe(browser, opName, input, args) {
  499. let recipeConfig;
  500. if (typeof(opName) === "string") {
  501. recipeConfig = JSON.stringify([{
  502. "op": opName,
  503. "args": args
  504. }]);
  505. } else if (opName instanceof Array) {
  506. recipeConfig = JSON.stringify(
  507. opName.map((op, i) => {
  508. return {
  509. op: op,
  510. args: args.length ? args[i] : []
  511. };
  512. })
  513. );
  514. } else {
  515. throw new Error("Invalid operation type. Must be string or array of strings. Received: " + typeof(opName));
  516. }
  517. clear(browser);
  518. setInput(browser, input, false);
  519. browser
  520. .urlHash("recipe=" + recipeConfig)
  521. .waitForElementPresent("#rec-list li.operation");
  522. }
  523. /** @function
  524. * Tests whether the output matches a given value
  525. *
  526. * @param {Browser} browser - Nightwatch client
  527. * @param {string} expected - The expected output value
  528. */
  529. function expectOutput(browser, expected) {
  530. browser.execute(expected => {
  531. return expected === window.app.manager.output.outputEditorView.state.doc.toString();
  532. }, [expected]);
  533. }