@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
JavaScript
;
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);
});
});
});