From da56fd307096a4c0f8168b92db62dc4e8fdc8a2b Mon Sep 17 00:00:00 2001 From: Mazen Touati <14861869+sunchayn@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:15:10 +0100 Subject: [PATCH] fix(response): reset response size on endpoint change (#9) * fix(response): reset response size on endpoint change * test(request): move test to the right directory --- .../ResponseStatus/ResponseStatus.vue | 13 +- resources/js/interfaces/http/request.ts | 3 + .../stores/request/useRequestBuilderStore.ts | 1 + .../stores/request/useRequestExecutorStore.ts | 1 + .../Request}/RequestBuilder.test.ts | 0 .../ResponseStatus/ResponseStatus.test.ts | 138 ++++++++++++++++++ 6 files changed, 152 insertions(+), 4 deletions(-) rename resources/js/tests/components/domain/{ => Client/Request}/RequestBuilder.test.ts (100%) create mode 100644 resources/js/tests/components/domain/Client/Response/ResponseStatus/ResponseStatus.test.ts diff --git a/resources/js/components/domain/Client/Response/ResponseStatus/ResponseStatus.vue b/resources/js/components/domain/Client/Response/ResponseStatus/ResponseStatus.vue index e0fa566..b92aedb 100644 --- a/resources/js/components/domain/Client/Response/ResponseStatus/ResponseStatus.vue +++ b/resources/js/components/domain/Client/Response/ResponseStatus/ResponseStatus.vue @@ -54,6 +54,15 @@ const status = computed(() => { return lastLog.value.response.status ?? STATUS.EMPTY; }); +const size = computed(() => + prettyBytes( + pendingRequestData.value?.wasExecuted + ? (lastLog.value?.response?.sizeInBytes ?? 0) + : 0, // <- When a new endpoint is initialized, we reset the size as well. + { space: false }, + ), +); + const duration = computed(() => { return prettyMs( // If there's a pending request that's processing, use its duration @@ -67,10 +76,6 @@ const duration = computed(() => { ); }); -const size = computed(() => - prettyBytes(lastLog.value?.response?.sizeInBytes ?? 0, { space: false }), -); - const readableTime = computed(() => { if (lastLog.value?.response === undefined) { return ''; diff --git a/resources/js/interfaces/http/request.ts b/resources/js/interfaces/http/request.ts index bdcfaa4..c4f14a7 100644 --- a/resources/js/interfaces/http/request.ts +++ b/resources/js/interfaces/http/request.ts @@ -101,6 +101,9 @@ export interface PendingRequest { /** Request execution duration in milliseconds */ durationInMs?: number; + + /** Whether the request was executed at least once */ + wasExecuted?: boolean; } export interface Request { diff --git a/resources/js/stores/request/useRequestBuilderStore.ts b/resources/js/stores/request/useRequestBuilderStore.ts index 78a570a..39fbc15 100644 --- a/resources/js/stores/request/useRequestBuilderStore.ts +++ b/resources/js/stores/request/useRequestBuilderStore.ts @@ -149,6 +149,7 @@ export const useRequestBuilderStore = defineStore('_requestBuilder', () => { supportedRoutes: availableRoutesForEndpoint, routeDefinition: route, isProcessing: false, + wasExecuted: false, durationInMs: 0, }; }; diff --git a/resources/js/stores/request/useRequestExecutorStore.ts b/resources/js/stores/request/useRequestExecutorStore.ts index 8fd7c09..0ee2f06 100644 --- a/resources/js/stores/request/useRequestExecutorStore.ts +++ b/resources/js/stores/request/useRequestExecutorStore.ts @@ -65,6 +65,7 @@ export const useRequestExecutorStore = defineStore('_requestExecutor', () => { } requestData.isProcessing = false; + requestData.wasExecuted = true; }; try { diff --git a/resources/js/tests/components/domain/RequestBuilder.test.ts b/resources/js/tests/components/domain/Client/Request/RequestBuilder.test.ts similarity index 100% rename from resources/js/tests/components/domain/RequestBuilder.test.ts rename to resources/js/tests/components/domain/Client/Request/RequestBuilder.test.ts diff --git a/resources/js/tests/components/domain/Client/Response/ResponseStatus/ResponseStatus.test.ts b/resources/js/tests/components/domain/Client/Response/ResponseStatus/ResponseStatus.test.ts new file mode 100644 index 0000000..c19df20 --- /dev/null +++ b/resources/js/tests/components/domain/Client/Response/ResponseStatus/ResponseStatus.test.ts @@ -0,0 +1,138 @@ +import ResponseStatus from '@/components/domain/Client/Response/ResponseStatus/ResponseStatus.vue'; +import { STATUS } from '@/interfaces/http'; +import { mountWithPlugins } from '@/tests/_utils/test-utils'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { computed, reactive, ref } from 'vue'; + +// Mock child to expose received props for assertions +vi.mock( + '@/components/domain/Client/Response/ResponseStatus/ResponseStatusCode.vue', + () => ({ + default: { + name: 'ResponseStatusCode', + props: ['status', 'response'], + template: + '
{{ status }}
', + }, + }), +); + +// Mock stores used via '@/stores' +const mockRequestStore = reactive({ + pendingRequestData: computed(() => mockPendingRequest.value), + cancelCurrentRequest: vi.fn(), +}); + +const mockHistoryStore = reactive({ + lastLog: ref(null), // eslint-disable-line @typescript-eslint/no-explicit-any +}); + +const mockPendingRequest = ref(null); // eslint-disable-line @typescript-eslint/no-explicit-any + +vi.mock('@/stores', () => ({ + useRequestStore: () => mockRequestStore, + useRequestsHistoryStore: () => mockHistoryStore, +})); + +describe('ResponseStatus', () => { + beforeEach(() => { + mockPendingRequest.value = null; + mockHistoryStore.lastLog = ref(null); + mockRequestStore.cancelCurrentRequest.mockReset(); + }); + + it('shows PENDING status and cancel button while processing', () => { + mockPendingRequest.value = { isProcessing: true, durationInMs: 1234 }; + const wrapper = mountWithPlugins(ResponseStatus); + + const statusCode = wrapper.get('[data-testid="status-code"]'); + expect(statusCode.attributes()['data-status']).toBe(String(STATUS.PENDING)); + + // Cancel button visible + const cancel = wrapper.find('button'); + expect(cancel.exists()).toBe(true); + }); + + it('shows EMPTY when no last response and not processing', () => { + mockPendingRequest.value = { isProcessing: false, durationInMs: 0 }; + const wrapper = mountWithPlugins(ResponseStatus); + + const statusCode = wrapper.get('[data-testid="status-code"]'); + expect(statusCode.attributes()['data-status']).toBe(String(STATUS.EMPTY)); + }); + + it('derives status from last response when available', () => { + mockPendingRequest.value = { + isProcessing: false, + durationInMs: 0, + wasExecuted: true, + }; + mockHistoryStore.lastLog = ref({ response: { status: 201, sizeInBytes: 1024 } }); + const wrapper = mountWithPlugins(ResponseStatus); + + const statusCode = wrapper.get('[data-testid="status-code"]'); + expect(statusCode.attributes()['data-status']).toBe('201'); + + expect(wrapper.text()).toMatch(/1.02kB/); + }); + + it('resets size to 0 when request not executed (new endpoint)', () => { + mockPendingRequest.value = { + isProcessing: false, + wasExecuted: false, + durationInMs: 0, + }; + mockHistoryStore.lastLog = ref({ response: { sizeInBytes: 12345 } }); + const wrapper = mountWithPlugins(ResponseStatus); + + // Expects "0B" formatting from pretty-bytes with { space: false } + expect(wrapper.text()).toMatch(/0B/); + }); + + it('uses pending duration when processing, otherwise last log duration', () => { + // Processing case + mockPendingRequest.value = { + isProcessing: true, + durationInMs: 1500, + wasExecuted: false, + }; + mockHistoryStore.lastLog = ref({ durationInMs: 9999 }); + let wrapper = mountWithPlugins(ResponseStatus); + expect(wrapper.text()).toMatch(/1\.50s/); + + // Completed case + mockPendingRequest.value = { + isProcessing: false, + durationInMs: 2500, + wasExecuted: true, + }; + mockHistoryStore.lastLog = ref({ + durationInMs: 3000, + response: { timestamp: 1700000000, status: STATUS.SUCCESS }, + }); + wrapper = mountWithPlugins(ResponseStatus); + expect(wrapper.text()).toMatch(/2\.50s/); // <- it prirotizes the one from the pending request. + }); + + it('shows readable time text when last response exists', () => { + mockPendingRequest.value = { isProcessing: false, durationInMs: 0 }; + mockHistoryStore.lastLog = ref({ + response: { timestamp: Math.floor(Date.now() / 1000) }, + }); + const wrapper = mountWithPlugins(ResponseStatus); + + // The content is time-ago text; assert non-empty text region where it renders + const small = wrapper.find('small'); + expect(small.exists()).toBe(true); + expect((small.text() ?? '').length).toBeGreaterThan(0); + }); + + it('cancels request when cancel button clicked', async () => { + mockPendingRequest.value = { isProcessing: true, durationInMs: 0 }; + const wrapper = mountWithPlugins(ResponseStatus); + + const btn = wrapper.get('button'); + await btn.trigger('click'); + expect(mockRequestStore.cancelCurrentRequest).toHaveBeenCalled(); + }); +});