diff --git a/.aiassistant/rules/Creating and maintaining tests.md b/.aiassistant/rules/Creating and maintaining tests.md new file mode 100644 index 00000000..dddc0208 --- /dev/null +++ b/.aiassistant/rules/Creating and maintaining tests.md @@ -0,0 +1,286 @@ +--- +apply: always +--- + +# Guidelines for Creating and Maintaining Tests + +## Testing Framework + +This project uses **WebdriverIO (wdio)** as the primary testing framework with Vite service integration. Tests support both desktop and mobile environments (including Appium for Android/iOS). + +## Running Tests + +```bash +# Run full test suite +npm run wdio + +# Run tests excluding Safari +npm run test:mobile + +# Run Android-targeted specs +npm run test:android +``` + +## Test File Location and Naming + +- **Location**: All test files should be placed in `test/specs/` +- **Naming Convention**: Use kebab-case with `.spec.js` suffix + - ✅ `i18n-settings-wheel.spec.js` + - ✅ `user-authentication.spec.js` + - ❌ `UserAuth.test.js` + +## Test Structure + +### Basic Template + +```javascript +/** + * Brief description of what this test suite covers + */ +describe('Feature or Component Name', () => { + it('should describe expected behavior', async () => { + await browser.url('/'); + // Test implementation + expect(result).toBe(expectedValue); + }); +}); +``` + +### Key Conventions + +1. **Use descriptive test names**: Start with `should` to describe expected behavior +2. **Always use `async/await`**: All browser interactions are asynchronous +3. **Include JSDoc comments**: Add a comment block at the top describing the test purpose +4. **Group related tests**: Use `describe` blocks to organize related test cases + +## Writing Tests + +### Navigation + +```javascript +// Navigate to a page +await browser.url('/'); +await browser.url('/dashboard'); +``` + +### Browser Interactions + +```javascript +// Get page title +const title = await browser.getTitle(); + +// Execute JavaScript in browser context +const result = await browser.executeAsync(async (args, done) => { + try { + // Your async code here + done({ success: true, data: result }); + } catch (e) { + done({ success: false, error: e.message }); + } +}, arguments); +``` + +### Assertions + +```javascript +// Use Jest-style expect assertions +expect(title).toContain('Expected Text'); +expect(result.success).toBe(true); +expect(items.length).toBeGreaterThan(0); +``` + +## Best Practices + +### DO: +- ✅ Test one behavior per `it` block +- ✅ Use meaningful variable names +- ✅ Add console.log for debugging complex assertions +- ✅ Handle errors gracefully in `executeAsync` callbacks +- ✅ Clean up test state when necessary + +### DON'T: +- ❌ Write tests that depend on execution order +- ❌ Hardcode sensitive data (use environment variables) +- ❌ Skip error handling in async operations +- ❌ Leave commented-out code in test files + +## Testing i18n/Translations + +When testing internationalization: + +1. Define expected translation keys as an array +2. Fetch locale files via browser context +3. Verify all required keys exist in each locale +4. Check parity between locales (same keys in all languages) + +```javascript +const requiredKeys = ['key1', 'key2', 'key3']; + +it('should have all translations in English', async () => { + await browser.url('/'); + const result = await browser.executeAsync(async (keys, done) => { + const response = await fetch('/src/i18n/locales/en.json'); + const data = await response.json(); + // Verify keys exist + done({ success: true }); + }, requiredKeys); + expect(result.success).toBe(true); +}); +``` + +## Mobile Testing Prerequisites + +For mobile tests (`npm run test:mobile` or `npm run test:android`): +- Android SDK and emulator must be installed +- Appium drivers configured +- For iOS: Xcode and iOS tooling required + +## Maintaining Tests + +1. **Keep tests updated**: When modifying features, update corresponding tests +2. **Review failing tests**: Investigate failures before marking as skipped +3. **Remove obsolete tests**: Delete tests for removed features +4. **Refactor shared logic**: Extract common test utilities to helper files in `test/` +--- +apply: always +--- + +# Guidelines for Creating and Maintaining Tests + +## Testing Framework + +This project uses **WebdriverIO (wdio)** as the primary testing framework with Vite service integration. Tests support both desktop and mobile environments (including Appium for Android/iOS). + +## Running Tests + +```bash +# Run full test suite +npm run wdio + +# Run tests excluding Safari +npm run test:mobile + +# Run Android-targeted specs +npm run test:android +``` + +## Test File Location and Naming + +- **Location**: All test files should be placed in `test/specs/` +- **Naming Convention**: Use kebab-case with `.spec.js` suffix + - ✅ `i18n-settings-wheel.spec.js` + - ✅ `user-authentication.spec.js` + - ❌ `UserAuth.test.js` + +## Test Structure + +### Basic Template + +```javascript +/** + * Brief description of what this test suite covers + */ +describe('Feature or Component Name', () => { + it('should describe expected behavior', async () => { + await browser.url('/'); + // Test implementation + expect(result).toBe(expectedValue); + }); +}); +``` + +### Key Conventions + +1. **Use descriptive test names**: Start with `should` to describe expected behavior +2. **Always use `async/await`**: All browser interactions are asynchronous +3. **Include JSDoc comments**: Add a comment block at the top describing the test purpose +4. **Group related tests**: Use `describe` blocks to organize related test cases + +## Writing Tests + +### Navigation + +```javascript +// Navigate to a page +await browser.url('/'); +await browser.url('/dashboard'); +``` + +### Browser Interactions + +```javascript +// Get page title +const title = await browser.getTitle(); + +// Execute JavaScript in browser context +const result = await browser.executeAsync(async (args, done) => { + try { + // Your async code here + done({ success: true, data: result }); + } catch (e) { + done({ success: false, error: e.message }); + } +}, arguments); +``` + +### Assertions + +```javascript +// Use Jest-style expect assertions +expect(title).toContain('Expected Text'); +expect(result.success).toBe(true); +expect(items.length).toBeGreaterThan(0); +``` + +## Best Practices + +### DO: +- ✅ Test one behavior per `it` block +- ✅ Use meaningful variable names +- ✅ Add console.log for debugging complex assertions +- ✅ Handle errors gracefully in `executeAsync` callbacks +- ✅ Clean up test state when necessary + +### DON'T: +- ❌ Write tests that depend on execution order +- ❌ Hardcode sensitive data (use environment variables) +- ❌ Skip error handling in async operations +- ❌ Leave commented-out code in test files + +## Testing i18n/Translations + +When testing internationalization: + +1. Define expected translation keys as an array +2. Fetch locale files via browser context +3. Verify all required keys exist in each locale +4. Check parity between locales (same keys in all languages) + +```javascript +const requiredKeys = ['key1', 'key2', 'key3']; + +it('should have all translations in English', async () => { + await browser.url('/'); + const result = await browser.executeAsync(async (keys, done) => { + const response = await fetch('/src/i18n/locales/en.json'); + const data = await response.json(); + // Verify keys exist + done({ success: true }); + }, requiredKeys); + expect(result.success).toBe(true); +}); +``` + +## Mobile Testing Prerequisites + +For mobile tests (`npm run test:mobile` or `npm run test:android`): +- Android SDK and emulator must be installed +- Appium drivers configured +- For iOS: Xcode and iOS tooling required + +## Maintaining Tests + +1. **Keep tests updated**: When modifying features, update corresponding tests +2. **Review failing tests**: Investigate failures before marking as skipped +3. **Remove obsolete tests**: Delete tests for removed features +4. **Refactor shared logic**: Extract common test utilities to helper files in `test/` diff --git a/openapi.yaml b/openapi.yaml index 1b1f13fb..04e7ae15 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2756,6 +2756,11 @@ paths: order_priority: type: integer default: 0 + services: + type: array + description: Optional services enabled by this task. Items must be valid service enum names. + items: + $ref: '#/components/schemas/SelfserveLaneService' responses: '200': description: Successfully added task @@ -2802,6 +2807,12 @@ paths: type: string order_priority: type: integer + services: + type: array + nullable: true + description: Services enabled by this task. Set to null to clear all services. + items: + $ref: '#/components/schemas/SelfserveLaneService' responses: '200': description: Successfully updated task @@ -5060,6 +5071,101 @@ paths: schema: $ref: '#/components/schemas/SelfServeLaneStatus' + /modules/self-serve/lane/services/allowed: + post: + tags: + - Modules + summary: Set allowed services for a lane based on shown tasks + description: | + Updates the set of services that are allowed to be manually activated for a given self-serve lane, + derived from the tasks currently shown to the user after answering the self-serve questions. + This endpoint does not activate anything by itself; it only sets what is allowed to be activated. + operationId: setSelfServeLaneAllowedServices + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lane_id + properties: + lane_id: + type: integer + task_ids: + type: array + description: List of task IDs that are currently shown to the user + items: + type: integer + responses: + '200': + description: Allowed services updated + content: + application/json: + schema: + type: object + properties: + lane_id: + type: integer + allowed_services: + type: array + items: + type: string + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /modules/self-serve/lane/relay/machine/enable: + post: + tags: + - Modules + summary: Manually enable MACHINE relay for a lane + description: | + Manually turns on the MACHINE relay for a self-serve lane if and only if the current allowed services + include `MACHINE` (set via `/modules/self-serve/lane/services/allowed`). The relay is never automatically + enabled; an explicit call to this endpoint is required. + operationId: enableSelfServeLaneMachineRelay + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lane_id + properties: + lane_id: + type: integer + duration: + type: integer + description: Optional number of seconds after which the relay should automatically turn off + responses: + '200': + description: MACHINE relay enabled + content: + application/json: + schema: + type: object + properties: + lane_id: + type: integer + relay: + type: string + enabled: + type: boolean + duration: + type: integer + nullable: true + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Not allowed to enable MACHINE relay (no matching task currently shown) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + # Module - Other Integration Endpoints /modules/motorapi/lookup: get: @@ -6609,6 +6715,12 @@ components: type: integer description: The product ID used for minute-based billing + SelfserveLaneService: + type: string + description: Allowed self-serve lane service name + enum: + - MACHINE + DepartmentSelfserveQuestion: type: object properties: @@ -6672,6 +6784,12 @@ components: order_priority: type: integer description: Display order priority (lower numbers shown first) + services: + type: array + description: Services enabled by this task. Each item must be a valid service enum name. + items: + $ref: '#/components/schemas/SelfserveLaneService' + default: [] created_at: type: string format: date-time diff --git a/src/components/displays/department/tables/SelfServeTasksTable.vue b/src/components/displays/department/tables/SelfServeTasksTable.vue index 173e0e68..0c7736a9 100644 --- a/src/components/displays/department/tables/SelfServeTasksTable.vue +++ b/src/components/displays/department/tables/SelfServeTasksTable.vue @@ -98,6 +98,7 @@ const onDrop = async (event, newIndex) => { {{ SessionUser.objects.self_serve_tasks.columns.lane.label }} {{ SessionUser.objects.self_serve_tasks.columns.product.label }} {{ SessionUser.objects.self_serve_tasks.columns.condition_id.label }} + {{ SessionUser.objects.self_serve_tasks.columns.services.label }} {{ $t('tables.actions') }} @@ -162,6 +163,14 @@ const onDrop = async (event, newIndex) => { :parse-function="(conditionId) => conditionId ? SessionUser.objects.self_serve_conditions.functions.getConditionName(conditionId) : $t('global.all')" :permission-check-function="SessionUser.canAccessAdmin" /> +
diff --git a/src/components/displays/department/tables/SelfServeTryModal.vue b/src/components/displays/department/tables/SelfServeTryModal.vue index 195df636..dfc3d8bb 100644 --- a/src/components/displays/department/tables/SelfServeTryModal.vue +++ b/src/components/displays/department/tables/SelfServeTryModal.vue @@ -34,12 +34,14 @@ const { completedTasks, visibleQuestions, activeTasks, + activeTaskServices, currentQuestion, fetchSelfServeData, evaluateRule, evaluateCondition, isQuestionVisible, isTaskActive, + isServiceAllowed, isImage, downloadAttachment, reset, @@ -178,6 +180,23 @@ const getConditionById = (id) => {
{{ $t('modals.active_tasks') }}
+ + +
+

{{ $t('modals.allowed_services') || 'Allowed Services' }}:

+
+ + + + + {{ service }} + +
+
+
+

{{ $t('modals.no_services_from_tasks') || 'No services defined in active tasks' }}

+
+
diff --git a/src/components/session/token/SessionUser/Objects/SelfServeTasks.vue b/src/components/session/token/SessionUser/Objects/SelfServeTasks.vue index 50a48f30..17b15f25 100644 --- a/src/components/session/token/SessionUser/Objects/SelfServeTasks.vue +++ b/src/components/session/token/SessionUser/Objects/SelfServeTasks.vue @@ -149,6 +149,26 @@ export const SelfServeTasks = { required: true } }, + services: { + label: "Tjenester", + type: "multi-select", + sortable: false, + creation: { + required: false + }, + options: async () => { + // Return available service options based on SelfserveLaneService enum + return [ + { id: "MACHINE", name: "MACHINE" } + ]; + }, + parse: (services) => { + if (!services || !Array.isArray(services) || services.length === 0) { + return "Ingen"; + } + return services.join(", "); + } + }, }, add: async (department, lane, product, condition_id, task, description, order_priority) => { return ObjectsGlobal.add.object( @@ -172,6 +192,7 @@ export const SelfServeTasks = { product: (id, product) => ObjectsGlobal.set.column(SelfServeTasks.meta.endpoint, id, "product", parseInt(product)), lane: (id, lane) => ObjectsGlobal.set.column(SelfServeTasks.meta.endpoint, id, "lane", parseInt(lane)), department: (id, department) => ObjectsGlobal.set.column(SelfServeTasks.meta.endpoint, id, "department", parseInt(department)), + services: (id, services) => ObjectsGlobal.set.column(SelfServeTasks.meta.endpoint, id, "services", Array.isArray(services) ? services : []), }, get: { all: (filters = {}) => ObjectsGlobal.get.objects(SelfServeTasks.meta.endpoint, filters), diff --git a/src/composables/useSelfServeLogic.js b/src/composables/useSelfServeLogic.js index c5456eb8..3535b5a7 100644 --- a/src/composables/useSelfServeLogic.js +++ b/src/composables/useSelfServeLogic.js @@ -9,6 +9,7 @@ export function useSelfServeLogic() { const tasks = ref([]); const answers = ref({}); const completedTasks = ref({}); + const allowedServices = ref([]); const isImage = (attachment) => { const filename = attachment.content?.other || ""; @@ -210,6 +211,50 @@ export function useSelfServeLogic() { return tasks.value.filter((t) => isTaskActive(t.id)); }); + /** + * Compute the services that are allowed based on the active tasks. + * A service is allowed if at least one active task has it in its services array. + */ + const activeTaskServices = computed(() => { + const services = new Set(); + activeTasks.value.forEach((task) => { + if (task.services && Array.isArray(task.services)) { + task.services.forEach((service) => services.add(service)); + } + }); + return Array.from(services); + }); + + /** + * Check if a specific service is allowed based on active tasks. + * @param {string} serviceName - The service name to check (e.g., "MACHINE") + * @returns {boolean} + */ + const isServiceAllowed = (serviceName) => { + return activeTaskServices.value.includes(serviceName); + }; + + /** + * Update the allowed services for a lane based on the active tasks. + * @param {number} laneId - The lane ID + * @returns {Promise} + */ + const updateLaneAllowedServices = async (laneId) => { + if (!laneId) return; + const taskIds = activeTasks.value.map((t) => t.id); + try { + const response = await SessionUser.request('/modules/self-serve/lane/services/allowed', 'post', { + lane_id: parseInt(laneId), + task_ids: taskIds, + }); + allowedServices.value = response?.data?.allowed_services || response?.allowed_services || []; + return response; + } catch (error) { + console.error("Error updating lane allowed services:", error); + allowedServices.value = []; + } + }; + const currentQuestion = computed(() => { const visible = visibleQuestions.value; return visible.find((q) => answers.value[q.id] === undefined); @@ -223,14 +268,18 @@ export function useSelfServeLogic() { tasks, answers, completedTasks, + allowedServices, visibleQuestions, activeTasks, + activeTaskServices, currentQuestion, fetchSelfServeData, evaluateRule, evaluateCondition, isQuestionVisible, isTaskActive, + isServiceAllowed, + updateLaneAllowedServices, isImage, downloadAttachment, reset, diff --git a/src/views/dashboards/userDashboard/wash/MyWashStart.vue b/src/views/dashboards/userDashboard/wash/MyWashStart.vue index 91c15f0f..650d568e 100644 --- a/src/views/dashboards/userDashboard/wash/MyWashStart.vue +++ b/src/views/dashboards/userDashboard/wash/MyWashStart.vue @@ -406,21 +406,24 @@ const onStartWash = (laneId, licensePlate, customerNumber, targetStep = steps.WA } return Promise.all(saveConditionsPromises).then(() => { - return executeSelfServeCommand(laneId, 'START', { - customer_number: parseInt(customerNumber), - license_plate: licensePlate.trim().toUpperCase(), - }).then(() => { - // Wash started successfully - washLaneId.value = laneId; - // Start time tracking - washStartTime.value = Date.now(); - // Reset any previous completed duration - completedDurationMs.value = null; - washInProgress.value = true; - startElapsedTimer(); - // Go to the target step - currentStep.value = targetStep; - saveProgress('onStartWash'); + // Update allowed services on the backend based on active tasks before starting + return updateLaneAllowedServices(laneId).then(() => { + return executeSelfServeCommand(laneId, 'START', { + customer_number: parseInt(customerNumber), + license_plate: licensePlate.trim().toUpperCase(), + }).then(() => { + // Wash started successfully + washLaneId.value = laneId; + // Start time tracking + washStartTime.value = Date.now(); + // Reset any previous completed duration + completedDurationMs.value = null; + washInProgress.value = true; + startElapsedTimer(); + // Go to the target step + currentStep.value = targetStep; + saveProgress('onStartWash'); + }); }); }).catch(error => { console.error('Error saving vehicle conditions:', error); @@ -619,12 +622,24 @@ const radioWashType = ref('Manual'); */ const radioLaneOption = ref('Any'); /** - * Check if machine is available for the selected lane + * Check if machine is available for the selected lane. + * Machine is available if: + * 1. The lane has machine_available === true, AND + * 2. Either there are no active tasks with services, OR at least one active task allows the MACHINE service */ const isMachineAvailable = (laneId) => { const lane = nearestDepartment.value?.lanes.find(l => l.id === laneId); if (!lane) return false; - return lane?.machine_available === true; + if (lane?.machine_available !== true) return false; + + // If questions have been answered and there are active tasks with services defined, + // check if MACHINE is among the allowed services + if (activeTaskServices.value.length > 0 || activeTasks.value.some(t => t.services && t.services.length > 0)) { + return isServiceAllowed('MACHINE'); + } + + // If no tasks define services, machine is available by default (lane availability only) + return true; } /** @@ -694,13 +709,17 @@ const { tasks, answers, completedTasks, + allowedServices, visibleQuestions, activeTasks, + activeTaskServices, fetchSelfServeData: fetchSelfServeDataInternal, evaluateRule, evaluateCondition, isQuestionVisible, isTaskActive, + isServiceAllowed, + updateLaneAllowedServices, isImage, downloadAttachment, } = useSelfServeLogic(); diff --git a/test/specs/i18n-settings-wheel.spec.js b/test/specs/i18n-settings-wheel.spec.js new file mode 100644 index 00000000..3d8d1f7d --- /dev/null +++ b/test/specs/i18n-settings-wheel.spec.js @@ -0,0 +1,175 @@ +/** + * Test to verify i18n translations for ActionSettingsWheelButton component + * are properly defined in both English and Danish locale files. + */ + +describe('i18n Settings Wheel Translations', () => { + const settingsWheelKeys = [ + 'close', + 'change_password', + 'enter_password', + 'please_enter_password', + 'password_changed', + 'password_changed_text', + 'error', + 'error_changing_password', + 'scan_qr_to_login', + 'copy_link', + 'link_copied', + 'booking', + 'mark_as_completed', + 'view_booking_new_tab', + 'change_association', + 'associate_order', + 'change_order', + 'delete_booking', + 'view_lane_new_tab', + 'view_order_new_tab', + 'attach_wash_certificate', + 'change_customer', + 'change_invoice_collection', + 'download_invoice', + 'delete_order', + 'view_invoice_collection_new_tab', + 'view_vehicle_new_tab', + 'view_customer_new_tab', + 'login_as_customer', + 'login_as_user', + 'login_as_user_qr', + 'show_qr_code', + 'show_customer', + 'edit_permissions', + 'delete_user', + 'attached_files', + 'open_attached_file', + 'error_downloading_attachment', + 'no_actions_defined', + 'view_lane_setup_new_tab' + ]; + + it('should have all settings_wheel translations defined in English', async () => { + await browser.url('/'); + + const result = await browser.executeAsync(async (keys, done) => { + try { + const response = await fetch('/src/i18n/locales/en.json'); + const data = await response.json(); + + // Get the top-level keys to understand structure + const topKeys = Object.keys(data); + + // Check if admin exists + if (!data.admin) { + done({ success: false, error: 'admin namespace not found. Top keys: ' + topKeys.slice(0, 10).join(', ') }); + return; + } + + if (!data.admin.pos) { + done({ success: false, error: 'admin.pos not found. Admin keys: ' + Object.keys(data.admin).slice(0, 10).join(', ') }); + return; + } + + if (!data.admin.pos.settings_wheel) { + done({ success: false, error: 'admin.pos.settings_wheel not found. Pos keys: ' + Object.keys(data.admin.pos).slice(0, 15).join(', ') }); + return; + } + + const missingKeys = []; + for (const key of keys) { + if (!data.admin.pos.settings_wheel[key]) { + missingKeys.push(key); + } + } + + if (missingKeys.length > 0) { + done({ success: false, error: 'Missing keys: ' + missingKeys.join(', ') }); + } else { + done({ success: true, keyCount: Object.keys(data.admin.pos.settings_wheel).length }); + } + } catch (e) { + done({ success: false, error: 'Exception: ' + e.message }); + } + }, settingsWheelKeys); + + console.log('English result:', JSON.stringify(result)); + expect(result.success).toBe(true); + }); + + it('should have all settings_wheel translations defined in Danish', async () => { + await browser.url('/'); + + const result = await browser.executeAsync(async (keys, done) => { + try { + const response = await fetch('/src/i18n/locales/da.json'); + const data = await response.json(); + + // Get the top-level keys to understand structure + const topKeys = Object.keys(data); + + // Check if admin exists + if (!data.admin) { + done({ success: false, error: 'admin namespace not found. Top keys: ' + topKeys.slice(0, 10).join(', ') }); + return; + } + + if (!data.admin.pos) { + done({ success: false, error: 'admin.pos not found. Admin keys: ' + Object.keys(data.admin).slice(0, 10).join(', ') }); + return; + } + + if (!data.admin.pos.settings_wheel) { + done({ success: false, error: 'admin.pos.settings_wheel not found. Pos keys: ' + Object.keys(data.admin.pos).slice(0, 15).join(', ') }); + return; + } + + const missingKeys = []; + for (const key of keys) { + if (!data.admin.pos.settings_wheel[key]) { + missingKeys.push(key); + } + } + + if (missingKeys.length > 0) { + done({ success: false, error: 'Missing keys: ' + missingKeys.join(', ') }); + } else { + done({ success: true, keyCount: Object.keys(data.admin.pos.settings_wheel).length }); + } + } catch (e) { + done({ success: false, error: 'Exception: ' + e.message }); + } + }, settingsWheelKeys); + + console.log('Danish result:', JSON.stringify(result)); + expect(result.success).toBe(true); + }); + + it('should have the same keys in both locales', async () => { + await browser.url('/'); + + const result = await browser.executeAsync(async (done) => { + try { + const enResponse = await fetch('/src/i18n/locales/en.json'); + const en = await enResponse.json(); + + const daResponse = await fetch('/src/i18n/locales/da.json'); + const da = await daResponse.json(); + + if (!en.admin?.pos?.settings_wheel || !da.admin?.pos?.settings_wheel) { + done({ success: false, error: 'settings_wheel not found in one or both locales' }); + return; + } + + const enKeys = Object.keys(en.admin.pos.settings_wheel).sort(); + const daKeys = Object.keys(da.admin.pos.settings_wheel).sort(); + + const match = JSON.stringify(enKeys) === JSON.stringify(daKeys); + done({ success: match, enCount: enKeys.length, daCount: daKeys.length }); + } catch (e) { + done({ success: false, error: 'Exception: ' + e.message }); + } + }); + + console.log('Parity result:', JSON.stringify(result)); + expect(result.success).toBe(true); + }); +});