UNPKG

@sports-alliance/sports-lib

Version:

A Library to for importing / exporting and processing GPX, TCX, FIT and JSON files from services such as Strava, Movescount, Garmin, Polar etc

157 lines (156 loc) 8.81 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const data_store_1 = require("./data.store"); const data_duration_1 = require("./data.duration"); const data_power_1 = require("./data.power"); const data_power_curve_1 = require("./data.power-curve"); const data_durability_evidence_1 = require("./data.durability-evidence"); const data_rider_position_change_event_1 = require("./data.rider-position-change-event"); const data_cycling_position_1 = require("./data.cycling-position"); const data_sport_profile_name_1 = require("./data.sport-profile-name"); const data_fused_location_1 = require("./data.fused-location"); const data_fused_altitude_1 = require("./data.fused-altitude"); const data_alti_baro_profile_1 = require("./data.alti-baro-profile"); const data_activity_types_1 = require("./data.activity-types"); const data_device_names_1 = require("./data.device-names"); const data_description_1 = require("./data.description"); const data_position_1 = require("./data.position"); const data_active_lap_1 = require("./data-active-lap"); const data_balance_1 = require("./data.balance"); const data_target_power_zone_1 = require("./data.target-power-zone"); const data_target_heart_rate_zone_1 = require("./data.target-heart-rate-zone"); const data_target_speed_zone_1 = require("./data.target-speed-zone"); const data_gender_1 = require("./data.gender"); const data_jump_event_1 = require("./data.jump-event"); const data_device_location_1 = require("./data.device-location"); const data_foot_pod_used_1 = require("./data.foot-pod-used"); const data_auto_pause_used_1 = require("./data.auto-pause-used"); const data_auto_lap_used_1 = require("./data.auto-lap-used"); const data_bike_pod_used_1 = require("./data.bike-pod-used"); const data_enabled_navigation_systems_1 = require("./data.enabled-navigation-systems"); const data_heart_rate_used_1 = require("./data.heart-rate-used"); const data_power_pod_used_1 = require("./data.power-pod-used"); const data_training_stress_score_method_1 = require("./data.training-stress-score-method"); const data_start_position_1 = require("./data.start-position"); const data_end_position_1 = require("./data.end-position"); const data_jump_distance_1 = require("./data.jump-distance"); describe('Data Serialization Safety', () => { // Map of classes that require specific constructor arguments or complex data const knownProviders = new Map([ [data_power_curve_1.DataPowerCurve, [[{ duration: new data_duration_1.DataDuration(1), power: new data_power_1.DataPower(100) }]]], [ data_durability_evidence_1.DataDurabilityEvidence, [ { protocolVersion: data_durability_evidence_1.DURABILITY_PROTOCOL_VERSION, sourceFingerprint: 'durability-v1:0000000000000000', discipline: 'cycling', outputSource: 'power', outputUnit: 'W', context: null, durationSeconds: 3600, qualifyingDurationSeconds: 0, coverageRatio: 0, eligibility: { eligible: false, reason: 'missing-output', validSampleCount: 0, comparisonSegments: 'halves', earlySampleCount: 0, lateSampleCount: 0, outputCoefficientOfVariation: null, hardZoneRatio: null }, evidence: null } ] ], [data_rider_position_change_event_1.DataRiderPositionChangeEvent, [1, data_cycling_position_1.RiderPosition.SEATED]], [data_sport_profile_name_1.DataSportProfileName, ['TestProfile']], [data_fused_location_1.DataFusedLocation, [true]], [data_fused_altitude_1.DataFusedAltitude, [true]], [data_alti_baro_profile_1.DataAltiBaroProfile, ['TestProfile']], [data_activity_types_1.DataActivityTypes, [['Running', 'Cycling']]], [data_device_names_1.DataDeviceNames, [['Garmin', 'Suunto']]], [data_description_1.DataDescription, ['Test Description']], [data_position_1.DataPosition, [{ latitudeDegrees: 0, longitudeDegrees: 0 }]], [data_active_lap_1.DataActiveLap, [true]], [data_balance_1.DataBalance, [50]], [data_target_power_zone_1.DataTargetPowerZone, ['Zone 1']], [data_target_heart_rate_zone_1.DataTargetHeartRateZone, ['Zone 1']], [data_target_speed_zone_1.DataTargetSpeedZone, ['Zone 1']], [data_gender_1.DataGender, ['Male']], [data_jump_event_1.DataJumpEvent, [1234567890, { distance: new data_jump_distance_1.DataJumpDistance(10), score: new data_jump_event_1.DataJumpScore(5) }]], [data_device_location_1.DataDeviceLocation, ['Wrist']], [data_foot_pod_used_1.DataFootPodUsed, [true]], [data_auto_pause_used_1.DataAutoPauseUsed, [true]], [data_auto_lap_used_1.DataAutoLapUsed, [true]], [data_bike_pod_used_1.DataBikePodUsed, [true]], [data_enabled_navigation_systems_1.DataEnabledNavigationSystems, ['GPS']], [data_heart_rate_used_1.DataHeartRateUsed, [true]], [data_power_pod_used_1.DataPowerPodUsed, [true]], [data_training_stress_score_method_1.DataTrainingStressScoreMethod, [data_training_stress_score_method_1.TrainingStressScoreMethod.POWER]], [data_start_position_1.DataStartPosition, [{ latitudeDegrees: 0, longitudeDegrees: 0 }]], [data_end_position_1.DataEndPosition, [{ latitudeDegrees: 0, longitudeDegrees: 0 }]] ]); // Abstract classes or classes that shouldn't be tested directly const ignoredClasses = new Set(['DataGroundContactTimeBalance', 'DataBalance']); /** * Recursively checks an object for any values that are custom class instances * (i.e. not plain Objects, Arrays, or primitives). * Returns a list of error strings describing the location of custom objects. */ const checkForCustomObjects = (obj, path = '') => { const errors = []; if (obj === null || typeof obj !== 'object') { return errors; } // Check the constructor of the object itself if (obj.constructor && obj.constructor.name !== 'Object' && obj.constructor.name !== 'Array') { errors.push(`Found custom object at path '${path}': ${obj.constructor.name}. Data classes must serialize to plain JSON objects.`); } // Recursively check properties for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { errors.push(...checkForCustomObjects(obj[key], path ? `${path}.${key}` : key)); } } return errors; }; // Iterate over all data classes registered in the DataStore Object.keys(data_store_1.DataStore).forEach(key => { const DataClass = data_store_1.DataStore[key]; // Skip if it's not a function (constructor) or explicitly ignored if (typeof DataClass !== 'function' || ignoredClasses.has(key)) return; it(`should serialize ${key} to plain JSON`, () => { let instance; try { // Use provided args or default to [123] for most simple data classes const args = knownProviders.get(DataClass) || [123]; instance = new DataClass(...args); } catch (e) { console.warn(`Failed to instantiate ${key} with default args. Error: ${e.message}`); // We intentionally fail the test if we can't instantiate it, // so the developer adds a provider for the new class. throw new Error(`Could not instantiate ${key}. Please add a provider to 'knownProviders' in src/data/data.serialization.spec.ts`); } const json = instance.toJSON(); // 1. Check that toJSON returns something expect(json).toBeDefined(); // 2. Check for custom objects in the serialized output const errors = checkForCustomObjects(json); if (errors.length > 0) { fail(`${key}.toJSON() returned invalid JSON structure:\n${errors.join('\n')}`); } // 3. Round-trip validation: verify that we can re-hydrate from this JSON // This is crucial to ensure Dynamic DataLoader works correctly. const type = instance.getType(); const dataValue = json[type]; const secondInstance = new DataClass(dataValue); const secondJson = secondInstance.toJSON(); expect(secondJson).toEqual(json); }); }); });