]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ui): factorize shared code across ui packages
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 17 Apr 2026 00:29:14 +0000 (02:29 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 17 Apr 2026 00:29:14 +0000 (02:29 +0200)
- Centralize ConnectionError, extractErrorMessage, and protocol defaults in ui-common
- Remove UUID duplication from ui-web (import from ui-common instead)
- Extract shared CSS (data-table, action-header) into shared.css
- Add buildHashIdsPayload() and pickDefined() helpers to eliminate CLI command duplication
- Replace 12 repetitive OCPP command definitions with declarative registry
- Extend useExecuteAction() composable with onFinally support and migrate action components
- Remove backward-compat re-export shims; consumers import directly from ui-common

29 files changed:
ui/cli/src/client/lifecycle.ts
ui/cli/src/commands/atg.ts
ui/cli/src/commands/connection.ts
ui/cli/src/commands/connector.ts
ui/cli/src/commands/ocpp.ts
ui/cli/src/commands/payload.ts [new file with mode: 0644]
ui/cli/src/commands/station.ts
ui/cli/src/commands/supervision.ts
ui/cli/src/commands/transaction.ts
ui/cli/src/config/defaults.ts [deleted file]
ui/cli/src/config/loader.ts
ui/cli/src/output/formatter.ts
ui/cli/src/output/json.ts
ui/cli/src/utils/errors.ts [deleted file]
ui/cli/tests/config.test.ts
ui/cli/tests/lifecycle.test.ts
ui/common/src/constants.ts
ui/common/src/errors.ts [moved from ui/cli/src/client/errors.ts with 76% similarity]
ui/common/src/index.ts
ui/web/src/assets/shared.css [new file with mode: 0644]
ui/web/src/components/actions/AddChargingStations.vue
ui/web/src/components/actions/SetSupervisionUrl.vue
ui/web/src/components/actions/StartTransaction.vue
ui/web/src/components/charging-stations/CSData.vue
ui/web/src/components/charging-stations/CSTable.vue
ui/web/src/composables/Utils.ts
ui/web/src/main.ts
ui/web/tests/unit/AddChargingStations.test.ts
ui/web/tests/unit/SetSupervisionUrl.test.ts

index b4e3d7bbf9c21b2fda453619c51fd14ba5719879..07546ae755b8242714159d8d6597285f6b2201b8 100644 (file)
@@ -1,6 +1,7 @@
 import process from 'node:process'
 import ora from 'ora'
 import {
+  ConnectionError,
   type ProcedureName,
   type RequestPayload,
   type ResponsePayload,
@@ -13,7 +14,6 @@ import { WebSocket as WsWebSocket } from 'ws'
 
 import type { Formatter } from '../output/formatter.js'
 
-import { ConnectionError } from './errors.js'
 import { createWsAdapter } from './ws-adapter.js'
 
 const wsFactory: WebSocketFactory = (url, protocols) =>
index 290d18272416a52c1fd87fc7cf4e2e2bfb15d258..0bac889f8251e20df6c0cc393974a11fd9b55aec 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { runAction } from './action.js'
+import { buildHashIdsPayload } from './payload.js'
 
 const parseCommaSeparatedInts = (value: string): number[] => {
   const parsed = value.split(',').map(s => Number.parseInt(s.trim(), 10))
@@ -21,7 +22,7 @@ export const createAtgCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { connectorIds?: number[] }) => {
       const payload: RequestPayload = {
         ...(options.connectorIds != null && { connectorIds: options.connectorIds }),
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, payload)
     })
@@ -33,7 +34,7 @@ export const createAtgCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { connectorIds?: number[] }) => {
       const payload: RequestPayload = {
         ...(options.connectorIds != null && { connectorIds: options.connectorIds }),
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, payload)
     })
index e201c05961fab039835b9e233f1a9a11b5bd3a7b..fcf6c5386093caec45dee58ae76a9da3c0cce6cb 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { runAction } from './action.js'
+import { buildHashIdsPayload } from './payload.js'
 
 export const createConnectionCommands = (program: Command): Command => {
   const cmd = new Command('connection').description('WebSocket connection management')
@@ -10,7 +11,7 @@ export const createConnectionCommands = (program: Command): Command => {
     .command('open [hashIds...]')
     .description('Open WebSocket connection')
     .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
+      const payload: RequestPayload = buildHashIdsPayload(hashIds)
       await runAction(program, ProcedureName.OPEN_CONNECTION, payload)
     })
 
@@ -18,7 +19,7 @@ export const createConnectionCommands = (program: Command): Command => {
     .command('close [hashIds...]')
     .description('Close WebSocket connection')
     .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
+      const payload: RequestPayload = buildHashIdsPayload(hashIds)
       await runAction(program, ProcedureName.CLOSE_CONNECTION, payload)
     })
 
index 902277f0275fa12e00556bf091601bd19ea7fc33..25ffadf9a0b4bfc2152874771aa3a4aa355f2301 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { parseInteger, runAction } from './action.js'
+import { buildHashIdsPayload } from './payload.js'
 
 export const createConnectorCommands = (program: Command): Command => {
   const cmd = new Command('connector').description('Connector management')
@@ -13,7 +14,7 @@ export const createConnectorCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { connectorId: number }) => {
       const payload: RequestPayload = {
         connectorId: options.connectorId,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.LOCK_CONNECTOR, payload)
     })
