httpUtil.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { beforeEach, describe, expect, it, vi } from 'vitest';
  2. const toast = vi.hoisted(() => ({
  3. success: vi.fn(),
  4. error: vi.fn(),
  5. warning: vi.fn(),
  6. info: vi.fn(),
  7. loading: vi.fn(),
  8. }));
  9. vi.mock('@/api/http-init', () => ({
  10. httpRequest: vi.fn(),
  11. HttpError: class HttpError extends Error {
  12. status: number;
  13. response: { status: number; statusText: string; data: unknown };
  14. constructor(status: number, statusText: string, data: unknown) {
  15. super(`Request failed with status ${status}`);
  16. this.status = status;
  17. this.response = { status, statusText, data };
  18. }
  19. },
  20. }));
  21. vi.mock('@/utils/messageBus', () => ({
  22. getMessage: () => toast,
  23. }));
  24. import { HttpUtil } from '@/utils';
  25. import { HttpError, httpRequest } from '@/api/http-init';
  26. import type { HttpResponse } from '@/api/http-init';
  27. const mockRequest = vi.mocked(httpRequest);
  28. const envelope = (data: unknown): HttpResponse => ({ ok: true, status: 200, statusText: 'OK', data });
  29. describe('HttpUtil', () => {
  30. beforeEach(() => {
  31. vi.clearAllMocks();
  32. vi.spyOn(console, 'error').mockImplementation(() => undefined);
  33. });
  34. it('unwraps a success envelope and shows a success toast', async () => {
  35. mockRequest.mockResolvedValue(envelope({ success: true, msg: 'done', obj: { id: 1 } }));
  36. const msg = await HttpUtil.post('/x', { a: 1 });
  37. expect(msg.success).toBe(true);
  38. expect(msg.obj).toEqual({ id: 1 });
  39. expect(toast.success).toHaveBeenCalledWith('done');
  40. });
  41. it('suppresses the success toast with silentSuccess but still warns on nodePending', async () => {
  42. mockRequest.mockResolvedValue(envelope({ success: true, msg: 'saved', obj: { nodePending: true } }));
  43. await HttpUtil.post('/x', { a: 1 }, { silentSuccess: true });
  44. expect(toast.success).not.toHaveBeenCalled();
  45. expect(toast.warning).toHaveBeenCalled();
  46. });
  47. it('shows an error toast for a failure envelope', async () => {
  48. mockRequest.mockResolvedValue(envelope({ success: false, msg: 'nope', obj: null }));
  49. const msg = await HttpUtil.post('/x');
  50. expect(msg.success).toBe(false);
  51. expect(toast.error).toHaveBeenCalledWith('nope');
  52. });
  53. it('suppresses all toasts with silent', async () => {
  54. mockRequest.mockResolvedValue(envelope({ success: false, msg: 'nope', obj: null }));
  55. await HttpUtil.post('/x', undefined, { silent: true });
  56. expect(toast.error).not.toHaveBeenCalled();
  57. });
  58. it('surfaces the backend error text from a thrown HttpError body (msg field)', async () => {
  59. mockRequest.mockRejectedValue(new HttpError(400, 'Bad Request', { success: false, msg: 'bad input' }));
  60. const msg = await HttpUtil.post('/x', undefined, { silent: true });
  61. expect(msg.success).toBe(false);
  62. expect(msg.msg).toBe('bad input');
  63. expect(console.error).not.toHaveBeenCalled();
  64. });
  65. it('maps a thrown native error to a failure Msg via its message', async () => {
  66. mockRequest.mockRejectedValue(new Error('Network down'));
  67. const msg = await HttpUtil.get('/x', undefined, { silent: true });
  68. expect(msg.msg).toBe('Network down');
  69. expect(console.error).not.toHaveBeenCalled();
  70. });
  71. it('returns "No response data" for an empty body', async () => {
  72. mockRequest.mockResolvedValue(envelope(''));
  73. const msg = await HttpUtil.get('/x', undefined, { silent: true });
  74. expect(msg.success).toBe(false);
  75. expect(msg.msg).toBe('No response data');
  76. });
  77. });