context.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { SessionContext } from '@jupyterlab/apputils';
  4. import {
  5. Context,
  6. DocumentRegistry,
  7. TextModelFactory
  8. } from '@jupyterlab/docregistry';
  9. import { RenderMimeRegistry } from '@jupyterlab/rendermime';
  10. import { Contents, ServiceManager } from '@jupyterlab/services';
  11. import {
  12. acceptDialog,
  13. dismissDialog,
  14. initNotebookContext,
  15. NBTestUtils,
  16. waitForDialog
  17. } from '@jupyterlab/testutils';
  18. import * as Mock from '@jupyterlab/testutils/lib/mock';
  19. import { UUID } from '@lumino/coreutils';
  20. import { Widget } from '@lumino/widgets';
  21. describe('docregistry/context', () => {
  22. let manager: ServiceManager.IManager;
  23. const factory = new TextModelFactory();
  24. beforeAll(() => {
  25. manager = new Mock.ServiceManagerMock();
  26. return manager.ready;
  27. });
  28. describe('Context', () => {
  29. let context: Context<DocumentRegistry.IModel>;
  30. beforeEach(() => {
  31. context = new Context({
  32. manager,
  33. factory,
  34. path: UUID.uuid4() + '.txt'
  35. });
  36. });
  37. afterEach(async () => {
  38. await context.sessionContext.shutdown();
  39. context.dispose();
  40. });
  41. describe('#constructor()', () => {
  42. it('should create a new context', () => {
  43. context = new Context({
  44. manager,
  45. factory,
  46. path: UUID.uuid4() + '.txt'
  47. });
  48. expect(context).toBeInstanceOf(Context);
  49. });
  50. });
  51. describe('#pathChanged', () => {
  52. it('should be emitted when the path changes', async () => {
  53. const newPath = UUID.uuid4() + '.txt';
  54. let called = false;
  55. context.pathChanged.connect((sender, args) => {
  56. expect(sender).toBe(context);
  57. expect(args).toBe(newPath);
  58. called = true;
  59. });
  60. await context.initialize(true);
  61. await manager.contents.rename(context.path, newPath);
  62. expect(called).toBe(true);
  63. });
  64. });
  65. describe('#fileChanged', () => {
  66. it('should be emitted when the file is saved', async () => {
  67. const path = context.path;
  68. let called = false;
  69. context.fileChanged.connect((sender, args) => {
  70. expect(sender).toBe(context);
  71. expect(args.path).toBe(path);
  72. called = true;
  73. });
  74. await context.initialize(true);
  75. expect(called).toBe(true);
  76. });
  77. });
  78. describe('#saving', () => {
  79. it("should emit 'starting' when the file starts saving", async () => {
  80. let called = false;
  81. let checked = false;
  82. context.saveState.connect((sender, args) => {
  83. if (!called) {
  84. expect(sender).toBe(context);
  85. expect(args).toBe('started');
  86. checked = true;
  87. }
  88. called = true;
  89. });
  90. await context.initialize(true);
  91. expect(called).toBe(true);
  92. expect(checked).toBe(true);
  93. });
  94. it("should emit 'completed' when the file ends saving", async () => {
  95. let called = 0;
  96. let checked = false;
  97. context.saveState.connect((sender, args) => {
  98. if (called > 0) {
  99. expect(sender).toBe(context);
  100. expect(args).toBe('completed');
  101. checked = true;
  102. }
  103. called += 1;
  104. });
  105. await context.initialize(true);
  106. expect(called).toBe(2);
  107. expect(checked).toBe(true);
  108. });
  109. it("should emit 'failed' when the save operation fails out", async () => {
  110. context = new Context({
  111. manager,
  112. factory,
  113. path: 'readonly.txt'
  114. });
  115. let called = 0;
  116. let checked;
  117. context.saveState.connect((sender, args) => {
  118. if (called > 0) {
  119. expect(sender).toBe(context);
  120. checked = args;
  121. }
  122. called += 1;
  123. });
  124. await expect(context.initialize(true)).rejects.toThrowError(
  125. 'Invalid response: 403 Forbidden'
  126. );
  127. expect(called).toBe(2);
  128. expect(checked).toBe('failed');
  129. await acceptDialog();
  130. });
  131. });
  132. describe('#isReady', () => {
  133. it('should indicate whether the context is ready', async () => {
  134. expect(context.isReady).toBe(false);
  135. const func = async () => {
  136. await context.ready;
  137. expect(context.isReady).toBe(true);
  138. };
  139. const promise = func();
  140. await context.initialize(true);
  141. await promise;
  142. });
  143. });
  144. describe('#ready()', () => {
  145. it('should resolve when the file is saved for the first time', async () => {
  146. await context.initialize(true);
  147. await context.ready;
  148. });
  149. it('should resolve when the file is reverted for the first time', async () => {
  150. await manager.contents.save(context.path, {
  151. type: factory.contentType,
  152. format: factory.fileFormat,
  153. content: 'foo'
  154. });
  155. await context.initialize(false);
  156. await context.ready;
  157. });
  158. it('should initialize the model when the file is saved for the first time', async () => {
  159. const context = await initNotebookContext({ manager });
  160. context.model.fromJSON(NBTestUtils.DEFAULT_CONTENT);
  161. expect(context.model.cells.canUndo).toBe(true);
  162. await context.initialize(true);
  163. await context.ready;
  164. expect(context.model.cells.canUndo).toBe(false);
  165. });
  166. it('should initialize the model when the file is reverted for the first time', async () => {
  167. const context = await initNotebookContext({ manager });
  168. await manager.contents.save(context.path, {
  169. type: 'notebook',
  170. format: 'json',
  171. content: NBTestUtils.DEFAULT_CONTENT
  172. });
  173. context.model.fromJSON(NBTestUtils.DEFAULT_CONTENT);
  174. expect(context.model.cells.canUndo).toBe(true);
  175. await context.initialize(false);
  176. await context.ready;
  177. expect(context.model.cells.canUndo).toBe(false);
  178. });
  179. });
  180. describe('#disposed', () => {
  181. it('should be emitted when the context is disposed', () => {
  182. let called = false;
  183. context.disposed.connect((sender, args) => {
  184. expect(sender).toBe(context);
  185. expect(args).toBeUndefined();
  186. called = true;
  187. });
  188. context.dispose();
  189. expect(called).toBe(true);
  190. });
  191. });
  192. describe('#model', () => {
  193. it('should be the model associated with the document', () => {
  194. expect(context.model.toString()).toBe('');
  195. });
  196. });
  197. describe('#sessionContext', () => {
  198. it('should be a ISessionContext object', () => {
  199. expect(context.sessionContext).toBeInstanceOf(SessionContext);
  200. });
  201. });
  202. describe('#path', () => {
  203. it('should be the current path for the context', () => {
  204. expect(typeof context.path).toBe('string');
  205. });
  206. });
  207. describe('#lastModifiedCheckMargin', () => {
  208. it('should be 500ms by default', () => {
  209. expect(context.lastModifiedCheckMargin).toBe(500);
  210. });
  211. it('should be set-able', () => {
  212. context.lastModifiedCheckMargin = 600;
  213. expect(context.lastModifiedCheckMargin).toBe(600);
  214. });
  215. });
  216. describe('#contentsModel', () => {
  217. it('should be `null` before population', () => {
  218. expect(context.contentsModel).toBeNull();
  219. });
  220. it('should be set after population', async () => {
  221. const { path } = context;
  222. void context.initialize(true);
  223. await context.ready;
  224. expect(context.contentsModel!.path).toBe(path);
  225. });
  226. });
  227. describe('#factoryName', () => {
  228. it('should be the name of the factory used by the context', () => {
  229. expect(context.factoryName).toBe(factory.name);
  230. });
  231. });
  232. describe('#isDisposed', () => {
  233. it('should test whether the context is disposed', () => {
  234. expect(context.isDisposed).toBe(false);
  235. context.dispose();
  236. expect(context.isDisposed).toBe(true);
  237. });
  238. });
  239. describe('#dispose()', () => {
  240. it('should dispose of the resources used by the context', () => {
  241. context.dispose();
  242. expect(context.isDisposed).toBe(true);
  243. context.dispose();
  244. expect(context.isDisposed).toBe(true);
  245. });
  246. });
  247. describe('#rename()', () => {
  248. it('should change the name of the file to the new name', async () => {
  249. await context.initialize(true);
  250. context.model.fromString('foo');
  251. const newName = UUID.uuid4() + '.txt';
  252. await context.rename(newName);
  253. await context.save();
  254. const opts: Contents.IFetchOptions = {
  255. format: factory.fileFormat,
  256. type: factory.contentType,
  257. content: true
  258. };
  259. const model = await manager.contents.get(newName, opts);
  260. expect(model.content).toBe('foo');
  261. });
  262. });
  263. describe('#save()', () => {
  264. it('should save the contents of the file to disk', async () => {
  265. await context.initialize(true);
  266. context.model.fromString('foo');
  267. await context.save();
  268. const opts: Contents.IFetchOptions = {
  269. format: factory.fileFormat,
  270. type: factory.contentType,
  271. content: true
  272. };
  273. const model = await manager.contents.get(context.path, opts);
  274. expect(model.content).toBe('foo');
  275. });
  276. it('should should preserve LF line endings upon save', async () => {
  277. await context.initialize(true);
  278. await manager.contents.save(context.path, {
  279. type: factory.contentType,
  280. format: factory.fileFormat,
  281. content: 'foo\nbar'
  282. });
  283. await context.revert();
  284. await context.save();
  285. const opts: Contents.IFetchOptions = {
  286. format: factory.fileFormat,
  287. type: factory.contentType,
  288. content: true
  289. };
  290. const model = await manager.contents.get(context.path, opts);
  291. expect(model.content).toBe('foo\nbar');
  292. });
  293. it('should should preserve CR line endings upon save', async () => {
  294. await context.initialize(true);
  295. await manager.contents.save(context.path, {
  296. type: factory.contentType,
  297. format: factory.fileFormat,
  298. content: 'foo\rbar'
  299. });
  300. await context.revert();
  301. await context.save();
  302. const opts: Contents.IFetchOptions = {
  303. format: factory.fileFormat,
  304. type: factory.contentType,
  305. content: true
  306. };
  307. const model = await manager.contents.get(context.path, opts);
  308. expect(model.content).toBe('foo\rbar');
  309. });
  310. it('should should preserve CRLF line endings upon save', async () => {
  311. await context.initialize(true);
  312. await manager.contents.save(context.path, {
  313. type: factory.contentType,
  314. format: factory.fileFormat,
  315. content: 'foo\r\nbar'
  316. });
  317. await context.revert();
  318. await context.save();
  319. const opts: Contents.IFetchOptions = {
  320. format: factory.fileFormat,
  321. type: factory.contentType,
  322. content: true
  323. };
  324. const model = await manager.contents.get(context.path, opts);
  325. expect(model.content).toBe('foo\r\nbar');
  326. });
  327. });
  328. describe('#saveAs()', () => {
  329. it('should save the document to a different path chosen by the user', async () => {
  330. const initialize = context.initialize(true);
  331. const newPath = UUID.uuid4() + '.txt';
  332. const func = async () => {
  333. await initialize;
  334. await waitForDialog();
  335. const dialog = document.body.getElementsByClassName('jp-Dialog')[0];
  336. const input = dialog.getElementsByTagName('input')[0];
  337. input.value = newPath;
  338. await acceptDialog();
  339. };
  340. const promise = func();
  341. await initialize;
  342. const oldPath = context.path;
  343. await context.saveAs();
  344. await promise;
  345. expect(context.path).toBe(newPath);
  346. // Make sure the both files are there now.
  347. const model = await manager.contents.get('', { content: true });
  348. expect(model.content.find((x: any) => x.name === oldPath)).toBeTruthy();
  349. expect(model.content.find((x: any) => x.name === newPath)).toBeTruthy();
  350. });
  351. it('should bring up a conflict dialog', async () => {
  352. const newPath = UUID.uuid4() + '.txt';
  353. const func = async () => {
  354. await waitForDialog();
  355. const dialog = document.body.getElementsByClassName('jp-Dialog')[0];
  356. const input = dialog.getElementsByTagName('input')[0];
  357. input.value = newPath;
  358. await acceptDialog(); // Accept rename dialog
  359. await acceptDialog(); // Accept conflict dialog
  360. };
  361. await manager.contents.save(newPath, {
  362. type: factory.contentType,
  363. format: factory.fileFormat,
  364. content: 'foo'
  365. });
  366. await context.initialize(true);
  367. const promise = func();
  368. await context.saveAs();
  369. await promise;
  370. expect(context.path).toBe(newPath);
  371. });
  372. it('should keep the file if overwrite is aborted', async () => {
  373. const oldPath = context.path;
  374. const newPath = UUID.uuid4() + '.txt';
  375. const func = async () => {
  376. await waitForDialog();
  377. const dialog = document.body.getElementsByClassName('jp-Dialog')[0];
  378. const input = dialog.getElementsByTagName('input')[0];
  379. input.value = newPath;
  380. await acceptDialog(); // Accept rename dialog
  381. await dismissDialog(); // Reject conflict dialog
  382. };
  383. await manager.contents.save(newPath, {
  384. type: factory.contentType,
  385. format: factory.fileFormat,
  386. content: 'foo'
  387. });
  388. await context.initialize(true);
  389. const promise = func();
  390. await context.saveAs();
  391. await promise;
  392. expect(context.path).toBe(oldPath);
  393. });
  394. it('should just save if the file name does not change', async () => {
  395. const path = context.path;
  396. await context.initialize(true);
  397. const promise = context.saveAs();
  398. await acceptDialog();
  399. await promise;
  400. expect(context.path).toBe(path);
  401. });
  402. });
  403. describe('#revert()', () => {
  404. it('should revert the contents of the file to the disk', async () => {
  405. await context.initialize(true);
  406. context.model.fromString('foo');
  407. await context.save();
  408. context.model.fromString('bar');
  409. await context.revert();
  410. expect(context.model.toString()).toBe('foo');
  411. });
  412. it('should normalize CRLF line endings to LF', async () => {
  413. await context.initialize(true);
  414. await manager.contents.save(context.path, {
  415. type: factory.contentType,
  416. format: factory.fileFormat,
  417. content: 'foo\r\nbar'
  418. });
  419. await context.revert();
  420. expect(context.model.toString()).toBe('foo\nbar');
  421. });
  422. });
  423. describe('#createCheckpoint()', () => {
  424. it('should create a checkpoint for the file', async () => {
  425. await context.initialize(true);
  426. const model = await context.createCheckpoint();
  427. expect(model.id).toBeTruthy();
  428. expect(model.last_modified).toBeTruthy();
  429. });
  430. });
  431. describe('#deleteCheckpoint()', () => {
  432. it('should delete the given checkpoint', async () => {
  433. await context.initialize(true);
  434. const model = await context.createCheckpoint();
  435. await context.deleteCheckpoint(model.id);
  436. const models = await context.listCheckpoints();
  437. expect(models.length).toBe(0);
  438. });
  439. });
  440. describe('#restoreCheckpoint()', () => {
  441. it('should restore the value to the last checkpoint value', async () => {
  442. context.model.fromString('bar');
  443. await context.initialize(true);
  444. const model = await context.createCheckpoint();
  445. context.model.fromString('foo');
  446. const id = model.id;
  447. await context.save();
  448. await context.restoreCheckpoint(id);
  449. await context.revert();
  450. expect(context.model.toString()).toBe('bar');
  451. });
  452. });
  453. describe('#listCheckpoints()', () => {
  454. it('should list the checkpoints for the file', async () => {
  455. await context.initialize(true);
  456. const model = await context.createCheckpoint();
  457. const id = model.id;
  458. const models = await context.listCheckpoints();
  459. let found = false;
  460. for (const model of models) {
  461. if (model.id === id) {
  462. found = true;
  463. }
  464. }
  465. expect(found).toBe(true);
  466. });
  467. });
  468. describe('#urlResolver', () => {
  469. it('should be a url resolver', () => {
  470. expect(context.urlResolver).toBeInstanceOf(
  471. RenderMimeRegistry.UrlResolver
  472. );
  473. });
  474. });
  475. describe('#addSibling()', () => {
  476. it('should add a sibling widget', () => {
  477. let called = false;
  478. const opener = (widget: Widget) => {
  479. called = true;
  480. };
  481. context = new Context({
  482. manager,
  483. factory,
  484. path: UUID.uuid4() + '.txt',
  485. opener
  486. });
  487. context.addSibling(new Widget());
  488. expect(called).toBe(true);
  489. });
  490. });
  491. });
  492. });