diff --git a/.github/workflows/app-test.yaml b/.github/workflows/app-test.yaml new file mode 100644 index 0000000..420070f --- /dev/null +++ b/.github/workflows/app-test.yaml @@ -0,0 +1,40 @@ +name: Browser-side app tests + +on: + pull_request: + push: + branches: + - main + +permissions: {} + +jobs: + app-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run the app tests + run: | + set -euo pipefail + dom="$(mktemp)" + python3 -m http.server 8124 --bind 127.0.0.1 & + server_pid=$! + trap 'kill "$server_pid" 2>/dev/null; rm -f "$dom"' EXIT + for _ in $(seq 1 50); do + wget -q --spider http://127.0.0.1:8124/ && break + sleep 0.1 + done + for url in \ + "http://127.0.0.1:8124/app-test/test_links.html" \ + "http://127.0.0.1:8124/app-test/test_links.html?platform=mac" \ + "http://127.0.0.1:8124/app-test/test_achievements_links.html"; do + echo "Running $url" + google-chrome --headless=new --virtual-time-budget=20000 \ + --dump-dom "$url" > "$dom" + grep -q "
/,/<\/pre>/p' "$dom" + exit 1 + } + done diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b89a71f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: trailing-whitespace + - id: mixed-line-ending + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml diff --git a/app-test/test_achievements_links.html b/app-test/test_achievements_links.html new file mode 100644 index 0000000..98f9446 --- /dev/null +++ b/app-test/test_achievements_links.html @@ -0,0 +1,372 @@ + + + + +Frogtab achievements link tests + + + +Frogtab achievements link tests
+Run
+just app-testfrom the repository root. To run the tests without just, serve the repository root and open this page in a browser:python3 -m http.server 8124++ The tests embed
+app/achievements.htmlin the iframe below and drive the real page. + They cover the clickable links in the task list: rendering, escaping, the editing views, + the import flow, and the todo.txt round trip. +The page title shows the result: Achievements link tests: all passed.
+ + + + + diff --git a/app-test/test_links.html b/app-test/test_links.html new file mode 100644 index 0000000..709e05f --- /dev/null +++ b/app-test/test_links.html @@ -0,0 +1,789 @@ + + + + +Frogtab link tests + + + +Frogtab link tests
+Run
+just app-testfrom the repository root. To run the tests without just, serve the repository root and open this page in a browser:python3 -m http.server 8124++ The tests embed
+app/index.htmlin the iframe below and drive the real app. + They cover the link overlay: rendering, re-render triggers, scroll sync, hit-testing, + the hand cursor, and the tooltip. ++ With
+?platform=macin the URL, the tests load the app with +navigator.platformspoofed asMacIntel, so the Mac branches of + the modifier logic run on any OS. CI runs both modes. +The page title shows the result: Link tests: all passed.
+ + + + + diff --git a/app/achievements.html b/app/achievements.html index 44885b2..f97c5a1 100644 --- a/app/achievements.html +++ b/app/achievements.html @@ -9,8 +9,8 @@ - - + +@@ -272,6 +272,55 @@ Achievements
localStorage.setItem("achievements", achievementsCached); } + function trimTrailingPunctuation(url) { + let trimmed = url; + while (trimmed.length > 0) { + const lastChar = trimmed.charAt(trimmed.length - 1); + if (".!,;:?".includes(lastChar)) { + trimmed = trimmed.substring(0, trimmed.length - 1); + } + else if (lastChar == ")" && trimmed.split(")").length > trimmed.split("(").length) { + trimmed = trimmed.substring(0, trimmed.length - 1); + } + else { + break; + } + } + return trimmed; + } + + // Render a task as plain text, with any URLs as clickable links. + // In the editing views the whole task is a move-back control, + // so URLs stay plain text there. + function displayTaskText(domTaskText, taskText) { + if (editingState != "") { + domTaskText.textContent = taskText; + return; + } + let copiedUpTo = 0; + // URL schemes are case-insensitive, so match any case + for (const match of taskText.matchAll(/\bhttps?:\/\/[^\s<>"']+/gi)) { + if (match.index > copiedUpTo) { + domTaskText.appendChild(document.createTextNode( + taskText.substring(copiedUpTo, match.index) + )); + } + const url = trimTrailingPunctuation(match[0]); + const domLink = document.createElement("a"); + domLink.href = url; + domLink.textContent = url; + domLink.target = "_blank"; + domLink.rel = "noopener noreferrer"; + domTaskText.appendChild(domLink); + copiedUpTo = match.index + url.length; + } + if (copiedUpTo < taskText.length) { + domTaskText.appendChild(document.createTextNode( + taskText.substring(copiedUpTo) + )); + } + } + function displayTasks() { const weekdayNameByDate = buildWeekdayNameByDate(); for (let indexDay = 0; indexDay < achievementsParsed.length; indexDay++) { @@ -294,7 +343,7 @@Achievements
const domTaskText = document.createElement("span"); domTaskListItem.appendChild(domTaskText); domTaskText.className = "task-text"; - domTaskText.textContent = day.tasks[indexTask]; + displayTaskText(domTaskText, day.tasks[indexTask]); domTaskText.addEventListener("mouseenter", () => { domTaskListItem.classList.add("inner-hover"); }); diff --git a/app/demo-week.html b/app/demo-week.html index 705e311..527bef8 100644 --- a/app/demo-week.html +++ b/app/demo-week.html @@ -4,7 +4,7 @@Demo of Weekly Routine - +diff --git a/app/demo.html b/app/demo.html index c75add5..0492a67 100644 --- a/app/demo.html +++ b/app/demo.html @@ -4,7 +4,7 @@Demo of Frogtab - +diff --git a/app/help.html b/app/help.html index cfd2309..c4468e7 100644 --- a/app/help.html +++ b/app/help.html @@ -9,8 +9,8 @@ - - + +@@ -61,7 +61,7 @@ Frogtab Help
Overview of Frogtab
- +Today is your to-do list for today. Begin the day by writing down everything you'd like to accomplish. @@ -79,7 +79,7 @@
Overview of Frogtab
Frogtab displays a notification badge to remind you to triage your inbox:
- +At the beginning of each day, Frogtab:
@@ -125,7 +125,7 @@Triaging your inbox
Snooze the task by writing # in front of the task:
- +Frogtab will unsnooze the task at the beginning of the next day.
@@ -161,12 +161,12 @@Setting up recurring tasks
Select a day, then write down your recurring tasks for that day. For example:
- +If you like planning in the evening, write down your recurring tasks under the previous day, with # in front of each task. For example:
- +You'll then be able to see the recurring tasks one day in advance. When Frogtab adds the tasks to your inbox, they'll be snoozed (because of the #), so they won't cause Frogtab to display a notification badge. @@ -230,6 +230,10 @@
Tracking your completed tasks
Click Edit at the top of the Achievements page, select Today or Inbox as the new location for the task, then click the task to move it. ++ Any URLs in your completed tasks are clickable. + While you're editing your completed tasks, URLs are not clickable, so that clicking a task always moves it. +
To make other changes to your completed tasks, click Edit at the top of the Achievements page, then click Export a todo.txt file. You can edit your completed tasks in todo.txt format, then import the modified todo.txt file. @@ -570,6 +574,17 @@
Keyboard shortcuts
Ctrl + B ++ + ++ Open a URL +
++ ⌘ + click + Ctrl + click + +@@ -1213,4 +1228,4 @@
Changing the theme
startApp(); -