tests/test_holdings_add_in_the_browser.py
Rama main · réplica de lectura del repositorio original.
"""Manage holdings' "+ Add an instrument", driven in a real browser.
Its lookup kept the first answer it gave: a NASDAQ ticker typed before its
exchange was changed stayed on the ASX guess, CODE.AX, which has no prices.
The lookup's own answers now give way to the next one, and typed text never
does. The class comes from the answer too, as it does when recording a trade.
"""
from __future__ import annotations
import json
import re
import subprocess
import pytest
from test_trade_dialog_in_the_browser import CHROME, HTML, _standalone
import factories as fac
from test_routes import bind_to_only_portfolio, make_login
pytestmark = pytest.mark.visual
BY_EXCHANGE = """
<script>
window.fetch = function (url) {
url = String(url);
if (url.indexOf("/holdings/lookup") !== 0) { return Promise.reject(new Error("offline")); }
var answer = url.indexOf("exchange=NASDAQ") > 0
? {found: true, name: "Alphabet Inc.", currency: "USD", symbol: "GOOG", asset_class: "share"}
: {found: false, name: "", currency: "", symbol: "GOOG.AX", asset_class: null};
return Promise.resolve({ok: true, json: function () { return Promise.resolve(answer); }});
};
</script>
"""
DRIVER = """
<script>
window.addEventListener("load", async function () {
var pause = function () { return new Promise(function (r) { setTimeout(r, 60); }); };
var form = document.querySelector('form[action="/holdings/add"]');
var field = function (name) { return form.querySelector('[name="' + name + '"]'); };
var change = function (name) { field(name).dispatchEvent(new Event("change", {bubbles: true})); };
var out = {};
document.getElementById("addinst-open").click(); await pause();
field("ticker").value = "GOOG"; change("ticker"); await pause();
out.onTheAsx = field("yahoo_symbol").value;
field("exchange").value = "NASDAQ"; change("exchange"); await pause();
out.onNasdaq = {symbol: field("yahoo_symbol").value, name: field("name").value,
currency: field("currency").value, cls: field("asset_class").value};
field("yahoo_symbol").value = "GOOGL";
field("asset_class").value = "etf"; change("asset_class");
change("exchange"); await pause(); // NASDAQ again: an answer with a class
out.typedKept = {symbol: field("yahoo_symbol").value, cls: field("asset_class").value};
var pre = document.createElement("pre");
pre.id = "result";
pre.textContent = JSON.stringify(out);
document.body.appendChild(pre);
});
</script>
"""
def test_changing_the_exchange_looks_the_ticker_up_again(client, session_factory, tmp_path):
make_login(client, session_factory)
page = client.get("/holdings", headers=HTML)
assert page.status_code == 200
target = tmp_path / "holdings.html"
target.write_text(_standalone(page.text, DRIVER).replace("<head>", "<head>" + BY_EXCHANGE, 1), encoding="utf-8")
dom = subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox",
"--allow-file-access-from-files", "--window-size=1400,1000",
"--virtual-time-budget=8000", target.as_uri()],
capture_output=True, text=True, timeout=90).stdout
found = re.search(r'<pre id="result">(.*?)</pre>', dom, re.S)
assert found, "the driver did not finish"
out = json.loads(found.group(1).replace(""", '"'))
assert out["onTheAsx"] == "GOOG.AX"
assert out["onNasdaq"] == {"symbol": "GOOG", "name": "Alphabet Inc.", "currency": "USD",
"cls": "share"}
assert out["typedKept"] == {"symbol": "GOOGL", "cls": "etf"}
# The ASX answer is slow and the NASDAQ one quick, so the ASX one lands last.
LATE_ASX = """
<script>
window.fetch = function (url) {
url = String(url);
var nasdaq = url.indexOf("exchange=NASDAQ") > 0;
var answer = nasdaq
? {found: true, name: "Alphabet Inc.", currency: "USD", symbol: "GOOG", asset_class: "share"}
: {found: false, name: null, currency: null, symbol: "GOOG.AX", asset_class: null};
return new Promise(function (resolve) {
setTimeout(function () {
resolve({ok: true, json: function () { return Promise.resolve(answer); }});
}, nasdaq ? 5 : 200);
});
};
</script>
"""
RACE = """
<script>
window.addEventListener("load", async function () {
var sleep = function (ms) { return new Promise(function (r) { setTimeout(r, ms); }); };
var form = document.querySelector('form[action="/holdings/add"]');
var field = function (name) { return form.querySelector('[name="' + name + '"]'); };
var change = function (name) { field(name).dispatchEvent(new Event("change", {bubbles: true})); };
document.getElementById("addinst-open").click(); await sleep(20);
field("ticker").value = "GOOG"; change("ticker");
field("exchange").value = "NASDAQ"; change("exchange");
await sleep(400);
var pre = document.createElement("pre");
pre.id = "result";
pre.textContent = JSON.stringify({symbol: field("yahoo_symbol").value,
name: field("name").value,
currency: field("currency").value});
document.body.appendChild(pre);
});
</script>
"""
def test_an_answer_that_arrives_late_does_not_overwrite_a_newer_one(
client, session_factory, tmp_path):
make_login(client, session_factory)
page = client.get("/holdings", headers=HTML)
target = tmp_path / "holdings.html"
target.write_text(_standalone(page.text, RACE).replace("<head>", "<head>" + LATE_ASX, 1), encoding="utf-8")
dom = subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox",
"--allow-file-access-from-files", "--window-size=1400,1000",
"--virtual-time-budget=8000", target.as_uri()],
capture_output=True, text=True, timeout=90).stdout
found = re.search(r'<pre id="result">(.*?)</pre>', dom, re.S)
assert found, "the driver did not finish"
assert json.loads(found.group(1).replace(""", '"')) == {
"symbol": "GOOG", "name": "Alphabet Inc.", "currency": "USD"}
# The page's own rows have fields named like the dialog's: name, currency,
# yahoo_symbol. A lookup that searched the whole page filled the first row's.
ROWS = """
<script>
window.addEventListener("load", async function () {
var pause = function () { return new Promise(function (r) { setTimeout(r, 60); }); };
var form = document.querySelector('form[action="/holdings/add"]');
var field = function (name) { return form.querySelector('[name="' + name + '"]'); };
var row = function (name) { return document.querySelector('[name="' + name + '"][form^="pref-"]'); };
var change = function (name) { field(name).dispatchEvent(new Event("change", {bubbles: true})); };
var read = function (of) {
return {name: of("name").value, currency: of("currency").value, symbol: of("yahoo_symbol").value};
};
var before = read(row);
document.getElementById("addinst-open").click(); await pause();
field("ticker").value = "GOOG"; change("ticker"); await pause();
field("exchange").value = "NASDAQ"; change("exchange"); await pause();
var pre = document.createElement("pre");
pre.id = "result";
pre.textContent = JSON.stringify({rowBefore: before, rowAfter: read(row), dialog: read(field)});
document.body.appendChild(pre);
});
</script>
"""
def test_the_lookup_fills_the_dialog_and_leaves_the_rows_alone(client, session_factory,
tmp_path):
make_login(client, session_factory)
with session_factory() as s:
bind_to_only_portfolio(s)
fac.hold(s, fac.make_instrument(s, "ACME", name="Acme")) # this portfolio's alone
s.commit()
page = client.get("/holdings", headers=HTML)
target = tmp_path / "holdings.html"
target.write_text(_standalone(page.text, ROWS).replace("<head>", "<head>" + BY_EXCHANGE, 1),
encoding="utf-8")
dom = subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox",
"--allow-file-access-from-files", "--window-size=1400,1000",
"--virtual-time-budget=8000", target.as_uri()],
capture_output=True, text=True, timeout=90).stdout
found = re.search(r'<pre id="result">(.*?)</pre>', dom, re.S)
assert found, "the driver did not finish"
out = json.loads(found.group(1).replace(""", '"'))
assert out["rowBefore"]["name"] == "Acme"
assert out["rowAfter"] == out["rowBefore"]
assert out["dialog"] == {"name": "Alphabet Inc.", "currency": "USD", "symbol": "GOOG"}
FEED_OFF = """
<script>
window.fetch = function () {
var answer = {found: false, off: true, name: null, currency: null, symbol: "GOOG",
asset_class: null};
return Promise.resolve({ok: true, json: function () { return Promise.resolve(answer); }});
};
</script>
"""
SAYS = """
<script>
window.addEventListener("load", async function () {
var form = document.querySelector('form[action="/holdings/add"]');
var ticker = form.querySelector('[name="ticker"]');
document.getElementById("addinst-open").click();
ticker.value = "GOOG"; ticker.dispatchEvent(new Event("change", {bubbles: true}));
await new Promise(function (r) { setTimeout(r, 60); });
var pre = document.createElement("pre");
pre.id = "result";
pre.textContent = JSON.stringify({status: document.getElementById("lookup-status").textContent});
document.body.appendChild(pre);
});
</script>
"""
def test_a_lookup_with_the_feed_off_says_so(client, session_factory, tmp_path):
"""Not "no match for GOOG", which sends somebody to check a ticker that
is fine."""
make_login(client, session_factory)
page = client.get("/holdings", headers=HTML)
target = tmp_path / "holdings.html"
target.write_text(_standalone(page.text, SAYS).replace("<head>", "<head>" + FEED_OFF, 1),
encoding="utf-8")
dom = subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox",
"--allow-file-access-from-files", "--virtual-time-budget=4000", target.as_uri()],
capture_output=True, text=True, timeout=90).stdout
found = re.search(r'<pre id="result">(.*?)</pre>', dom, re.S)
assert found, "the driver did not finish"
assert json.loads(found.group(1).replace(""", '"')) == {
"status": "not looked up: the price feed is off"}