aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_downloads.py
blob: cc4fdd17c29212d5a171b5e011e6257017aa9081 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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]