checkout.test.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { readFile } from 'node:fs/promises';
  4. import { parse, compileScript } from '@vue/compiler-sfc';
  5. import ts from 'typescript';
  6. import * as vue from 'vue';
  7. const cartSource = await readFile(new URL('../src/stores/cart.ts', import.meta.url), 'utf8');
  8. const formatSource = await readFile(new URL('../src/utils/format.ts', import.meta.url), 'utf8');
  9. const componentSource = await readFile(
  10. new URL('../src/pages/packages/cart.vue', import.meta.url),
  11. 'utf8',
  12. );
  13. const { descriptor } = parse(componentSource, { filename: 'cart.vue' });
  14. const script = compileScript(descriptor, { id: 'checkout-test' });
  15. function evaluate(source, dependencies) {
  16. const { outputText } = ts.transpileModule(source, {
  17. compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
  18. });
  19. const exports = {};
  20. new Function('require', 'exports', outputText)((name) => {
  21. if (name in dependencies) return dependencies[name];
  22. if (name.endsWith('.vue')) return {};
  23. throw new Error('Unexpected dependency: ' + name);
  24. }, exports);
  25. return exports;
  26. }
  27. globalThis.uni = { showToast() {}, navigateTo() {}, redirectTo() {}, navigateBack() {} };
  28. const product = () => ({
  29. id: 1,
  30. name: '旧套餐',
  31. sku_id: 1,
  32. sku_name: '旧规格',
  33. liters: 18.9,
  34. buckets: 30,
  35. price_cents: 69900,
  36. unit_price_cents: 2330,
  37. category: null,
  38. valid_months: null,
  39. includes_device: null,
  40. });
  41. const quote = (name = '新套餐') => ({
  42. items: [
  43. {
  44. package_id: 1,
  45. name,
  46. sku_id: 2,
  47. quantity: 2,
  48. buckets: 120,
  49. unit_price_cents: 69950,
  50. amount_cents: 139900,
  51. },
  52. ],
  53. amount_cents: 139900,
  54. discount_cents: 0,
  55. quote_revision: 'a'.repeat(64),
  56. });
  57. function deferred() {
  58. let resolve, reject;
  59. const promise = new Promise((yes, no) => {
  60. resolve = yes;
  61. reject = no;
  62. });
  63. return { promise, resolve, reject };
  64. }
  65. function fixture() {
  66. const requests = [];
  67. const commands = [];
  68. const session = vue.reactive({ token: 'token', epoch: 0 });
  69. const format = evaluate(formatSource, {});
  70. const store = evaluate(cartSource, { vue, '../utils/format': format });
  71. store.cart.items.push({ product: product(), quantity: 2 });
  72. const state = evaluate(script.content, {
  73. vue,
  74. '@dcloudio/uni-app': { onShow() {} },
  75. '../../api/client': {
  76. get: async () => ({}),
  77. post: () => {
  78. const pending = deferred();
  79. requests.push(pending);
  80. return pending.promise;
  81. },
  82. },
  83. '../../stores/session': {
  84. session,
  85. canManageEnterprise: () => true,
  86. resourcePath: () => '/enterprises/1',
  87. },
  88. '../../stores/cart': store,
  89. '../../utils/format': format,
  90. '../../composables/useCommand': {
  91. useCommand: () => ({ busy: false, execute: (payload) => commands.push(payload) }),
  92. },
  93. }).default.setup({}, { expose() {} });
  94. state.address.value = { address: '企业地址' };
  95. state.agreed.value = true;
  96. return { state, session, requests, commands, store, format };
  97. }
  98. test('a new server quote refreshes selected price, buckets and SKU without rounding cents', async () => {
  99. const app = fixture();
  100. const request = app.state.calculate();
  101. app.requests[0].resolve(quote());
  102. await request;
  103. assert.equal(app.store.cart.items[0].product.name, '新套餐');
  104. assert.equal(app.store.cart.items[0].product.buckets, 60);
  105. assert.equal(app.store.cart.items[0].product.price_cents, 69950);
  106. assert.equal(app.store.cart.items[0].product.liters, null, 'old SKU dimensions are cleared');
  107. assert.equal(app.format.money(69950), '699.50');
  108. assert.equal(app.state.canSubmit.value, true);
  109. app.state.submit();
  110. assert.equal(app.commands[0].expected_amount_cents, 139900);
  111. assert.equal(app.commands[0].quote_revision, 'a'.repeat(64));
  112. });
  113. test('requote clears the old price immediately and a failed quote cannot be submitted', async () => {
  114. const app = fixture();
  115. app.state.quote.value = quote();
  116. const request = app.state.calculate();
  117. assert.equal(app.state.quote.value, null);
  118. app.state.submit();
  119. assert.equal(app.commands.length, 0);
  120. app.requests[0].reject(new Error('套餐已下架'));
  121. await request;
  122. assert.equal(app.state.canSubmit.value, false);
  123. app.state.submit();
  124. assert.equal(app.commands.length, 0);
  125. });
  126. test('an older quote response cannot overwrite the latest quote', async () => {
  127. const app = fixture();
  128. const old = app.state.calculate();
  129. const latest = app.state.calculate();
  130. app.requests[1].resolve(quote('最新报价'));
  131. await latest;
  132. app.requests[0].resolve(quote('过时报价'));
  133. await old;
  134. assert.equal(app.state.quote.value.items[0].name, '最新报价');
  135. assert.equal(app.store.cart.items[0].product.name, '最新报价');
  136. });
  137. test('identity changes invalidate quote and address and reject a late response', async () => {
  138. const app = fixture();
  139. app.state.quote.value = quote();
  140. const request = app.state.calculate();
  141. app.session.epoch++;
  142. assert.equal(app.state.address.value, null);
  143. assert.equal(app.state.quote.value, null);
  144. app.requests[0].resolve(quote());
  145. await request;
  146. assert.equal(app.state.quote.value, null);
  147. assert.equal(app.store.cart.items[0].product.name, '旧套餐');
  148. assert.equal(app.state.canSubmit.value, false);
  149. });
  150. test('removing the final item invalidates a pending quote and incomplete catalog pages preserve selections', async () => {
  151. const app = fixture();
  152. app.store.refreshCartProducts([{ ...product(), id: 2 }]);
  153. assert.equal(app.store.cart.items.length, 1);
  154. const request = app.state.calculate();
  155. app.store.clearCart();
  156. await app.state.calculate();
  157. app.requests[0].resolve(quote());
  158. await request;
  159. assert.equal(app.state.quote.value, null);
  160. assert.equal(app.state.canSubmit.value, false);
  161. });