settingregistry.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import 'jest';
  4. import {
  5. DefaultSchemaValidator,
  6. ISettingRegistry,
  7. SettingRegistry,
  8. Settings
  9. } from '@jupyterlab/settingregistry';
  10. import { StateDB } from '@jupyterlab/statedb';
  11. import { signalToPromise } from '@jupyterlab/testutils';
  12. import { JSONObject } from '@lumino/coreutils';
  13. class TestConnector extends StateDB {
  14. schemas: { [key: string]: ISettingRegistry.ISchema } = {};
  15. async fetch(id: string): Promise<ISettingRegistry.IPlugin | undefined> {
  16. const fetched = await super.fetch(id);
  17. if (!fetched && !this.schemas[id]) {
  18. return undefined;
  19. }
  20. const schema: ISettingRegistry.ISchema = this.schemas[id] || {
  21. type: 'object'
  22. };
  23. const composite = {};
  24. const user = {};
  25. const raw = (fetched as string) || '{ }';
  26. const version = 'test';
  27. return { id, data: { composite, user }, raw, schema, version };
  28. }
  29. async list(): Promise<any> {
  30. return Promise.reject('list method not implemented');
  31. }
  32. }
  33. describe('@jupyterlab/settingregistry', () => {
  34. describe('DefaultSchemaValidator', () => {
  35. describe('#constructor()', () => {
  36. it('should create a new schema validator', () => {
  37. const validator = new DefaultSchemaValidator();
  38. expect(validator).toBeInstanceOf(DefaultSchemaValidator);
  39. });
  40. });
  41. describe('#validateData()', () => {
  42. it('should validate data against a schema', () => {
  43. const id = 'foo';
  44. const validator = new DefaultSchemaValidator();
  45. const schema: ISettingRegistry.ISchema = {
  46. additionalProperties: false,
  47. properties: {
  48. bar: { type: 'string' }
  49. },
  50. type: 'object'
  51. };
  52. const composite = {};
  53. const user = {};
  54. const raw = '{ "bar": "baz" }';
  55. const version = 'test';
  56. const plugin = { id, data: { composite, user }, raw, schema, version };
  57. const errors = validator.validateData(plugin);
  58. expect(errors).toBe(null);
  59. });
  60. it('should return errors if the data fails to validate', () => {
  61. const id = 'foo';
  62. const validator = new DefaultSchemaValidator();
  63. const schema: ISettingRegistry.ISchema = {
  64. additionalProperties: false,
  65. properties: {
  66. bar: { type: 'string' }
  67. },
  68. type: 'object'
  69. };
  70. const composite = {};
  71. const user = {};
  72. const raw = '{ "baz": "qux" }';
  73. const version = 'test';
  74. const plugin = { id, data: { composite, user }, raw, schema, version };
  75. const errors = validator.validateData(plugin);
  76. expect(errors).not.toBe(null);
  77. });
  78. it('should populate the composite data', () => {
  79. const id = 'foo';
  80. const validator = new DefaultSchemaValidator();
  81. const schema: ISettingRegistry.ISchema = {
  82. additionalProperties: false,
  83. properties: {
  84. bar: { type: 'string', default: 'baz' }
  85. },
  86. type: 'object'
  87. };
  88. const composite = {} as JSONObject;
  89. const user = {} as JSONObject;
  90. const raw = '{ }';
  91. const version = 'test';
  92. const plugin = { id, data: { composite, user }, raw, schema, version };
  93. const errors = validator.validateData(plugin);
  94. expect(errors).toBe(null);
  95. expect(plugin.data.user.bar).toBeUndefined();
  96. expect(plugin.data.composite.bar).toBe(schema.properties!.bar.default);
  97. });
  98. });
  99. });
  100. describe('SettingRegistry', () => {
  101. const connector = new TestConnector();
  102. const timeout = 500;
  103. let registry: SettingRegistry;
  104. afterEach(() => {
  105. connector.schemas = {};
  106. return connector.clear();
  107. });
  108. beforeEach(() => {
  109. registry = new SettingRegistry({ connector, timeout });
  110. });
  111. describe('#constructor()', () => {
  112. it('should create a new setting registry', () => {
  113. expect(registry).toBeInstanceOf(SettingRegistry);
  114. });
  115. });
  116. describe('#pluginChanged', () => {
  117. it('should emit when a plugin changes', async () => {
  118. const id = 'foo';
  119. const key = 'bar';
  120. const value = 'baz';
  121. connector.schemas[id] = { type: 'object' };
  122. let called = false;
  123. registry.pluginChanged.connect((sender: any, plugin: string) => {
  124. expect(id).toBe(plugin);
  125. called = true;
  126. });
  127. await registry.load(id);
  128. await registry.set(id, key, value);
  129. expect(called).toBe(true);
  130. });
  131. });
  132. describe('#plugins', () => {
  133. it('should return a list of registered plugins in registry', async () => {
  134. const one = 'foo';
  135. const two = 'bar';
  136. expect(Object.keys(registry.plugins)).toHaveLength(0);
  137. connector.schemas[one] = { type: 'object' };
  138. connector.schemas[two] = { type: 'object' };
  139. await registry.load(one);
  140. expect(Object.keys(registry.plugins)).toHaveLength(1);
  141. await registry.load(two);
  142. expect(Object.keys(registry.plugins)).toHaveLength(2);
  143. });
  144. });
  145. describe('#get()', () => {
  146. it('should get a setting item from a loaded plugin', async () => {
  147. const id = 'foo';
  148. const key = 'bar';
  149. const value = 'baz';
  150. connector.schemas[id] = { type: 'object' };
  151. await connector.save(id, JSON.stringify({ [key]: value }));
  152. (await registry.load(id)) as Settings;
  153. const saved = await registry.get(id, key);
  154. expect(saved.user).toBe(value);
  155. });
  156. it('should get a setting item from a plugin that is not loaded', async () => {
  157. const id = 'alpha';
  158. const key = 'beta';
  159. const value = 'gamma';
  160. connector.schemas[id] = { type: 'object' };
  161. await connector.save(id, JSON.stringify({ [key]: value }));
  162. const saved = await registry.get(id, key);
  163. expect(saved.composite).toBe(value);
  164. });
  165. it('should use schema default if user data not available', async () => {
  166. const id = 'alpha';
  167. const key = 'beta';
  168. const value = 'gamma';
  169. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  170. type: 'object',
  171. properties: {
  172. [key]: {
  173. type: typeof value as ISettingRegistry.Primitive,
  174. default: value
  175. }
  176. }
  177. });
  178. const saved = await registry.get(id, key);
  179. expect(saved.composite).toBe(schema.properties![key].default);
  180. expect(saved.composite).not.toBe(saved.user);
  181. });
  182. it('should let user value override schema default', async () => {
  183. const id = 'alpha';
  184. const key = 'beta';
  185. const value = 'gamma';
  186. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  187. type: 'object',
  188. properties: {
  189. [key]: {
  190. type: typeof value as ISettingRegistry.Primitive,
  191. default: 'delta'
  192. }
  193. }
  194. });
  195. await connector.save(id, JSON.stringify({ [key]: value }));
  196. const saved = await registry.get(id, key);
  197. expect(saved.composite).toBe(value);
  198. expect(saved.user).toBe(value);
  199. expect(saved.composite).not.toBe(schema.properties![key].default);
  200. expect(saved.user).not.toBe(schema.properties![key].default);
  201. });
  202. it('should reject if a plugin does not exist', async () => {
  203. let failed = false;
  204. try {
  205. await registry.get('foo', 'bar');
  206. } catch (e) {
  207. failed = true;
  208. }
  209. expect(failed).toBe(true);
  210. });
  211. it('should resolve `undefined` if a key does not exist', async () => {
  212. const id = 'foo';
  213. const key = 'bar';
  214. connector.schemas[id] = { type: 'object' };
  215. const saved = await registry.get(id, key);
  216. expect(saved.composite).toBeUndefined();
  217. expect(saved.user).toBeUndefined();
  218. });
  219. });
  220. describe('#load()', () => {
  221. it(`should resolve a registered plugin's settings`, async () => {
  222. const id = 'foo';
  223. expect(Object.keys(registry.plugins)).toHaveLength(0);
  224. connector.schemas[id] = { type: 'object' };
  225. const settings = (await registry.load(id)) as Settings;
  226. expect(settings.id).toBe(id);
  227. });
  228. it(`should reject if a plugin transformation times out`, async () => {
  229. const id = 'foo';
  230. let failed = false;
  231. connector.schemas[id] = {
  232. 'jupyter.lab.transform': true,
  233. type: 'object'
  234. };
  235. try {
  236. await registry.load(id);
  237. } catch (e) {
  238. failed = true;
  239. }
  240. expect(failed).toBe(true);
  241. });
  242. it('should reject if a plugin does not exist', async () => {
  243. let failed = false;
  244. try {
  245. await registry.load('foo');
  246. } catch (e) {
  247. failed = true;
  248. }
  249. expect(failed).toBe(true);
  250. });
  251. });
  252. describe('#reload()', () => {
  253. it(`should load a registered plugin's settings`, async () => {
  254. const id = 'foo';
  255. expect(Object.keys(registry.plugins)).toHaveLength(0);
  256. connector.schemas[id] = { type: 'object' };
  257. const settings = await registry.reload(id);
  258. expect(settings.id).toBe(id);
  259. });
  260. it(`should replace a registered plugin's settings`, async () => {
  261. const id = 'foo';
  262. const first = 'Foo';
  263. const second = 'Bar';
  264. expect(Object.keys(registry.plugins)).toHaveLength(0);
  265. connector.schemas[id] = { type: 'object', title: first };
  266. let settings = await registry.reload(id);
  267. expect(settings.schema.title).toBe(first);
  268. await Promise.resolve(undefined);
  269. connector.schemas[id].title = second;
  270. settings = await registry.reload(id);
  271. expect(settings.schema.title).toBe(second);
  272. });
  273. it('should reject if a plugin does not exist', async () => {
  274. let failed = false;
  275. try {
  276. await registry.reload('foo');
  277. } catch (e) {
  278. failed = true;
  279. }
  280. expect(failed).toBe(true);
  281. });
  282. });
  283. describe('#transform()', () => {
  284. it(`should transform a plugin during the fetch phase`, async () => {
  285. const id = 'foo';
  286. const version = 'transform-test';
  287. expect(Object.keys(registry.plugins)).toHaveLength(0);
  288. connector.schemas[id] = {
  289. 'jupyter.lab.transform': true,
  290. type: 'object'
  291. };
  292. registry.transform(id, {
  293. fetch: plugin => {
  294. plugin.version = version;
  295. return plugin;
  296. }
  297. });
  298. expect((await registry.load(id)).version).toBe(version);
  299. });
  300. it(`should transform a plugin during the compose phase`, async () => {
  301. const id = 'foo';
  302. const composite = { a: 1 };
  303. expect(Object.keys(registry.plugins)).toHaveLength(0);
  304. connector.schemas[id] = {
  305. 'jupyter.lab.transform': true,
  306. type: 'object'
  307. };
  308. registry.transform(id, {
  309. compose: plugin => {
  310. plugin.data = { user: plugin.data.user, composite };
  311. return plugin;
  312. }
  313. });
  314. expect((await registry.load(id)).composite).toEqual(composite);
  315. });
  316. it(`should disallow a transform that changes the plugin ID`, async () => {
  317. const id = 'foo';
  318. let failed = false;
  319. expect(Object.keys(registry.plugins)).toHaveLength(0);
  320. connector.schemas[id] = {
  321. 'jupyter.lab.transform': true,
  322. type: 'object'
  323. };
  324. registry.transform(id, {
  325. compose: plugin => {
  326. plugin.id = 'bar';
  327. return plugin;
  328. }
  329. });
  330. try {
  331. await registry.load(id);
  332. } catch (e) {
  333. failed = true;
  334. }
  335. expect(failed).toBe(true);
  336. });
  337. });
  338. });
  339. describe('Settings', () => {
  340. const connector = new TestConnector();
  341. let registry: SettingRegistry;
  342. let settings: Settings | null;
  343. afterEach(() => {
  344. if (settings) {
  345. settings.dispose();
  346. settings = null;
  347. }
  348. connector.schemas = {};
  349. return connector.clear();
  350. });
  351. beforeEach(() => {
  352. registry = new SettingRegistry({ connector });
  353. });
  354. describe('#constructor()', () => {
  355. it('should create a new settings object for a plugin', () => {
  356. const id = 'alpha';
  357. const data = { composite: {}, user: {} };
  358. const schema: ISettingRegistry.ISchema = { type: 'object' };
  359. const raw = '{ }';
  360. const version = 'test';
  361. const plugin = { id, data, raw, schema, version };
  362. settings = new Settings({ plugin, registry });
  363. expect(settings).toBeInstanceOf(Settings);
  364. });
  365. });
  366. describe('#changed', () => {
  367. it('should emit when a plugin changes', async () => {
  368. const id = 'alpha';
  369. const schema: ISettingRegistry.ISchema = { type: 'object' };
  370. connector.schemas[id] = schema;
  371. settings = (await registry.load(id)) as Settings;
  372. const promise = signalToPromise(settings.changed);
  373. await settings.set('foo', 'bar');
  374. await promise;
  375. });
  376. });
  377. describe('#composite', () => {
  378. it('should contain the merged user and default data', async () => {
  379. const id = 'alpha';
  380. const key = 'beta';
  381. const value = 'gamma';
  382. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  383. type: 'object',
  384. properties: {
  385. [key]: {
  386. type: typeof value as ISettingRegistry.Primitive,
  387. default: value
  388. }
  389. }
  390. });
  391. connector.schemas[id] = schema;
  392. settings = (await registry.load(id)) as Settings;
  393. expect(settings.composite[key]).toBe(value);
  394. });
  395. it('should privilege user data', async () => {
  396. const id = 'alpha';
  397. const key = 'beta';
  398. const value = 'gamma';
  399. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  400. type: 'object',
  401. properties: {
  402. [key]: {
  403. type: typeof value as ISettingRegistry.Primitive,
  404. default: 'delta'
  405. }
  406. }
  407. });
  408. connector.schemas[id] = schema;
  409. settings = (await registry.load(id)) as Settings;
  410. await settings.set(key, value);
  411. expect(settings.composite[key]).toBe(value);
  412. });
  413. });
  414. describe('#id', () => {
  415. it('should expose the plugin ID', () => {
  416. const id = 'alpha';
  417. const data = { composite: {}, user: {} };
  418. const schema: ISettingRegistry.ISchema = { type: 'object' };
  419. const raw = '{ }';
  420. const version = 'test';
  421. const plugin = { id, data, raw, schema, version };
  422. settings = new Settings({ plugin, registry });
  423. expect(settings.id).toBe(id);
  424. });
  425. });
  426. describe('#isDisposed', () => {
  427. it('should test whether the settings object is disposed', () => {
  428. const id = 'alpha';
  429. const data = { composite: {}, user: {} };
  430. const schema: ISettingRegistry.ISchema = { type: 'object' };
  431. const raw = '{ }';
  432. const version = 'test';
  433. const plugin = { id, data, raw, schema, version };
  434. settings = new Settings({ plugin, registry });
  435. expect(settings.isDisposed).toBe(false);
  436. settings.dispose();
  437. expect(settings.isDisposed).toBe(true);
  438. });
  439. });
  440. describe('#schema', () => {
  441. it('should expose the plugin schema', async () => {
  442. const id = 'alpha';
  443. const schema: ISettingRegistry.ISchema = { type: 'object' };
  444. connector.schemas[id] = schema;
  445. settings = (await registry.load(id)) as Settings;
  446. expect(settings.schema).toEqual(schema);
  447. });
  448. });
  449. describe('#user', () => {
  450. it('should privilege user data', async () => {
  451. const id = 'alpha';
  452. const key = 'beta';
  453. const value = 'gamma';
  454. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  455. type: 'object',
  456. properties: {
  457. [key]: {
  458. type: typeof value as ISettingRegistry.Primitive,
  459. default: 'delta'
  460. }
  461. }
  462. });
  463. connector.schemas[id] = schema;
  464. settings = (await registry.load(id)) as Settings;
  465. await settings.set(key, value);
  466. expect(settings.user[key]).toBe(value);
  467. });
  468. });
  469. describe('#registry', () => {
  470. it('should expose the setting registry', () => {
  471. const id = 'alpha';
  472. const data = { composite: {}, user: {} };
  473. const schema: ISettingRegistry.ISchema = { type: 'object' };
  474. const raw = '{ }';
  475. const version = 'test';
  476. const plugin = { id, data, raw, schema, version };
  477. settings = new Settings({ plugin, registry });
  478. expect(settings.registry).toBe(registry);
  479. });
  480. });
  481. describe('#dispose()', () => {
  482. it('should dispose the settings object', () => {
  483. const id = 'alpha';
  484. const data = { composite: {}, user: {} };
  485. const schema: ISettingRegistry.ISchema = { type: 'object' };
  486. const raw = '{ }';
  487. const version = 'test';
  488. const plugin = { id, data, raw, schema, version };
  489. settings = new Settings({ plugin, registry });
  490. expect(settings.isDisposed).toBe(false);
  491. settings.dispose();
  492. expect(settings.isDisposed).toBe(true);
  493. });
  494. });
  495. describe('#default()', () => {
  496. it('should return a fully extrapolated schema default', async () => {
  497. const id = 'omicron';
  498. const defaults = {
  499. foo: 'one',
  500. bar: 100,
  501. baz: {
  502. qux: 'two',
  503. quux: 'three',
  504. quuz: {
  505. corge: { grault: 200 }
  506. }
  507. }
  508. };
  509. connector.schemas[id] = {
  510. type: 'object',
  511. properties: {
  512. foo: { type: 'string', default: defaults.foo },
  513. bar: { type: 'number', default: defaults.bar },
  514. baz: {
  515. type: 'object',
  516. default: {},
  517. properties: {
  518. qux: { type: 'string', default: defaults.baz.qux },
  519. quux: { type: 'string', default: defaults.baz.quux },
  520. quuz: {
  521. type: 'object',
  522. default: {},
  523. properties: {
  524. corge: {
  525. type: 'object',
  526. default: {},
  527. properties: {
  528. grault: {
  529. type: 'number',
  530. default: defaults.baz.quuz.corge.grault
  531. }
  532. }
  533. }
  534. }
  535. }
  536. }
  537. },
  538. 'nonexistent-default': { type: 'string' }
  539. }
  540. };
  541. settings = (await registry.load(id)) as Settings;
  542. expect(settings.default('nonexistent-key')).toBeUndefined();
  543. expect(settings.default('foo')).toBe(defaults.foo);
  544. expect(settings.default('bar')).toBe(defaults.bar);
  545. expect(settings.default('baz')).toEqual(defaults.baz);
  546. expect(settings.default('nonexistent-default')).toBeUndefined();
  547. });
  548. });
  549. describe('#get()', () => {
  550. it('should get a setting item', async () => {
  551. const id = 'foo';
  552. const key = 'bar';
  553. const value = 'baz';
  554. connector.schemas[id] = { type: 'object' };
  555. await connector.save(id, JSON.stringify({ [key]: value }));
  556. settings = (await registry.load(id)) as Settings;
  557. const saved = settings.get(key);
  558. expect(saved.user).toBe(value);
  559. });
  560. it('should use schema default if user data not available', async () => {
  561. const id = 'alpha';
  562. const key = 'beta';
  563. const value = 'gamma';
  564. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  565. type: 'object',
  566. properties: {
  567. [key]: {
  568. type: typeof value as ISettingRegistry.Primitive,
  569. default: value
  570. }
  571. }
  572. });
  573. settings = (await registry.load(id)) as Settings;
  574. const saved = settings.get(key);
  575. expect(saved.composite).toBe(schema.properties![key].default);
  576. expect(saved.composite).not.toBe(saved.user);
  577. });
  578. it('should let user value override schema default', async () => {
  579. const id = 'alpha';
  580. const key = 'beta';
  581. const value = 'gamma';
  582. const schema: ISettingRegistry.ISchema = (connector.schemas[id] = {
  583. type: 'object',
  584. properties: {
  585. [key]: {
  586. type: typeof value as ISettingRegistry.Primitive,
  587. default: 'delta'
  588. }
  589. }
  590. });
  591. await connector.save(id, JSON.stringify({ [key]: value }));
  592. settings = (await registry.load(id)) as Settings;
  593. const saved = settings.get(key);
  594. expect(saved.composite).toBe(value);
  595. expect(saved.user).toBe(value);
  596. expect(saved.composite).not.toBe(schema.properties![key].default);
  597. expect(saved.user).not.toBe(schema.properties![key].default);
  598. });
  599. it('should be `undefined` if a key does not exist', async () => {
  600. const id = 'foo';
  601. const key = 'bar';
  602. connector.schemas[id] = { type: 'object' };
  603. settings = (await registry.load(id)) as Settings;
  604. const saved = settings.get(key);
  605. expect(saved.composite).toBeUndefined();
  606. expect(saved.user).toBeUndefined();
  607. });
  608. });
  609. describe('#remove()', () => {
  610. it('should remove a setting item', async () => {
  611. const id = 'foo';
  612. const key = 'bar';
  613. const value = 'baz';
  614. connector.schemas[id] = { type: 'object' };
  615. await connector.save(id, JSON.stringify({ [key]: value }));
  616. settings = (await registry.load(id)) as Settings;
  617. let saved = settings.get(key);
  618. expect(saved.user).toBe(value);
  619. await settings.remove(key);
  620. saved = settings.get(key);
  621. expect(saved.composite).toBeUndefined();
  622. expect(saved.user).toBeUndefined();
  623. });
  624. });
  625. describe('#save()', () => {
  626. it('should save user setting data', async () => {
  627. const id = 'foo';
  628. const one = 'one';
  629. const two = 'two';
  630. connector.schemas[id] = { type: 'object' };
  631. settings = (await registry.load(id)) as Settings;
  632. await settings.save(JSON.stringify({ one, two }));
  633. let saved = settings.get('one');
  634. expect(saved.composite).toBe(one);
  635. expect(saved.user).toBe(one);
  636. saved = settings.get('two');
  637. expect(saved.composite).toBe(two);
  638. expect(saved.user).toBe(two);
  639. });
  640. });
  641. describe('#set()', () => {
  642. it('should set a user setting item', async () => {
  643. const id = 'foo';
  644. const one = 'one';
  645. connector.schemas[id] = { type: 'object' };
  646. settings = (await registry.load(id)) as Settings;
  647. await settings.set('one', one);
  648. const saved = settings.get('one');
  649. expect(saved.composite).toBe(one);
  650. expect(saved.user).toBe(one);
  651. });
  652. });
  653. });
  654. });