@@ -25,7 +26,7 @@ export const createConnectorCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { connectorId: number }) => {
       const payload: RequestPayload = {
         connectorId: options.connectorId,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.UNLOCK_CONNECTOR, payload)
     })
index 3c4e2267ce54abee9fa00664691bb8bab27dafcd..2536d482fdc34c06d82c6bb5ae5ff199257325af 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { parseInteger, runAction } from './action.js'
+import { buildHashIdsPayload, pickDefined } from './payload.js'
 
 export const createOcppCommands = (program: Command): Command => {
   const cmd = new Command('ocpp').description('OCPP protocol commands')
@@ -13,19 +14,11 @@ export const createOcppCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { idTag: string }) => {
       const payload: RequestPayload = {
         idTag: options.idTag,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.AUTHORIZE, payload)
     })
 
-  cmd
-    .command('boot-notification [hashIds...]')
-    .description('Send OCPP BootNotification')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.BOOT_NOTIFICATION, payload)
-    })
-
   cmd
     .command('data-transfer [hashIds...]')
     .description('Send OCPP DataTransfer')
@@ -38,63 +31,17 @@ export const createOcppCommands = (program: Command): Command => {
         options: { data?: string; messageId?: string; vendorId?: string }
       ) => {
         const payload: RequestPayload = {
-          ...(options.vendorId != null && { vendorId: options.vendorId }),
-          ...(options.messageId != null && { messageId: options.messageId }),
-          ...(options.data != null && { data: options.data }),
-          ...(hashIds.length > 0 && { hashIds }),
-        }
+          ...pickDefined(options as Record<string, unknown>, {
+            data: 'data',
+            messageId: 'messageId',
+            vendorId: 'vendorId',
+          }),
+          ...buildHashIdsPayload(hashIds),
+        } as RequestPayload
         await runAction(program, ProcedureName.DATA_TRANSFER, payload)
       }
     )
 
-  cmd
-    .command('diagnostics-status-notification [hashIds...]')
-    .description('Send OCPP DiagnosticsStatusNotification')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION, payload)
-    })
-
-  cmd
-    .command('firmware-status-notification [hashIds...]')
-    .description('Send OCPP FirmwareStatusNotification')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.FIRMWARE_STATUS_NOTIFICATION, payload)
-    })
-
-  cmd
-    .command('get-15118-ev-certificate [hashIds...]')
-    .description('Send OCPP Get15118EVCertificate')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.GET_15118_EV_CERTIFICATE, payload)
-    })
-
-  cmd
-    .command('get-certificate-status [hashIds...]')
-    .description('Send OCPP GetCertificateStatus')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.GET_CERTIFICATE_STATUS, payload)
-    })
-
-  cmd
-    .command('heartbeat [hashIds...]')
-    .description('Send OCPP Heartbeat')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.HEARTBEAT, payload)
-    })
-
-  cmd
-    .command('log-status-notification [hashIds...]')
-    .description('Send OCPP LogStatusNotification')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.LOG_STATUS_NOTIFICATION, payload)
-    })
-
   cmd
     .command('meter-values [hashIds...]')
     .description('Send OCPP MeterValues')
@@ -102,43 +49,11 @@ export const createOcppCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { connectorId: number }) => {
       const payload: RequestPayload = {
         connectorId: options.connectorId,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.METER_VALUES, payload)
     })
 
-  cmd
-    .command('notify-customer-information [hashIds...]')
-    .description('Send OCPP NotifyCustomerInformation')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.NOTIFY_CUSTOMER_INFORMATION, payload)
-    })
-
-  cmd
-    .command('notify-report [hashIds...]')
-    .description('Send OCPP NotifyReport')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.NOTIFY_REPORT, payload)
-    })
-
-  cmd
-    .command('security-event-notification [hashIds...]')
-    .description('Send OCPP SecurityEventNotification')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.SECURITY_EVENT_NOTIFICATION, payload)
-    })
-
-  cmd
-    .command('sign-certificate [hashIds...]')
-    .description('Send OCPP SignCertificate')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.SIGN_CERTIFICATE, payload)
-    })
-
   cmd
     .command('status-notification [hashIds...]')
     .description('Send OCPP StatusNotification')
@@ -154,19 +69,35 @@ export const createOcppCommands = (program: Command): Command => {
           connectorId: options.connectorId,
           errorCode: options.errorCode,
           status: options.status,
-          ...(hashIds.length > 0 && { hashIds }),
+          ...buildHashIdsPayload(hashIds),
         }
         await runAction(program, ProcedureName.STATUS_NOTIFICATION, payload)
       }
     )
 
