registry.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { expect } from 'chai';
  4. import { UUID, JSONObject } from '@lumino/coreutils';
  5. import { Contents, Drive, ServiceManager, Session } from '@jupyterlab/services';
  6. import { toArray } from '@lumino/algorithm';
  7. import { PageConfig } from '@jupyterlab/coreutils';
  8. import { Widget } from '@lumino/widgets';
  9. import { SessionContext } from '@jupyterlab/apputils';
  10. import { MathJaxTypesetter } from '@jupyterlab/mathjax2';
  11. import {
  12. MimeModel,
  13. IRenderMime,
  14. RenderedText,
  15. RenderMimeRegistry
  16. } from '@jupyterlab/rendermime';
  17. import {
  18. defaultRenderMime,
  19. createFileContextWithKernel
  20. } from '@jupyterlab/testutils';
  21. function createModel(data: JSONObject): IRenderMime.IMimeModel {
  22. return new MimeModel({ data });
  23. }
  24. const fooFactory: IRenderMime.IRendererFactory = {
  25. mimeTypes: ['text/foo'],
  26. safe: true,
  27. defaultRank: 1000,
  28. createRenderer: options => new RenderedText(options)
  29. };
  30. describe('rendermime/registry', () => {
  31. let r: RenderMimeRegistry;
  32. let RESOLVER: IRenderMime.IResolver;
  33. before(async () => {
  34. let fileContext = await createFileContextWithKernel();
  35. await fileContext.initialize(true);
  36. // The context initialization kicks off a sessionContext initialization,
  37. // but does not wait for it. We need to wait for it so our url resolver
  38. // has access to the session.
  39. await fileContext.sessionContext.initialize();
  40. RESOLVER = fileContext.urlResolver;
  41. });
  42. beforeEach(() => {
  43. r = defaultRenderMime();
  44. });
  45. describe('RenderMimeRegistry', () => {
  46. describe('#constructor()', () => {
  47. it('should create a new rendermime instance', () => {
  48. expect(r instanceof RenderMimeRegistry).to.equal(true);
  49. });
  50. });
  51. describe('#resolver', () => {
  52. it('should be the resolver used by the rendermime', () => {
  53. expect(r.resolver).to.be.null;
  54. const clone = r.clone({ resolver: RESOLVER });
  55. expect(clone.resolver).to.equal(RESOLVER);
  56. });
  57. });
  58. describe('#linkHandler', () => {
  59. it('should be the link handler used by the rendermime', () => {
  60. expect(r.linkHandler).to.be.null;
  61. const handler = {
  62. handleLink: () => {
  63. /* no-op */
  64. }
  65. };
  66. const clone = r.clone({ linkHandler: handler });
  67. expect(clone.linkHandler).to.equal(handler);
  68. });
  69. });
  70. describe('#latexTypesetter', () => {
  71. it('should be the null typesetter by default', () => {
  72. expect(r.latexTypesetter).to.be.null;
  73. });
  74. it('should be clonable', () => {
  75. const args = {
  76. url: PageConfig.getOption('mathjaxUrl'),
  77. config: PageConfig.getOption('mathjaxConfig')
  78. };
  79. const typesetter1 = new MathJaxTypesetter(args);
  80. const clone1 = r.clone({ latexTypesetter: typesetter1 });
  81. expect(clone1.latexTypesetter).to.equal(typesetter1);
  82. const typesetter2 = new MathJaxTypesetter(args);
  83. const clone2 = r.clone({ latexTypesetter: typesetter2 });
  84. expect(clone2.latexTypesetter).to.equal(typesetter2);
  85. });
  86. });
  87. describe('#createRenderer()', () => {
  88. it('should create a mime renderer', () => {
  89. const w = r.createRenderer('text/plain');
  90. expect(w instanceof Widget).to.equal(true);
  91. });
  92. it('should raise an error for an unregistered mime type', () => {
  93. expect(() => {
  94. r.createRenderer('text/fizz');
  95. }).to.throw();
  96. });
  97. it('should render json data', async () => {
  98. const model = createModel({
  99. 'application/json': { foo: 1 }
  100. });
  101. const w = r.createRenderer('application/json');
  102. await w.renderModel(model);
  103. expect(w.node.textContent).to.equal('{\n "foo": 1\n}');
  104. });
  105. it('should send a url resolver', async () => {
  106. const model = createModel({
  107. 'text/html': '<img src="./foo%2520">foo</img>'
  108. });
  109. let called0 = false;
  110. let called1 = false;
  111. r = r.clone({
  112. resolver: {
  113. resolveUrl: (url: string) => {
  114. called0 = true;
  115. return Promise.resolve(url);
  116. },
  117. getDownloadUrl: (url: string) => {
  118. expect(called0).to.equal(true);
  119. called1 = true;
  120. expect(url).to.equal('./foo%2520');
  121. return Promise.resolve(url);
  122. }
  123. }
  124. });
  125. const w = r.createRenderer('text/html');
  126. await w.renderModel(model);
  127. expect(called1).to.equal(true);
  128. });
  129. it('should send a link handler', async () => {
  130. const model = createModel({
  131. 'text/html': '<a href="./foo/bar.txt">foo</a>'
  132. });
  133. let called = false;
  134. r = r.clone({
  135. resolver: RESOLVER,
  136. linkHandler: {
  137. handleLink: (node: HTMLElement, url: string) => {
  138. expect(url).to.equal('foo/bar.txt');
  139. called = true;
  140. }
  141. }
  142. });
  143. const w = r.createRenderer('text/html');
  144. await w.renderModel(model);
  145. expect(called).to.equal(true);
  146. });
  147. it('should send decoded paths to link handler', async () => {
  148. const model = createModel({
  149. 'text/html': '<a href="foo%2520/b%C3%A5r.txt">foo</a>'
  150. });
  151. let called = false;
  152. r = r.clone({
  153. resolver: RESOLVER,
  154. linkHandler: {
  155. handleLink: (node: HTMLElement, path: string) => {
  156. expect(path).to.equal('foo%20/bår.txt');
  157. called = true;
  158. }
  159. }
  160. });
  161. const w = r.createRenderer('text/html');
  162. await w.renderModel(model);
  163. expect(called).to.equal(true);
  164. });
  165. });
  166. describe('#preferredMimeType()', () => {
  167. it('should find the preferred mimeType in a bundle', () => {
  168. const model = createModel({
  169. 'text/plain': 'foo',
  170. 'text/html': '<h1>foo</h1>'
  171. });
  172. expect(r.preferredMimeType(model.data, 'any')).to.equal('text/html');
  173. });
  174. it('should return `undefined` if there are no registered mimeTypes', () => {
  175. const model = createModel({ 'text/fizz': 'buzz' });
  176. expect(r.preferredMimeType(model.data, 'any')).to.be.undefined;
  177. });
  178. it('should select the mimeType that is safe', () => {
  179. const model = createModel({
  180. 'text/plain': 'foo',
  181. 'text/javascript': 'window.x = 1',
  182. 'image/png':
  183. 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
  184. });
  185. expect(r.preferredMimeType(model.data)).to.equal('image/png');
  186. });
  187. it('should render the mimeType that is sanitizable', () => {
  188. const model = createModel({
  189. 'text/plain': 'foo',
  190. 'text/html': '<h1>foo</h1>'
  191. });
  192. expect(r.preferredMimeType(model.data)).to.equal('text/html');
  193. });
  194. it('should return `undefined` if only unsafe options with default `ensure`', () => {
  195. const model = createModel({
  196. 'image/svg+xml': ''
  197. });
  198. expect(r.preferredMimeType(model.data)).to.be.undefined;
  199. });
  200. it('should return `undefined` if only unsafe options with `ensure`', () => {
  201. const model = createModel({
  202. 'image/svg+xml': ''
  203. });
  204. expect(r.preferredMimeType(model.data, 'ensure')).to.be.undefined;
  205. });
  206. it('should return safe option if called with `prefer`', () => {
  207. const model = createModel({
  208. 'image/svg+xml': '',
  209. 'text/plain': ''
  210. });
  211. expect(r.preferredMimeType(model.data, 'prefer')).to.equal(
  212. 'text/plain'
  213. );
  214. });
  215. it('should return unsafe option if called with `prefer`, and no safe alternative', () => {
  216. const model = createModel({
  217. 'image/svg+xml': ''
  218. });
  219. expect(r.preferredMimeType(model.data, 'prefer')).to.equal(
  220. 'image/svg+xml'
  221. );
  222. });
  223. });
  224. describe('#clone()', () => {
  225. it('should clone the rendermime instance with shallow copies of data', () => {
  226. const c = r.clone();
  227. expect(toArray(c.mimeTypes)).to.deep.equal(r.mimeTypes);
  228. c.addFactory(fooFactory);
  229. expect(r).to.not.equal(c);
  230. });
  231. });
  232. describe('#addFactory()', () => {
  233. it('should add a factory', () => {
  234. r.addFactory(fooFactory);
  235. const index = r.mimeTypes.indexOf('text/foo');
  236. expect(index).to.equal(r.mimeTypes.length - 1);
  237. });
  238. it('should take an optional rank', () => {
  239. const len = r.mimeTypes.length;
  240. r.addFactory(fooFactory, 0);
  241. const index = r.mimeTypes.indexOf('text/foo');
  242. expect(index).to.equal(0);
  243. expect(r.mimeTypes.length).to.equal(len + 1);
  244. });
  245. });
  246. describe('#removeMimeType()', () => {
  247. it('should remove a factory by mimeType', () => {
  248. r.removeMimeType('text/html');
  249. const model = createModel({ 'text/html': '<h1>foo</h1>' });
  250. expect(r.preferredMimeType(model.data, 'any')).to.be.undefined;
  251. });
  252. it('should be a no-op if the mimeType is not registered', () => {
  253. r.removeMimeType('text/foo');
  254. });
  255. });
  256. describe('#getFactory()', () => {
  257. it('should get a factory by mimeType', () => {
  258. const f = r.getFactory('text/plain')!;
  259. expect(f.mimeTypes).to.contain('text/plain');
  260. });
  261. it('should return undefined for missing mimeType', () => {
  262. expect(r.getFactory('hello/world')).to.be.undefined;
  263. });
  264. });
  265. describe('#mimeTypes', () => {
  266. it('should get the ordered list of mimeTypes', () => {
  267. expect(r.mimeTypes.indexOf('text/html')).to.not.equal(-1);
  268. });
  269. });
  270. describe('.UrlResolver', () => {
  271. let manager: ServiceManager;
  272. let resolver: RenderMimeRegistry.UrlResolver;
  273. let contents: Contents.IManager;
  274. let session: Session.ISessionConnection;
  275. const pathParent = 'has%20üni';
  276. const urlParent = encodeURI(pathParent);
  277. before(async () => {
  278. manager = new ServiceManager({ standby: 'never' });
  279. const drive = new Drive({ name: 'extra' });
  280. const path = pathParent + '/pr%25 ' + UUID.uuid4();
  281. contents = manager.contents;
  282. contents.addDrive(drive);
  283. await manager.ready;
  284. session = await manager.sessions.startNew({
  285. name: '',
  286. path: path,
  287. type: 'test'
  288. });
  289. resolver = new RenderMimeRegistry.UrlResolver({
  290. session,
  291. contents: manager.contents
  292. });
  293. });
  294. after(() => {
  295. return session.shutdown();
  296. });
  297. context('#constructor', () => {
  298. it('should create a UrlResolver instance', () => {
  299. expect(resolver).to.be.an.instanceof(RenderMimeRegistry.UrlResolver);
  300. });
  301. });
  302. context('#resolveUrl()', () => {
  303. it('should resolve a relative url', async () => {
  304. const path = await resolver.resolveUrl('./foo');
  305. expect(path).to.equal(urlParent + '/foo');
  306. });
  307. it('should resolve a relative url with no active session', async () => {
  308. const resolver = new RenderMimeRegistry.UrlResolver({
  309. session: new SessionContext({
  310. sessionManager: manager.sessions,
  311. specsManager: manager.kernelspecs,
  312. path: pathParent + '/pr%25 ' + UUID.uuid4(),
  313. kernelPreference: { canStart: false, shouldStart: false }
  314. }),
  315. contents: manager.contents
  316. });
  317. const path = await resolver.resolveUrl('./foo');
  318. expect(path).to.equal(urlParent + '/foo');
  319. });
  320. it('should ignore urls that have a protocol', async () => {
  321. const path = await resolver.resolveUrl('http://foo');
  322. expect(path).to.equal('http://foo');
  323. });
  324. it('should resolve URLs with escapes', async () => {
  325. const url = await resolver.resolveUrl('has%20space');
  326. expect(url).to.equal(urlParent + '/has%20space');
  327. });
  328. });
  329. context('#getDownloadUrl()', () => {
  330. it('should resolve an absolute server url to a download url', async () => {
  331. const contextPromise = resolver.getDownloadUrl('foo');
  332. const contentsPromise = contents.getDownloadUrl('foo');
  333. const values = await Promise.all([contextPromise, contentsPromise]);
  334. expect(values[0]).to.equal(values[1]);
  335. });
  336. it('should resolve escapes correctly', async () => {
  337. const contextPromise = resolver.getDownloadUrl('foo%2520test');
  338. const contentsPromise = contents.getDownloadUrl('foo%20test');
  339. const values = await Promise.all([contextPromise, contentsPromise]);
  340. expect(values[0]).to.equal(values[1]);
  341. });
  342. it('should ignore urls that have a protocol', async () => {
  343. const path = await resolver.getDownloadUrl('http://foo');
  344. expect(path).to.equal('http://foo');
  345. });
  346. });
  347. context('#isLocal', () => {
  348. it('should return true for a registered IDrive`', () => {
  349. expect(resolver.isLocal('extra:path/to/file')).to.equal(true);
  350. });
  351. it('should return false for an unrecognized Drive`', () => {
  352. expect(resolver.isLocal('unregistered:path/to/file')).to.equal(false);
  353. });
  354. it('should return true for a normal filesystem-like path`', () => {
  355. expect(resolver.isLocal('path/to/file')).to.equal(true);
  356. });
  357. });
  358. });
  359. });
  360. });