summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_downloads.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_downloads.py')
-rw-r--r--packages/meshbay-hub/tests/test_downloads.py97
1 files changed, 97 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_downloads.py b/packages/meshbay-hub/tests/test_downloads.py
new file mode 100644
index 0000000..cc4fdd1
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_downloads.py
@@ -0,0 +1,97 @@
+"""
+Where downloads are written.
+
+The module is mostly browser plumbing — a directory handle from a picker, kept
+in IndexedDB — but two pieces decide behaviour and can be checked here: that
+automatic is the default, and that writing into the same folder repeatedly does
+not quietly replace what is already there. The second one is the whole risk of
+the automatic mode: a Save As dialog warns you about a collision, and a folder
+you never look at does not.
+"""
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+DOWNLOADS = STATIC / "downloads.js"
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("node") is None or not DOWNLOADS.exists(),
+ reason="node or the SPA sources are not available")
+
+
+def _run(body, tmp_path):
+ module = tmp_path / "downloads.mjs"
+ module.write_text(DOWNLOADS.read_text())
+ script = tmp_path / "case.mjs"
+ script.write_text(
+ # A localStorage good enough for a preference, so the module can be
+ # imported outside a browser at all.
+ "const store = new Map();\n"
+ "globalThis.localStorage = {\n"
+ " getItem: k => (store.has(k) ? store.get(k) : null),\n"
+ " setItem: (k, v) => store.set(k, String(v)),\n"
+ "};\n"
+ f"const M = await import('{module.as_posix()}');\n"
+ "const out = [];\n"
+ "const say = (...a) => out.push(...a);\n"
+ f"{body}\n"
+ "console.log(JSON.stringify(out));\n")
+ proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
+ assert proc.returncode == 0, proc.stderr
+ return json.loads(proc.stdout)
+
+
+def test_saving_automatically_is_the_default(tmp_path):
+ """
+ Grouped downloads are the reason this setting exists: twelve files must not
+ mean twelve dialogs unless someone asked for that.
+ """
+ assert _run("say(M.getMode());", tmp_path) == ["auto"]
+
+
+def test_the_choice_is_remembered_and_nothing_else_is_accepted(tmp_path):
+ result = _run("""
+M.setMode('ask'); say(M.getMode());
+M.setMode('auto'); say(M.getMode());
+M.setMode('nonsense'); say(M.getMode());
+""", tmp_path)
+ assert result == ["ask", "auto", "auto"]
+
+
+def test_a_second_copy_does_not_replace_the_first(tmp_path):
+ result = _run("""
+const taken = new Set(['clip.mp4', 'clip (2).mp4', 'notes']);
+const exists = async n => taken.has(n);
+say(await M.freeName('clip.mp4', exists));
+say(await M.freeName('other.mp4', exists));
+say(await M.freeName('notes', exists));
+say(await M.freeName('archive.tar.gz', exists));
+""", tmp_path)
+ assert result == [
+ "clip (3).mp4", # (2) was taken as well
+ "other.mp4", # free: left alone
+ "notes (2)", # no extension to keep
+ "archive.tar.gz", # free, and the double extension is not mangled
+ ]
+
+
+def test_the_suffix_goes_before_the_extension(tmp_path):
+ """
+ "clip.mp4 (2)" would stop being a video as far as the operating system is
+ concerned, which is how a download folder ends up full of files nothing opens.
+ """
+ result = _run("""
+say(await M.freeName('clip.mp4', async () => false));
+say(await M.freeName('clip.mp4', async n => n === 'clip.mp4'));
+""", tmp_path)
+ assert result == ["clip.mp4", "clip (2).mp4"]
+
+
+def test_a_browser_without_the_api_reports_it(tmp_path):
+ """`SUPPORTED` decides whether Settings offers a choice or an explanation."""
+ assert _run("say(M.SUPPORTED);", tmp_path) == [False]