-  cmd
-    .command('transaction-event [hashIds...]')
-    .description('Send OCPP TransactionEvent')
-    .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
-      await runAction(program, ProcedureName.TRANSACTION_EVENT, payload)
-    })
+  const simpleOcppCommands: [string, string, ProcedureName][] = [
+    ['boot-notification', 'Send OCPP BootNotification', ProcedureName.BOOT_NOTIFICATION],
+    ['diagnostics-status-notification', 'Send OCPP DiagnosticsStatusNotification', ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION],
+    ['firmware-status-notification', 'Send OCPP FirmwareStatusNotification', ProcedureName.FIRMWARE_STATUS_NOTIFICATION],
+    ['get-15118-ev-certificate', 'Send OCPP Get15118EVCertificate', ProcedureName.GET_15118_EV_CERTIFICATE],
+    ['get-certificate-status', 'Send OCPP GetCertificateStatus', ProcedureName.GET_CERTIFICATE_STATUS],
+    ['heartbeat', 'Send OCPP Heartbeat', ProcedureName.HEARTBEAT],
+    ['log-status-notification', 'Send OCPP LogStatusNotification', ProcedureName.LOG_STATUS_NOTIFICATION],
+    ['notify-customer-information', 'Send OCPP NotifyCustomerInformation', ProcedureName.NOTIFY_CUSTOMER_INFORMATION],
+    ['notify-report', 'Send OCPP NotifyReport', ProcedureName.NOTIFY_REPORT],
+    ['security-event-notification', 'Send OCPP SecurityEventNotification', ProcedureName.SECURITY_EVENT_NOTIFICATION],
+    ['sign-certificate', 'Send OCPP SignCertificate', ProcedureName.SIGN_CERTIFICATE],
+    ['transaction-event', 'Send OCPP TransactionEvent', ProcedureName.TRANSACTION_EVENT],
+  ]
+
+  for (const [name, description, procedureName] of simpleOcppCommands) {
+    cmd
+      .command(`${name} [hashIds...]`)
+      .description(description)
+      .action(async (hashIds: string[]) => {
+        await runAction(program, procedureName, buildHashIdsPayload(hashIds))
+      })
+  }
 
   return cmd
 }
diff --git a/ui/cli/src/commands/payload.ts b/ui/cli/src/commands/payload.ts
new file mode 100644 (file)
index 0000000..da28585
--- /dev/null
@@ -0,0 +1,17 @@
+import type { RequestPayload } from 'ui-common'
+
+export const buildHashIdsPayload = (hashIds: string[]): RequestPayload =>
+  hashIds.length > 0 ? { hashIds } : {}
+
+export const pickDefined = (
+  source: Record<string, unknown>,
+  keyMap: Record<string, string>
+): Record<string, unknown> => {
+  const result: Record<string, unknown> = {}
+  for (const [sourceKey, targetKey] of Object.entries(keyMap)) {
+    if (source[sourceKey] != null) {
+      result[targetKey] = source[sourceKey]
+    }
+  }
+  return result
+}
index 33b75a29c022e589ca254c695c38daa36d2e6136..8e082b8a1582b0c5f26d8ab24deae18b4fe41e78 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { parseInteger, runAction } from './action.js'
+import { buildHashIdsPayload, pickDefined } from './payload.js'
 
 export const createStationCommands = (program: Command): Command => {
   const cmd = new Command('station').description('Charging station management')
@@ -17,7 +18,7 @@ export const createStationCommands = (program: Command): Command => {
     .command('start [hashIds...]')
     .description('Start charging station(s)')
     .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
+      const payload: RequestPayload = buildHashIdsPayload(hashIds)
       await runAction(program, ProcedureName.START_CHARGING_STATION, payload)
     })
 
@@ -25,7 +26,7 @@ export const createStationCommands = (program: Command): Command => {
     .command('stop [hashIds...]')
     .description('Stop charging station(s)')
     .action(async (hashIds: string[]) => {
-      const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {}
+      const payload: RequestPayload = buildHashIdsPayload(hashIds)
       await runAction(program, ProcedureName.STOP_CHARGING_STATION, payload)
     })
 
@@ -49,18 +50,12 @@ export const createStationCommands = (program: Command): Command => {
       }) => {
         const payload: RequestPayload = {
           numberOfStations: options.count,
-          options: {
-            ...(options.autoStart != null && { autoStart: options.autoStart }),
-            ...(options.ocppStrict != null && {
-              ocppStrictCompliance: options.ocppStrict,
-            }),
-            ...(options.persistentConfig != null && {
-              persistentConfiguration: options.persistentConfig,
-            }),
-            ...(options.supervisionUrl != null && {
-              supervisionUrls: options.supervisionUrl,
-            }),
-          },
+          options: pickDefined(options as Record<string, unknown>, {
+            autoStart: 'autoStart',
+            ocppStrict: 'ocppStrictCompliance',
+            persistentConfig: 'persistentConfiguration',
+            supervisionUrl: 'supervisionUrls',
+          }) as RequestPayload,
           template: options.template,
         }
         await runAction(program, ProcedureName.ADD_CHARGING_STATIONS, payload)
@@ -74,7 +69,7 @@ export const createStationCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { deleteConfig?: true }) => {
       const payload: RequestPayload = {
         ...(options.deleteConfig != null && { deleteConfiguration: options.deleteConfig }),
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.DELETE_CHARGING_STATIONS, payload)
     })
index 36db9407e7d98e2b8e9907fe00fe96e73d42036d..782181ec45b7e77335d4d320c3095b091a784668 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { runAction } from './action.js'
+import { buildHashIdsPayload } from './payload.js'
 
 export const createSupervisionCommands = (program: Command): Command => {
   const cmd = new Command('supervision').description('Supervision URL management')
@@ -13,7 +14,7 @@ export const createSupervisionCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { url: string }) => {
       const payload: RequestPayload = {
         url: options.url,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.SET_SUPERVISION_URL, payload)
     })
