From eeea16be38f8fa038e461130729d2ea2b08795ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sat, 7 Mar 2026 07:31:59 +0800 Subject: [PATCH] Fix getters mutating state on repeated access Co-Authored-By: Claude Opus 4.6 --- src/stores/allMapData.ts | 17 +++++---- src/stores/conformance.ts | 59 ++++++++++++++++++-------------- tests/stores/allMapData.test.js | 23 +++++++++++++ tests/stores/conformance.test.js | 13 +++++++ 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/src/stores/allMapData.ts b/src/stores/allMapData.ts index d2d47f8..0ccba63 100644 --- a/src/stores/allMapData.ts +++ b/src/stores/allMapData.ts @@ -124,23 +124,22 @@ export const useAllMapDataStore = defineStore('allMapDataStore', { }, filterAttrs: state => { if(state.allFilterAttrs !== null){ - state.allFilterAttrs.forEach(att => { - switch (att.type) { + return state.allFilterAttrs.map(att => { + const copy = { ...att }; + switch (copy.type) { case 'date': - att.min = att.min !== null ? moment(att.min).format('YYYY/MM/DD HH:mm') : null; - att.max = att.max !== null ? moment(att.max).format('YYYY/MM/DD HH:mm') : null; + copy.min = copy.min !== null ? moment(copy.min).format('YYYY/MM/DD HH:mm') : null; + copy.max = copy.max !== null ? moment(copy.max).format('YYYY/MM/DD HH:mm') : null; break; case 'float': - // Decimal.ROUND_UP|0: 無條件進位; Decimal.ROUND_DOWN|1: 無條件捨去。 - att.min = att.min !== null ? Number(new Decimal(att.min).toFixed(2, 1)) : null; - att.max = att.max !== null ? Number(new Decimal(att.max).toFixed(2, 0)) : null; + copy.min = copy.min !== null ? Number(new Decimal(copy.min).toFixed(2, 1)) : null; + copy.max = copy.max !== null ? Number(new Decimal(copy.max).toFixed(2, 0)) : null; break default: break; } - return att; + return copy; }); - return state.allFilterAttrs; } }, allFunnels: state => { diff --git a/src/stores/conformance.ts b/src/stores/conformance.ts index 096a4f8..65c5807 100644 --- a/src/stores/conformance.ts +++ b/src/stores/conformance.ts @@ -163,38 +163,42 @@ export const useConformanceStore = defineStore('conformanceStore', { }, cases: state => { if(state.allCases !== null){ - const newData = state.allCases.map(c => { - c.started_at = moment(c.started_at).format('YYYY/MM/DD HH:mm'); - c.completed_at = moment(c.completed_at).format('YYYY/MM/DD HH:mm'); - c.facets.forEach(fac => { - switch(fac.type) { + return state.allCases.map(c => { + const facets = c.facets.map(fac => { + const copy = { ...fac }; + switch(copy.type) { case 'dummy': //sonar-qube case 'duration-list': - fac.value = fac.value.map(v => v !== null ? abbreviateNumber(new Decimal(v.toFixed(2))) : null); - fac.value = (fac.value).map(v => v.trim()).join(', '); + copy.value = copy.value.map(v => v !== null ? abbreviateNumber(new Decimal(v.toFixed(2))) : null); + copy.value = (copy.value).map(v => v.trim()).join(', '); break; default: break; }; - return fac; + return copy; }); - c.attributes.forEach(att => { - switch (att.type) { + const attributes = c.attributes.map(att => { + const copy = { ...att }; + switch (copy.type) { case 'date': - att.value = att.value !== null ? moment(att.value).format('YYYY/MM/DD HH:mm:ss') : null; + copy.value = copy.value !== null ? moment(copy.value).format('YYYY/MM/DD HH:mm:ss') : null; break; case 'float': - att.value = att.value !== null ? new Decimal(att.value).toFixed(2) : null; + copy.value = copy.value !== null ? new Decimal(copy.value).toFixed(2) : null; break default: break; } - return att; + return copy; }); - const { facets, attributes, ...rest } = c; - return { ...rest, facets, attributes }; + return { + ...c, + started_at: moment(c.started_at).format('YYYY/MM/DD HH:mm'), + completed_at: moment(c.completed_at).format('YYYY/MM/DD HH:mm'), + facets, + attributes, + }; }); - return newData }; }, loopTraces: state => { @@ -205,25 +209,28 @@ export const useConformanceStore = defineStore('conformanceStore', { }, loopCases: state => { if(state.allLoopCases !== null){ - const newData = state.allLoopCases.map(c => { - c.started_at = moment(c.started_at).format('YYYY/MM/DD HH:mm'); - c.completed_at = moment(c.completed_at).format('YYYY/MM/DD HH:mm'); - c.attributes.forEach(att => { - switch (att.type) { + return state.allLoopCases.map(c => { + const attributes = c.attributes.map(att => { + const copy = { ...att }; + switch (copy.type) { case 'date': - att.value = att.value !== null ? moment(att.value).format('YYYY/MM/DD HH:mm:ss') : null; + copy.value = copy.value !== null ? moment(copy.value).format('YYYY/MM/DD HH:mm:ss') : null; break; case 'float': - att.value = att.value !== null ? new Decimal(att.value).toFixed(2) : null; + copy.value = copy.value !== null ? new Decimal(copy.value).toFixed(2) : null; break default: break; } - return att; + return copy; }); - return c; + return { + ...c, + started_at: moment(c.started_at).format('YYYY/MM/DD HH:mm'), + completed_at: moment(c.completed_at).format('YYYY/MM/DD HH:mm'), + attributes, + }; }); - return newData; }; }, }, diff --git a/tests/stores/allMapData.test.js b/tests/stores/allMapData.test.js index 21403aa..4e707f2 100644 --- a/tests/stores/allMapData.test.js +++ b/tests/stores/allMapData.test.js @@ -237,5 +237,28 @@ describe('allMapDataStore', () => { store.allBaseCase = [{ id: 1 }]; expect(store.BaseInfiniteFirstCases).toBeUndefined(); }); + + it('filterAttrs getter does not corrupt original state on repeated access', () => { + const originalDate = '2023-06-15T10:30:00Z'; + store.allFilterAttrs = [ + { type: 'date', min: originalDate, max: '2023-12-31T23:59:00Z' }, + ]; + // Access the getter once + store.filterAttrs; + // The original state should still contain the ISO date, not a formatted string + expect(store.allFilterAttrs[0].min).toBe(originalDate); + }); + + it('filterAttrs getter returns consistent float values on repeated access', () => { + store.allFilterAttrs = [ + { type: 'float', min: 1.239, max: 5.671 }, + ]; + const first = store.filterAttrs; + const second = store.filterAttrs; + // min rounds down (ROUND_DOWN=1), max rounds up (ROUND_UP=0) + // Both accesses should produce the same result + expect(first[0].min).toBe(second[0].min); + expect(first[0].max).toBe(second[0].max); + }); }); }); diff --git a/tests/stores/conformance.test.js b/tests/stores/conformance.test.js index f68a5aa..203ee3a 100644 --- a/tests/stores/conformance.test.js +++ b/tests/stores/conformance.test.js @@ -196,5 +196,18 @@ describe('conformanceStore', () => { // The date attribute should show minutes (30), not month (06) expect(result[0].attributes[0].value).toMatch(/:30:/); }); + + it('cases getter does not corrupt original state on repeated access', () => { + const originalDate = '2023-06-15T10:30:00Z'; + store.allCases = [{ + started_at: originalDate, + completed_at: '2023-06-15T11:45:00Z', + facets: [], + attributes: [], + }]; + store.cases; + // Original state should still contain the ISO date + expect(store.allCases[0].started_at).toBe(originalDate); + }); }); });