@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
162 lines (161 loc) • 8.45 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
// import { describe, it, expect, beforeEach } from 'vitest';
const activity_utilities_1 = require("./activity.utilities");
const activity_1 = require("../../activities/activity");
const data_power_1 = require("../../data/data.power");
const data_duration_1 = require("../../data/data.duration");
// @ts-ignore
const activity_types_1 = require("../../activities/activity.types");
describe('ActivityUtilities Power Curve', () => {
let activity;
// Mock Activity creation
const createActivity = (powerValues) => {
// Create a mock activity spanning enough time
const startDate = new Date();
const endDate = new Date(startDate.getTime() + powerValues.length * 1000);
const mockCreator = { toJSON: () => ({}) };
const act = new activity_1.Activity(startDate, endDate, activity_types_1.ActivityTypes.Cycling, mockCreator);
// Add power stream
const powerStream = act.createStream(data_power_1.DataPower.type);
powerStream.setData(powerValues);
act.addStream(powerStream);
return act;
};
it('should return empty curve if no power stream exists', () => {
const act = new activity_1.Activity(new Date(), new Date(), activity_types_1.ActivityTypes.Cycling, {});
const curve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(act, [1, 5, 10]);
expect(curve.getValue()).toHaveLength(0);
});
it('should calculate correct max average power for specific durations', () => {
// [100, 100, 200, 200, 200, 100, 100]
// 3s max avg should be (200+200+200)/3 = 200
// 5s max avg should be (100+200+200+200+100)/5 = 160
const powerValues = [100, 100, 200, 200, 200, 100, 100];
activity = createActivity(powerValues);
const curve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(activity, [3, 5]);
const values = curve.getValue();
expect(values).toHaveLength(2);
// Check 3s
const p3s = values.find(v => v.duration.getValue() === 3);
expect(p3s).toBeDefined();
expect(p3s === null || p3s === void 0 ? void 0 : p3s.power.getValue()).toBe(200);
// Check 5s
const p5s = values.find(v => v.duration.getValue() === 5);
expect(p5s).toBeDefined();
expect(p5s === null || p5s === void 0 ? void 0 : p5s.power.getValue()).toBe(160);
});
it('should handle durations longer than activity', () => {
const powerValues = [100, 200, 300];
activity = createActivity(powerValues);
const curve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(activity, [1, 10]);
const values = curve.getValue();
expect(values).toHaveLength(1);
expect(values[0].duration.getValue()).toBe(1);
expect(values[0].power.getValue()).toBe(300);
});
it('should handle gaps (nulls) in power stream conservatively', () => {
// Current implementation treats nulls as whatever arithmetic logic does (likely 0 or NaN depending on impl, let's verify)
// TypeScript usually enforces number or null. Our impl uses `currentSum += powerData[i]`.
// If powerData[i] is null, in JS `number + null = number`.
// So nulls are effectively 0.
const powerValues = [100, null, 100];
// If null treated as 0: 100, 0, 100.
// 3s avg = (100+0+100)/3 = 66.66
activity = createActivity(powerValues);
const curve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(activity, [3]);
const values = curve.getValue();
expect(values).toHaveLength(1);
expect(Math.round(values[0].power.getValue())).toBe(67);
});
it('should correctly serialize toJSON', () => {
const powerValues = [300, 300, 300];
activity = createActivity(powerValues);
const curve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(activity, [1]);
const json = curve.toJSON();
// Expected: { PowerCurve: [ { duration: { Duration: 1 }, power: { Power: 300 } } ] }
expect(json).toHaveProperty('PowerCurve');
const points = json['PowerCurve'];
expect(points).toHaveLength(1);
expect(points[0]).toHaveProperty('duration');
expect(points[0]).toHaveProperty('power');
// Check that they are primitive values (numbers), not Data class instances
// toJSON now returns the raw plain object structure for Firestore compatibility.
expect(typeof points[0].duration).toBe('number');
expect(typeof points[0].power).toBe('number');
expect(points[0].duration).toBe(1);
expect(points[0].power).toBe(300);
});
it('should use default "Best-in-Class" granularity when no durations are provided', () => {
// Create a mock activity with enough data for all default durations (max 18000s)
const longActivity = {
hasStreamData: jest.fn().mockReturnValue(true),
getStreamData: jest.fn().mockReturnValue(new Array(20000).fill(200)), // 20000s of data
getStat: jest.fn().mockReturnValue(null) // Mock getStat to return null (no weight)
// Add other necessary properties if type checking involves them
};
const powerCurve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(longActivity);
const points = powerCurve.getValue();
// Expect 45 default points as per the "Best-in-Class" set
expect(points.length).toBe(45);
// Check a few key durations
const durations = points.map(p => p.duration.getValue());
expect(durations).toContain(1); // 1s
expect(durations).toContain(10); // 10s
expect(durations).toContain(60); // 1m
expect(durations).toContain(300); // 5m
expect(durations).toContain(1200); // 20m
expect(durations).toContain(3600); // 1h
expect(durations).toContain(18000); // 5h
});
it('should calculate W/kg if DataWeight is present', () => {
// Mock activity with weight
const weightValue = 75; // 75kg
const powerValue = 300; // 300W
const expectedWKg = 4; // 300 / 75 = 4.0 W/kg
const weightedActivity = {
hasStreamData: jest.fn().mockReturnValue(true),
getStreamData: jest.fn().mockReturnValue(new Array(100).fill(powerValue)),
getStat: jest.fn().mockImplementation(type => {
if (type === 'Weight') {
// Start of Mock DataWeight
return { getValue: () => weightValue };
}
return null;
})
// Add other necessary properties
};
const powerCurve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(weightedActivity, [10]);
const points = powerCurve.getValue();
expect(points).toHaveLength(1);
expect(points[0].power.getValue()).toBe(powerValue);
expect(points[0].wattsPerKg).toBeDefined();
// @ts-ignore
expect(points[0].wattsPerKg.getValue()).toBe(expectedWKg);
});
it("should calculate Critical Power and W' correctly", () => {
// CP = 250, W' = 20000
// Power @ 180s = 250 + 20000/180 = 361.11
// Power @ 1200s = 250 + 20000/1200 = 266.66
const mockPoints = [
{ duration: new data_duration_1.DataDuration(180), power: new data_power_1.DataPower(361) },
{ duration: new data_duration_1.DataDuration(1200), power: new data_power_1.DataPower(267) }
];
const mockActivity = {
getStat: jest.fn().mockImplementation(type => {
if (type === 'PowerCurve')
return { getValue: () => mockPoints };
return null;
})
};
const result = activity_utilities_1.ActivityUtilities.calculateCriticalPowerAndWPrime(mockActivity);
expect(result).not.toBeNull();
if (result) {
// Allow small margin of error due to integer inputs and rounding
expect(result.cp.getValue()).toBeGreaterThanOrEqual(245);
expect(result.cp.getValue()).toBeLessThanOrEqual(260);
expect(result.wPrime.getValue()).toBeGreaterThanOrEqual(19000);
expect(result.wPrime.getValue()).toBeLessThanOrEqual(21000);
}
});
});