index 67a1b8bd9f71f7dee31f837efb36b2261150bfce..59bfdae0b706ed77d9614109f080a9e49f9e2af4 100644 (file)
@@ -2,6 +2,7 @@ import { Command } from 'commander'
 import { ProcedureName, type RequestPayload } from 'ui-common'
 
 import { parseInteger, runAction } from './action.js'
+import { buildHashIdsPayload } from './payload.js'
 
 export const createTransactionCommands = (program: Command): Command => {
   const cmd = new Command('transaction').description('Transaction management')
@@ -15,7 +16,7 @@ export const createTransactionCommands = (program: Command): Command => {
       const payload: RequestPayload = {
         connectorId: options.connectorId,
         idTag: options.idTag,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.START_TRANSACTION, payload)
     })
@@ -27,7 +28,7 @@ export const createTransactionCommands = (program: Command): Command => {
     .action(async (hashIds: string[], options: { transactionId: number }) => {
       const payload: RequestPayload = {
         transactionId: options.transactionId,
-        ...(hashIds.length > 0 && { hashIds }),
+        ...buildHashIdsPayload(hashIds),
       }
       await runAction(program, ProcedureName.STOP_TRANSACTION, payload)
     })
diff --git a/ui/cli/src/config/defaults.ts b/ui/cli/src/config/defaults.ts
deleted file mode 100644 (file)
index 90f5114..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Protocol, ProtocolVersion } from 'ui-common'
-
-export const DEFAULT_PROTOCOL = Protocol.UI
-export const DEFAULT_VERSION = ProtocolVersion['0.0.1']
-export const DEFAULT_SECURE = false
index 56d9e59a8fe99ceb0cd487d68b052e7a33272187..135f215679b5129b8173f008962de0d68a4236ef 100644 (file)
@@ -5,13 +5,14 @@ import process from 'node:process'
 import {
   DEFAULT_HOST,
   DEFAULT_PORT,
+  DEFAULT_PROTOCOL,
+  DEFAULT_PROTOCOL_VERSION,
+  DEFAULT_SECURE,
+  extractErrorMessage,
   uiServerConfigSchema,
   type UIServerConfigurationSection,
 } from 'ui-common'
 
