index.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { simulate } from 'simulate-event';
  4. import { ServiceManager, Session } from '@jupyterlab/services';
  5. import { SessionContext } from '@jupyterlab/apputils';
  6. import { PromiseDelegate, UUID } from '@phosphor/coreutils';
  7. import { ISignal, Signal } from '@phosphor/signaling';
  8. import {
  9. TextModelFactory,
  10. DocumentRegistry,
  11. Context
  12. } from '@jupyterlab/docregistry';
  13. import { INotebookModel, NotebookModelFactory } from '@jupyterlab/notebook';
  14. export { NBTestUtils } from './notebook-utils';
  15. export { defaultRenderMime } from './rendermime';
  16. /**
  17. * Test a single emission from a signal.
  18. *
  19. * @param signal - The signal we are listening to.
  20. * @param find - An optional function to determine which emission to test,
  21. * defaulting to the first emission.
  22. * @param test - An optional function which contains the tests for the emission.
  23. * @param value - An optional value that the promise resolves to if it is
  24. * successful.
  25. *
  26. * @returns a promise that rejects if the function throws an error (e.g., if an
  27. * expect test doesn't pass), and resolves otherwise.
  28. *
  29. * #### Notes
  30. * The first emission for which the find function returns true will be tested in
  31. * the test function. If the find function is not given, the first signal
  32. * emission will be tested.
  33. *
  34. * You can test to see if any signal comes which matches a criteria by just
  35. * giving a find function. You can test the very first signal by just giving a
  36. * test function. And you can test the first signal matching the find criteria
  37. * by giving both.
  38. *
  39. * The reason this function is asynchronous is so that the thing causing the
  40. * signal emission (such as a websocket message) can be asynchronous.
  41. */
  42. export async function testEmission<T, U, V>(
  43. signal: ISignal<T, U>,
  44. options: {
  45. find?: (a: T, b: U) => boolean;
  46. test?: (a: T, b: U) => void;
  47. value?: V;
  48. }
  49. ): Promise<V> {
  50. const done = new PromiseDelegate<V>();
  51. const object = {};
  52. signal.connect((sender: T, args: U) => {
  53. if (!options.find || options.find(sender, args)) {
  54. try {
  55. Signal.disconnectReceiver(object);
  56. if (options.test) {
  57. options.test(sender, args);
  58. }
  59. } catch (e) {
  60. done.reject(e);
  61. }
  62. done.resolve(options.value || undefined);
  63. }
  64. }, object);
  65. return done.promise;
  66. }
  67. /**
  68. * Expect a failure on a promise with the given message.
  69. */
  70. export async function expectFailure(
  71. promise: Promise<any>,
  72. message?: string
  73. ): Promise<void> {
  74. let called = false;
  75. try {
  76. await promise;
  77. called = true;
  78. } catch (err) {
  79. if (message && err.message.indexOf(message) === -1) {
  80. throw Error(`Error "${message}" not in: "${err.message}"`);
  81. }
  82. }
  83. if (called) {
  84. throw Error(`Failure was not triggered, message was: ${message}`);
  85. }
  86. }
  87. /**
  88. * Do something in the future ensuring total ordering with respect to promises.
  89. */
  90. export async function doLater(cb: () => void): Promise<void> {
  91. await Promise.resolve(void 0);
  92. cb();
  93. }
  94. /**
  95. * Convert a signal into an array of promises.
  96. *
  97. * @param signal - The signal we are listening to.
  98. * @param numberValues - The number of values to store.
  99. *
  100. * @returns a Promise that resolves with an array of `(sender, args)` pairs.
  101. */
  102. export function signalToPromises<T, U>(
  103. signal: ISignal<T, U>,
  104. numberValues: number
  105. ): Promise<[T, U]>[] {
  106. const values: Promise<[T, U]>[] = new Array(numberValues);
  107. const resolvers: Array<(value: [T, U]) => void> = new Array(numberValues);
  108. for (let i = 0; i < numberValues; i++) {
  109. values[i] = new Promise<[T, U]>(resolve => {
  110. resolvers[i] = resolve;
  111. });
  112. }
  113. let current = 0;
  114. function slot(sender: T, args: U) {
  115. resolvers[current++]([sender, args]);
  116. if (current === numberValues) {
  117. cleanup();
  118. }
  119. }
  120. signal.connect(slot);
  121. function cleanup() {
  122. signal.disconnect(slot);
  123. }
  124. return values;
  125. }
  126. /**
  127. * Convert a signal into a promise for the first emitted value.
  128. *
  129. * @param signal - The signal we are listening to.
  130. *
  131. * @returns a Promise that resolves with a `(sender, args)` pair.
  132. */
  133. export function signalToPromise<T, U>(signal: ISignal<T, U>): Promise<[T, U]> {
  134. return signalToPromises(signal, 1)[0];
  135. }
  136. /**
  137. * Test to see if a promise is fulfilled.
  138. *
  139. * @param delay - optional delay in milliseconds before checking
  140. * @returns true if the promise is fulfilled (either resolved or rejected), and
  141. * false if the promise is still pending.
  142. */
  143. export async function isFulfilled<T>(
  144. p: PromiseLike<T>,
  145. delay = 0
  146. ): Promise<boolean> {
  147. let x = Object.create(null);
  148. let race: any;
  149. if (delay > 0) {
  150. race = sleep(delay, x);
  151. } else {
  152. race = x;
  153. }
  154. let result = await Promise.race([p, race]).catch(() => false);
  155. return result !== x;
  156. }
  157. /**
  158. * Convert a requestAnimationFrame into a Promise.
  159. */
  160. export function framePromise(): Promise<void> {
  161. const done = new PromiseDelegate<void>();
  162. requestAnimationFrame(() => {
  163. done.resolve(void 0);
  164. });
  165. return done.promise;
  166. }
  167. /**
  168. * Return a promise that resolves in the given milliseconds with the given value.
  169. */
  170. export function sleep<T>(milliseconds: number = 0, value?: T): Promise<T> {
  171. return new Promise((resolve, reject) => {
  172. setTimeout(() => {
  173. resolve(value);
  174. }, milliseconds);
  175. });
  176. }
  177. /**
  178. * Create a client session object.
  179. */
  180. export async function createSessionContext(
  181. options: Partial<SessionContext.IOptions> = {}
  182. ): Promise<SessionContext> {
  183. const manager = options.sessionManager ?? Private.getManager().sessions;
  184. const specsManager = options.specsManager ?? Private.getManager().kernelspecs;
  185. await manager.ready;
  186. return new SessionContext({
  187. sessionManager: manager,
  188. specsManager,
  189. path: options.path || UUID.uuid4(),
  190. name: options.name,
  191. type: options.type,
  192. kernelPreference: options.kernelPreference || {
  193. shouldStart: true,
  194. canStart: true,
  195. name: specsManager.specs.default
  196. }
  197. });
  198. }
  199. /**
  200. * Create a session and return a session connection.
  201. */
  202. export async function createSession(
  203. options: Session.IRequest
  204. ): Promise<Session.ISessionConnection> {
  205. const manager = Private.getManager().sessions;
  206. await manager.ready;
  207. return manager.startNew(options);
  208. }
  209. /**
  210. * Create a context for a file.
  211. */
  212. export function createFileContext(
  213. path?: string,
  214. manager?: ServiceManager.IManager
  215. ): Context<DocumentRegistry.IModel> {
  216. const factory = Private.textFactory;
  217. manager = manager || Private.getManager();
  218. path = path || UUID.uuid4() + '.txt';
  219. return new Context({ manager, factory, path });
  220. }
  221. /**
  222. * Create and initialize context for a notebook.
  223. */
  224. export async function initNotebookContext(
  225. options: {
  226. path?: string;
  227. manager?: ServiceManager.IManager;
  228. startKernel?: boolean;
  229. } = {}
  230. ): Promise<Context<INotebookModel>> {
  231. const factory = Private.notebookFactory;
  232. const manager = options.manager || Private.getManager();
  233. const path = options.path || UUID.uuid4() + '.ipynb';
  234. const startKernel =
  235. options.startKernel === undefined ? false : options.startKernel;
  236. await manager.ready;
  237. let context = new Context({
  238. manager,
  239. factory,
  240. path,
  241. kernelPreference: {
  242. shouldStart: startKernel,
  243. canStart: startKernel,
  244. shutdownOnDispose: true,
  245. name: manager.kernelspecs.specs.default
  246. }
  247. });
  248. await context.initialize(true);
  249. if (startKernel) {
  250. await context.sessionContext.initialize();
  251. await context.sessionContext.kernel.info;
  252. }
  253. return context;
  254. }
  255. /**
  256. * Wait for a dialog to be attached to an element.
  257. */
  258. export async function waitForDialog(
  259. host: HTMLElement = document.body,
  260. timeout: number = 250
  261. ): Promise<void> {
  262. const interval = 25;
  263. const limit = Math.floor(timeout / interval);
  264. for (let counter = 0; counter < limit; counter++) {
  265. if (host.getElementsByClassName('jp-Dialog')[0]) {
  266. return;
  267. }
  268. await sleep(interval);
  269. }
  270. throw new Error('Dialog not found');
  271. }
  272. /**
  273. * Accept a dialog after it is attached by accepting the default button.
  274. */
  275. export async function acceptDialog(
  276. host: HTMLElement = document.body,
  277. timeout: number = 250
  278. ): Promise<void> {
  279. await waitForDialog(host, timeout);
  280. const node = host.getElementsByClassName('jp-Dialog')[0];
  281. if (node) {
  282. simulate(node as HTMLElement, 'keydown', { keyCode: 13 });
  283. }
  284. }
  285. /**
  286. * Dismiss a dialog after it is attached.
  287. *
  288. * #### Notes
  289. * This promise will always resolve successfully.
  290. */
  291. export async function dismissDialog(
  292. host: HTMLElement = document.body,
  293. timeout: number = 250
  294. ): Promise<void> {
  295. try {
  296. await waitForDialog(host, timeout);
  297. } catch (error) {
  298. return; // Ignore calls to dismiss the dialog if there is no dialog.
  299. }
  300. const node = host.getElementsByClassName('jp-Dialog')[0];
  301. if (node) {
  302. simulate(node as HTMLElement, 'keydown', { keyCode: 27 });
  303. }
  304. }
  305. /**
  306. * A namespace for private data.
  307. */
  308. namespace Private {
  309. let manager: ServiceManager;
  310. export const textFactory = new TextModelFactory();
  311. export const notebookFactory = new NotebookModelFactory({});
  312. /**
  313. * Get or create the service manager singleton.
  314. */
  315. export function getManager(): ServiceManager {
  316. if (!manager) {
  317. manager = new ServiceManager({ standby: 'never' });
  318. }
  319. return manager;
  320. }
  321. }