diff --git a/frontend/cypress/e2e/unit/modelParameterService.cy.ts b/frontend/cypress/e2e/unit/modelParameterService.cy.ts
new file mode 100644
index 00000000..99351cce
--- /dev/null
+++ b/frontend/cypress/e2e/unit/modelParameterService.cy.ts
@@ -0,0 +1,168 @@
+///
+
+import { createCSVFiles, runModel } from '@/services/modelParameterService'
+import { SelectedExecutionOptionsEnum } from '@/services/vdyp-api'
+import * as apiActions from '@/services/apiActions'
+import { CONSTANTS, DEFAULTS, OPTIONS } from '@/constants'
+import sinon from 'sinon'
+
+describe('Model Parameter Service Unit Tests', () => {
+ const mockModelParameterStore = {
+ derivedBy: DEFAULTS.DEFAULT_VALUES.DERIVED_BY,
+ speciesList: [
+ { species: 'PL', percent: '30.0' },
+ { species: 'AC', percent: '30.0' },
+ { species: 'H', percent: '30.0' },
+ { species: 'S', percent: '10.0' },
+ { species: null, percent: '0.0' },
+ { species: null, percent: '0.0' },
+ ],
+ becZone: DEFAULTS.DEFAULT_VALUES.BEC_ZONE,
+ ecoZone: OPTIONS.ecoZoneOptions[0].value,
+ incSecondaryHeight: false,
+ highestPercentSpecies: 'PL',
+ bha50SiteIndex: DEFAULTS.DEFAULT_VALUES.BHA50_SITE_INDEX,
+ percentStockableArea: DEFAULTS.DEFAULT_VALUES.PERCENT_STOCKABLE_AREA,
+ startingAge: DEFAULTS.DEFAULT_VALUES.STARTING_AGE,
+ finishingAge: DEFAULTS.DEFAULT_VALUES.FINISHING_AGE,
+ ageIncrement: DEFAULTS.DEFAULT_VALUES.AGE_INCREMENT,
+ }
+
+ let projectionStub: sinon.SinonStub
+
+ beforeEach(() => {
+ projectionStub = sinon
+ .stub(apiActions, 'projectionHcsvPost')
+ .resolves(new Blob(['mock response'], { type: 'application/json' }))
+ })
+
+ afterEach(() => {
+ projectionStub.restore()
+ })
+
+ it('should call projectionHcsvPost once', async () => {
+ await runModel(mockModelParameterStore)
+ expect(projectionStub.calledOnce).to.be.true
+ })
+
+ it('should create CSV files correctly', () => {
+ const { blobPolygon, blobLayer } = createCSVFiles(mockModelParameterStore)
+
+ cy.wrap(blobPolygon).should('be.instanceOf', Blob)
+ cy.wrap(blobLayer).should('be.instanceOf', Blob)
+
+ const derivedByCode =
+ mockModelParameterStore.derivedBy === CONSTANTS.DERIVED_BY.VOLUME
+ ? CONSTANTS.INVENTORY_CODES.FIP
+ : mockModelParameterStore.derivedBy === CONSTANTS.DERIVED_BY.BASAL_AREA
+ ? CONSTANTS.INVENTORY_CODES.VRI
+ : ''
+
+ cy.wrap(blobPolygon.text()).then((text) => {
+ expect(text).to.include(derivedByCode)
+ expect(text).to.include(mockModelParameterStore.becZone)
+ expect(text).to.include(mockModelParameterStore.ecoZone)
+ expect(text).to.include(mockModelParameterStore.percentStockableArea)
+ })
+
+ cy.wrap(blobLayer.text()).then((text) => {
+ expect(text).to.include(mockModelParameterStore.highestPercentSpecies)
+ expect(text).to.include(mockModelParameterStore.bha50SiteIndex)
+ expect(text).to.include('PL,30.0')
+ expect(text).to.include('AC,30.0')
+ expect(text).to.include('H,30.0')
+ expect(text).to.include('S,10.0')
+ })
+ })
+
+ it('should reject with error for invalid model parameters', async () => {
+ const invalidModelParameterStore = {
+ ...mockModelParameterStore,
+ startingAge: null,
+ }
+
+ try {
+ await runModel(invalidModelParameterStore)
+ } catch (error) {
+ expect(error).to.be.an('error')
+ }
+ })
+
+ it('should call projectionHcsvPost with correct form data', async () => {
+ await runModel(mockModelParameterStore)
+
+ expect(projectionStub.calledOnce).to.be.true
+ const formDataArg = projectionStub.getCall(0).args[0] as FormData
+
+ expect(formDataArg.has('polygonInputData')).to.be.true
+ expect(formDataArg.has('layersInputData')).to.be.true
+ expect(formDataArg.has('projectionParameters')).to.be.true
+ })
+
+ it('should include additional options when secondary height is enabled', async () => {
+ const updatedModelParameterStore = {
+ ...mockModelParameterStore,
+ incSecondaryHeight: true,
+ }
+
+ await runModel(updatedModelParameterStore)
+
+ const formDataArg = projectionStub.getCall(0).args[0] as FormData
+ const projectionParamsBlob = formDataArg.get('projectionParameters') as Blob
+
+ const projectionParamsText = await projectionParamsBlob.text()
+ const projectionParams = JSON.parse(projectionParamsText)
+
+ expect(projectionParams.selectedExecutionOptions).to.include(
+ SelectedExecutionOptionsEnum.DoIncludeSecondarySpeciesDominantHeightInYieldTable,
+ )
+ })
+
+ it('should contain expected execution options', async () => {
+ await runModel(mockModelParameterStore)
+
+ const formDataArg = projectionStub.getCall(0).args[0] as FormData
+ const projectionParamsBlob = formDataArg.get('projectionParameters') as Blob
+
+ const projectionParamsText = await projectionParamsBlob.text()
+ const projectionParams = JSON.parse(projectionParamsText)
+
+ expect(projectionParams.selectedExecutionOptions).to.include(
+ SelectedExecutionOptionsEnum.DoEnableProgressLogging,
+ SelectedExecutionOptionsEnum.DoEnableErrorLogging,
+ )
+ })
+
+ it('should handle empty species list without errors', async () => {
+ const emptySpeciesStore = {
+ ...mockModelParameterStore,
+ speciesList: new Array(6).fill({ species: null, percent: '0.0' }),
+ }
+
+ await runModel(emptySpeciesStore)
+ expect(projectionStub.calledOnce).to.be.true
+ })
+
+ it('should handle missing fields in model parameter store', () => {
+ const invalidStore = {
+ ...mockModelParameterStore,
+ becZone: undefined,
+ }
+
+ const { blobPolygon } = createCSVFiles(invalidStore)
+ cy.wrap(blobPolygon.text()).then((text) => {
+ expect(text).not.to.include(DEFAULTS.DEFAULT_VALUES.BEC_ZONE)
+ })
+ })
+
+ it('should handle projectionHcsvPost failure', async () => {
+ projectionStub.rejects(new Error('API call failed'))
+
+ try {
+ await runModel(mockModelParameterStore)
+ } catch (error) {
+ expect(error).to.be.an('error')
+ expect(error.message).to.equal('API call failed')
+ }
+ })
+})
diff --git a/frontend/cypress/e2e/unit/modelParameterStore.cy.ts b/frontend/cypress/e2e/unit/modelParameterStore.cy.ts
new file mode 100644
index 00000000..97c2d7ce
--- /dev/null
+++ b/frontend/cypress/e2e/unit/modelParameterStore.cy.ts
@@ -0,0 +1,120 @@
+///
+
+import { setActivePinia, createPinia } from 'pinia'
+import { useModelParameterStore } from '@/stores/modelParameterStore'
+import { CONSTANTS, DEFAULTS } from '@/constants'
+
+describe('ModelParameterStore Unit Tests', () => {
+ let store: ReturnType
+
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ store = useModelParameterStore()
+ })
+
+ it('should initialize with default values', () => {
+ expect(store.panelOpenStates.speciesInfo).to.equal(CONSTANTS.PANEL.OPEN)
+ expect(store.panelOpenStates.siteInfo).to.equal(CONSTANTS.PANEL.CLOSE)
+ expect(store.panelOpenStates.standDensity).to.equal(CONSTANTS.PANEL.CLOSE)
+ expect(store.panelOpenStates.reportInfo).to.equal(CONSTANTS.PANEL.CLOSE)
+
+ expect(store.panelState.speciesInfo.confirmed).to.be.false
+ expect(store.panelState.speciesInfo.editable).to.be.true
+
+ expect(store.panelState.siteInfo.confirmed).to.be.false
+ expect(store.panelState.siteInfo.editable).to.be.false
+
+ expect(store.panelState.standDensity.confirmed).to.be.false
+ expect(store.panelState.standDensity.editable).to.be.false
+
+ expect(store.panelState.reportInfo.confirmed).to.be.false
+ expect(store.panelState.reportInfo.editable).to.be.false
+
+ expect(store.runModelEnabled).to.be.false
+ })
+
+ it('should confirm panel and enable the next panel', () => {
+ store.confirmPanel(CONSTANTS.MODEL_PARAMETER_PANEL.SPECIES_INFO)
+
+ expect(store.panelState.speciesInfo.confirmed).to.be.true
+ expect(store.panelState.speciesInfo.editable).to.be.false
+ expect(store.panelOpenStates.siteInfo).to.equal(CONSTANTS.PANEL.OPEN)
+ expect(store.panelState.siteInfo.editable).to.be.true
+ })
+
+ it('should edit panel and disable subsequent panels', () => {
+ store.confirmPanel(CONSTANTS.MODEL_PARAMETER_PANEL.SPECIES_INFO)
+ store.editPanel(CONSTANTS.MODEL_PARAMETER_PANEL.SPECIES_INFO)
+
+ expect(store.panelOpenStates.speciesInfo).to.equal(CONSTANTS.PANEL.OPEN)
+ expect(store.panelState.speciesInfo.confirmed).to.be.false
+ expect(store.panelState.speciesInfo.editable).to.be.true
+
+ expect(store.panelOpenStates.siteInfo).to.equal(CONSTANTS.PANEL.CLOSE)
+ expect(store.panelState.siteInfo.confirmed).to.be.false
+ expect(store.panelState.siteInfo.editable).to.be.false
+
+ expect(store.panelOpenStates.standDensity).to.equal(CONSTANTS.PANEL.CLOSE)
+ expect(store.panelState.standDensity.confirmed).to.be.false
+ expect(store.panelState.standDensity.editable).to.be.false
+
+ expect(store.panelOpenStates.reportInfo).to.equal(CONSTANTS.PANEL.CLOSE)
+ expect(store.panelState.reportInfo.confirmed).to.be.false
+ expect(store.panelState.reportInfo.editable).to.be.false
+ })
+
+ it('should calculate total species percent correctly', () => {
+ store.speciesList = [
+ { species: 'PL', percent: '40.0' },
+ { species: 'AC', percent: '35.5' },
+ { species: 'H', percent: '24.5' },
+ { species: 'S', percent: null },
+ { species: null, percent: null },
+ { species: null, percent: null },
+ ]
+ expect(store.totalSpeciesPercent).to.equal('100.0')
+ })
+
+ it('should update species groups correctly', () => {
+ store.speciesList = [
+ { species: 'PL', percent: '50.0' },
+ { species: 'PL', percent: '30.0' },
+ { species: 'AC', percent: '20.0' },
+ { species: null, percent: null },
+ ]
+ store.updateSpeciesGroup()
+
+ expect(store.speciesGroups.length).to.equal(2)
+ expect(store.speciesGroups[0].group).to.equal('PL')
+ expect(store.speciesGroups[0].percent).to.equal('80.0')
+ expect(store.speciesGroups[1].group).to.equal('AC')
+ expect(store.speciesGroups[1].percent).to.equal('20.0')
+ })
+
+ it('should set default values correctly', () => {
+ store.setDefaultValues()
+
+ expect(store.derivedBy).to.equal(DEFAULTS.DEFAULT_VALUES.DERIVED_BY)
+ expect(store.speciesList[0].species).to.equal('PL')
+ expect(store.speciesList[0].percent).to.equal('30.0')
+ expect(store.becZone).to.equal(DEFAULTS.DEFAULT_VALUES.BEC_ZONE)
+ expect(store.startingAge).to.equal(DEFAULTS.DEFAULT_VALUES.STARTING_AGE)
+ })
+
+ it('should handle empty species list without errors', () => {
+ store.speciesList = []
+ store.updateSpeciesGroup()
+
+ expect(store.speciesGroups.length).to.equal(0)
+ expect(store.highestPercentSpecies).to.be.null
+ })
+
+ it('should enable run model button when all panels are confirmed', () => {
+ store.confirmPanel(CONSTANTS.MODEL_PARAMETER_PANEL.SPECIES_INFO)
+ store.confirmPanel(CONSTANTS.MODEL_PARAMETER_PANEL.SITE_INFO)
+ store.confirmPanel(CONSTANTS.MODEL_PARAMETER_PANEL.STAND_DENSITY)
+ store.confirmPanel(CONSTANTS.MODEL_PARAMETER_PANEL.REPORT_INFO)
+
+ expect(store.runModelEnabled).to.be.true
+ })
+})
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index acb18605..f8f2d4d1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "vdyp",
- "version": "0.0.1.113",
+ "version": "0.0.1.114",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vdyp",
- "version": "0.0.1.113",
+ "version": "0.0.1.114",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@bcgov/bc-sans": "2.1.0",
@@ -44,6 +44,7 @@
"@storybook/vue3-vite": "8.4.7",
"@types/lodash": "4.17.14",
"@types/node": "20.14.5",
+ "@types/sinon": "17.0.3",
"@vue/eslint-config-prettier": "9.0.0",
"@vue/eslint-config-typescript": "13.0.0",
"cypress": "13.17.0",
@@ -54,6 +55,7 @@
"prettier": "3.3.3",
"sass": "1.77.6",
"sass-loader": "14.2.1",
+ "sinon": "19.0.2",
"storybook": "8.4.7",
"typescript": "5.6.3",
"vite": "5.4.8",
@@ -1249,6 +1251,50 @@
"integrity": "sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ==",
"dev": true
},
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz",
+ "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.1.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam/node_modules/type-detect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@sinonjs/text-encoding": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz",
+ "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==",
+ "dev": true
+ },
"node_modules/@storybook/addon-actions": {
"version": "8.4.7",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.7.tgz",
@@ -1892,6 +1938,15 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/sinon": {
+ "version": "17.0.3",
+ "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz",
+ "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==",
+ "dev": true,
+ "dependencies": {
+ "@types/sinonjs__fake-timers": "*"
+ }
+ },
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
@@ -3493,6 +3548,15 @@
"node": ">=6"
}
},
+ "node_modules/diff": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+ "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -5397,6 +5461,12 @@
"setimmediate": "^1.0.5"
}
},
+ "node_modules/just-extend": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
+ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==",
+ "dev": true
+ },
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
@@ -5500,6 +5570,13 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
+ "dev": true
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5791,6 +5868,19 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "node_modules/nise": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz",
+ "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1",
+ "@sinonjs/fake-timers": "^13.0.1",
+ "@sinonjs/text-encoding": "^0.7.3",
+ "just-extend": "^6.2.0",
+ "path-to-regexp": "^8.1.0"
+ }
+ },
"node_modules/node-html-parser": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.3.3.tgz",
@@ -6109,6 +6199,15 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
},
+ "node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -7063,6 +7162,24 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
+ "node_modules/sinon": {
+ "version": "19.0.2",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz",
+ "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1",
+ "@sinonjs/fake-timers": "^13.0.2",
+ "@sinonjs/samsam": "^8.0.1",
+ "diff": "^7.0.0",
+ "nise": "^6.1.1",
+ "supports-color": "^7.2.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -7465,6 +7582,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 5f2fc0d8..b4466a97 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "vdyp",
- "version": "0.0.1.113",
+ "version": "0.0.1.114",
"private": true,
"type": "module",
"description": "Variable Density Yield Projection",
@@ -55,6 +55,7 @@
"@storybook/vue3-vite": "8.4.7",
"@types/lodash": "4.17.14",
"@types/node": "20.14.5",
+ "@types/sinon": "17.0.3",
"@vue/eslint-config-prettier": "9.0.0",
"@vue/eslint-config-typescript": "13.0.0",
"cypress": "13.17.0",
@@ -65,6 +66,7 @@
"prettier": "3.3.3",
"sass": "1.77.6",
"sass-loader": "14.2.1",
+ "sinon": "19.0.2",
"storybook": "8.4.7",
"typescript": "5.6.3",
"vite": "5.4.8",