-import { extractErrorMessage } from '../utils/errors.js'
-import { DEFAULT_PROTOCOL, DEFAULT_SECURE, DEFAULT_VERSION } from './defaults.js'
-
 interface LoadConfigOptions {
   configPath?: string
   url?: string
@@ -86,7 +87,7 @@ export const loadConfig = async (
     port: DEFAULT_PORT,
     protocol: DEFAULT_PROTOCOL,
     secure: DEFAULT_SECURE,
-    version: DEFAULT_VERSION,
+    version: DEFAULT_PROTOCOL_VERSION,
   }
 
   const fileConfig = await loadConfigFile(options.configPath)
index d3ae953a699445c6e9677f4b104862c5e3b4e4dc..8e8f21b543aa44f65b1ba8dc06bae137d9b5648c 100644 (file)
@@ -1,6 +1,5 @@
-import type { ResponsePayload } from 'ui-common'
+import { extractErrorMessage, type ResponsePayload } from 'ui-common'
 
-import { extractErrorMessage } from '../utils/errors.js'
 import { printError } from './human.js'
 import { outputJson, outputJsonError } from './json.js'
 import { outputTable } from './table.js'
index 9287e761b1096405965fb79296c957bc0e3f28e5..28c6a97d7280fd123f3219ba5fe992c1e7fe045d 100644 (file)
@@ -1,7 +1,5 @@
 import process from 'node:process'
-import { type ResponsePayload, ResponseStatus } from 'ui-common'
-
-import { extractErrorMessage } from '../utils/errors.js'
+import { extractErrorMessage, type ResponsePayload, ResponseStatus } from 'ui-common'
 
 export const outputJson = (payload: ResponsePayload): void => {
   process.stdout.write(JSON.stringify(payload, null, 2) + '\n')
diff --git a/ui/cli/src/utils/errors.ts b/ui/cli/src/utils/errors.ts
deleted file mode 100644 (file)
index d3e998c..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export const extractErrorMessage = (error: unknown): string =>
-  error instanceof Error ? error.message : String(error)
index 23026360f6431caa72f340674f02eb2a2f5b1dc4..9bff41f2a4d13d0554290536453502333d38fb2f 100644 (file)
@@ -3,9 +3,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { afterEach, beforeEach, describe, it } from 'node:test'
-import { DEFAULT_HOST, DEFAULT_PORT } from 'ui-common'
+import { DEFAULT_HOST, DEFAULT_PORT, DEFAULT_PROTOCOL, DEFAULT_PROTOCOL_VERSION, DEFAULT_SECURE } from 'ui-common'
 
-import { DEFAULT_PROTOCOL, DEFAULT_SECURE, DEFAULT_VERSION } from '../src/config/defaults.js'
 import { loadConfig } from '../src/config/loader.js'
 
 let tempDir: string
@@ -32,7 +31,7 @@ await describe('CLI config loader', async () => {
     assert.strictEqual(config.host, DEFAULT_HOST)
     assert.strictEqual(config.port, DEFAULT_PORT)
     assert.strictEqual(config.protocol, DEFAULT_PROTOCOL)
-    assert.strictEqual(config.version, DEFAULT_VERSION)
+    assert.strictEqual(config.version, DEFAULT_PROTOCOL_VERSION)
     assert.strictEqual(config.secure, DEFAULT_SECURE)
   })
 
index c6d54a601d47d8eb7b60c27100855fc68da9a788..3b6e21a7919bc64242ca76a874264022215b8ff2 100644 (file)
@@ -2,9 +2,8 @@
 
 import assert from 'node:assert'
 import { describe, it } from 'node:test'
-import { Protocol, ProtocolVersion } from 'ui-common'
+import { ConnectionError, Protocol, ProtocolVersion } from 'ui-common'
 
-import { ConnectionError } from '../src/client/errors.js'
 import { executeCommand } from '../src/client/lifecycle.js'
 
 await describe('lifecycle', async () => {
index 2c3a3b98335da11bae81cdf8adad671bd65c3a3c..b733b07a9e5a728a3e8d8934b01daf70bfb2637d 100644 (file)
@@ -1,3 +1,8 @@
+import { Protocol, ProtocolVersion } from './types/UIProtocol.js'
+
 export const DEFAULT_HOST = 'localhost'
 export const DEFAULT_PORT = 8080
+export const DEFAULT_PROTOCOL = Protocol.UI
+export const DEFAULT_PROTOCOL_VERSION = ProtocolVersion['0.0.1']
+export const DEFAULT_SECURE = false
 export const UI_WEBSOCKET_REQUEST_TIMEOUT_MS = 60_000
similarity index 76%
rename from ui/cli/src/client/errors.ts
rename to ui/common/src/errors.ts
index 80b1fbea6a0f93cf70993ef2e6dcfff34c257a06..64a9e2618a174f02b12236a9fdb48ab7eb7b1ab2 100644 (file)
@@ -11,3 +11,6 @@ export class ConnectionError extends Error {
     }
   }
 }
+
+export const extractErrorMessage = (error: unknown): string =>
+  error instanceof Error ? error.message : String(error)
index d50d402625b1932c020169720bfb8403fa07a785..c18620e2881a9443fc681c21035b03a7d755267f 100644 (file)
@@ -4,6 +4,7 @@ export * from './client/types.js'
 export * from './client/WebSocketClient.js'
 export * from './config/schema.js'
 export * from './constants.js'
+export * from './errors.js'
 export * from './types/ChargingStationType.js'
 export * from './types/ConfigurationType.js'
 export * from './types/JsonType.js'
diff --git a/ui/web/src/assets/shared.css b/ui/web/src/assets/shared.css
new file mode 100644 (file)
index 0000000..65e55b6
--- /dev/null
@@ -0,0 +1,59 @@
+/* Shared component styles */
+
+/* ── Data table ────────────────────────────────────────────────────── */
+.data-table {
+  width: 100%;
+  table-layout: fixed;
+  background-color: var(--color-bg-surface);
+  border-collapse: collapse;
+  empty-cells: show;
+}
+
+.data-table--bordered {
+  border: solid 0.25px var(--color-border);
+}
+
+.data-table__caption {
+  color: var(--color-text-strong);
+  background-color: var(--color-bg-caption);
+  font-size: 1.5rem;
+  font-weight: bold;
+  padding: 0.5rem;
+}
+
+.data-table__head > tr {
+  background-color: var(--color-bg-header);
+}
+
+.data-table tr {
+  border: solid 0.25px var(--color-border-row);
+}
+
+.data-table tr:nth-of-type(even) {
+  background-color: var(--color-bg-hover);
+}
+
+.data-table th,
+.data-table td {
+  text-align: center;
+  vertical-align: middle;
+  padding: 0.25rem;
+}
+
+.data-table td {
+  overflow-wrap: break-word;
+}
+
+/* ── Action views ──────────────────────────────────────────────────── */
+.action-header {
+  min-width: max-content;
+  color: var(--color-text-strong);
+  background-color: var(--color-bg-caption);
+  padding: var(--spacing-lg);
+}
+
+/* ── Focus ring ────────────────────────────────────────────────────── */
+.focus-outline:focus-visible {
+  outline: 2px solid var(--color-accent);
+  outline-offset: -2px;
+}
index 816fe2e9651fe6a7add10e21e5f1f5194121dcec..fe820e27830bded2a55bd3aacba6866cf4250887 100644 (file)
@@ -1,5 +1,5 @@
 <template>
-  <h1 class="action">
+  <h1 class="action-header">
     Add Charging Stations
   </h1>
   <p>Template:</p>
   <br>
   <Button
     id="action-button"
-    @click="
-      () => {
-        $uiClient
-          .addChargingStations(state.template, state.numberOfStations, {
-            supervisionUrls: state.supervisionUrl.length > 0 ? state.supervisionUrl : undefined,
-            autoStart: convertToBoolean(state.autoStart),
-            persistentConfiguration: convertToBoolean(state.persistentConfiguration),
-            ocppStrictCompliance: convertToBoolean(state.ocppStrictCompliance),
-            enableStatistics: convertToBoolean(state.enableStatistics),
-          })
-          .then(() => {
-            $toast.success('Charging stations successfully added')
-          })
-          .finally(() => {
-            resetToggleButtonState('add-charging-stations', true)
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
-          })
-          .catch((error: Error) => {
-            $toast.error('Error at adding charging stations')
-            console.error('Error at adding charging stations:', error)
-          })
-      }
-    "
+    @click="addChargingStations()"
   >
     Add Charging Stations
   </Button>
 import type { UUIDv4 } from 'ui-common'
 
 import { ref, watch } from 'vue'
+import { useRouter } from 'vue-router'
 
 import Button from '@/components/buttons/Button.vue'
 import {
@@ -123,6 +102,7 @@ import {
   randomUUID,
   resetToggleButtonState,
   ROUTE_NAMES,
+  useExecuteAction,
   useTemplates,
   useUIClient,
 } from '@/composables'
@@ -148,21 +128,35 @@ const state = ref<{
 })
 
 const $uiClient = useUIClient()
+const $router = useRouter()
 const $templates = useTemplates()
+const executeAction = useExecuteAction()
 
 watch($templates, () => {
   state.value.renderTemplates = randomUUID()
 })
-</script>
 
-<style scoped>
-.action {
-  min-width: max-content;
-  color: var(--color-text-strong);
-  background-color: var(--color-bg-caption);
-  padding: var(--spacing-lg);
+const addChargingStations = (): void => {
+  executeAction(
+    $uiClient.addChargingStations(state.value.template, state.value.numberOfStations, {
+      autoStart: convertToBoolean(state.value.autoStart),
+      enableStatistics: convertToBoolean(state.value.enableStatistics),
+      ocppStrictCompliance: convertToBoolean(state.value.ocppStrictCompliance),
+      persistentConfiguration: convertToBoolean(state.value.persistentConfiguration),
+      supervisionUrls:
+        state.value.supervisionUrl.length > 0 ? state.value.supervisionUrl : undefined,
+    }),
+    'Charging stations successfully added',
+    'Error at adding charging stations',
+    () => {
+      resetToggleButtonState('add-charging-stations', true)
+      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+    }
+  )
 }
+</script>
 
+<style scoped>
 .number-of-stations {
   width: auto;
   max-width: 6rem;
index c3ec902c4b2eadc747a90485ec9cd68dc212278e..e64346574a3d7d56ac122f9273279272be9c72a9 100644 (file)
@@ -1,5 +1,5 @@
 <template>
-  <h1 class="action">
+  <h1 class="action-header">
     Set Supervision Url
   </h1>
   <h2>{{ chargingStationId }}</h2>
   <br>
   <Button
     id="action-button"
-    @click="
-      () => {
-        $uiClient
-          .setSupervisionUrl(hashId, state.supervisionUrl)
-          .then(() => {
-            $toast.success('Supervision url successfully set')
-          })
-          .finally(() => {
-            resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
-          })
-          .catch((error: Error) => {
-            $toast.error('Error at setting supervision url')
-            console.error('Error at setting supervision url:', error)
-          })
-      }
-    "
+    @click="setSupervisionUrl()"
   >
     Set Supervision Url
   </Button>
 
 <script setup lang="ts">
 import { ref } from 'vue'
+import { useRouter } from 'vue-router'
 
 import Button from '@/components/buttons/Button.vue'
-import { resetToggleButtonState, ROUTE_NAMES, useUIClient } from '@/composables'
+import { resetToggleButtonState, ROUTE_NAMES, useExecuteAction, useUIClient } from '@/composables'
 
 const props = defineProps<{
   chargingStationId: string
@@ -53,16 +38,23 @@ const state = ref<{ supervisionUrl: string }>({
 })
 
 const $uiClient = useUIClient()
-</script>
+const $router = useRouter()
+const executeAction = useExecuteAction()
 
-<style scoped>
-.action {
-  min-width: max-content;
-  color: var(--color-text-strong);
-  background-color: var(--color-bg-caption);
-  padding: var(--spacing-lg);
+const setSupervisionUrl = (): void => {
+  executeAction(
+    $uiClient.setSupervisionUrl(props.hashId, state.value.supervisionUrl),
+    'Supervision url successfully set',
+    'Error at setting supervision url',
+    () => {
+      resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
+      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+    }
+  )
 }
+</script>
 
+<style scoped>
 .supervision-url {
   width: 100%;
   max-width: 40rem;
index cd0cac67e9d4f884781d87dc622c23e06fe761c5..275408020887cb0c3c4c7615a1169d48d8a285b3 100644 (file)
@@ -1,5 +1,5 @@
 <template>
-  <h1 class="action">
+  <h1 class="action-header">
     Start Transaction
   </h1>
   <h2>{{ chargingStationId }}</h2>
@@ -109,13 +109,6 @@ const handleStartTransaction = async (): Promise<void> => {
 </script>
 
 <style scoped>
-.action {
-  min-width: max-content;
-  color: var(--color-text-strong);
-  background-color: var(--color-bg-caption);
-  padding: var(--spacing-lg);
-}
-
 .idtag {
   text-align: center;
 }
index 0a3a53915fb73420666964428d69fa0ba09a0f64..72f62b0a3a53040aa4f19982b4a065096b4f3eb7 100644 (file)
@@ -1,36 +1,36 @@
 <template>
-  <tr class="cs-table__row">
-    <td class="cs-table__column">
+  <tr>
+    <td>
       {{ chargingStation.stationInfo.chargingStationId }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.started === true ? 'Yes' : 'No' }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ getSupervisionUrl() }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ getWebSocketStateName(chargingStation.wsState) }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.bootNotificationResponse?.status ?? EMPTY_VALUE_PLACEHOLDER }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.stationInfo.ocppVersion ?? EMPTY_VALUE_PLACEHOLDER }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.stationInfo.templateName }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.stationInfo.chargePointVendor }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.stationInfo.chargePointModel }}
     </td>
