import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { parse, compileScript } from '@vue/compiler-sfc'; import ts from 'typescript'; import * as vue from 'vue'; const cartSource = await readFile(new URL('../src/stores/cart.ts', import.meta.url), 'utf8'); const formatSource = await readFile(new URL('../src/utils/format.ts', import.meta.url), 'utf8'); const componentSource = await readFile( new URL('../src/pages/packages/cart.vue', import.meta.url), 'utf8', ); const { descriptor } = parse(componentSource, { filename: 'cart.vue' }); const script = compileScript(descriptor, { id: 'checkout-test' }); function evaluate(source, dependencies) { const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }, }); const exports = {}; new Function('require', 'exports', outputText)((name) => { if (name in dependencies) return dependencies[name]; if (name.endsWith('.vue')) return {}; throw new Error('Unexpected dependency: ' + name); }, exports); return exports; } globalThis.uni = { showToast() {}, navigateTo() {}, redirectTo() {}, navigateBack() {} }; const product = () => ({ id: 1, name: '旧套餐', sku_id: 1, sku_name: '旧规格', liters: 18.9, buckets: 30, price_cents: 69900, unit_price_cents: 2330, category: null, valid_months: null, includes_device: null, }); const quote = (name = '新套餐') => ({ items: [ { package_id: 1, name, sku_id: 2, quantity: 2, buckets: 120, unit_price_cents: 69950, amount_cents: 139900, }, ], amount_cents: 139900, discount_cents: 0, quote_revision: 'a'.repeat(64), }); function deferred() { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } function fixture() { const requests = []; const commands = []; const session = vue.reactive({ token: 'token', epoch: 0 }); const format = evaluate(formatSource, {}); const store = evaluate(cartSource, { vue, '../utils/format': format }); store.cart.items.push({ product: product(), quantity: 2 }); const state = evaluate(script.content, { vue, '@dcloudio/uni-app': { onShow() {} }, '../../api/client': { get: async () => ({}), post: () => { const pending = deferred(); requests.push(pending); return pending.promise; }, }, '../../stores/session': { session, canManageEnterprise: () => true, resourcePath: () => '/enterprises/1', }, '../../stores/cart': store, '../../utils/format': format, '../../composables/useCommand': { useCommand: () => ({ busy: false, execute: (payload) => commands.push(payload) }), }, }).default.setup({}, { expose() {} }); state.address.value = { address: '企业地址' }; state.agreed.value = true; return { state, session, requests, commands, store, format }; } test('a new server quote refreshes selected price, buckets and SKU without rounding cents', async () => { const app = fixture(); const request = app.state.calculate(); app.requests[0].resolve(quote()); await request; assert.equal(app.store.cart.items[0].product.name, '新套餐'); assert.equal(app.store.cart.items[0].product.buckets, 60); assert.equal(app.store.cart.items[0].product.price_cents, 69950); assert.equal(app.store.cart.items[0].product.liters, null, 'old SKU dimensions are cleared'); assert.equal(app.format.money(69950), '699.50'); assert.equal(app.state.canSubmit.value, true); app.state.submit(); assert.equal(app.commands[0].expected_amount_cents, 139900); assert.equal(app.commands[0].quote_revision, 'a'.repeat(64)); }); test('requote clears the old price immediately and a failed quote cannot be submitted', async () => { const app = fixture(); app.state.quote.value = quote(); const request = app.state.calculate(); assert.equal(app.state.quote.value, null); app.state.submit(); assert.equal(app.commands.length, 0); app.requests[0].reject(new Error('套餐已下架')); await request; assert.equal(app.state.canSubmit.value, false); app.state.submit(); assert.equal(app.commands.length, 0); }); test('an older quote response cannot overwrite the latest quote', async () => { const app = fixture(); const old = app.state.calculate(); const latest = app.state.calculate(); app.requests[1].resolve(quote('最新报价')); await latest; app.requests[0].resolve(quote('过时报价')); await old; assert.equal(app.state.quote.value.items[0].name, '最新报价'); assert.equal(app.store.cart.items[0].product.name, '最新报价'); }); test('identity changes invalidate quote and address and reject a late response', async () => { const app = fixture(); app.state.quote.value = quote(); const request = app.state.calculate(); app.session.epoch++; assert.equal(app.state.address.value, null); assert.equal(app.state.quote.value, null); app.requests[0].resolve(quote()); await request; assert.equal(app.state.quote.value, null); assert.equal(app.store.cart.items[0].product.name, '旧套餐'); assert.equal(app.state.canSubmit.value, false); }); test('removing the final item invalidates a pending quote and incomplete catalog pages preserve selections', async () => { const app = fixture(); app.store.refreshCartProducts([{ ...product(), id: 2 }]); assert.equal(app.store.cart.items.length, 1); const request = app.state.calculate(); app.store.clearCart(); await app.state.calculate(); app.requests[0].resolve(quote()); await request; assert.equal(app.state.quote.value, null); assert.equal(app.state.canSubmit.value, false); });