<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw, watch } from 'vue'
+import { ASYNC_COMPONENT_DELAY_MS, ASYNC_COMPONENT_TIMEOUT_MS } from '@/core/index.js'
import SkinLoadError from '@/shared/components/SkinLoadError.vue'
import SkinLoading from '@/shared/components/SkinLoading.vue'
import { useSkin } from '@/shared/composables/useSkin.js'
s.id,
markRaw(
defineAsyncComponent({
- delay: 200,
+ delay: ASYNC_COMPONENT_DELAY_MS,
errorComponent: SkinLoadError,
loader: () => s.loadLayout(),
loadingComponent: SkinLoading,
- timeout: 10000,
+ timeout: ASYNC_COMPONENT_TIMEOUT_MS,
})
),
])
// Local UI project constants
+export const ASYNC_COMPONENT_DELAY_MS = 200
+export const ASYNC_COMPONENT_TIMEOUT_MS = 10_000
export const EMPTY_VALUE_PLACEHOLDER = 'Ø'
+export const MAX_SKIN_ERROR_RELOADS = 2
+export const MAX_STATIONS_PER_ADD = 100
+export const WH_PER_KWH = 1000
export const ROUTE_NAMES = {
ADD_CHARGING_STATIONS: 'add-charging-stations',
export {
+ ASYNC_COMPONENT_DELAY_MS,
+ ASYNC_COMPONENT_TIMEOUT_MS,
EMPTY_VALUE_PLACEHOLDER,
LEGACY_UI_SERVER_CONFIG_KEY,
+ MAX_SKIN_ERROR_RELOADS,
+ MAX_STATIONS_PER_ADD,
ROUTE_NAMES,
SHARED_TOGGLE_BUTTON_KEY_PREFIX,
TOGGLE_BUTTON_KEY_PREFIX,
UI_SERVER_CONFIGURATION_INDEX_KEY,
+ WH_PER_KWH,
} from './Constants.js'
export {
chargingStationsKey,
UIClient,
uiClientKey,
} from '@/core/index.js'
-import { router } from '@/router'
+import { router } from '@/router/index.js'
import { SKIN_STORAGE_KEY, useSkin } from '@/shared/composables/useSkin.js'
import { DEFAULT_THEME, THEME_STORAGE_KEY, useTheme } from '@/shared/composables/useTheme.js'
import { DEFAULT_SKIN } from '@/skins/registry.js'
const initializeApp = async (app: AppType, config: ConfigurationData): Promise<void> => {
app.config.errorHandler = (error, instance, info) => {
console.error('Error:', error)
- console.info('Vue instance:', instance)
- console.info('Error info:', info)
+ if (import.meta.env.DEV) {
+ console.info('Vue instance:', instance)
+ console.info('Error info:', info)
+ }
// TODO: add code for UI notifications or other error handling logic
}
</template>
<script setup lang="ts">
-import { setToLocalStorage } from '@/core/index.js'
+import { MAX_SKIN_ERROR_RELOADS, setToLocalStorage } from '@/core/index.js'
import { SKIN_STORAGE_KEY } from '@/shared/composables/useSkin.js'
import { DEFAULT_SKIN, skins } from '@/skins/registry.js'
} catch {
// sessionStorage unavailable (e.g. Safari private browsing)
}
- if (count >= 2) {
+ if (count >= MAX_SKIN_ERROR_RELOADS) {
// Stop infinite reload loop — show message instead
return
}
+++ /dev/null
-export { useAddStationsForm } from './useAddStationsForm.js'
-export { useAsyncAction } from './useAsyncAction.js'
-export { useConnectorActions } from './useConnectorActions.js'
-export { useFetchData } from './useFetchData.js'
-export { useLayoutData } from './useLayoutData.js'
-export { useSetUrlForm } from './useSetUrlForm.js'
-export { useSimulatorControl } from './useSimulatorControl.js'
-export { SKIN_STORAGE_KEY, useSkin } from './useSkin.js'
-export type { SkinName } from './useSkin.js'
-export { useStartTxForm } from './useStartTxForm.js'
-export { useStationActions } from './useStationActions.js'
-export { AVAILABLE_THEMES, DEFAULT_THEME, THEME_STORAGE_KEY, useTheme } from './useTheme.js'
-export type { ThemeName } from './useTheme.js'
const fetching = vueRef(false)
const $toast = useToast()
const fetch = (): void => {
- if (!fetching.value) {
- fetching.value = true
- clientFn()
- .then((response: ResponsePayload) => {
- onSuccess(response)
- return undefined
- })
- .finally(() => {
- fetching.value = false
- })
- .catch((error: unknown) => {
- try {
- onError?.()
- } catch (callbackError: unknown) {
- console.error('Error in onError callback:', callbackError)
- }
- $toast.error(errorMsg)
- console.error(`${errorMsg}:`, error)
- })
+ if (fetching.value) {
+ return
}
+ fetching.value = true
+ // eslint-disable-next-line promise/catch-or-return -- .catch() is present; .finally() at end is idiomatic
+ clientFn()
+ .then((response: ResponsePayload) => {
+ onSuccess(response)
+ return undefined
+ })
+ .catch((error: unknown) => {
+ try {
+ onError?.()
+ } catch (callbackError: unknown) {
+ console.error('Error in onError callback:', callbackError)
+ }
+ $toast.error(errorMsg)
+ console.error(`${errorMsg}:`, error)
+ })
+ .finally(() => {
+ fetching.value = false
+ })
}
return { fetch, fetching }
}
/* sessionStorage unavailable */
}
return true
- } catch (error) {
+ } catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.warn(`[useSkin] Failed to load CSS for skin '${skinId}':`, message)
lastError.value = message
import { convertToInt, type OCPPVersion } from 'ui-common'
-import { readonly, ref, type Ref } from 'vue'
+import { type MaybeRef, readonly, ref, type Ref, toValue } from 'vue'
import { useToast } from 'vue-toast-notification'
import { useUIClient } from '@/core/index.js'
export interface StartTxFormConfig {
connectorId: string
- evseId?: number
+ evseId?: MaybeRef<number | undefined>
hashId: string
- ocppVersion?: OCPPVersion
+ ocppVersion?: MaybeRef<OCPPVersion | undefined>
options?: {
onCleanup?: () => void
onError?: (error: unknown, step?: 'authorize' | 'startTransaction') => void
}
try {
await $uiClient.authorize(hashId, idTag)
- } catch (error) {
+ } catch (error: unknown) {
$toast.error('Error at authorizing RFID tag')
console.error('Error at authorizing RFID tag:', error)
options?.onError?.(error, 'authorize')
try {
await $uiClient.startTransaction(hashId, {
connectorId: convertToInt(connectorId),
- evseId,
+ evseId: toValue(evseId),
idTag,
- ocppVersion,
+ ocppVersion: toValue(ocppVersion),
})
$toast.success('Transaction successfully started')
return true
- } catch (error) {
+ } catch (error: unknown) {
$toast.error('Error at starting transaction')
console.error('Error at starting transaction:', error)
options?.onError?.(error, 'startTransaction')
-import { EMPTY_VALUE_PLACEHOLDER } from '@/core/Constants.js'
+import { EMPTY_VALUE_PLACEHOLDER } from '@/core/index.js'
export interface FormatSupervisionUrlOptions {
/** Insert zero-width-space after dots for word-break in table cells. */
TOGGLE_BUTTON_KEY_PREFIX,
UI_SERVER_CONFIGURATION_INDEX_KEY,
useChargingStations,
-} from '@/core'
+} from '@/core/index.js'
import { useLayoutData } from '@/shared/composables/useLayoutData.js'
import { useSimulatorControl } from '@/shared/composables/useSimulatorControl.js'
import { useSkin } from '@/shared/composables/useSkin.js'
id="number-of-stations"
v-model="formState.numberOfStations"
class="classic-number-of-stations"
- max="100"
+ :max="MAX_STATIONS_PER_ADD"
min="1"
name="number-of-stations"
placeholder="number of stations"
<script setup lang="ts">
import { useRouter } from 'vue-router'
-import { resetToggleButtonState, ROUTE_NAMES } from '@/core'
+import { MAX_STATIONS_PER_ADD, resetToggleButtonState, ROUTE_NAMES } from '@/core/index.js'
import { useAddStationsForm } from '@/shared/composables/useAddStationsForm.js'
import Button from '../buttons/ClassicButton.vue'
<script setup lang="ts">
import { useRouter } from 'vue-router'
-import { resetToggleButtonState, ROUTE_NAMES } from '@/core'
+import { resetToggleButtonState, ROUTE_NAMES } from '@/core/index.js'
import { useSetUrlForm } from '@/shared/composables/useSetUrlForm.js'
import Button from '../buttons/ClassicButton.vue'
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
-import { resetToggleButtonState, ROUTE_NAMES } from '@/core'
+import { resetToggleButtonState, ROUTE_NAMES } from '@/core/index.js'
import { useStartTxForm } from '@/shared/composables/useStartTxForm.js'
import Button from '../buttons/ClassicButton.vue'
const { formState, submitForm } = useStartTxForm({
connectorId: props.connectorId,
- evseId: evseId.value,
+ evseId,
hashId: props.hashId,
- ocppVersion: ocppVersion.value,
+ ocppVersion,
options: {
onCleanup: () => {
resetToggleButtonState(toggleButtonId.value, true)
setToLocalStorage,
SHARED_TOGGLE_BUTTON_KEY_PREFIX,
TOGGLE_BUTTON_KEY_PREFIX,
-} from '@/core'
+} from '@/core/index.js'
import Button from './ClassicButton.vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
-import { EMPTY_VALUE_PLACEHOLDER, ROUTE_NAMES } from '@/core'
+import { EMPTY_VALUE_PLACEHOLDER, ROUTE_NAMES } from '@/core/index.js'
import { useConnectorActions } from '@/shared/composables/useConnectorActions.js'
import Button from '../buttons/ClassicButton.vue'
} from 'ui-common'
import { computed } from 'vue'
-import { deleteLocalStorageByKeyPattern, EMPTY_VALUE_PLACEHOLDER, ROUTE_NAMES } from '@/core'
+import {
+ deleteLocalStorageByKeyPattern,
+ EMPTY_VALUE_PLACEHOLDER,
+ ROUTE_NAMES,
+} from '@/core/index.js'
import { useStationActions } from '@/shared/composables/useStationActions.js'
import { formatSupervisionUrl } from '@/shared/utils/formatSupervisionUrl.js'
import { getATGStatus, getConnectorEntries } from '@/shared/utils/stationStatus.js'
<template>
<main class="modern-app">
<SimulatorBar
- :refresh-pending="loading"
:selected-server-index="uiServerIndex"
:server-switch-pending="serverSwitchPending"
:simulator-pending="simulatorPending"
:simulator-state="simulatorState"
:ui-server-configurations="uiServerConfigurations"
@add="showAddDialog = true"
- @refresh="getData"
@switch-server="handleUIServerChange"
@toggle-simulator="toggleSimulator"
/>
import { type OCPPVersion } from 'ui-common'
import { defineAsyncComponent, ref } from 'vue'
-import { getFromLocalStorage, UI_SERVER_CONFIGURATION_INDEX_KEY, useChargingStations } from '@/core'
+import {
+ ASYNC_COMPONENT_DELAY_MS,
+ ASYNC_COMPONENT_TIMEOUT_MS,
+ getFromLocalStorage,
+ UI_SERVER_CONFIGURATION_INDEX_KEY,
+ useChargingStations,
+} from '@/core/index.js'
import SkinLoadError from '@/shared/components/SkinLoadError.vue'
import SkinLoading from '@/shared/components/SkinLoading.vue'
import { useLayoutData } from '@/shared/composables/useLayoutData.js'
*/
function defineAsyncDialog (loader: () => Promise<{ default: unknown }>) {
return defineAsyncComponent({
- delay: 200,
+ delay: ASYNC_COMPONENT_DELAY_MS,
errorComponent: SkinLoadError,
loader: loader as () => Promise<{ default: import('vue').Component }>,
loadingComponent: SkinLoading,
- timeout: 10000,
+ timeout: ASYNC_COMPONENT_TIMEOUT_MS,
})
}
const $chargingStations = useChargingStations()
const layoutData = useLayoutData()
-const { getChargingStations, getData, loading, simulatorState, uiServerConfigurations } = layoutData
+const { getChargingStations, simulatorState, uiServerConfigurations } = layoutData
const uiServerIndex = ref(getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0))
import { computed } from 'vue'
+import { WH_PER_KWH } from '@/core/index.js'
import { useConnectorActions } from '@/shared/composables/useConnectorActions.js'
import { getConnectorStatusVariant } from '@/shared/utils/stationStatus.js'
const txEnergy = computed(() => {
const wh = props.connector.transactionEnergyActiveImportRegisterValue
if (wh == null) return '—'
- if (wh >= 1000) return `${(wh / 1000).toFixed(2)} kWh`
+ if (wh >= WH_PER_KWH) return `${(wh / WH_PER_KWH).toFixed(2)} kWh`
return `${Math.round(wh)} Wh`
})
</select>
</div>
<div class="modern-bar__group">
- <!-- <ActionButton
- variant="ghost"
- :pending="refreshPending"
- title="Refresh charging stations"
- @click="$emit('refresh')"
- >
- Refresh
- </ActionButton> -->
<ActionButton
variant="primary"
@click="$emit('add')"
}
const props = defineProps<{
- refreshPending?: boolean
selectedServerIndex: number
serverSwitchPending?: boolean
simulatorPending?: boolean
defineEmits<{
add: []
- refresh: []
'switch-server': [index: number]
'toggle-simulator': []
}>()
} from 'ui-common'
import { computed, ref } from 'vue'
-import { deleteLocalStorageByKeyPattern, EMPTY_VALUE_PLACEHOLDER as EMPTY } from '@/core'
+import { deleteLocalStorageByKeyPattern, EMPTY_VALUE_PLACEHOLDER as EMPTY } from '@/core/index.js'
import { useStationActions } from '@/shared/composables/useStationActions.js'
import { formatSupervisionUrl } from '@/shared/utils/formatSupervisionUrl.js'
import {
id="modern-add-count"
v-model.number="formState.numberOfStations"
class="modern-form__input"
- max="100"
+ :max="MAX_STATIONS_PER_ADD"
min="1"
type="number"
required
</template>
<script setup lang="ts">
+import { MAX_STATIONS_PER_ADD } from '@/core/index.js'
import { useAddStationsForm } from '@/shared/composables/useAddStationsForm.js'
import ActionButton from '../ActionButton.vue'
import { computed, ref } from 'vue'
import { useToast } from 'vue-toast-notification'
-import { useUIClient } from '@/core'
+import { useUIClient } from '@/core/index.js'
import { type FailureInfo, getFailureInfo } from '../../utils/errors.js'
import ActionButton from '../ActionButton.vue'
await $uiClient.authorize(props.hashId, idTag.value)
$toast.success(`Authorized ${idTag.value}`)
close()
- } catch (error) {
+ } catch (error: unknown) {
console.error('Error authorizing:', error)
const info = getFailureInfo(error)
lastFailure.value = info
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
-import { useChargingStations, useUIClient } from '@/core'
+import { useChargingStations, useUIClient } from '@/core/index.js'
import { useSetUrlForm } from '@/shared/composables/useSetUrlForm.js'
import { stripStationId } from '@/shared/utils/stripStationId.js'
import { useStartTxForm } from '@/shared/composables/useStartTxForm.js'
-import { type FailureInfo, getFailureInfo } from '../../utils/errors'
+import { type FailureInfo, getFailureInfo } from '../../utils/errors.js'
import ActionButton from '../ActionButton.vue'
import Modal from '../ModernModal.vue'
} from 'ui-common'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { UIClient } from '@/core'
+import { UIClient } from '@/core/index.js'
import { toastMock } from '../setup'
import { createUIServerConfig, TEST_HASH_ID, TEST_ID_TAG } from './constants'
useChargingStations,
useConfiguration,
useTemplates,
-} from '@/core'
+} from '@/core/index.js'
import { useFetchData } from '@/shared/composables/useFetchData.js'
import { toastMock } from '../setup'
*/
import { describe, expect, it, vi } from 'vitest'
-import { ROUTE_NAMES } from '@/core'
+import { ROUTE_NAMES } from '@/core/index.js'
import { router } from '@/router/index.js'
vi.mock('@/shared/composables/useSkin.js', async importOriginal => {
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { type Ref, ref } from 'vue'
-import { useChargingStations, useConfiguration, useTemplates, useUIClient } from '@/core'
+import { useChargingStations, useConfiguration, useTemplates, useUIClient } from '@/core/index.js'
-vi.mock('@/core', async importOriginal => {
+vi.mock('@/core/index.js', async importOriginal => {
const actual = await importOriginal()
return {
...(actual as Record<string, unknown>),
useChargingStations,
useConfiguration,
useUIClient,
-} from '@/core'
+} from '@/core/index.js'
import { toastMock } from '../../../setup.js'
import { createUIServerConfig } from '../../constants'
import { createMockUIClient, type MockUIClient, withSetup } from '../../helpers.js'
-vi.mock('@/core', async importOriginal => {
+vi.mock('@/core/index.js', async importOriginal => {
const actual = await importOriginal()
return {
...(actual as Record<string, unknown>),
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref, shallowRef } from 'vue'
-import { chargingStationsKey, configurationKey, templatesKey, uiClientKey } from '@/core'
+import { chargingStationsKey, configurationKey, templatesKey, uiClientKey } from '@/core/index.js'
import AddChargingStations from '@/skins/classic/components/actions/AddChargingStations.vue'
import SetSupervisionUrl from '@/skins/classic/components/actions/SetSupervisionUrl.vue'
import StartTransaction from '@/skins/classic/components/actions/StartTransaction.vue'
import { type ConnectorStatus, OCPP16ChargePointStatus, OCPPVersion } from 'ui-common'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { uiClientKey } from '@/core'
+import { uiClientKey } from '@/core/index.js'
import CSConnector from '@/skins/classic/components/charging-stations/CSConnector.vue'
import { toastMock } from '../../../setup.js'
import { type ChargingStationData, OCPP16AvailabilityType, OCPPVersion } from 'ui-common'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { uiClientKey } from '@/core'
+import { uiClientKey } from '@/core/index.js'
import CSData from '@/skins/classic/components/charging-stations/CSData.vue'
import { toastMock } from '../../../setup.js'
import { shallowMount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { uiClientKey } from '@/core'
+import { uiClientKey } from '@/core/index.js'
import CSTable from '@/skins/classic/components/charging-stations/CSTable.vue'
import { createChargingStationData } from '../../constants.js'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, shallowRef } from 'vue'
-import { chargingStationsKey, configurationKey, templatesKey, uiClientKey } from '@/core'
+import { chargingStationsKey, configurationKey, templatesKey, uiClientKey } from '@/core/index.js'
import ToggleButton from '@/skins/classic/components/buttons/ToggleButton.vue'
import CSTable from '@/skins/classic/components/charging-stations/CSTable.vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
-import { chargingStationsKey, configurationKey, templatesKey, uiClientKey } from '@/core'
+import { chargingStationsKey, configurationKey, templatesKey, uiClientKey } from '@/core/index.js'
import ClassicLayout from '@/skins/classic/ClassicLayout.vue'
import { createUIServerConfig } from '../../constants'
} from 'ui-common'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { uiClientKey } from '@/core'
+import { uiClientKey } from '@/core/index.js'
import ConnectorRow from '@/skins/modern/components/ConnectorRow.vue'
import { toastMock } from '../../../setup.js'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, ref } from 'vue'
-import { chargingStationsKey, templatesKey, uiClientKey } from '@/core'
+import { chargingStationsKey, templatesKey, uiClientKey } from '@/core/index.js'
// Mock Modal to render slots inline (no Teleport), so `wrapper.find()` works.
vi.mock('@/skins/modern/components/ModernModal.vue', () => ({
import { type ResponsePayload, ResponseStatus, ServerFailureError } from 'ui-common'
import { describe, expect, it } from 'vitest'
-import { getFailureInfo } from '@/skins/modern/utils/errors'
+import { getFailureInfo } from '@/skins/modern/utils/errors.js'
/**
* Builds a ResponsePayload-shaped object with custom responsesFailed entries.
UI_SERVER_CONFIGURATION_INDEX_KEY,
uiClientKey,
useUIClient,
-} from '@/core'
+} from '@/core/index.js'
import ModernLayout from '@/skins/modern/ModernLayout.vue'
import { toastMock } from '../../../setup.js'
import { createChargingStationData, createUIServerConfig } from '../../constants'
import { createMockUIClient, type MockUIClient } from '../../helpers.js'
-vi.mock('@/core', async importOriginal => {
+vi.mock('@/core/index.js', async importOriginal => {
const actual = await importOriginal()
return { ...(actual as Record<string, unknown>), useUIClient: vi.fn() }
})
},
SetSupervisionUrlDialog: true,
SimulatorBar: {
- emits: ['add', 'refresh', 'switch-server', 'toggle-simulator'],
+ emits: ['add', 'switch-server', 'toggle-simulator'],
props: [
- 'refreshPending',
'selectedServerIndex',
'simulatorPending',
'simulatorState',
'uiServerConfigurations',
],
template: `<div class="stub-sim-bar">
- <button class="stub-refresh" @click="$emit('refresh')">r</button>
<button class="stub-add" @click="$emit('add')">+</button>
<button class="stub-toggle" @click="$emit('toggle-simulator')">t</button>
<button class="stub-switch" @click="$emit('switch-server', 1)">s</button>
})
const wrapper = mountView()
await flushPromises()
- await wrapper.find('.stub-refresh').trigger('click')
+ getWSHandler('open')?.()
await flushPromises()
await wrapper.find('.stub-toggle').trigger('click')
await flushPromises()
})
const wrapper = mountView()
await flushPromises()
- await wrapper.find('.stub-refresh').trigger('click')
+ getWSHandler('open')?.()
await flushPromises()
await wrapper.find('.stub-toggle').trigger('click')
await flushPromises()
})
const wrapper = mountView()
await flushPromises()
- await wrapper.find('.stub-refresh').trigger('click')
+ getWSHandler('open')?.()
await flushPromises()
await wrapper.find('.stub-toggle').trigger('click')
await flushPromises()
})
const wrapper = mountView()
await flushPromises()
- await wrapper.find('.stub-refresh').trigger('click')
+ getWSHandler('open')?.()
await flushPromises()
await wrapper.find('.stub-toggle').trigger('click')
await flushPromises()
mockClient.startSimulator = vi.fn().mockRejectedValue(new Error('x'))
const wrapper = mountView()
await flushPromises()
- await wrapper.find('.stub-refresh').trigger('click')
+ getWSHandler('open')?.()
await flushPromises()
await wrapper.find('.stub-toggle').trigger('click')
await flushPromises()
expect(wrapper.emitted('switch-server')).toEqual([[1]])
})
- // it('should emit refresh when refresh button is clicked', async () => {
- // const wrapper = mountBar()
- // const [refreshBtn] = wrapper.findAll('.modern-btn')
- // await refreshBtn.trigger('click')
- // expect(wrapper.emitted('refresh')).toHaveLength(1)
- // })
-
it('should emit add when add-stations button is clicked', async () => {
const wrapper = mountBar()
const buttons = wrapper.findAll('.modern-btn')
import { type ChargingStationData, OCPP16AvailabilityType } from 'ui-common'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { uiClientKey } from '@/core'
+import { uiClientKey } from '@/core/index.js'
import StationCard from '@/skins/modern/components/StationCard.vue'
import { toastMock } from '../../../setup.js'