-    <td class="cs-table__column">
+    <td>
       {{ chargingStation.stationInfo.firmwareVersion ?? EMPTY_VALUE_PLACEHOLDER }}
     </td>
-    <td class="cs-table__column">
+    <td>
       <StateButton
         :active="chargingStation.started === true"
         :off="() => stopChargingStation()"
         Delete Charging Station
       </Button>
     </td>
-    <td class="cs-table__connectors-column">
-      <table class="connectors-table">
-        <thead class="connectors-table__head">
-          <tr class="connectors-table__row">
-            <th
-              class="connectors-table__column"
-              scope="col"
-            >
+    <td class="cs-data__connectors-cell">
+      <table class="data-table">
+        <thead class="data-table__head">
+          <tr>
+            <th scope="col">
               Identifier
             </th>
-            <th
-              class="connectors-table__column"
-              scope="col"
-            >
+            <th scope="col">
               Status
             </th>
-            <th
-              class="connectors-table__column"
-              scope="col"
-            >
+            <th scope="col">
               Locked
             </th>
-            <th
-              class="connectors-table__column"
-              scope="col"
-            >
+            <th scope="col">
               Transaction
             </th>
-            <th
-              class="connectors-table__column"
-              scope="col"
-            >
+            <th scope="col">
               ATG Started
             </th>
