@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
165 lines (164 loc) • 7.51 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const index_1 = require("../index");
const data_ftp_1 = require("../data/data.ftp");
const data_critical_power_1 = require("../data/data.critical-power");
const data_w_prime_1 = require("../data/data.w-prime");
const data_power_curve_1 = require("../data/data.power-curve");
describe('FTP and CP on real FIT files', () => {
const fixtureCases = [
{
label: 'Garmin Edge 1000 long ride',
fixturePath: __dirname + '/fixtures/rides/fit/7432332116.fit',
ftp: 180,
cp: 199,
wPrime: 7059
},
{
label: 'Garmin ride with high variability',
fixturePath: __dirname + '/fixtures/rides/fit/7386755164.fit',
ftp: 228,
cp: 197,
wPrime: 13417
},
{
label: 'Garmin ride with stable sustained effort',
fixturePath: __dirname + '/fixtures/rides/fit/971150603.fit',
ftp: 157,
cp: 157,
wPrime: 7575
},
{
label: 'Garmin ride with moderate variability',
fixturePath: __dirname + '/fixtures/rides/fit/7445393868.fit',
ftp: 201,
cp: 149,
wPrime: 8395
}
];
const withPowerSampleCases = [
{
label: 'withpower sample 2025-11-02_10-30',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2025-11-02_10-30.fit',
ftp: 200,
cp: 175,
wPrime: 17016
},
{
label: 'withpower sample 2025-11-11_15-47',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2025-11-11_15-47.fit',
ftp: 210,
cp: 241,
wPrime: 8989
},
{
label: 'withpower sample 2025-12-29_14-12',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2025-12-29_14-12.fit',
ftp: 234,
cp: 155,
wPrime: 2080
},
{
label: 'withpower sample 2026-01-02_14-24',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2026-01-02_14-24.fit',
ftp: 234,
cp: 202,
wPrime: 5909
},
{
label: 'withpower sample 2026-01-05_14-41',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2026-01-05_14-41.fit',
ftp: 234,
cp: 169,
wPrime: 6307
},
{
label: 'withpower sample 2026-01-16_15-01',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2026-01-16_15-01.fit',
ftp: 234,
cp: 176,
wPrime: 6908
},
{
label: 'withpower sample 2026-01-19_14-57',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2026-01-19_14-57.fit',
ftp: 234,
cp: 141,
wPrime: 7224
},
{
label: 'withpower sample 2026-01-27_14-37',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2026-01-27_14-37.fit',
ftp: 234,
cp: 149,
wPrime: 4331
},
{
label: 'withpower sample 2026-02-02_14-32',
fixturePath: __dirname + '/fixtures/rides/fit/withpower/2026-02-02_14-32.fit',
ftp: 234,
cp: 195,
wPrime: 11461
}
];
const assertPowerMetrics = (testCase) => {
it(`should calculate FTP, CP and W' for ${testCase.label}`, () => __awaiter(void 0, void 0, void 0, function* () {
const buffer = fs_1.default.readFileSync(testCase.fixturePath);
const event = yield index_1.SportsLib.importFromFit(buffer);
const activity = event.getFirstActivity();
const ftp = activity.getStat(data_ftp_1.DataFTP.type);
const cp = activity.getStat(data_critical_power_1.DataCriticalPower.type);
const wPrime = activity.getStat(data_w_prime_1.DataWPrime.type);
expect(ftp).toBeDefined();
expect(cp).toBeDefined();
expect(wPrime).toBeDefined();
expect(ftp.getValue()).toBe(testCase.ftp);
expect(cp.getValue()).toBe(testCase.cp);
expect(wPrime.getValue()).toBe(testCase.wPrime);
}));
};
fixtureCases.forEach(assertPowerMetrics);
withPowerSampleCases.forEach(assertPowerMetrics);
it('should show that FTP and CP are not always close', () => __awaiter(void 0, void 0, void 0, function* () {
const variableRide = fs_1.default.readFileSync(__dirname + '/fixtures/rides/fit/7386755164.fit');
const steadyRide = fs_1.default.readFileSync(__dirname + '/fixtures/rides/fit/971150603.fit');
const [variableEvent, steadyEvent] = yield Promise.all([
index_1.SportsLib.importFromFit(variableRide),
index_1.SportsLib.importFromFit(steadyRide)
]);
const variableActivity = variableEvent.getFirstActivity();
const steadyActivity = steadyEvent.getFirstActivity();
const variableFTP = variableActivity.getStat(data_ftp_1.DataFTP.type).getValue();
const variableCP = variableActivity.getStat(data_critical_power_1.DataCriticalPower.type).getValue();
const steadyFTP = steadyActivity.getStat(data_ftp_1.DataFTP.type).getValue();
const steadyCP = steadyActivity.getStat(data_critical_power_1.DataCriticalPower.type).getValue();
expect(Math.abs(variableCP - variableFTP)).toBeGreaterThanOrEqual(20);
expect(Math.abs(steadyCP - steadyFTP)).toBeLessThanOrEqual(2);
}));
it('should prefer imported Garmin FTP over computed 20-minute FTP when available', () => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const buffer = fs_1.default.readFileSync(__dirname + '/fixtures/rides/fit/7432332116.fit');
const event = yield index_1.SportsLib.importFromFit(buffer);
const activity = event.getFirstActivity();
const importedFTP = activity.getStat(data_ftp_1.DataFTP.type).getValue();
const curvePoints = (_a = activity.getStat(data_power_curve_1.DataPowerCurve.type)) === null || _a === void 0 ? void 0 : _a.getValue();
const twentyMinutePoint = curvePoints.find(point => point.duration.getValue() === 1200);
const computedFtpFromCurve = Math.round(twentyMinutePoint.power.getValue() * 0.95);
expect(importedFTP).toBe(180);
expect(computedFtpFromCurve).toBe(191);
expect(importedFTP).not.toBe(computedFtpFromCurve);
}));
});