traffic-ui.spec.ts 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. import { mount, flushPromises } from '@vue/test-utils'
  2. import { beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { defineComponent, ref } from 'vue'
  4. import AccountTrafficControls from '../../src/components/account/AccountTrafficControls.vue'
  5. import BaseDialog from '../../src/components/common/BaseDialog.vue'
  6. import { accountTrafficAPI, defaultTrafficPolicy } from '../../src/api/admin/accountTraffic'
  7. vi.mock('@/api/admin/accountTraffic', async () => ({ ...await vi.importActual('@/api/admin/accountTraffic'), accountTrafficAPI: { get: vi.fn(), save: vi.fn() } }))
  8. const response = (enabled = false, limit = 128) => ({ policy: { ...defaultTrafficPolicy(), strict_rpm_enabled: enabled }, state_available: true, hard_limit: limit, state: { effective_concurrency: limit, recommended_concurrency: limit, in_flight: 0, requests_last_minute: 0, accepted: 0, rejected_rpm: 0, rejected_concurrency: 0, upstream_429: 0, upstream_5xx: 0, completed: 0, average_duration_ms: 0 } })
  9. async function togglePanel(wrapper: ReturnType<typeof mount>, open: boolean) {
  10. (wrapper.get('details').element as HTMLDetailsElement).open = open
  11. await wrapper.get('details').trigger('toggle'); await flushPromises()
  12. }
  13. function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(r => { resolve = r }); return { promise, resolve } }
  14. beforeEach(() => { vi.clearAllMocks(); vi.mocked(accountTrafficAPI.get).mockResolvedValue(response()); vi.mocked(accountTrafficAPI.save).mockResolvedValue(response(true)) })
  15. describe('review reproductions: assertions express expected correct behavior', () => {
  16. it('old status refresh must not overwrite a just-saved enabled policy', async () => {
  17. const w = mount(AccountTrafficControls, { props: { accountId: 42 } }); await togglePanel(w, true)
  18. const old = deferred<ReturnType<typeof response>>()
  19. vi.mocked(accountTrafficAPI.get).mockReturnValueOnce(old.promise).mockResolvedValue(response(true))
  20. await w.findAll('button').find(b => b.text() === '刷新状态')!.trigger('click')
  21. await w.get('[data-testid=strict-rpm-toggle]').setValue(true)
  22. await w.findAll('button').find(b => b.text() === '保存流量控制')!.trigger('click'); await flushPromises()
  23. old.resolve(response(false)); await flushPromises()
  24. const stale = w.text().includes('当前未启用流量控制')
  25. w.unmount(); expect(stale).toBe(false)
  26. })
  27. it('invalid independent traffic draft must not block saving the surrounding account form', async () => {
  28. const w = mount(defineComponent({ components: { AccountTrafficControls }, template: '<form><AccountTrafficControls :account-id="42" /><button type="submit">保存账号</button></form>' }))
  29. await togglePanel(w, true); await w.get('[data-testid=strict-rpm-toggle]').setValue(true)
  30. await w.findAll('input[type=number]')[0].setValue(0)
  31. await togglePanel(w, false)
  32. const valid = (w.get('form').element as HTMLFormElement).checkValidity()
  33. w.unmount(); expect(valid).toBe(true)
  34. })
  35. it('only the top nested dialog should respond to Escape', async () => {
  36. const w = mount(defineComponent({ components: { BaseDialog }, setup() { return { child: ref(true), parent: ref(true) } }, template: '<BaseDialog :show="parent" title="账号" @close="parent=false"><BaseDialog :show="child" title="策略预览" @close="child=false" /></BaseDialog>' }), { attachTo: document.body })
  37. await flushPromises(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); await flushPromises()
  38. const parentStillOpen = (w.vm as unknown as { parent: boolean }).parent
  39. w.unmount(); expect(parentStillOpen).toBe(true)
  40. })
  41. it('closing the child dialog should keep scrolling locked while the account dialog is open', async () => {
  42. const w = mount(defineComponent({ components: { BaseDialog }, setup() { return { child: ref(true) } }, template: '<BaseDialog :show="true" title="账号"><BaseDialog :show="child" title="策略预览" @close="child=false" /></BaseDialog>' }), { attachTo: document.body })
  43. await flushPromises()
  44. const dialogs = document.querySelectorAll('[role=dialog]')
  45. ;(dialogs[dialogs.length - 1].querySelector('button') as HTMLButtonElement).click(); await flushPromises()
  46. const locked = document.body.classList.contains('modal-open')
  47. w.unmount(); expect(locked).toBe(true)
  48. })
  49. })