running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

cross-calculator.spec.js (56899B)


      1 import { test, expect } from '@playwright/test';
      2 
      3 test('Cross-calculator', async ({ page }) => {
      4   await page.goto('/');
      5 
      6   // Set various options in the different calculators
      7   {
      8     // Go to batch calculator
      9     await page.getByRole('button', { name: 'Batch Calculator' }).click();
     10 
     11     // Enter input pace (2 mi in 10:30)
     12     await page.getByLabel('Input distance value').fill('2');
     13     await page.getByLabel('Input distance unit').selectOption('Miles');
     14     await page.getByLabel('Input duration hours').fill('0');
     15     await page.getByLabel('Input duration minutes').fill('10');
     16     await page.getByLabel('Input duration seconds').fill('30');
     17 
     18     // Enter batch options (15 x 10s increments)
     19     await page.getByLabel('Duration increment minutes').fill('0');
     20     await page.getByLabel('Duration increment seconds').fill('10');
     21     await page.getByLabel('Number of rows').fill('15');
     22 
     23     // Change calculator
     24     await page.getByLabel('Calculator').selectOption('Race Calculator');
     25 
     26     // Change prediction model
     27     await page.getByText('Advanced Options').click();
     28     await page.getByLabel('Prediction model').selectOption('Riegel\'s Model');
     29 
     30     // Go to pace calculator
     31     await page.goto('/');
     32     await page.getByRole('button', { name: 'Pace Calculator' }).click();
     33 
     34     // Enter input pace (2 mi in 15:30)
     35     await page.getByLabel('Input distance value').fill('2');
     36     await page.getByLabel('Input distance unit').selectOption('Miles');
     37     await page.getByLabel('Input duration hours').fill('0');
     38     await page.getByLabel('Input duration minutes').fill('15');
     39     await page.getByLabel('Input duration seconds').fill('30');
     40 
     41     // Create custom target set
     42     await page.getByText('Advanced Options').click();
     43     await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
     44     await expect(page.getByRole('row').nth(4)).toHaveText('There aren\'t any targets in this set yet.');
     45     await expect(page.getByRole('row')).toHaveCount(5);
     46 
     47     // Edit new target set
     48     await expect(page.getByRole('row').nth(1)).toHaveText('There aren\'t any targets in this set yet.');
     49     await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
     50     await page.getByLabel('Target set label').fill('800m Splits');
     51     await page.getByRole('button', { name: 'Add distance target' }).click();
     52     await page.getByLabel('Target distance value').nth(0).fill('0.4');
     53     await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
     54     await page.getByRole('button', { name: 'Add distance target' }).click();
     55     await page.getByLabel('Target distance value').nth(1).fill('800');
     56     await page.getByLabel('Target distance unit').nth(1).selectOption('Meters');
     57     await page.getByRole('button', { name: 'Add time target' }).click();
     58     await page.getByRole('button', { name: 'Close' }).click();
     59 
     60     // Go to race calculator
     61     await page.getByRole('link', { name: 'Back' }).click();
     62     await page.getByRole('button', { name: 'Race Calculator' }).click();
     63 
     64     // Enter input race (2 mi in 10:30)
     65     await page.getByLabel('Input race distance value').fill('2');
     66     await page.getByLabel('Input race distance unit').selectOption('Miles');
     67     await page.getByLabel('Input race duration hours').fill('0');
     68     await page.getByLabel('Input race duration minutes').fill('10');
     69     await page.getByLabel('Input race duration seconds').fill('30');
     70 
     71     // Go to split calculator
     72     await page.getByRole('link', { name: 'Back' }).click();
     73     await page.getByRole('button', { name: 'Split Calculator' }).click();
     74 
     75     // Edit target set
     76     await page.getByRole('button', { name: 'Edit target set' }).click();
     77     await page.getByLabel('Target set label').fill('5K 1600m Splits');
     78     await page.getByLabel('Target distance value').nth(0).fill('1.6');
     79     await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
     80     await page.getByLabel('Target distance value').nth(1).fill('3.2');
     81     await page.getByLabel('Target distance unit').nth(1).selectOption('Kilometers');
     82     await page.getByRole('button', { name: 'Close' }).click();
     83 
     84     // Enter input 5K splits (7:00, 6:30, 6:30)
     85     await page.getByLabel('Split duration minutes').nth(0).fill('7');
     86     await page.getByLabel('Split duration seconds').nth(0).fill('0');
     87     await page.getByLabel('Split duration minutes').nth(1).fill('6');
     88     await page.getByLabel('Split duration seconds').nth(1).fill('30');
     89     await page.getByLabel('Split duration minutes').nth(2).fill('6');
     90     await page.getByLabel('Split duration seconds').nth(2).fill('30');
     91 
     92     // Go to unit calculator
     93     await page.getByRole('link', { name: 'Back' }).click();
     94     await page.getByRole('button', { name: 'Unit Calculator' }).click();
     95 
     96     // Convert speed and pace units (10 kph to time per mile)
     97     await page.getByLabel('Selected unit category').selectOption('Speed & Pace');
     98     await page.getByLabel('Input units').selectOption('Kilometers per Hour');
     99     await page.getByLabel('Input value').fill('10');
    100     await page.getByLabel('Output units').selectOption('Time per Mile');
    101 
    102     // Go to workout calculator
    103     await page.getByRole('link', { name: 'Back' }).click();
    104     await page.getByRole('button', { name: 'Workout Calculator' }).click();
    105 
    106     // Enter input race (1 mi in 5:01)
    107     await page.getByLabel('Input race distance value').fill('1');
    108     await page.getByLabel('Input race distance unit').selectOption('Miles');
    109     await page.getByLabel('Input race duration hours').fill('0');
    110     await page.getByLabel('Input race duration minutes').fill('5');
    111     await page.getByLabel('Input race duration seconds').fill('1');
    112 
    113     // Change riegel exponent and enable target name customization
    114     await page.getByText('Advanced Options').click();
    115     await page.getByLabel('Riegel Exponent').fill('1.12');
    116     await page.getByLabel('Workout name customization').selectOption('Enabled');
    117 
    118     // Change default units (should update on other calculators too)
    119     await page.getByLabel('Default units').selectOption('Kilometers');
    120   }
    121 
    122   // Go back and assert the options are not reset
    123   {
    124     // Return to batch calculator
    125     await page.getByRole('link', { name: 'Back' }).click();
    126     await page.getByRole('button', { name: 'Batch Calculator' }).click();
    127 
    128     // Assert race results are correct (inputs and options not reset)
    129     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    130     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m');
    131     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17);
    132     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    133     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:12');
    134     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17);
    135     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    136     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:42');
    137     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17);
    138     await expect(page.getByRole('row')).toHaveCount(16);
    139 
    140     // Assert pace results are correct (inputs and options not reset, new pace targets loaded)
    141     await page.getByLabel('Calculator').selectOption('Pace Calculator');
    142     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    143     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m');
    144     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(4);
    145     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    146     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:37');
    147     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(4);
    148     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    149     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:11');
    150     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(4);
    151     await expect(page.getByRole('row')).toHaveCount(16);
    152 
    153     // Assert workout results are correct (new workout options loaded)
    154     await page.getByLabel('Calculator').selectOption('Workout Calculator');
    155     await expect(page.getByLabel('Workout name customization')).toHaveValue("true");
    156     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    157     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km');
    158     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5);
    159     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    160     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:45');
    161     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5);
    162     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    163     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:22');
    164     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5);
    165     await expect(page.getByRole('row')).toHaveCount(16);
    166 
    167     // Reset selected calculator
    168     await page.getByLabel('Calculator').selectOption('Race Calculator');
    169 
    170     // Return to pace calculator
    171     await page.getByRole('link', { name: 'Back' }).click();
    172     await page.getByRole('button', { name: 'Pace Calculator' }).click();
    173 
    174     // Assert paces are correct (input pace not reset)
    175     await expect(page.getByRole('row').nth(1)).toHaveText('0.4 km' + '1:55.58');
    176     await expect(page.getByRole('row').nth(2)).toHaveText('800 m' + '3:51.15');
    177     await expect(page.getByRole('row').nth(3)).toHaveText('2.08 km' + '10:00');
    178     await expect(page.getByRole('row')).toHaveCount(4);
    179 
    180     // Return to race calculator
    181     await page.getByRole('link', { name: 'Back' }).click();
    182     await page.getByRole('button', { name: 'Race Calculator' }).click();
    183 
    184     // Assert race predictions are correct (input race not resset and new prediction model loaded)
    185     await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:49.86' + '3:00 / km');
    186     await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '17:11.78' + '3:26 / km');
    187     await expect(page.getByRole('row')).toHaveCount(17);
    188 
    189     // Return to split calculator
    190     await page.getByRole('link', { name: 'Back' }).click();
    191     await page.getByRole('button', { name: 'Split Calculator' }).click();
    192 
    193     // Assert times and paces are correct (split times not reset)
    194     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
    195     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('4:23 / km');
    196     await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
    197     await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('4:04 / km');
    198     await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('20:00.00');
    199     await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('3:37 / km');
    200     await expect(page.getByRole('row')).toHaveCount(4);
    201 
    202     // Return to unit calculator
    203     await page.getByRole('link', { name: 'Back' }).click();
    204     await page.getByRole('button', { name: 'Unit Calculator' }).click();
    205 
    206     // Assert result is correct (state not reset)
    207     await expect(page.getByLabel('Output value')).toHaveText('00:09:39.364');
    208 
    209     // Return to workout calculator
    210     await page.getByRole('link', { name: 'Back' }).click();
    211     await page.getByRole('button', { name: 'Workout Calculator' }).click();
    212 
    213     // Assert workout splits are correct (input race and prediction model not reset)
    214     await expect(page.getByRole('row').nth(1)).toHaveText('400 m @ 1 mi' + '1:14.81');
    215     await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '6:30.40');
    216     await expect(page.getByRole('row')).toHaveCount(5);
    217   }
    218 
    219   // Assert localStorage entries are correct
    220   {
    221     // Assert global localStorage entries are correct
    222     expect(await page.evaluate(() => localStorage.length)).toEqual(12);
    223     expect(await page.evaluate(() => localStorage.getItem('running-tools.global-options')))
    224       .toEqual(JSON.stringify({
    225         defaultUnitSystem: 'metric',
    226         racePredictionOptions: {
    227           model: 'RiegelModel',
    228           riegelExponent: 1.12,
    229         },
    230       }));
    231 
    232     // Assert localStorage entries for the batch calculator are correct
    233     expect(await page.evaluate(() =>
    234       localStorage.getItem('running-tools.batch-calculator-options'))).toEqual(JSON.stringify({
    235         calculator: 'race',
    236         increment: 10,
    237         input: {
    238           distanceValue: 2,
    239           distanceUnit: 'miles',
    240           time: 630,
    241         },
    242         label: '',
    243         rows: 15,
    244       }));
    245 
    246     // Assert localStorage entries for the pace calculator are correct
    247     const paceCalculatorKey = parseInt(JSON.parse(await page.evaluate(() =>
    248       localStorage.getItem('running-tools.pace-calculator-options'))).selectedTargetSet);
    249     expect(paceCalculatorKey - parseInt(Date.now().toString())).toBeLessThan(100000);
    250     expect(await page.evaluate(() =>
    251       localStorage.getItem('running-tools.pace-calculator-options'))).toEqual(JSON.stringify({
    252         input: {
    253           distanceValue: 2,
    254           distanceUnit: 'miles',
    255           time: 930,
    256         },
    257         selectedTargetSet: paceCalculatorKey.toString(),
    258       }));
    259     expect(await page.evaluate(() =>
    260       localStorage.getItem('running-tools.pace-calculator-target-sets'))).toEqual(JSON.stringify({
    261         _pace_targets: {
    262           name: 'Common Pace Targets',
    263           targets: [
    264             { type: 'distance', distanceValue: 100, distanceUnit: 'meters' },
    265             { type: 'distance', distanceValue: 200, distanceUnit: 'meters' },
    266             { type: 'distance', distanceValue: 300, distanceUnit: 'meters' },
    267             { type: 'distance', distanceValue: 400, distanceUnit: 'meters' },
    268             { type: 'distance', distanceValue: 600, distanceUnit: 'meters' },
    269             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    270             { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' },
    271             { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' },
    272             { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' },
    273             { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' },
    274             { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
    275             { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' },
    276             { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' },
    277             { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' },
    278             { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
    279             { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' },
    280             { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
    281             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
    282             { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' },
    283             { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' },
    284             { type: 'distance', distanceValue: 5, distanceUnit: 'miles' },
    285             { type: 'distance', distanceValue: 6, distanceUnit: 'miles' },
    286             { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
    287             { type: 'distance', distanceValue: 8, distanceUnit: 'miles' },
    288             { type: 'distance', distanceValue: 10, distanceUnit: 'miles' },
    289             { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' },
    290             { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' },
    291             { type: 'time', time: 600 },
    292             { type: 'time', time: 1800 },
    293             { type: 'time', time: 3600 },
    294           ],
    295         },
    296         [paceCalculatorKey.toString()]: {
    297           name: '800m Splits',
    298           targets: [
    299             { type: 'distance', distanceValue: 0.4, distanceUnit: 'kilometers' },
    300             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    301             { type: 'time', time: 600 },
    302           ],
    303         },
    304       }));
    305 
    306     // Assert localStorage entries for the race calculator are correct
    307     expect(await page.evaluate(() =>
    308       localStorage.getItem('running-tools.race-calculator-options'))).toEqual(JSON.stringify({
    309         input: {
    310           distanceValue: 2,
    311           distanceUnit: 'miles',
    312           time: 630,
    313         },
    314         selectedTargetSet: '_race_targets',
    315       }));
    316     expect(await page.evaluate(() =>
    317       localStorage.getItem('running-tools.race-calculator-target-sets'))).toEqual(JSON.stringify({
    318         _race_targets: {
    319           name: 'Common Race Targets',
    320           targets: [
    321             { type: 'distance', distanceValue: 400, distanceUnit: 'meters' },
    322             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    323             { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' },
    324             { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' },
    325             { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
    326             { type: 'distance', distanceValue: 3000, distanceUnit: 'meters' },
    327             { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' },
    328             { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
    329             { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
    330             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
    331             { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' },
    332             { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' },
    333             { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
    334             { type: 'distance', distanceValue: 15, distanceUnit: 'kilometers' },
    335             { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' },
    336             { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' },
    337           ],
    338         },
    339       }));
    340 
    341     // Assert localStorage entries for the split calculator are correct
    342     expect(await page.evaluate(() =>
    343       localStorage.getItem('running-tools.split-calculator-options')))
    344         .toEqual(JSON.stringify({
    345           selectedTargetSet: '_split_targets',
    346         }));
    347     expect(await page.evaluate(() =>
    348       localStorage.getItem('running-tools.split-calculator-target-sets'))).toEqual(JSON.stringify({
    349         _split_targets: {
    350           name: '5K 1600m Splits',
    351           targets: [
    352             { type: 'distance', distanceValue: 1.6, distanceUnit: 'kilometers', splitTime: 420 },
    353             { type: 'distance', distanceValue: 3.2, distanceUnit: 'kilometers', splitTime: 390 },
    354             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers', splitTime: 390 },
    355           ],
    356         },
    357       }));
    358 
    359     // Assert localStorage entries for the unit calculator are correct
    360     expect(await page.evaluate(() => localStorage.getItem('running-tools.unit-calculator-category')))
    361       .toEqual(JSON.stringify('speed_and_pace'));
    362     expect(await page.evaluate(() =>
    363       localStorage.getItem('running-tools.unit-calculator-inputs'))).toEqual(JSON.stringify({
    364         distance: {
    365           inputValue: 1,
    366           inputUnit: 'miles',
    367           outputUnit: 'kilometers',
    368         },
    369         time: {
    370           inputValue: 1,
    371           inputUnit: 'seconds',
    372           outputUnit: 'hh:mm:ss',
    373         },
    374         speed_and_pace: {
    375           inputValue: 10,
    376           inputUnit: 'kilometers_per_hour',
    377           outputUnit: 'seconds_per_mile',
    378         },
    379       }));
    380 
    381     // Assert localStorage entries for the workout calculator are correct
    382     expect(await page.evaluate(() =>
    383       localStorage.getItem('running-tools.workout-calculator-options'))).toEqual(JSON.stringify({
    384         customTargetNames: true,
    385         input: {
    386           distanceValue: 1,
    387           distanceUnit: 'miles',
    388           time: 301,
    389         },
    390         selectedTargetSet: '_workout_targets',
    391       }));
    392     expect(await page.evaluate(() =>
    393       localStorage.getItem('running-tools.workout-calculator-target-sets'))).toEqual(JSON.stringify({
    394         _workout_targets: {
    395           name: 'Common Workout Targets',
    396           targets: [
    397             {
    398               splitValue: 400, splitUnit: 'meters',
    399               type: 'distance', distanceValue: 1, distanceUnit: 'miles',
    400             },
    401             {
    402               splitValue: 800, splitUnit: 'meters',
    403               type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
    404             },
    405             {
    406               splitValue: 1600, splitUnit: 'meters',
    407               type: 'time', time: 3600,
    408             },
    409             {
    410               splitValue: 1, splitUnit: 'miles',
    411               type: 'distance', distanceValue: 1, distanceUnit: 'marathons',
    412             },
    413           ],
    414         },
    415       }));
    416   }
    417 
    418   // Reload app and assert the updated options are loaded
    419   // Identical to the previous "go back and assert the options are not resset" section
    420   {
    421     // Return to batch calculator
    422     await page.getByRole('link', { name: 'Back' }).click();
    423     await page.getByRole('button', { name: 'Batch Calculator' }).click();
    424 
    425     // Assert race results are correct (inputs and options not reset)
    426     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    427     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m');
    428     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17);
    429     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    430     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:12');
    431     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17);
    432     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    433     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:42');
    434     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17);
    435     await expect(page.getByRole('row')).toHaveCount(16);
    436 
    437     // Assert pace results are correct (inputs and options not reset, new pace targets loaded)
    438     await page.getByLabel('Calculator').selectOption('Pace Calculator');
    439     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    440     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m');
    441     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(4);
    442     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    443     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:37');
    444     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(4);
    445     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    446     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:11');
    447     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(4);
    448     await expect(page.getByRole('row')).toHaveCount(16);
    449 
    450     // Assert workout results are correct (new workout options loaded)
    451     await page.getByLabel('Calculator').selectOption('Workout Calculator');
    452     await expect(page.getByLabel('Workout name customization')).toHaveValue("true");
    453     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    454     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km');
    455     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5);
    456     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    457     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:45');
    458     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5);
    459     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    460     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:22');
    461     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5);
    462     await expect(page.getByRole('row')).toHaveCount(16);
    463 
    464     // Reset selected calculator
    465     await page.getByLabel('Calculator').selectOption('Race Calculator');
    466 
    467     // Return to pace calculator
    468     await page.getByRole('link', { name: 'Back' }).click();
    469     await page.getByRole('button', { name: 'Pace Calculator' }).click();
    470 
    471     // Assert paces are correct (input pace not reset)
    472     await expect(page.getByRole('row').nth(1)).toHaveText('0.4 km' + '1:55.58');
    473     await expect(page.getByRole('row').nth(2)).toHaveText('800 m' + '3:51.15');
    474     await expect(page.getByRole('row').nth(3)).toHaveText('2.08 km' + '10:00');
    475     await expect(page.getByRole('row')).toHaveCount(4);
    476 
    477     // Return to race calculator
    478     await page.getByRole('link', { name: 'Back' }).click();
    479     await page.getByRole('button', { name: 'Race Calculator' }).click();
    480 
    481     // Assert race predictions are correct (input race not resset and new prediction model loaded)
    482     await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:49.86' + '3:00 / km');
    483     await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '17:11.78' + '3:26 / km');
    484     await expect(page.getByRole('row')).toHaveCount(17);
    485 
    486     // Return to split calculator
    487     await page.getByRole('link', { name: 'Back' }).click();
    488     await page.getByRole('button', { name: 'Split Calculator' }).click();
    489 
    490     // Assert times and paces are correct (split times not reset)
    491     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
    492     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('4:23 / km');
    493     await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
    494     await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('4:04 / km');
    495     await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('20:00.00');
    496     await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('3:37 / km');
    497     await expect(page.getByRole('row')).toHaveCount(4);
    498 
    499     // Return to unit calculator
    500     await page.getByRole('link', { name: 'Back' }).click();
    501     await page.getByRole('button', { name: 'Unit Calculator' }).click();
    502 
    503     // Assert result is correct (state not reset)
    504     await expect(page.getByLabel('Output value')).toHaveText('00:09:39.364');
    505 
    506     // Return to workout calculator
    507     await page.getByRole('link', { name: 'Back' }).click();
    508     await page.getByRole('button', { name: 'Workout Calculator' }).click();
    509 
    510     // Assert workout splits are correct (input race and prediction model not reset)
    511     await expect(page.getByRole('row').nth(1)).toHaveText('400 m @ 1 mi' + '1:14.81');
    512     await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '6:30.40');
    513     await expect(page.getByRole('row')).toHaveCount(5);
    514   }
    515 });
    516 
    517 test('v1.4.1 Migration', async ({ page }) => {
    518   await page.goto('/');
    519 
    520   // Set v1.4.1 localStorage entries
    521   {
    522     // Set general localStorage entries
    523     await page.evaluate(() => localStorage.setItem('running-tools.default-unit-system',
    524       JSON.stringify('metric')));
    525 
    526     // Set batch calculator localStorage entries
    527     await page.evaluate(() => localStorage.setItem('running-tools.batch-calculator-input',
    528       JSON.stringify({
    529         distanceValue: 2,
    530         distanceUnit: 'miles',
    531         time: 630,
    532       })
    533     ));
    534     await page.evaluate(() => localStorage.setItem('running-tools.batch-calculator-options',
    535       JSON.stringify({
    536         calculator: 'race',
    537         increment: 10,
    538         rows: 15,
    539       })
    540     ));
    541 
    542     // Set pace calculator localStorage entries
    543     await page.evaluate(() => localStorage.setItem('running-tools.pace-calculator-input',
    544       JSON.stringify({
    545         distanceValue: 2,
    546         distanceUnit: 'miles',
    547         time: 930,
    548       })
    549     ));
    550     await page.evaluate(() => localStorage.setItem('running-tools.pace-calculator-target-set',
    551       JSON.stringify('123456789'))); // Property moved after v1.4.1
    552     await page.evaluate(() => localStorage.setItem('running-tools.pace-calculator-target-sets',
    553       JSON.stringify({
    554         _pace_targets: {
    555           name: 'Common Pace Targets',
    556           targets: [
    557             { type: 'distance', distanceValue: 100, distanceUnit: 'meters' },
    558             { type: 'distance', distanceValue: 200, distanceUnit: 'meters' },
    559             { type: 'distance', distanceValue: 300, distanceUnit: 'meters' },
    560             { type: 'distance', distanceValue: 400, distanceUnit: 'meters' },
    561             { type: 'distance', distanceValue: 600, distanceUnit: 'meters' },
    562             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    563             { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' },
    564             { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' },
    565             { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' },
    566             { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' },
    567             { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
    568             { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' },
    569             { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' },
    570             { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' },
    571             { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
    572             { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' },
    573             { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
    574             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
    575             { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' },
    576             { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' },
    577             { type: 'distance', distanceValue: 5, distanceUnit: 'miles' },
    578             { type: 'distance', distanceValue: 6, distanceUnit: 'miles' },
    579             { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
    580             { type: 'distance', distanceValue: 8, distanceUnit: 'miles' },
    581             { type: 'distance', distanceValue: 10, distanceUnit: 'miles' },
    582             { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' },
    583             { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' },
    584             { type: 'time', time: 600 },
    585             { type: 'time', time: 1800 },
    586             { type: 'time', time: 3600 },
    587           ],
    588         },
    589         '123456789': {
    590           name: '800m Splits',
    591           targets: [
    592             { type: 'distance', distanceValue: 0.4, distanceUnit: 'kilometers' },
    593             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    594             { type: 'time', time: 600 },
    595           ],
    596         },
    597       })
    598     ));
    599 
    600     // Set race calculator localStorage entries
    601     await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-input',
    602       JSON.stringify({
    603         distanceValue: 2,
    604         distanceUnit: 'miles',
    605         time: 630,
    606       })
    607     ));
    608     await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-options',
    609       JSON.stringify({
    610         model: 'RiegelModel',
    611         riegelExponent: 1.06,
    612       })
    613     ));
    614     await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-target-set',
    615       JSON.stringify('_race_targets'))); // Property moved after v1.4.1
    616     await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-target-sets',
    617       JSON.stringify({
    618         _race_targets: {
    619           name: 'Common Race Targets',
    620           targets: [
    621             { type: 'distance', distanceValue: 400, distanceUnit: 'meters' },
    622             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    623             { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' },
    624             { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' },
    625             { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
    626             { type: 'distance', distanceValue: 3000, distanceUnit: 'meters' },
    627             { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' },
    628             { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
    629             { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
    630             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
    631             { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' },
    632             { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' },
    633             { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
    634             { type: 'distance', distanceValue: 15, distanceUnit: 'kilometers' },
    635             { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' },
    636             { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' },
    637           ],
    638         },
    639       })
    640     ));
    641 
    642     // Set split calculator localStorage entries
    643     await page.evaluate(() => localStorage.setItem('running-tools.split-calculator-target-set',
    644       JSON.stringify('_split_targets'))); // Property moved after v1.4.1
    645     await page.evaluate(() => localStorage.setItem('running-tools.split-calculator-target-sets',
    646       JSON.stringify({
    647         _split_targets: {
    648           name: '5K 1600m Splits',
    649           targets: [
    650             { type: 'distance', distanceValue: 1.6, distanceUnit: 'kilometers', splitTime: 420 },
    651             { type: 'distance', distanceValue: 3.2, distanceUnit: 'kilometers', splitTime: 390 },
    652             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers', splitTime: 390 },
    653           ],
    654         },
    655       })
    656     ));
    657 
    658     // Set unit calculator localStorage entries
    659     await page.evaluate(() => localStorage.setItem('running-tools.unit-calculator-category',
    660       JSON.stringify('speed_and_pace')));
    661     await page.evaluate(() => localStorage.setItem('running-tools.unit-calculator-inputs',
    662       JSON.stringify({
    663         distance: {
    664           inputValue: 1,
    665           inputUnit: 'miles',
    666           outputUnit: 'kilometers',
    667         },
    668         time: {
    669           inputValue: 1,
    670           inputUnit: 'seconds',
    671           outputUnit: 'hh:mm:ss',
    672         },
    673         speed_and_pace: {
    674           inputValue: 10,
    675           inputUnit: 'kilometers_per_hour',
    676           outputUnit: 'seconds_per_mile',
    677         },
    678       })
    679     ));
    680 
    681     // Set workout calculator localStorage entries
    682     await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-input',
    683       JSON.stringify({
    684         distanceValue: 1,
    685         distanceUnit: 'miles',
    686         time: 301,
    687       })
    688     ));
    689     await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-options',
    690       JSON.stringify({
    691         // customTargetNames property added after v1.4.1
    692         model: 'VO2MaxModel',
    693         riegelExponent: 1.06,
    694       })
    695     ));
    696     await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-target-set',
    697       JSON.stringify('_workout_targets'))); // Property moved after v1.4.1
    698     await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-target-sets',
    699       JSON.stringify({
    700         _workout_targets: {
    701           name: 'Common Workout Targets',
    702           targets: [
    703             {
    704               splitValue: 400, splitUnit: 'meters',
    705               type: 'distance', distanceValue: 1, distanceUnit: 'miles',
    706             },
    707             {
    708               splitValue: 800, splitUnit: 'meters',
    709               type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
    710             },
    711             {
    712               splitValue: 1600, splitUnit: 'meters',
    713               type: 'time', time: 3600,
    714             },
    715             {
    716               splitValue: 1, splitUnit: 'miles',
    717               type: 'distance', distanceValue: 1, distanceUnit: 'marathons',
    718             },
    719           ],
    720         },
    721       })
    722     ));
    723   }
    724 
    725   // Reload app and assert the proper localStorage migrations were performed
    726   {
    727     // Reload the app and assert general localStorage entries are correct
    728     await page.goto('/');
    729     expect(await page.evaluate(() => localStorage.length)).toEqual(12);
    730     expect(await page.evaluate(() =>
    731       localStorage.getItem('running-tools.default-unit-system'))).toBeNull();
    732     expect(await page.evaluate(() =>
    733       localStorage.getItem('running-tools.global-options'))).toEqual(JSON.stringify({
    734         defaultUnitSystem: 'metric',
    735         racePredictionOptions: {
    736           model: 'RiegelModel',
    737           riegelExponent: 1.06,
    738         },
    739       }));
    740 
    741     // Assert localStorage entries for the batch calculator are correct
    742     expect(await page.evaluate(() =>
    743       localStorage.getItem('running-tools.batch-calculator-input'))).toBeNull();
    744     expect(await page.evaluate(() =>
    745       localStorage.getItem('running-tools.batch-calculator-options'))).toEqual(JSON.stringify({
    746         calculator: 'race',
    747         increment: 10,
    748         rows: 15,
    749         label: '',
    750         input: {
    751           distanceValue: 2,
    752           distanceUnit: 'miles',
    753           time: 630,
    754         },
    755       }));
    756 
    757     // Assert localStorage entries for the pace calculator are correct
    758     expect(await page.evaluate(() =>
    759       localStorage.getItem('running-tools.pace-calculator-input'))).toBeNull();
    760     expect(await page.evaluate(() => localStorage.getItem('running-tools.pace-calculator-options')))
    761       .toEqual(JSON.stringify({
    762         input: {
    763           distanceValue: 2,
    764           distanceUnit: 'miles',
    765           time: 930,
    766         },
    767         selectedTargetSet: '123456789',
    768       }));
    769     expect(await page.evaluate(() =>
    770       localStorage.getItem('running-tools.pace-calculator-target-set'))).toBeNull();
    771     expect(await page.evaluate(() =>
    772       localStorage.getItem('running-tools.pace-calculator-target-sets'))).toEqual(JSON.stringify({
    773         _pace_targets: {
    774           name: 'Common Pace Targets',
    775           targets: [
    776             { type: 'distance', distanceValue: 100, distanceUnit: 'meters' },
    777             { type: 'distance', distanceValue: 200, distanceUnit: 'meters' },
    778             { type: 'distance', distanceValue: 300, distanceUnit: 'meters' },
    779             { type: 'distance', distanceValue: 400, distanceUnit: 'meters' },
    780             { type: 'distance', distanceValue: 600, distanceUnit: 'meters' },
    781             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    782             { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' },
    783             { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' },
    784             { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' },
    785             { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' },
    786             { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
    787             { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' },
    788             { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' },
    789             { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' },
    790             { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
    791             { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' },
    792             { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
    793             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
    794             { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' },
    795             { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' },
    796             { type: 'distance', distanceValue: 5, distanceUnit: 'miles' },
    797             { type: 'distance', distanceValue: 6, distanceUnit: 'miles' },
    798             { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
    799             { type: 'distance', distanceValue: 8, distanceUnit: 'miles' },
    800             { type: 'distance', distanceValue: 10, distanceUnit: 'miles' },
    801             { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' },
    802             { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' },
    803             { type: 'time', time: 600 },
    804             { type: 'time', time: 1800 },
    805             { type: 'time', time: 3600 },
    806           ],
    807         },
    808         '123456789': {
    809           name: '800m Splits',
    810           targets: [
    811             { type: 'distance', distanceValue: 0.4, distanceUnit: 'kilometers' },
    812             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    813             { type: 'time', time: 600 },
    814           ],
    815         },
    816       }));
    817 
    818     // Assert localStorage entries for the race calculator are correct
    819     expect(await page.evaluate(() =>
    820       localStorage.getItem('running-tools.race-calculator-input'))).toBeNull();
    821     expect(await page.evaluate(() => localStorage.getItem('running-tools.race-calculator-options')))
    822       .toEqual(JSON.stringify({
    823         input: {
    824           distanceValue: 2,
    825           distanceUnit: 'miles',
    826           time: 630,
    827         },
    828         selectedTargetSet: '_race_targets',
    829       }));
    830     expect(await page.evaluate(() =>
    831       localStorage.getItem('running-tools.race-calculator-target-set'))).toBeNull();
    832     expect(await page.evaluate(() =>
    833       localStorage.getItem('running-tools.race-calculator-target-sets'))).toEqual(JSON.stringify({
    834         _race_targets: {
    835           name: 'Common Race Targets',
    836           targets: [
    837             { type: 'distance', distanceValue: 400, distanceUnit: 'meters' },
    838             { type: 'distance', distanceValue: 800, distanceUnit: 'meters' },
    839             { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' },
    840             { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' },
    841             { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
    842             { type: 'distance', distanceValue: 3000, distanceUnit: 'meters' },
    843             { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' },
    844             { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
    845             { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
    846             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
    847             { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' },
    848             { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' },
    849             { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
    850             { type: 'distance', distanceValue: 15, distanceUnit: 'kilometers' },
    851             { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' },
    852             { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' },
    853           ],
    854         },
    855       }));
    856 
    857     // Assert localStorage entries for the split calculator are correct
    858     expect(await page.evaluate(() => localStorage.getItem('running-tools.split-calculator-options')))
    859       .toEqual(JSON.stringify({
    860         selectedTargetSet: '_split_targets',
    861       }));
    862     expect(await page.evaluate(() =>
    863       localStorage.getItem('running-tools.split-calculator-target-set'))).toBeNull();
    864     expect(await page.evaluate(() =>
    865       localStorage.getItem('running-tools.split-calculator-target-sets'))).toEqual(JSON.stringify({
    866         _split_targets: {
    867           name: '5K 1600m Splits',
    868           targets: [
    869             { type: 'distance', distanceValue: 1.6, distanceUnit: 'kilometers', splitTime: 420 },
    870             { type: 'distance', distanceValue: 3.2, distanceUnit: 'kilometers', splitTime: 390 },
    871             { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers', splitTime: 390 },
    872           ],
    873         },
    874       }));
    875 
    876     // Assert localStorage entries for the unit calculator are correct
    877     expect(await page.evaluate(() => localStorage.getItem('running-tools.unit-calculator-category')))
    878       .toEqual(JSON.stringify('speed_and_pace'));
    879     expect(await page.evaluate(() =>
    880       localStorage.getItem('running-tools.unit-calculator-inputs'))).toEqual(JSON.stringify({
    881         distance: {
    882           inputValue: 1,
    883           inputUnit: 'miles',
    884           outputUnit: 'kilometers',
    885         },
    886         time: {
    887           inputValue: 1,
    888           inputUnit: 'seconds',
    889           outputUnit: 'hh:mm:ss',
    890         },
    891         speed_and_pace: {
    892           inputValue: 10,
    893           inputUnit: 'kilometers_per_hour',
    894           outputUnit: 'seconds_per_mile',
    895         },
    896       }));
    897 
    898     // Assert localStorage entries for the workout calculator are correct
    899     expect(await page.evaluate(() =>
    900       localStorage.getItem('running-tools.workout-calculator-input'))).toBeNull();
    901     expect(await page.evaluate(() =>
    902       localStorage.getItem('running-tools.workout-calculator-options'))).toEqual(JSON.stringify({
    903         customTargetNames: false,
    904         input: {
    905           distanceValue: 1,
    906           distanceUnit: 'miles',
    907           time: 301,
    908         },
    909         selectedTargetSet: '_workout_targets',
    910       }));
    911     expect(await page.evaluate(() =>
    912       localStorage.getItem('running-tools.workout-calculator-target-set'))).toBeNull();
    913     expect(await page.evaluate(() =>
    914       localStorage.getItem('running-tools.workout-calculator-target-sets'))).toEqual(JSON.stringify({
    915         _workout_targets: {
    916           name: 'Common Workout Targets',
    917           targets: [
    918             {
    919               splitValue: 400, splitUnit: 'meters',
    920               type: 'distance', distanceValue: 1, distanceUnit: 'miles',
    921             },
    922             {
    923               splitValue: 800, splitUnit: 'meters',
    924               type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
    925             },
    926             {
    927               splitValue: 1600, splitUnit: 'meters',
    928               type: 'time', time: 3600,
    929             },
    930             {
    931               splitValue: 1, splitUnit: 'miles',
    932               type: 'distance', distanceValue: 1, distanceUnit: 'marathons',
    933             },
    934           ],
    935         },
    936       }));
    937   }
    938 
    939   // Assert UI options are up to date
    940   // Very similar to the previous "go back and assert the options are not resset" section
    941   {
    942     // Assert batch options are correct
    943     await page.getByRole('button', { name: 'Batch Calculator' }).click();
    944     await expect(page.getByLabel('Input distance value')).toHaveValue('2.00');
    945     await expect(page.getByLabel('Input distance unit')).toHaveValue('miles');
    946     await expect(page.getByLabel('Input duration hours')).toHaveValue('0');
    947     await expect(page.getByLabel('Input duration minutes')).toHaveValue('10');
    948     await expect(page.getByLabel('Input duration seconds')).toHaveValue('30.00');
    949     await expect(page.getByLabel('Duration increment minutes')).toHaveValue('00');
    950     await expect(page.getByLabel('Duration increment seconds')).toHaveValue('10.00');
    951     await expect(page.getByLabel('Number of rows')).toHaveValue('15');
    952     await expect(page.getByLabel('Calculator')).toHaveValue('race');
    953 
    954     // Assert advanced options are correct for race calculator mode
    955     await page.getByText('Advanced Options').click();
    956     await expect(page.getByLabel('Default units')).toHaveValue('metric');
    957     await expect(page.getByLabel('Selected target set')).toHaveValue('_race_targets');
    958     await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
    959     await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
    960 
    961     // Assert race results are correct (inputs and options not reset)
    962     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    963     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m');
    964     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17);
    965     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    966     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:24');
    967     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17);
    968     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    969     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:56');
    970     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17);
    971     await expect(page.getByRole('row')).toHaveCount(16);
    972 
    973     // Assert advanced options are correct for pace calculator mode
    974     await page.getByLabel('Calculator').selectOption('Pace Calculator');
    975     await expect(page.getByLabel('Default units')).toHaveValue('metric');
    976     await expect(page.getByLabel('Selected target set')).toHaveValue('123456789');
    977 
    978     // Assert pace results are correct (inputs and options not reset, new pace targets loaded)
    979     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
    980     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m');
    981     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(4);
    982     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
    983     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:37');
    984     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(4);
    985     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
    986     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:11');
    987     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(4);
    988     await expect(page.getByRole('row')).toHaveCount(16);
    989 
    990     // Assert advanced options are correct for workout calculator mode
    991     await page.getByLabel('Calculator').selectOption('Workout Calculator');
    992     await page.getByText('Advanced Options').click();
    993     await expect(page.getByLabel('Default units')).toHaveValue('metric');
    994     await expect(page.getByLabel('Selected target set')).toHaveValue('_workout_targets');
    995     await expect(page.getByLabel('Workout name customization')).toHaveValue('false');
    996     await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
    997     await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
    998 
    999     // Assert workout results are correct (new workout options loaded)
   1000     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi');
   1001     await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km');
   1002     await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5);
   1003     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30');
   1004     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:41');
   1005     await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5);
   1006     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50');
   1007     await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:17');
   1008     await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5);
   1009     await expect(page.getByRole('row')).toHaveCount(16);
   1010 
   1011     // Reset selected calculator
   1012     await page.getByLabel('Calculator').selectOption('Race Calculator');
   1013 
   1014     // Return to pace calculator
   1015     await page.getByRole('link', { name: 'Back' }).click();
   1016     await page.getByRole('button', { name: 'Pace Calculator' }).click();
   1017 
   1018     // Assert pace calculator options are correct
   1019     await expect(page.getByLabel('Input distance value')).toHaveValue('2.00');
   1020     await expect(page.getByLabel('Input distance unit')).toHaveValue('miles');
   1021     await expect(page.getByLabel('Input duration hours')).toHaveValue('0');
   1022     await expect(page.getByLabel('Input duration minutes')).toHaveValue('15');
   1023     await expect(page.getByLabel('Input duration seconds')).toHaveValue('30.00');
   1024     await page.getByText('Advanced Options').click();
   1025     await expect(page.getByLabel('Default units')).toHaveValue('metric');
   1026     await expect(page.getByLabel('Selected target set')).toHaveValue('123456789');
   1027 
   1028     // Assert paces are correct (input pace not reset)
   1029     await expect(page.getByRole('row').nth(1)).toHaveText('0.4 km' + '1:55.58');
   1030     await expect(page.getByRole('row').nth(2)).toHaveText('800 m' + '3:51.15');
   1031     await expect(page.getByRole('row').nth(3)).toHaveText('2.08 km' + '10:00');
   1032     await expect(page.getByRole('row')).toHaveCount(4);
   1033 
   1034     // Return to race calculator
   1035     await page.getByRole('link', { name: 'Back' }).click();
   1036     await page.getByRole('button', { name: 'Race Calculator' }).click();
   1037 
   1038     // Assert race calculator options are correct
   1039     await expect(page.getByLabel('Input race distance value')).toHaveValue('2.00');
   1040     await expect(page.getByLabel('Input race distance unit')).toHaveValue('miles');
   1041     await expect(page.getByLabel('Input race duration hours')).toHaveValue('0');
   1042     await expect(page.getByLabel('Input race duration minutes')).toHaveValue('10');
   1043     await expect(page.getByLabel('Input race duration seconds')).toHaveValue('30.00');
   1044     await page.getByText('Advanced Options').click();
   1045     await expect(page.getByLabel('Default units')).toHaveValue('metric');
   1046     await expect(page.getByLabel('Selected target set')).toHaveValue('_race_targets');
   1047     await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
   1048     await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
   1049 
   1050     // Assert race predictions are correct (input race not resset and new prediction model loaded)
   1051     await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '5:02.17' + '3:08 / km');
   1052     await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:44.87' + '3:21 / km');
   1053     await expect(page.getByRole('row')).toHaveCount(17);
   1054 
   1055     // Return to split calculator
   1056     await page.getByRole('link', { name: 'Back' }).click();
   1057     await page.getByRole('button', { name: 'Split Calculator' }).click();
   1058 
   1059     // Assert split calculator options are correct
   1060     await expect(page.getByLabel('Default units')).toHaveValue('metric');
   1061     await expect(page.getByLabel('Selected target set')).toHaveValue('_split_targets');
   1062 
   1063     // Assert times and paces are correct (split times not reset)
   1064     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
   1065     await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('4:23 / km');
   1066     await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
   1067     await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('4:04 / km');
   1068     await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('20:00.00');
   1069     await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('3:37 / km');
   1070     await expect(page.getByRole('row')).toHaveCount(4);
   1071 
   1072     // Return to unit calculator
   1073     await page.getByRole('link', { name: 'Back' }).click();
   1074     await page.getByRole('button', { name: 'Unit Calculator' }).click();
   1075 
   1076     // Assert result is correct (state not reset)
   1077     await expect(page.getByLabel('Output value')).toHaveText('00:09:39.364');
   1078 
   1079     // Return to workout calculator
   1080     await page.getByRole('link', { name: 'Back' }).click();
   1081     await page.getByRole('button', { name: 'Workout Calculator' }).click();
   1082 
   1083     // Assert workout calculator options are correct
   1084     await expect(page.getByLabel('Input race distance value')).toHaveValue('1.00');
   1085     await expect(page.getByLabel('Input race distance unit')).toHaveValue('miles');
   1086     await expect(page.getByLabel('Input race duration hours')).toHaveValue('0');
   1087     await expect(page.getByLabel('Input race duration minutes')).toHaveValue('05');
   1088     await expect(page.getByLabel('Input race duration seconds')).toHaveValue('01.00');
   1089     await page.getByText('Advanced Options').click();
   1090     await expect(page.getByLabel('Default units')).toHaveValue('metric');
   1091     await expect(page.getByLabel('Selected target set')).toHaveValue('_workout_targets');
   1092     await expect(page.getByLabel('Workout name customization')).toHaveValue('false');
   1093     await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
   1094     await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
   1095 
   1096     // Assert workout splits are correct (input race and prediction model not reset)
   1097     await expect(page.getByRole('row').nth(1)).toHaveText('400 m @ 1 mi' + '1:14.81');
   1098     await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '5:44.38');
   1099     await expect(page.getByRole('row')).toHaveCount(5);
   1100   }
   1101 });