feat: 支持新版的首页 UI
This commit is contained in:
55
packages/app/preview/generate-tab-icons.py
Normal file
55
packages/app/preview/generate-tab-icons.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Render the homepage's simple native tab symbols from editable vector geometry.
|
||||
Run with Python + Pillow. No external images or fonts are used.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
DEST = Path(__file__).resolve().parents[1] / 'src/static/tab'
|
||||
SIZE, SCALE = 81, 24
|
||||
FACTOR = SIZE * 4 / SCALE
|
||||
|
||||
def render(name, active):
|
||||
image = Image.new('RGBA', (SIZE * 4, SIZE * 4))
|
||||
draw = ImageDraw.Draw(image)
|
||||
color = '#6b8276' if active else '#99948e'
|
||||
weight = round(1.35 * FACTOR)
|
||||
def box(coords): return tuple(round(value * FACTOR) for value in coords)
|
||||
def line(points, width=weight):
|
||||
points = [box(point) for point in points]
|
||||
draw.line(points, fill=color, width=width, joint='curve')
|
||||
r = width / 2
|
||||
for x, y in (points[0], points[-1]):
|
||||
draw.ellipse((x-r, y-r, x+r, y+r), fill=color)
|
||||
if name == 'home':
|
||||
roof = [(3.8,10), (12,3.2), (20.2,10)]
|
||||
if active:
|
||||
draw.polygon([box(p) for p in roof + [(20.2,20.4),(14.6,20.4),(14.6,13.2),(9.4,13.2),(9.4,20.4),(3.8,20.4)]], fill=color)
|
||||
else:
|
||||
line(roof)
|
||||
line([(5.2,9.5),(5.2,20.4),(9.4,20.4),(9.4,13.2),(14.6,13.2),(14.6,20.4),(18.8,20.4),(18.8,9.5)])
|
||||
elif name == 'booking':
|
||||
draw.rounded_rectangle(box((4.2,5.7,19.8,21)), radius=round(3*FACTOR), outline=color, width=weight)
|
||||
line([(4.5,10.5),(19.5,10.5)])
|
||||
line([(8,3.2),(8,7.8)])
|
||||
line([(16,3.2),(16,7.8)])
|
||||
if active:
|
||||
line([(8.5,15.8),(11,18),(15.8,13.8)])
|
||||
elif name == 'learn':
|
||||
draw.ellipse(box((3,3,21,21)), outline=color, width=weight)
|
||||
points = [(15.8,8.2),(13.7,13.7),(8.2,15.8),(10.3,10.3),(15.8,8.2)]
|
||||
if active:
|
||||
draw.polygon([box(p) for p in points], fill=color)
|
||||
else:
|
||||
line(points, round(1.1*FACTOR))
|
||||
elif name == 'profile':
|
||||
draw.ellipse(box((8,2.8,16,10.8)), outline=color, width=weight)
|
||||
draw.rounded_rectangle(box((4.2,14,19.8,21.5)), radius=round(3.6*FACTOR), outline=color, width=weight)
|
||||
if active:
|
||||
draw.ellipse(box((9.2,4,14.8,9.6)), fill=color)
|
||||
image = image.resize((SIZE,SIZE), Image.Resampling.LANCZOS)
|
||||
image.save(DEST / f"{name}{'-active' if active else ''}.png", optimize=True)
|
||||
|
||||
for name in ['home','booking','learn','profile']:
|
||||
for active in [False,True]:
|
||||
render(name,active)
|
||||
print('Rendered 8 tab icons, 81×81px, sage active / warm-gray inactive.')
|
||||
79
packages/app/preview/home-fixtures.ts
Normal file
79
packages/app/preview/home-fixtures.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
const cover = '/static/home/studio-morning.jpg'
|
||||
const membership = {
|
||||
id: 'preview-membership', userId: 'preview-user', cardTypeId: 'preview-card',
|
||||
remainingTimes: 8, totalTimes: 10, status: 'ACTIVE',
|
||||
startDate: '2026-10-01T00:00:00.000Z', expireDate: '2027-10-01T00:00:00.000Z',
|
||||
cardType: { id: 'preview-card', name: '私教课', type: 'TIMES', totalTimes: 10, price: 388000, validityDays: 365 },
|
||||
}
|
||||
const booking = {
|
||||
id: 'preview-booking', membership, status: 'CONFIRMED',
|
||||
timeSlot: { id: 'preview-slot', date: '2026-10-26T00:00:00.000Z', startTime: '10:00:00', endTime: '11:00:00' },
|
||||
}
|
||||
const story = {
|
||||
id: 'preview-story', displayName: '学员 A', title: '找到更舒展的自己', lessonCount: 12,
|
||||
achievement: '', background: '',
|
||||
frames: [{ kind: 'pair', beforeId: 'preview-before', afterId: 'preview-after', beforeUrl: cover, afterUrl: cover, caption: '' }],
|
||||
}
|
||||
|
||||
export function createHomeFixture(scenario: string, path: string, count: number): { status?: number; data: unknown } {
|
||||
if (scenario === 'error' && ((path === '/membership/my' && count <= 2) || (path === '/booking/my/upcoming' && count === 1))) return { status: 503, data: null }
|
||||
const isNew = scenario === 'new'
|
||||
if (path === '/studio/info') return { data: { name: scenario === 'long' ? 'Focus Core 普拉提私教工作室' : 'Focus Core', logo: '', bannerUrl: null, ownerProfile: null } }
|
||||
if (path === '/user/profile') return { data: { id: 'preview-user', nickname: scenario === 'long' ? '这是一个特别特别长的学员昵称' : '晓晓', role: 'MEMBER', activeMembershipCount: isNew ? 0 : 1, inviteShareEligible: false, subscriptionMessageTemplates: {}, createdAt: '2026-01-01T00:00:00.000Z' } }
|
||||
if (path === '/membership/my') return { data: isNew ? [] : scenario === 'expired' ? [{ ...membership, status: 'EXPIRED', expireDate: '2026-10-01T00:00:00.000Z' }] : [membership] }
|
||||
if (path === '/booking/my/upcoming') return { data: ['new', 'empty', 'expired'].includes(scenario) ? [] : [booking] }
|
||||
if (path === '/teaching-cases') return { data: { items: scenario === 'empty' ? [] : [story], total: 1, page: 1, limit: 3 } }
|
||||
if (path.startsWith('/teaching-cases/')) return { data: story }
|
||||
if (path === '/auth/renew') return { data: { token: 'home-preview-token' } }
|
||||
if (path === '/invite/summary') return { data: { inviteCode: '', invitedCount: 0, rewards: [] } }
|
||||
if (path.includes('review')) return { data: { total: 0, averageRating: 0, items: [], distribution: {} } }
|
||||
if (path.includes('products')) return { data: { items: [], data: [], total: 0 } }
|
||||
if (path.includes('card-types')) return { data: [] }
|
||||
if (path.includes('/booking/preview-booking')) return { data: booking }
|
||||
return { data: [] }
|
||||
}
|
||||
|
||||
export function fixtureBootstrap(scenario: string) {
|
||||
return `(() => {
|
||||
const scenario = ${JSON.stringify(scenario)};
|
||||
const params = new URL(location.href).searchParams;
|
||||
const instance = params.get('instance') || String(Math.random());
|
||||
const storage = new Map();
|
||||
const api = {
|
||||
getItem: key => storage.has(String(key)) ? storage.get(String(key)) : null,
|
||||
setItem: (key, value) => storage.set(String(key), String(value)),
|
||||
removeItem: key => storage.delete(String(key)),
|
||||
clear: () => storage.clear(),
|
||||
key: index => Array.from(storage.keys())[index] || null,
|
||||
get length() { return storage.size; }
|
||||
};
|
||||
if (scenario !== 'guest') {
|
||||
api.setItem('token', 'home-preview-token');
|
||||
api.setItem('token_renewed_at', JSON.stringify({ type: 'number', data: 1792892460000 }));
|
||||
}
|
||||
Object.defineProperty(window, 'localStorage', { value: api });
|
||||
const RealDate = Date;
|
||||
const previewTime = new RealDate('2026-10-25T01:41:00.000Z').getTime();
|
||||
window.Date = class extends RealDate {
|
||||
constructor(...args) { super(...(args.length ? args : [previewTime])); }
|
||||
static now() { return previewTime; }
|
||||
};
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function(method, rawUrl, ...args) {
|
||||
const url = new URL(rawUrl, location.href);
|
||||
if (url.hostname === 'focus.richarjiang.com' && url.pathname.startsWith('/api')) {
|
||||
rawUrl = '/__home-fixture-api?' + new URLSearchParams({ path: url.pathname.slice(4), scenario, instance });
|
||||
}
|
||||
return originalOpen.call(this, method, rawUrl, ...args);
|
||||
};
|
||||
})();`
|
||||
}
|
||||
|
||||
export const previewPage = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>首页设计验收</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}body{margin:0;background:#f0eeea;color:#403c36;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',sans-serif}header{padding:26px 30px 20px;position:sticky;top:0;z-index:50;background:#f0eeea}h1{font-size:21px;margin:0 0 10px;font-weight:500}p{font-size:13px;color:#8b817b;margin:0 0 16px}.controls{display:flex;gap:18px;align-items:center;font-size:13px}select,button{border:1px solid #d7d2ca;background:#fbf9f6;color:#403c36;border-radius:8px;padding:8px 12px;font:inherit}main{display:flex;gap:32px;padding:0 30px 30px;align-items:flex-start;flex-wrap:wrap}.screen{background:#fbf9f6;border-radius:18px;overflow:hidden;box-shadow:0 4px 24px #403c3608;position:relative}.caption{font-size:14px;margin:0 0 12px;color:#756a60}.status{height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;font-size:15px;font-weight:600;background:#fbf9f6}.status span:last-child{font-size:12px;letter-spacing:2px}iframe{display:block;width:100%;height:800px;border:0}.capsule{position:absolute;top:55px;right:16px;border:1px solid #ded9d1;border-radius:20px;height:32px;width:94px;display:flex;align-items:center;justify-content:center;gap:14px;color:#292a27;pointer-events:none}.capsule b{font-size:16px}.capsule i{height:16px;border-left:1px solid #ded9d1}.capsule em{font-size:21px;font-style:normal;line-height:1}.home-indicator{position:absolute;bottom:6px;left:calc(50% - 65px);width:130px;height:5px;border-radius:8px;background:#171817;pointer-events:none}
|
||||
</style></head><body><header><h1>首页顶部 · 轻盈陪伴</h1><p>实际 Vue 页面与真实交互 · 以下均为隔离的模拟数据,案例图片仅用于检查布局</p><div class="controls"><label>右侧状态 <select id="scenario"><option value="member">老学员</option><option value="new">已登录新用户</option><option value="expired">会员卡已过期</option><option value="empty">暂无预约和案例</option><option value="error">首次请求失败</option><option value="loading">慢网络</option><option value="long">长昵称和馆名</option></select></label><label>屏幕宽度 <select id="width"><option>390</option><option>320</option><option>375</option><option>430</option></select></label><button id="reload">重新进入</button></div></header><main><section><p class="caption">新朋友</p><div class="screen"><div class="status"><span>9:41</span><span>▮▮▮ ◉ ▰</span></div><iframe title="新用户首页" id="guest"></iframe><div class="capsule"><b>•••</b><i></i><em>⊙</em></div><div class="home-indicator"></div></div></section><section><p class="caption">老学员 / 边界状态</p><div class="screen"><div class="status"><span>9:41</span><span>▮▮▮ ◉ ▰</span></div><iframe title="老学员首页" id="member"></iframe><div class="capsule"><b>•••</b><i></i><em>⊙</em></div><div class="home-indicator"></div></div></section></main><script>
|
||||
const scenario=document.getElementById('scenario'),width=document.getElementById('width');
|
||||
function load(){document.querySelectorAll('.screen').forEach(screen=>screen.style.width=width.value+'px');document.getElementById('guest').src='/?homeFixture=guest&instance='+Math.random();document.getElementById('member').src='/?homeFixture='+scenario.value+'&instance='+Math.random()}
|
||||
scenario.onchange=load;width.onchange=load;document.getElementById('reload').onclick=load;load();
|
||||
</script></body></html>`
|
||||
38
packages/app/preview/home.config.ts
Normal file
38
packages/app/preview/home.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig, mergeConfig, type Plugin } from 'vite'
|
||||
import baseConfig from '../vite.config'
|
||||
import { createHomeFixture, fixtureBootstrap, previewPage } from './home-fixtures'
|
||||
|
||||
// This config is opt-in and is never loaded by the WeChat production build.
|
||||
const fixtures: Plugin = {
|
||||
name: 'home-design-preview',
|
||||
transformIndexHtml(html, context) {
|
||||
const scenario = new URL(context.originalUrl || '/', 'http://localhost').searchParams.get('homeFixture')
|
||||
if (!scenario) return html
|
||||
return { html, tags: [{ tag: 'script', children: fixtureBootstrap(scenario), injectTo: 'head-prepend' }] }
|
||||
},
|
||||
configureServer(server) {
|
||||
const requests = new Map<string, number>()
|
||||
server.middlewares.use((req, res, next) => {
|
||||
const url = new URL(req.url || '/', 'http://localhost')
|
||||
if (url.pathname === '/__home-preview') {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
res.end(previewPage)
|
||||
return
|
||||
}
|
||||
if (url.pathname !== '/__home-fixture-api') return next()
|
||||
const scenario = url.searchParams.get('scenario') || 'guest'
|
||||
const path = url.searchParams.get('path') || ''
|
||||
const key = `${url.searchParams.get('instance')}:${path}`
|
||||
const count = (requests.get(key) || 0) + 1
|
||||
requests.set(key, count)
|
||||
const result = createHomeFixture(scenario, path, count)
|
||||
const respond = () => {
|
||||
res.statusCode = result.status || 200
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
res.end(JSON.stringify({ success: res.statusCode < 400, data: result.data, message: '预览中的模拟网络错误' }))
|
||||
}
|
||||
setTimeout(respond, scenario === 'loading' ? 5000 : 120)
|
||||
})
|
||||
},
|
||||
}
|
||||
export default mergeConfig(baseConfig, defineConfig({ plugins: [fixtures] }))
|
||||
158
packages/app/preview/home.spec.mjs
Normal file
158
packages/app/preview/home.spec.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { transform } from 'esbuild'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const source = await readFile(new URL('../src/utils/home.ts', import.meta.url), 'utf8')
|
||||
const { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
const { getHomeMoment, getHomeGreeting, getHomeBookingDate, getHomeMembershipDays, getHomeUpcomingBookings } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
|
||||
test('问候按中国时间在 5、9、12、18 点切换', () => {
|
||||
for (const [time, expected] of [
|
||||
['2026-10-24T20:59:59Z', '晚上好'], ['2026-10-24T21:00:00Z', '早上好'],
|
||||
['2026-10-25T00:59:59Z', '早上好'], ['2026-10-25T01:00:00Z', '上午好'],
|
||||
['2026-10-25T03:59:59Z', '上午好'], ['2026-10-25T04:00:00Z', '下午好'],
|
||||
['2026-10-25T09:59:59Z', '下午好'], ['2026-10-25T10:00:00Z', '晚上好'],
|
||||
]) assert.equal(getHomeMoment(new Date(time)).period, expected, time)
|
||||
})
|
||||
|
||||
test('中国时间跨日更新日期和提醒;同一天刷新不随机跳动', () => {
|
||||
const today = getHomeMoment(new Date('2026-10-25T01:41:00Z'))
|
||||
assert.equal(today.dateLabel, '10月25日 · 星期日')
|
||||
assert.equal(today.reminder, '练习前,给自己一次深呼吸。')
|
||||
assert.equal(getHomeMoment(new Date('2026-10-25T15:59:59Z')).reminder, today.reminder)
|
||||
const tomorrow = getHomeMoment(new Date('2026-10-25T16:00:00Z'))
|
||||
assert.equal(tomorrow.dateLabel, '10月26日 · 星期一')
|
||||
assert.notEqual(tomorrow.reminder, today.reminder)
|
||||
assert.ok(getHomeMoment(new Date('2020-01-01T00:00:00Z')).reminder)
|
||||
})
|
||||
|
||||
test('访客、加载状态、真实昵称和微信默认昵称分别显示合适问候', () => {
|
||||
assert.equal(getHomeGreeting('上午好', 'guest', '晓晓'), '上午好,欢迎你')
|
||||
assert.equal(getHomeGreeting('上午好', 'loading', '晓晓'), '上午好,欢迎回来')
|
||||
assert.equal(getHomeGreeting('上午好', 'member', ' 晓晓 '), '上午好,晓晓')
|
||||
for (const name of [null, '', ' ', '微信用户', '用户12345', '新用户']) {
|
||||
assert.equal(getHomeGreeting('上午好', 'member', name), '上午好,欢迎回来')
|
||||
}
|
||||
})
|
||||
|
||||
test('预约自然日不受设备时区影响,非法日期安全降级', () => {
|
||||
assert.deepEqual(getHomeBookingDate('2026-10-26T00:00:00.000Z'), { day: '26', caption: '10月 周一' })
|
||||
assert.deepEqual(getHomeBookingDate('2026-10-26'), { day: '26', caption: '10月 周一' })
|
||||
assert.deepEqual(getHomeBookingDate('invalid'), { day: '—', caption: '' })
|
||||
})
|
||||
|
||||
test('会员剩余天数不会显示负值或 NaN', () => {
|
||||
const now = new Date('2026-10-25T01:41:00Z')
|
||||
assert.equal(getHomeMembershipDays('2026-10-26T01:41:00Z', now), 1)
|
||||
assert.equal(getHomeMembershipDays('2026-10-25T02:00:00Z', now), 1)
|
||||
assert.equal(getHomeMembershipDays('2026-10-25T00:00:00Z', now), 0)
|
||||
assert.equal(getHomeMembershipDays('invalid', now), 0)
|
||||
})
|
||||
|
||||
test('即将上课排除已开始和取消课程,按中国时间排序并最多显示两节', () => {
|
||||
const make = (id, date, startTime, status = 'CONFIRMED') => ({ id, status, timeSlot: { date, startTime } })
|
||||
const bookings = [
|
||||
make('later', '2026-10-27T00:00:00.000Z', '10:00:00'),
|
||||
make('past', '2026-10-25T00:00:00.000Z', '09:00:00'),
|
||||
make('cancelled', '2026-10-26T00:00:00.000Z', '08:00:00', 'CANCELLED'),
|
||||
make('next', '2026-10-26T00:00:00.000Z', '10:00:00', 'PENDING_CONFIRMATION'),
|
||||
make('today', '2026-10-25T00:00:00.000Z', '10:00:00'),
|
||||
make('invalid', 'invalid', 'invalid'),
|
||||
]
|
||||
const snapshot = bookings.slice()
|
||||
assert.deepEqual(getHomeUpcomingBookings(bookings, Date.parse('2026-10-25T01:41:00Z')).map(item => item.id), ['today', 'next'])
|
||||
assert.deepEqual(bookings, snapshot, '展示排序不得修改 store 数组')
|
||||
assert.deepEqual(getHomeUpcomingBookings([], Date.now()), [])
|
||||
})
|
||||
|
||||
// Bundle actual Pinia stores while replacing only network and unrelated store
|
||||
// dependencies. These checks catch stale responses, rather than mirroring CSS.
|
||||
const { build } = require('esbuild')
|
||||
const piniaPath = require.resolve('pinia')
|
||||
const vuePath = require.resolve('vue')
|
||||
const requestQueue = []
|
||||
globalThis.__homeRequest = () => new Promise((resolve, reject) => requestQueue.push({ resolve, reject }))
|
||||
const storage = new Map([['token', 'test-token']])
|
||||
globalThis.uni = {
|
||||
getStorageSync: key => storage.get(key) || '', setStorageSync: (key, value) => storage.set(key, value),
|
||||
removeStorageSync: key => storage.delete(key), showToast() {},
|
||||
}
|
||||
const dependencies = {
|
||||
'/utils/request': 'export const get = (...args) => globalThis.__homeRequest(...args); export const post = get; export const put = get;',
|
||||
'/stores/body-portrait': 'export const useBodyPortraitStore = () => ({ keepAnonymousOnLogout() {}, claimed: false });',
|
||||
'/stores/invite': 'export const useInviteStore = () => ({ refresh: async () => {}, reset() {} });',
|
||||
'/utils/wechat-subscription': 'export const cacheSubscriptionMessageTemplateConfig = () => {}; export const resetSubscriptionMessageTemplateCache = () => {};',
|
||||
'/utils/auth': "export const isLoggedIn = () => !!uni.getStorageSync('token'); export const wxLogin = async () => ({}); export const logout = () => uni.removeStorageSync('token');",
|
||||
}
|
||||
const output = await build({
|
||||
stdin: { contents: "export { useUserStore } from './src/stores/user'; export { useBookingStore } from './src/stores/booking';", resolveDir: new URL('..', import.meta.url).pathname, loader: 'ts' },
|
||||
bundle: true, write: false, format: 'cjs', platform: 'node',
|
||||
plugins: [{ name: 'test-dependencies', setup(build) {
|
||||
build.onResolve({ filter: /^(vue|pinia)$/ }, args => ({ path: args.path === 'vue' ? vuePath : piniaPath, external: true }))
|
||||
build.onResolve({ filter: /.*/ }, args => {
|
||||
if (!args.path.startsWith('.')) return
|
||||
const path = new URL(args.path, `file://${args.resolveDir}/`).pathname.replace(/\.ts$/, '')
|
||||
const key = Object.keys(dependencies).find(key => path.endsWith(key))
|
||||
if (key) return { path: key, namespace: 'test-stub' }
|
||||
})
|
||||
build.onLoad({ filter: /.*/, namespace: 'test-stub' }, args => ({ contents: dependencies[args.path], loader: 'js' }))
|
||||
} }],
|
||||
})
|
||||
const module = { exports: {} }
|
||||
new Function('require', 'module', 'exports', output.outputFiles[0].text)(require, module, module.exports)
|
||||
const { useUserStore, useBookingStore } = module.exports
|
||||
const { createPinia, setActivePinia } = require('pinia')
|
||||
|
||||
function reset() { requestQueue.length = 0; storage.set('token', 'test-token'); setActivePinia(createPinia()) }
|
||||
|
||||
test('退出登录后,迟到的个人资料不能重新显示上一位学员', async () => {
|
||||
reset()
|
||||
const user = useUserStore()
|
||||
const task = user.fetchProfile()
|
||||
assert.equal(requestQueue.length, 1)
|
||||
user.logout()
|
||||
requestQueue.shift().resolve({ id: 'old-user', nickname: '旧学员' })
|
||||
await task
|
||||
assert.equal(user.user, null)
|
||||
assert.equal(user.loggedIn, false)
|
||||
})
|
||||
|
||||
test('退出后清空预约,并丢弃已在途的旧预约响应', async () => {
|
||||
reset()
|
||||
const bookings = useBookingStore()
|
||||
const task = bookings.fetchUpcomingBookings()
|
||||
bookings.resetUpcomingBookings()
|
||||
requestQueue.shift().resolve([{ id: 'old-booking' }])
|
||||
await task
|
||||
assert.deepEqual(bookings.upcomingBookings, [])
|
||||
assert.equal(bookings.upcomingLoaded, false)
|
||||
assert.equal(bookings.upcomingLoading, false)
|
||||
})
|
||||
|
||||
test('预约刷新失败保留已加载预约;重试后清除错误', async () => {
|
||||
reset()
|
||||
const bookings = useBookingStore()
|
||||
let task = bookings.fetchUpcomingBookings()
|
||||
requestQueue.shift().resolve([{ id: 'booking-a' }]); await task
|
||||
task = bookings.fetchUpcomingBookings()
|
||||
requestQueue.shift().reject(new Error('offline')); await task
|
||||
assert.equal(bookings.upcomingBookings[0].id, 'booking-a')
|
||||
assert.equal(bookings.upcomingError, true)
|
||||
task = bookings.fetchUpcomingBookings()
|
||||
requestQueue.shift().resolve([]); await task
|
||||
assert.equal(bookings.upcomingError, false)
|
||||
assert.equal(bookings.upcomingLoaded, true)
|
||||
})
|
||||
|
||||
test('较晚返回的旧预约请求不能覆盖最新结果', async () => {
|
||||
reset()
|
||||
const bookings = useBookingStore()
|
||||
const old = bookings.fetchUpcomingBookings(), latest = bookings.fetchUpcomingBookings()
|
||||
const olderResponse = requestQueue.shift(), latestResponse = requestQueue.shift()
|
||||
latestResponse.resolve([{ id: 'latest' }]); await latest
|
||||
olderResponse.resolve([{ id: 'stale' }]); await old
|
||||
assert.equal(bookings.upcomingBookings[0].id, 'latest')
|
||||
})
|
||||
Reference in New Issue
Block a user