context.spec.ts 16 KB

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