-            <th
-              class="connectors-table__column"
-              scope="col"
-            >
+            <th scope="col">
               Actions
             </th>
           </tr>
         </thead>
-        <tbody class="connectors-table__body">
+        <tbody>
           <CSConnector
             v-for="entry in getConnectorEntries()"
             :key="entry.evseId != null ? `${entry.evseId}-${entry.connectorId}` : entry.connectorId"
@@ -245,29 +227,8 @@ const deleteChargingStation = (): void => {
 </script>
 
 <style scoped>
-.connectors-table {
-  width: 100%;
-  table-layout: fixed;
-  background-color: var(--color-bg-surface);
-  border-collapse: collapse;
-  empty-cells: show;
-}
-
-.connectors-table__head .connectors-table__row {
-  background-color: var(--color-bg-header);
-}
-
-:deep(.connectors-table__row) {
-  border: solid 0.25px var(--color-border-row);
-}
-
-:deep(.connectors-table__row:nth-of-type(even)) {
-  background-color: var(--color-bg-hover);
-}
-
-:deep(.connectors-table__column) {
-  text-align: center;
-  vertical-align: middle;
-  padding: 0.25rem;
+.cs-data__connectors-cell {
+  vertical-align: top;
+  padding: 0;
 }
 </style>
index d76ce71d36b5f51caa1128794922fbecffc3ca3f..a69723e9fdff8392aa716a1c05ce41a81340f6a6 100644 (file)
@@ -1,7 +1,7 @@
 <template>
   <div class="cs-table__wrapper">
-    <table class="cs-table">
-      <caption class="cs-table__caption">
+    <table class="data-table data-table--bordered">
+      <caption class="data-table__caption">
         Charging Stations
       </caption>
       <colgroup>
         <col>
         <col class="cs-table__col--connectors">
       </colgroup>
-      <thead class="cs-table__head">
-        <tr class="cs-table__row">
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+      <thead class="data-table__head">
+        <tr>
+          <th scope="col">
             Name
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Started
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Supervision Url
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             WebSocket State
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Registration Status
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             OCPP Version
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Template
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Vendor
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Model
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Firmware
           </th>
-          <th
-            class="cs-table__column"
-            scope="col"
-          >
+          <th scope="col">
             Actions
           </th>
           <th
@@ -94,7 +61,7 @@
           </th>
         </tr>
       </thead>
-      <tbody class="cs-table__body">
+      <tbody>
         <CSData
           v-for="chargingStation in chargingStations"
           :key="chargingStation.stationInfo.hashId"
@@ -123,47 +90,11 @@ const $emit = defineEmits(['need-refresh'])
   overflow-x: auto;
 }
 
-.cs-table {
-  width: 100%;
-  table-layout: fixed;
-  background-color: var(--color-bg-surface);
-  border: solid 0.25px var(--color-border);
-  border-collapse: collapse;
-  empty-cells: show;
-}
-
-.cs-table__caption {
-  color: var(--color-text-strong);
-  background-color: var(--color-bg-caption);
-  font-size: 1.5rem;
-  font-weight: bold;
-  padding: 0.5rem;
-}
-
-.cs-table__head .cs-table__row {
-  background-color: var(--color-bg-header);
-}
-
-:deep(.cs-table__row) {
-  border: solid 0.25px var(--color-border-row);
-}
-
-:deep(.cs-table__row:nth-of-type(even)) {
-  background-color: var(--color-bg-hover);
-}
-
-:deep(.cs-table__column) {
-  text-align: center;
-  vertical-align: middle;
-  padding: 0.25rem;
-  overflow-wrap: break-word;
-}
-
 .cs-table__col--connectors {
   width: 33%;
 }
 
-:deep(.cs-table__connectors-column) {
+.cs-table__connectors-column {
   vertical-align: top;
   padding: 0;
 }
index 97908d8c6930496a8a841863bca2e5b6222e4100..301b89ecbe59eae17caf63f60d894d02169f501c 100644 (file)
@@ -1,6 +1,7 @@
-import type { ChargingStationData, ConfigurationData, UUIDv4 } from 'ui-common'
+import type { ChargingStationData, ConfigurationData } from 'ui-common'
 import type { InjectionKey, Ref } from 'vue'
 
+import { randomUUID, validateUUID } from 'ui-common'
 import { inject } from 'vue'
 import { useToast } from 'vue-toast-notification'
 
@@ -120,18 +121,7 @@ export const resetToggleButtonState = (id: string, shared = false): void => {
   deleteFromLocalStorage(key)
 }
 
-export const randomUUID = (): UUIDv4 => {
-  return crypto.randomUUID() as UUIDv4
-}
-
-export const validateUUID = (uuid: unknown): uuid is UUIDv4 => {
-  if (typeof uuid !== 'string') {
-    return false
-  }
-  return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(
-    uuid
-  )
-}
+export { randomUUID, validateUUID }
 
 export const useUIClient = (): UIClient => {
   const injected = inject(uiClientKey, undefined)
@@ -157,14 +147,20 @@ export const useTemplates = (): Ref<string[]> => {
   throw new Error('templates not provided')
 }
 
-export const useExecuteAction = (emit: (event: 'need-refresh') => void) => {
+export const useExecuteAction = (emit?: (event: 'need-refresh') => void) => {
   const $toast = useToast()
-  return (action: Promise<unknown>, successMsg: string, errorMsg: string): void => {
+  return (
+    action: Promise<unknown>,
+    successMsg: string,
+    errorMsg: string,
+    onFinally?: () => void
+  ): void => {
     action
       .then(() => {
-        emit('need-refresh')
+        emit?.('need-refresh')
         return $toast.success(successMsg)
       })
+      .finally(onFinally)
       .catch((error: unknown) => {
         $toast.error(errorMsg)
         console.error(`${errorMsg}:`, error)
index 9e1a7020d727170199e0d9f0dd0d3f44b735a9e7..efa8f3eb6fd9e105cd29291eb0e954eedb9a71d6 100644 (file)
@@ -22,6 +22,8 @@ import { router } from '@/router'
 
 import 'vue-toast-notification/dist/theme-bootstrap.css'
 
+import './assets/shared.css'
+
 const DEFAULT_THEME = 'tokyo-night-storm'
 
 const loadTheme = async (theme: string): Promise<void> => {
index 4d6b88cb600a2d5e0c76acb001a3e63b6a647c80..656a8947fd0aa007660483b9a1fde07405756682 100644 (file)
@@ -12,25 +12,27 @@ import { templatesKey, uiClientKey } from '@/composables'
 import { toastMock } from '../setup'
 import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
 
+vi.mock('vue-router', async importOriginal => {
+  const actual: Record<string, unknown> = await importOriginal()
+  return {
+    ...actual,
+    useRouter: vi.fn(),
+  }
+})
+
+import { useRouter } from 'vue-router'
+
 describe('AddChargingStations', () => {
   let mockClient: MockUIClient
   let mockRouter: { push: ReturnType<typeof vi.fn> }
 
-  /**
-   * Mounts AddChargingStations with mock UIClient, router, and templates.
-   * @returns Mounted component wrapper
-   */
+  /** @returns Mounted component wrapper */
   function mountComponent () {
     mockClient = createMockUIClient()
     mockRouter = { push: vi.fn() }
+    vi.mocked(useRouter).mockReturnValue(mockRouter as unknown as ReturnType<typeof useRouter>)
     return mount(AddChargingStations, {
       global: {
-        config: {
-          globalProperties: {
-            $router: mockRouter,
-            $toast: toastMock,
-          } as never,
-        },
         provide: {
           [templatesKey as symbol]: ref(['template-A.json', 'template-B.json']),
           [uiClientKey as symbol]: mockClient,
index 273e08cbfb752c5a1ed0fa4506b380fbc7bdd0a4..48f918b7d21de80b6414d4bfd85fc394e2e5fca6 100644 (file)
@@ -12,26 +12,30 @@ import { toastMock } from '../setup'
 import { TEST_HASH_ID, TEST_STATION_ID } from './constants'
 import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
 
+vi.mock('vue-router', async importOriginal => {
+  const actual: Record<string, unknown> = await importOriginal()
+  return {
+    ...actual,
+    useRouter: vi.fn(),
+  }
+})
+
+import { useRouter } from 'vue-router'
+
 describe('SetSupervisionUrl', () => {
   let mockClient: MockUIClient
   let mockRouter: { push: ReturnType<typeof vi.fn> }
 
   /**
-   * Mounts SetSupervisionUrl with mock UIClient, router, and toast.
    * @param props - Props to override defaults
    * @returns Mounted component wrapper
    */
   function mountComponent (props = {}) {
     mockClient = createMockUIClient()
     mockRouter = { push: vi.fn() }
+    vi.mocked(useRouter).mockReturnValue(mockRouter as unknown as ReturnType<typeof useRouter>)
     return mount(SetSupervisionUrl, {
       global: {
-        config: {
-          globalProperties: {
-            $router: mockRouter,
-            $toast: toastMock,
-          } as never,
-        },
         provide: {
           [uiClientKey as symbol]: mockClient,
         },