spec-helper.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. /*
  2. * decaffeinate suggestions:
  3. * DS101: Remove unnecessary use of Array.from
  4. * DS102: Remove unnecessary code created because of implicit returns
  5. * DS201: Simplify complex destructure assignments
  6. * DS205: Consider reworking code to avoid use of IIFEs
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
  9. */
  10. let specDirectory, specPackageName, specPackagePath, specProjectPath;
  11. require('jasmine-json');
  12. require('../src/window');
  13. require('../vendor/jasmine-jquery');
  14. const path = require('path');
  15. const _ = require('underscore-plus');
  16. const fs = require('fs-plus');
  17. const Grim = require('grim');
  18. const pathwatcher = require('pathwatcher');
  19. const FindParentDir = require('find-parent-dir');
  20. const {CompositeDisposable} = require('event-kit');
  21. const TextEditor = require('../src/text-editor');
  22. const TextEditorElement = require('../src/text-editor-element');
  23. const TextMateLanguageMode = require('../src/text-mate-language-mode');
  24. const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode');
  25. const {clipboard} = require('electron');
  26. const {mockDebounce} = require("./spec-helper-functions.js");
  27. const jasmineStyle = document.createElement('style');
  28. jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine'));
  29. document.head.appendChild(jasmineStyle);
  30. const fixturePackagesPath = path.resolve(__dirname, './fixtures/packages');
  31. atom.packages.packageDirPaths.unshift(fixturePackagesPath);
  32. document.querySelector('html').style.overflow = 'auto';
  33. document.body.style.overflow = 'auto';
  34. Set.prototype.jasmineToString = function() {
  35. let result = "Set {";
  36. let first = true;
  37. this.forEach(function(element) {
  38. if (!first) { result += ", "; }
  39. return result += element.toString();
  40. });
  41. first = false;
  42. return result + "}";
  43. };
  44. Set.prototype.isEqual = function(other) {
  45. if (other instanceof Set) {
  46. let next;
  47. if (this.size !== other.size) { return false; }
  48. const values = this.values();
  49. while (!(next = values.next()).done) {
  50. if (!other.has(next.value)) { return false; }
  51. }
  52. return true;
  53. } else {
  54. return false;
  55. }
  56. };
  57. jasmine.getEnv().addEqualityTester(function(a, b) {
  58. // Match jasmine.any's equality matching logic
  59. if ((a != null ? a.jasmineMatches : undefined) != null) { return a.jasmineMatches(b); }
  60. if ((b != null ? b.jasmineMatches : undefined) != null) { return b.jasmineMatches(a); }
  61. // Use underscore's definition of equality for toEqual assertions
  62. return _.isEqual(a, b);
  63. });
  64. if (process.env.CI) {
  65. jasmine.getEnv().defaultTimeoutInterval = 120000;
  66. } else {
  67. jasmine.getEnv().defaultTimeoutInterval = 5000;
  68. }
  69. const {testPaths} = atom.getLoadSettings();
  70. if (specPackagePath = FindParentDir.sync(testPaths[0], 'package.json')) {
  71. const packageMetadata = require(path.join(specPackagePath, 'package.json'));
  72. specPackageName = packageMetadata.name;
  73. }
  74. if ((specDirectory = FindParentDir.sync(testPaths[0], 'fixtures'))) {
  75. specProjectPath = path.join(specDirectory, 'fixtures');
  76. } else {
  77. specProjectPath = require('os').tmpdir();
  78. }
  79. beforeEach(function() {
  80. // Do not clobber recent project history
  81. spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve());
  82. atom.project.setPaths([specProjectPath]);
  83. window.resetTimeouts();
  84. spyOn(_._, "now").andCallFake(() => window.now);
  85. spyOn(Date, 'now').andCallFake(() => window.now);
  86. spyOn(window, "setTimeout").andCallFake(window.fakeSetTimeout);
  87. spyOn(window, "clearTimeout").andCallFake(window.fakeClearTimeout);
  88. spyOn(_, "debounce").andCallFake(mockDebounce);
  89. const spy = spyOn(atom.packages, 'resolvePackagePath').andCallFake(function(packageName) {
  90. if (specPackageName && (packageName === specPackageName)) {
  91. return resolvePackagePath(specPackagePath);
  92. } else {
  93. return resolvePackagePath(packageName);
  94. }
  95. });
  96. var resolvePackagePath = _.bind(spy.originalValue, atom.packages);
  97. // prevent specs from modifying Atom's menus
  98. spyOn(atom.menu, 'sendToBrowserProcess');
  99. // reset config before each spec
  100. atom.config.set("core.destroyEmptyPanes", false);
  101. atom.config.set("editor.fontFamily", "Courier");
  102. atom.config.set("editor.fontSize", 16);
  103. atom.config.set("editor.autoIndent", false);
  104. atom.config.set("core.disabledPackages", ["package-that-throws-an-exception",
  105. "package-with-broken-package-json", "package-with-broken-keymap"]);
  106. advanceClock(1000);
  107. window.setTimeout.reset();
  108. // make editor display updates synchronous
  109. TextEditorElement.prototype.setUpdatedSynchronously(true);
  110. spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake(function() { return this.detectResurrection(); });
  111. spyOn(TextEditor.prototype, "shouldPromptToSave").andReturn(false);
  112. // make tokenization synchronous
  113. TextMateLanguageMode.prototype.chunkSize = Infinity;
  114. TreeSitterLanguageMode.prototype.syncTimeoutMicros = Infinity;
  115. spyOn(TextMateLanguageMode.prototype, "tokenizeInBackground").andCallFake(function() { return this.tokenizeNextChunk(); });
  116. // Without this spy, TextEditor.onDidTokenize callbacks would not be called
  117. // after the buffer's language mode changed, because by the time the editor
  118. // called its new language mode's onDidTokenize method, the language mode
  119. // would already be fully tokenized.
  120. spyOn(TextEditor.prototype, "onDidTokenize").andCallFake(function(callback) {
  121. return new CompositeDisposable(
  122. this.emitter.on("did-tokenize", callback),
  123. this.onDidChangeGrammar(() => {
  124. const languageMode = this.buffer.getLanguageMode();
  125. if (languageMode.tokenizeInBackground != null ? languageMode.tokenizeInBackground.originalValue : undefined) {
  126. return callback();
  127. }
  128. })
  129. );
  130. });
  131. let clipboardContent = 'initial clipboard content';
  132. spyOn(clipboard, 'writeText').andCallFake(text => clipboardContent = text);
  133. spyOn(clipboard, 'readText').andCallFake(() => clipboardContent);
  134. return addCustomMatchers(this);
  135. });
  136. afterEach(function() {
  137. ensureNoDeprecatedFunctionCalls();
  138. ensureNoDeprecatedStylesheets();
  139. waitsForPromise(() => atom.reset());
  140. return runs(function() {
  141. if (!window.debugContent) { document.getElementById('jasmine-content').innerHTML = ''; }
  142. warnIfLeakingPathSubscriptions();
  143. return waits(0);
  144. });
  145. }); // yield to ui thread to make screen update more frequently
  146. var warnIfLeakingPathSubscriptions = function() {
  147. const watchedPaths = pathwatcher.getWatchedPaths();
  148. if (watchedPaths.length > 0) {
  149. console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", "));
  150. }
  151. return pathwatcher.closeAllWatchers();
  152. };
  153. var ensureNoDeprecatedFunctionCalls = function() {
  154. const deprecations = _.clone(Grim.getDeprecations());
  155. Grim.clearDeprecations();
  156. if (deprecations.length > 0) {
  157. const originalPrepareStackTrace = Error.prepareStackTrace;
  158. Error.prepareStackTrace = function(error, stack) {
  159. const output = [];
  160. for (let deprecation of Array.from(deprecations)) {
  161. output.push(`${deprecation.originName} is deprecated. ${deprecation.message}`);
  162. output.push(_.multiplyString("-", output[output.length - 1].length));
  163. for (stack of Array.from(deprecation.getStacks())) {
  164. for (let {functionName, location} of Array.from(stack)) {
  165. output.push(`${functionName} -- ${location}`);
  166. }
  167. }
  168. output.push("");
  169. }
  170. return output.join("\n");
  171. };
  172. const error = new Error(`Deprecated function(s) ${deprecations.map(({originName}) => originName).join(', ')}) were called.`);
  173. error.stack;
  174. Error.prepareStackTrace = originalPrepareStackTrace;
  175. throw error;
  176. }
  177. };
  178. var ensureNoDeprecatedStylesheets = function() {
  179. const deprecations = _.clone(atom.styles.getDeprecations());
  180. atom.styles.clearDeprecations();
  181. return (() => {
  182. const result = [];
  183. for (let sourcePath in deprecations) {
  184. const deprecation = deprecations[sourcePath];
  185. const title =
  186. sourcePath !== 'undefined' ?
  187. `Deprecated stylesheet at '${sourcePath}':`
  188. :
  189. "Deprecated stylesheet:";
  190. throw new Error(`${title}\n${deprecation.message}`);
  191. }
  192. return result;
  193. })();
  194. };
  195. const {
  196. emitObject
  197. } = jasmine.StringPrettyPrinter.prototype;
  198. jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {
  199. if (obj.inspect) {
  200. return this.append(obj.inspect());
  201. } else {
  202. return emitObject.call(this, obj);
  203. }
  204. };
  205. jasmine.unspy = function(object, methodName) {
  206. if (!object[methodName].hasOwnProperty('originalValue')) { throw new Error("Not a spy"); }
  207. return object[methodName] = object[methodName].originalValue;
  208. };
  209. jasmine.attachToDOM = function(element) {
  210. const jasmineContent = document.querySelector('#jasmine-content');
  211. if (!jasmineContent.contains(element)) { return jasmineContent.appendChild(element); }
  212. };
  213. let grimDeprecationsSnapshot = null;
  214. let stylesDeprecationsSnapshot = null;
  215. jasmine.snapshotDeprecations = function() {
  216. grimDeprecationsSnapshot = _.clone(Grim.deprecations);
  217. return stylesDeprecationsSnapshot = _.clone(atom.styles.deprecationsBySourcePath);
  218. };
  219. jasmine.restoreDeprecationsSnapshot = function() {
  220. Grim.deprecations = grimDeprecationsSnapshot;
  221. return atom.styles.deprecationsBySourcePath = stylesDeprecationsSnapshot;
  222. };
  223. jasmine.useRealClock = function() {
  224. jasmine.unspy(window, 'setTimeout');
  225. jasmine.unspy(window, 'clearTimeout');
  226. jasmine.unspy(_._, 'now');
  227. return jasmine.unspy(Date, 'now');
  228. };
  229. // The clock is halfway mocked now in a sad and terrible way... only setTimeout
  230. // and clearTimeout are included. This method will also include setInterval. We
  231. // would do this everywhere if didn't cause us to break a bunch of package tests.
  232. jasmine.useMockClock = function() {
  233. spyOn(window, 'setInterval').andCallFake(fakeSetInterval);
  234. return spyOn(window, 'clearInterval').andCallFake(fakeClearInterval);
  235. };
  236. var addCustomMatchers = function(spec) {
  237. return spec.addMatchers({
  238. toBeInstanceOf(expected) {
  239. const beOrNotBe = this.isNot ? "not be" : "be";
  240. this.message = () => `Expected ${jasmine.pp(this.actual)} to ${beOrNotBe} instance of ${expected.name} class`;
  241. return this.actual instanceof expected;
  242. },
  243. toHaveLength(expected) {
  244. if ((this.actual == null)) {
  245. this.message = () => `Expected object ${this.actual} has no length method`;
  246. return false;
  247. } else {
  248. const haveOrNotHave = this.isNot ? "not have" : "have";
  249. this.message = () => `Expected object with length ${this.actual.length} to ${haveOrNotHave} length ${expected}`;
  250. return this.actual.length === expected;
  251. }
  252. },
  253. toExistOnDisk(expected) {
  254. const toOrNotTo = (this.isNot && "not to") || "to";
  255. this.message = function() { return `Expected path '${this.actual}' ${toOrNotTo} exist.`; };
  256. return fs.existsSync(this.actual);
  257. },
  258. toHaveFocus() {
  259. const toOrNotTo = (this.isNot && "not to") || "to";
  260. if (!document.hasFocus()) {
  261. console.error("Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner.");
  262. }
  263. this.message = function() { return `Expected element '${this.actual}' or its descendants ${toOrNotTo} have focus.`; };
  264. let element = this.actual;
  265. if (element.jquery) { element = element.get(0); }
  266. return (element === document.activeElement) || element.contains(document.activeElement);
  267. },
  268. toShow() {
  269. const toOrNotTo = (this.isNot && "not to") || "to";
  270. let element = this.actual;
  271. if (element.jquery) { element = element.get(0); }
  272. this.message = () => `Expected element '${element}' or its descendants ${toOrNotTo} show.`;
  273. const computedStyle = getComputedStyle(element);
  274. return (computedStyle.display !== 'none') && (computedStyle.visibility === 'visible') && !element.hidden;
  275. },
  276. toEqualPath(expected) {
  277. const actualPath = path.normalize(this.actual);
  278. const expectedPath = path.normalize(expected);
  279. this.message = () => `Expected path '${actualPath}' to be equal to '${expectedPath}'.`;
  280. return actualPath === expectedPath;
  281. },
  282. toBeNear(expected, acceptedError, actual) {
  283. if (acceptedError == null) { acceptedError = 1; }
  284. return (typeof expected === 'number') && (typeof acceptedError === 'number') && (typeof this.actual === 'number') && ((expected - acceptedError) <= this.actual) && (this.actual <= (expected + acceptedError));
  285. },
  286. toHaveNearPixels(expected, acceptedError, actual) {
  287. if (acceptedError == null) { acceptedError = 1; }
  288. const expectedNumber = parseFloat(expected);
  289. const actualNumber = parseFloat(this.actual);
  290. return (typeof expected === 'string') && (typeof acceptedError === 'number') && (typeof this.actual === 'string') && (expected.indexOf('px') >= 1) && (this.actual.indexOf('px') >= 1) && ((expectedNumber - acceptedError) <= actualNumber) && (actualNumber <= (expectedNumber + acceptedError));
  291. }
  292. });
  293. };
  294. window.waitsForPromise = function(...args) {
  295. let shouldReject, timeout;
  296. let label = null;
  297. if (args.length > 1) {
  298. ({shouldReject, timeout, label} = args[0]);
  299. } else {
  300. shouldReject = false;
  301. }
  302. if (label == null) { label = 'promise to be resolved or rejected'; }
  303. const fn = _.last(args);
  304. return window.waitsFor(label, timeout, function(moveOn) {
  305. const promise = fn();
  306. if (shouldReject) {
  307. promise.catch.call(promise, moveOn);
  308. return promise.then(function() {
  309. jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved");
  310. return moveOn();
  311. });
  312. } else {
  313. promise.then(moveOn);
  314. return promise.catch.call(promise, function(error) {
  315. jasmine.getEnv().currentSpec.fail(`Expected promise to be resolved, but it was rejected with: ${(error != null ? error.message : undefined)} ${jasmine.pp(error)}`);
  316. return moveOn();
  317. });
  318. }
  319. });
  320. };
  321. window.resetTimeouts = function() {
  322. window.now = 0;
  323. window.timeoutCount = 0;
  324. window.intervalCount = 0;
  325. window.timeouts = [];
  326. return window.intervalTimeouts = {};
  327. };
  328. window.fakeSetTimeout = function(callback, ms) {
  329. if (ms == null) { ms = 0; }
  330. const id = ++window.timeoutCount;
  331. window.timeouts.push([id, window.now + ms, callback]);
  332. return id;
  333. };
  334. window.fakeClearTimeout = idToClear => window.timeouts = window.timeouts.filter(function(...args) { const [id] = Array.from(args[0]); return id !== idToClear; });
  335. window.fakeSetInterval = function(callback, ms) {
  336. const id = ++window.intervalCount;
  337. var action = function() {
  338. callback();
  339. return window.intervalTimeouts[id] = window.fakeSetTimeout(action, ms);
  340. };
  341. window.intervalTimeouts[id] = window.fakeSetTimeout(action, ms);
  342. return id;
  343. };
  344. window.fakeClearInterval = function(idToClear) {
  345. return window.fakeClearTimeout(this.intervalTimeouts[idToClear]);
  346. };
  347. window.advanceClock = function(delta) {
  348. if (delta == null) { delta = 1; }
  349. window.now += delta;
  350. const callbacks = [];
  351. window.timeouts = window.timeouts.filter(function(...args) {
  352. let id, strikeTime;
  353. let callback;
  354. [id, strikeTime, callback] = Array.from(args[0]);
  355. if (strikeTime <= window.now) {
  356. callbacks.push(callback);
  357. return false;
  358. } else {
  359. return true;
  360. }
  361. });
  362. return (() => {
  363. const result = [];
  364. for (let callback of Array.from(callbacks)) { result.push(callback());
  365. }
  366. return result;
  367. })();
  368. };
  369. exports.mockLocalStorage = function() {
  370. const items = {};
  371. spyOn(global.localStorage, 'setItem').andCallFake(function(key, item) { items[key] = item.toString(); return undefined; });
  372. spyOn(global.localStorage, 'getItem').andCallFake(key => items[key] != null ? items[key] : null);
  373. return spyOn(global.localStorage, 'removeItem').andCallFake(function(key) { delete items[key]; return undefined; });
  374. };