cacheable-object.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import t from 'tap';
  2. import CacheableObject from '#cacheable-object';
  3. function newCacheableObject(PD) {
  4. return new (class extends CacheableObject {
  5. static [CacheableObject.propertyDescriptors] = PD;
  6. });
  7. }
  8. t.test(`CacheableObject simple separate update & expose`, t => {
  9. const obj = newCacheableObject({
  10. number: {
  11. flags: {
  12. update: true
  13. }
  14. },
  15. timesTwo: {
  16. flags: {
  17. expose: true
  18. },
  19. expose: {
  20. dependencies: ['number'],
  21. compute: ({ number }) => number * 2
  22. }
  23. }
  24. });
  25. t.plan(1);
  26. obj.number = 5;
  27. t.equal(obj.timesTwo, 10);
  28. });
  29. t.test(`CacheableObject basic cache behavior`, t => {
  30. let computeCount = 0;
  31. const obj = newCacheableObject({
  32. string: {
  33. flags: {
  34. update: true
  35. }
  36. },
  37. karkat: {
  38. flags: {
  39. expose: true
  40. },
  41. expose: {
  42. dependencies: ['string'],
  43. compute: ({ string }) => {
  44. computeCount++;
  45. return string.toUpperCase();
  46. }
  47. }
  48. }
  49. });
  50. t.plan(8);
  51. t.equal(computeCount, 0);
  52. obj.string = 'hello world';
  53. t.equal(computeCount, 0);
  54. obj.karkat;
  55. t.equal(computeCount, 1);
  56. obj.karkat;
  57. t.equal(computeCount, 1);
  58. obj.string = 'testing once again';
  59. t.equal(computeCount, 1);
  60. obj.karkat;
  61. t.equal(computeCount, 2);
  62. obj.string = 'testing once again';
  63. t.equal(computeCount, 2);
  64. obj.karkat;
  65. t.equal(computeCount, 2);
  66. });
  67. t.test(`CacheableObject combined update & expose (no transform)`, t => {
  68. const obj = newCacheableObject({
  69. directory: {
  70. flags: {
  71. update: true,
  72. expose: true
  73. }
  74. }
  75. });
  76. t.plan(2);
  77. obj.directory = 'the-world-revolving';
  78. t.equal(obj.directory, 'the-world-revolving');
  79. obj.directory = 'chaos-king';
  80. t.equal(obj.directory, 'chaos-king');
  81. });
  82. t.test(`CacheableObject combined update & expose (basic transform)`, t => {
  83. const obj = newCacheableObject({
  84. getsRepeated: {
  85. flags: {
  86. update: true,
  87. expose: true
  88. },
  89. expose: {
  90. transform: value => value.repeat(2)
  91. }
  92. }
  93. });
  94. t.plan(1);
  95. obj.getsRepeated = 'dog';
  96. t.equal(obj.getsRepeated, 'dogdog');
  97. });
  98. t.test(`CacheableObject combined update & expose (transform with dependency)`, t => {
  99. const obj = newCacheableObject({
  100. customRepeat: {
  101. flags: {
  102. update: true,
  103. expose: true
  104. },
  105. expose: {
  106. dependencies: ['times'],
  107. transform: (value, { times }) => value.repeat(times)
  108. }
  109. },
  110. times: {
  111. flags: {
  112. update: true
  113. }
  114. }
  115. });
  116. t.plan(3);
  117. obj.customRepeat = 'dog';
  118. obj.times = 1;
  119. t.equal(obj.customRepeat, 'dog');
  120. obj.times = 5;
  121. t.equal(obj.customRepeat, 'dogdogdogdogdog');
  122. obj.customRepeat = 'cat';
  123. t.equal(obj.customRepeat, 'catcatcatcatcat');
  124. });
  125. t.test(`CacheableObject validate on update`, t => {
  126. const mockError = new TypeError(`Expected a string, not ${typeof value}`);
  127. const obj = newCacheableObject({
  128. directory: {
  129. flags: {
  130. update: true,
  131. expose: true
  132. },
  133. update: {
  134. validate: value => {
  135. if (typeof value !== 'string') {
  136. throw mockError;
  137. }
  138. return true;
  139. }
  140. }
  141. },
  142. date: {
  143. flags: {
  144. update: true,
  145. expose: true
  146. },
  147. update: {
  148. validate: value => (value instanceof Date)
  149. }
  150. }
  151. });
  152. let thrownError;
  153. t.plan(6);
  154. obj.directory = 'megalovania';
  155. t.equal(obj.directory, 'megalovania');
  156. t.throws(
  157. () => { obj.directory = 25; },
  158. {cause: mockError});
  159. t.equal(obj.directory, 'megalovania');
  160. const date = new Date(`25 December 2009`);
  161. obj.date = date;
  162. t.equal(obj.date, date);
  163. t.throws(
  164. () => { obj.date = `TWELFTH PERIGEE'S EVE`; },
  165. {cause: TypeError});
  166. t.equal(obj.date, date);
  167. });
  168. t.test(`CacheableObject transform on null value`, t => {
  169. let computed = false;
  170. const obj = newCacheableObject({
  171. spookyFactor: {
  172. flags: {
  173. update: true,
  174. expose: true,
  175. },
  176. expose: {
  177. transform: value => {
  178. computed = true;
  179. return (value ? 2 * value : -1);
  180. },
  181. },
  182. },
  183. });
  184. t.plan(4);
  185. t.equal(obj.spookyFactor, -1);
  186. t.ok(computed);
  187. computed = false;
  188. obj.spookyFactor = 1;
  189. t.equal(obj.spookyFactor, 2);
  190. t.ok(computed);
  191. });
  192. t.test(`CacheableObject don't transform on successful update`, t => {
  193. let computed = false;
  194. const obj = newCacheableObject({
  195. original: {
  196. flags: {
  197. update: true,
  198. expose: true,
  199. },
  200. update: {
  201. validate: value => value.startsWith('track:'),
  202. },
  203. expose: {
  204. transform: value => {
  205. computed = true;
  206. return (value ? value.split(':')[1] : null);
  207. },
  208. },
  209. },
  210. });
  211. t.plan(4);
  212. t.doesNotThrow(() => obj.original = 'track:foo');
  213. t.notOk(computed);
  214. t.equal(obj.original, 'foo');
  215. t.ok(computed);
  216. });
  217. t.test(`CacheableObject don't transform on failed update`, t => {
  218. let computed = false;
  219. const obj = newCacheableObject({
  220. original: {
  221. flags: {
  222. update: true,
  223. expose: true,
  224. },
  225. update: {
  226. validate: value => value.startsWith('track:'),
  227. },
  228. expose: {
  229. transform: value => {
  230. computed = true;
  231. return (value ? value.split(':')[1] : null);
  232. },
  233. },
  234. },
  235. });
  236. t.plan(4);
  237. t.throws(() => obj.original = 'album:foo');
  238. t.notOk(computed);
  239. t.equal(obj.original, null);
  240. t.ok(computed);
  241. });
  242. t.test(`CacheableObject default update property value`, t => {
  243. const obj = newCacheableObject({
  244. fruit: {
  245. flags: {
  246. update: true,
  247. expose: true
  248. },
  249. update: {
  250. default: 'potassium'
  251. }
  252. }
  253. });
  254. t.plan(1);
  255. t.equal(obj.fruit, 'potassium');
  256. });
  257. t.test(`CacheableObject default property throws if invalid`, t => {
  258. const mockError = new TypeError(`Expected a string, not ${typeof value}`);
  259. t.plan(1);
  260. let thrownError;
  261. t.throws(
  262. () => newCacheableObject({
  263. string: {
  264. flags: {
  265. update: true
  266. },
  267. update: {
  268. default: 123,
  269. validate: value => {
  270. if (typeof value !== 'string') {
  271. throw mockError;
  272. }
  273. return true;
  274. }
  275. }
  276. }
  277. }),
  278. {cause: mockError});
  279. });