diff --git a/browser-extensions/common/js/README.md b/browser-extensions/common/js/README.md index dcc74ffa..e16e8ae2 100644 --- a/browser-extensions/common/js/README.md +++ b/browser-extensions/common/js/README.md @@ -1,31 +1,55 @@ -To run the tests locally you will need a number of Docker containers that are running Selenium in order to run those tests. +# Testing Running Challenges -You can run them all at once: +## Unit testing + +This project uses [Mocha] for unit testing. + +To run the unit tests locally: + +```sh +cd browser-extensions/common/js/tests +npm install +npm run test-with-coverage ``` -docker run --name selenium-firefox -d -p 4444:4444 -p 7900:7900 selenium/standalone-chrome + +## Automated browser testing + +This project uses [Selenium] for automated browser testing. + +To run the tests locally you will need a number of Docker containers running Selenium. You can run them all at once: + +```sh +docker run --name selenium-chrome -d -p 4444:4444 -p 7900:7900 selenium/standalone-chrome docker run --name selenium-firefox -d -p 4445:4444 -p 7901:7900 selenium/standalone-firefox +docker run --name selenium-edge -d -p 4446:4444 -p 7902:7900 selenium/standalone-edge ``` -These "latest" tagged images appear to be the latest in the 3.x line - which I assume is selenium, -rather than any bearing on the browser version -I assume this will work too: -``` -docker run --name selenium-firefox -d -p 4446:4444 -p 7902:7900 selenium/standalone-edge -``` +These "latest" tagged images are the latest in [Selenium Grid releases], not in particular browser versions. -When run in Github Actions we can define the port to be constant, or for it to be dynamically -generated at runtime, and pick it up from a variable. This is probably preferable +When run in the [Extension Builder] workflow in [GitHub Actions], we can define the port to be constant, or for it to be dynamically +generated at runtime, and pick it up from a variable. This is probably preferable. -You need to set the value of EXTENSION_ZIP to point to a built extension, on Windows you do it like: -``` +You need to set the value of `EXTENSION_ZIP` to point to a built extension. On Windows you do it like: + +```dos $Env:EXTENSION_ZIP="C:\\Users\\andre\\Downloads\\running_challenges-chrome-1.1.4.26.zip" ``` -If you want you can set the browser, but it will default to chrome - although perhaps we could derrive this from the zip name?: -``` +If you want you can set the browser, but it will default to chrome - although perhaps we could derive this from the zip name?: + +```dos $Env:EXTENSION_BROWSER="firefox" ``` + The port defaults to 4444, but you can override it with: -``` + +```dos $Env:SELENIUM_PORT="4445" -``` \ No newline at end of file +``` + + +[Extension Builder]: https://github.com/fraz3alpha/running-challenges/actions/workflows/build-extension.yml +[GitHub Actions]: https://github.com/actions +[Mocha]: https://mochajs.org +[Selenium Grid releases]: https://github.com/SeleniumHQ/docker-selenium/releases +[Selenium]: https://www.selenium.dev diff --git a/browser-extensions/common/js/lib/challenges.js b/browser-extensions/common/js/lib/challenges.js index 7c374230..ddb6d5e6 100644 --- a/browser-extensions/common/js/lib/challenges.js +++ b/browser-extensions/common/js/lib/challenges.js @@ -880,49 +880,71 @@ function generate_stat_tourist_quotient(parkrun_results) { } } -// Maximum number of consecutive different parkrun events function generate_stat_longest_tourism_streak(parkrun_results) { - var t_streak = 0 - var t_date = 0 - var t_last = "" - let event_streak = [] - - parkrun_results.forEach(function (parkrun_event, index) { - - // If we get a duplicate parkrun, chop off the start of the streak - // up until the streak becomes unique again. - // - // e.g. - // [1,2,3,4] - going to add [1] - // will chop off the first element with splice(0,1) - // [1,2,3,4,5,6] - going to add [3] - // will chop off the first 3 elements - if (event_streak.includes(parkrun_event.name)) { - - var f = 0 - var filteredElements = event_streak.some(function(item, index) { - f = index; return item == parkrun_event.name - }) + const display_name = "Longest tourism streak" + const help = "The highest number of consecutive different events attended." + var longest_start = 0, longest_finish = 0, start_index = 0, finish_index = 0; + + function longest_streak() { + return longest_finish - longest_start + 1 + } - event_streak.splice(0,f+1) + for (; finish_index < parkrun_results.length; finish_index++) { + previous_visit = visit_to_event_in_streak(start_index, finish_index - 1, parkrun_results[finish_index].name, parkrun_results) + if (previous_visit !== null) { + // If a participant has visited an event multiple times, + // a streak must start after their previous visit. + start_index = previous_visit + 1 } - // Add the new parkrun in - it will be unique in the list as we removed the - // existing entries in the list above. - event_streak.push(parkrun_event.name) - if (event_streak.length >= t_streak) { - t_date = parkrun_event.datelink - t_last = parkrun_event.eventlink - t_streak = event_streak.length + if ((finish_index - start_index + 1) >= longest_streak()) { + // We have a new long streak, store the start and finish indexes. + longest_start = start_index + longest_finish = finish_index } + } - }) - return { - "display_name": "Longest tourism streak", - "help": "The highest number of consecutive different events attended.", - "value": t_streak + " parkruns (achieved " + t_last + " " + t_date + ")" + var value = "No parkruns: Yet to start (let's go!)" + if (parkrun_results.length > 0 && longest_streak() == 1) { + value = `1 parkrun: ${parkrun_results[longest_finish].eventlink} (${parkrun_results[longest_finish].datelink})` + } else if (longest_streak() > 1) { + value = `${longest_streak()} parkruns: ${parkrun_results[longest_start].eventlink} (${parkrun_results[longest_start].datelink}) - ${parkrun_results[longest_finish].eventlink} (${parkrun_results[longest_finish].datelink})` + } + return { display_name, help, value } +} + +function generate_stat_current_tourism_streak(parkrun_results) { + const display_name = "Current tourism streak" + const help = "The number of consecutive different events attended up until the most recent event." + + var finish_index = parkrun_results.length - 1 + var start_index = finish_index + + for (;start_index > 0; start_index--) { + if (visit_to_event_in_streak(start_index, finish_index, parkrun_results[start_index - 1].name, parkrun_results) !== null) { + break + } + } + + const streak = finish_index - start_index + 1 + var value = "No parkruns: Yet to start (let's go!)" + if (parkrun_results.length > 0 && streak == 1) { + value = `1 parkrun: ${parkrun_results[start_index].eventlink} (${parkrun_results[start_index].datelink})` + } else if (streak > 1) { + value = `${streak} parkruns: ${parkrun_results[start_index].eventlink} (${parkrun_results[start_index].datelink}) - ${parkrun_results[finish_index].eventlink} (${parkrun_results[finish_index].datelink})` + } + + return { display_name, help, value } +} + +function visit_to_event_in_streak(start, finish, event_name, parkrun_results) { + for (var i = start; i <= finish; ++i) { + if (parkrun_results[i].name === event_name) { + return i + } } + return null } function generate_stat_runs_this_year(parkrun_results) { @@ -1266,7 +1288,8 @@ function generate_stats(data) { stats['years_parkrunning'] = generate_stat_years_parkrunning(data.parkrun_results) stats['events_run'] = generate_stat_events_run(data.parkrun_results) stats['tourist_quotient'] = generate_stat_tourist_quotient(data.parkrun_results) - stats['tourism_streak'] = generate_stat_longest_tourism_streak(data.parkrun_results) + stats['longest_tourism_streak'] = generate_stat_longest_tourism_streak(data.parkrun_results) + stats['current_tourism_streak'] = generate_stat_current_tourism_streak(data.parkrun_results) } // Stats that need a list of parkruns, and additional geo data to determine where they are diff --git a/browser-extensions/common/js/tests/test/test_challenges.js b/browser-extensions/common/js/tests/test/test_challenges.js index 4280a480..6eef6a2a 100644 --- a/browser-extensions/common/js/tests/test/test_challenges.js +++ b/browser-extensions/common/js/tests/test/test_challenges.js @@ -136,7 +136,7 @@ function getNextParkrunDate() { } function createParkrunResult(specificData) { - parkrunResult = { + return { name: "Fell Foot", eventlink: "Fell Foot", date: "22/11/2014", @@ -145,13 +145,9 @@ function createParkrunResult(specificData) { event_number: "6", position: "44", time: "26:19", - pb: false - } - // If we have been given a name, find it in the geodata - if (specificData.name !== undefined) { - parkrunResult.name = specificData.name + pb: false, + ...specificData } - return parkrunResult } var geoData = getGeoData() @@ -312,6 +308,173 @@ describe("challenges.js", function() { }) + describe("generate_stat_longest_tourism_streak", () => { + const generate_stat_longest_tourism_streak = challenges.__get__('generate_stat_longest_tourism_streak'); + + describe("given no results", () => { + const parkrunResults = []; + it("is expected to be zero", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "No parkruns: Yet to start (let's go!)") + }) + }) + + describe("given one result", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }) + ]; + it("is expected to be one", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "1 parkrun: eventa (date1)") + }) + }) + + describe("given two identical results", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "A", datelink: "date2", eventlink: "eventa" }) + ]; + it("is expected to be the latest one", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "1 parkrun: eventa (date2)") + }) + }) + + describe("given two different results", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }) + ]; + it("is expected to be two", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "2 parkruns: eventa (date1) - eventb (date2)") + }) + }) + + describe("a third result the same as the first", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }), + createParkrunResult({ name: "A", datelink: "date3", eventlink: "eventa" }) + ]; + it("is expected to remove the first event from the streak", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "2 parkruns: eventb (date2) - eventa (date3)") + }) + }) + + describe("a third result the same as the second", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }), + createParkrunResult({ name: "B", datelink: "date3", eventlink: "eventb" }) + ]; + it("is expected to end the streak", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "2 parkruns: eventa (date1) - eventb (date2)") + }) + }) + + describe("a third unique result", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }), + createParkrunResult({ name: "C", datelink: "date3", eventlink: "eventc" }) + ]; + it("is expected to extend the streak", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "3 parkruns: eventa (date1) - eventc (date3)") + }) + }) + + describe("two identical streaks", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }), + createParkrunResult({ name: "A", datelink: "date3", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date4", eventlink: "eventb" }), + ]; + it("is expected to be the second streak", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "2 parkruns: eventa (date3) - eventb (date4)") + }) + }) + + describe("overlapping streaks (ABC, BCAD, ADCE)", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }), + createParkrunResult({ name: "C", datelink: "date3", eventlink: "eventc" }), + createParkrunResult({ name: "A", datelink: "date4", eventlink: "eventa" }), + createParkrunResult({ name: "D", datelink: "date5", eventlink: "eventd" }), + createParkrunResult({ name: "C", datelink: "date6", eventlink: "eventc" }), + createParkrunResult({ name: "E", datelink: "date7", eventlink: "evente" }) + ]; + it("is expected to be the latest, longest streak", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "4 parkruns: eventa (date4) - evente (date7)") + }) + }) + + describe("A long streak followed by a shorter one (ABCD, DC)", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventA" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventB" }), + createParkrunResult({ name: "C", datelink: "date3", eventlink: "eventC" }), + createParkrunResult({ name: "D", datelink: "date4", eventlink: "eventD" }), + createParkrunResult({ name: "C", datelink: "date5", eventlink: "eventC" }) + ]; + + it("is expected to be the longer streak", () => { + const r = generate_stat_longest_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "4 parkruns: eventA (date1) - eventD (date4)") + }) + }) + }) + + describe("generate_stat_current_tourism_streak", () => { + const generate_stat_current_tourism_streak = challenges.__get__('generate_stat_current_tourism_streak'); + + describe("given no results", () => { + const parkrunResults = []; + it("is expected to be zero", () => { + const r = generate_stat_current_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "No parkruns: Yet to start (let's go!)") + }) + }) + + describe("given one result", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }) + ]; + it("is expected to be one", () => { + const r = generate_stat_current_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "1 parkrun: eventa (date1)") + }) + }) + + describe("given two identical results", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "A", datelink: "date2", eventlink: "eventa" }) + ]; + it("is expected to be the latest one", () => { + const r = generate_stat_current_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "1 parkrun: eventa (date2)") + }) + }) + + describe("given two different results", () => { + const parkrunResults = [ + createParkrunResult({ name: "A", datelink: "date1", eventlink: "eventa" }), + createParkrunResult({ name: "B", datelink: "date2", eventlink: "eventb" }) + ]; + it("is expected to be two", () => { + const r = generate_stat_current_tourism_streak(parkrunResults) + assert.strictEqual(r.value, "2 parkruns: eventa (date1) - eventb (date2)") + }) + }) + }) }) describe("challenges", function() { diff --git a/build/extension-firefox/build.sh b/build/extension-firefox/build.sh index d20011d4..6eb9a5df 100755 --- a/build/extension-firefox/build.sh +++ b/build/extension-firefox/build.sh @@ -1,5 +1,9 @@ #!/bin/bash -xe +if ! type -p web-ext &>/dev/null; then + npm install --global web-ext +fi + # Set up version variables source build/version.sh # Set up tools variables diff --git a/website/_data/stats.yml b/website/_data/stats.yml index 616c54f4..2f93d109 100644 --- a/website/_data/stats.yml +++ b/website/_data/stats.yml @@ -74,12 +74,18 @@ stats: If you never repeat a parkrun event it will be 100%, if you never tourist at all it will tend towards 0%. - - shortname: tourism_streak - name: Tourism streak + - shortname: longest_tourism_streak + name: Longest tourism streak description: >- The longest unbroken run of different events visited. If this happens to include your 'home' parkrun, this still counts in the calculation. + - shortname: current_tourism_streak + name: Current tourism streak + description: >- + The current unbroken run of different events visited. Useful for those + looking to improve on their Longest tourism streak. + - shortname: parkruns_this_year name: parkruns this year description: >-