f33415df6a
test_measure.py non ricarica piu' config/blueprints.measure (monkeypatch.setattr, annullato al teardown); TestTaskComplete e test_recipe_rules_ui.py fissano la stazione esplicitamente con una fixture autouse. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
227 lines
8.4 KiB
Python
227 lines
8.4 KiB
Python
"""The screens that carry the rules of a recipe (points 8, 9 and 11).
|
|
|
|
The server refuses what the recipe forbids; these tests are about the operator
|
|
being told before the refusal rather than after it - a keypad that is not there,
|
|
a start button that will not start, a description that reads the way it was
|
|
written.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_station(monkeypatch):
|
|
# These tests assume no station is configured (e.g. _task_list feeds the mock a
|
|
# fixed number of replies, with no /production-runs/current among them); pin it
|
|
# so a STATION_CODE left behind by an earlier test cannot change the call count (#79).
|
|
# Tests that need a station set it themselves, after this fixture has run.
|
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Point 11 - line breaks and bold, and nothing else
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _render(flask_app, template_string, **context):
|
|
with flask_app.test_request_context():
|
|
return flask_app.jinja_env.from_string(template_string).render(**context)
|
|
|
|
|
|
def test_rich_text_keeps_line_breaks(flask_app):
|
|
out = _render(flask_app, "{{ v|rich_text }}", v="prima\nseconda")
|
|
assert out == "prima<br>seconda"
|
|
|
|
|
|
def test_rich_text_makes_bold(flask_app):
|
|
out = _render(flask_app, "{{ v|rich_text }}", v="quota **critica** qui")
|
|
assert out == "quota <strong>critica</strong> qui"
|
|
|
|
|
|
def test_rich_text_escapes_everything_else(flask_app):
|
|
"""No HTML is accepted, so there is nothing to sanitise away later."""
|
|
out = _render(flask_app, "{{ v|rich_text }}", v="<script>alert(1)</script>")
|
|
assert "<script>" not in out
|
|
assert "<script>" in out
|
|
|
|
|
|
def test_rich_text_cannot_be_tricked_into_tags(flask_app):
|
|
"""Bold markers around a tag must not reassemble it."""
|
|
out = _render(flask_app, "{{ v|rich_text }}", v="**<b onclick='x'>ciao</b>**")
|
|
assert "onclick" not in out or "'" in out
|
|
assert "<b " not in out
|
|
assert out.startswith("<strong><b")
|
|
|
|
|
|
def test_rich_text_of_nothing_is_nothing(flask_app):
|
|
assert _render(flask_app, "{{ v|rich_text }}", v=None) == ""
|
|
assert _render(flask_app, "{{ v|rich_text }}", v="") == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Point 9 - the keypad is not offered when the recipe forbids typing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
TASK = {
|
|
"id": 11,
|
|
"recipe_id": 3,
|
|
"version_id": 5,
|
|
"task_type": "measure",
|
|
"title": "Quota",
|
|
"directive": None,
|
|
"description": "prima riga\n**grassetto**",
|
|
"file_path": None,
|
|
"file_type": None,
|
|
"annotations_json": None,
|
|
"subtasks": [{
|
|
"id": 21, "marker_number": 1, "description": "Altezza", "order_index": 0,
|
|
"nominal": 10.0, "utl": 10.5, "uwl": 10.2, "lwl": 9.8, "ltl": 9.5,
|
|
"unit": "mm",
|
|
}],
|
|
}
|
|
|
|
|
|
def _execute_page(client, mock_api_client, monkeypatch, allow_manual):
|
|
# task_execute() only renders a recipe assigned to this station (#24).
|
|
# STATION_CODE defaults to unset in the test env, so it has to be forced.
|
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
|
mock_api_client.get_station_recipes.return_value = [{"id": 3}]
|
|
mock_api_client.get.side_effect = [
|
|
TASK,
|
|
[{"id": 11, "order_index": 0}],
|
|
{
|
|
"id": 3, "measurement_interval_minutes": 30,
|
|
"allow_manual_input": allow_manual,
|
|
},
|
|
]
|
|
resp = client.get("/measure/execute/11")
|
|
assert resp.status_code == 200
|
|
return resp.get_data(as_text=True)
|
|
|
|
|
|
def test_keypad_digits_are_absent_when_typing_is_forbidden(
|
|
logged_in_client, mock_api_client, monkeypatch,
|
|
):
|
|
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, allow_manual=False)
|
|
assert "allowManual: false" in html
|
|
assert "addDigit('7')" not in html, "i tasti numerici non devono essere in pagina"
|
|
# The corrections and the confirm stay: a wrong caliper reading has to be
|
|
# clearable, and the value still has to be confirmable.
|
|
assert "clearAll()" in html
|
|
assert "confirm()" in html
|
|
|
|
|
|
def test_keypad_is_there_when_the_recipe_allows_typing(
|
|
logged_in_client, mock_api_client, monkeypatch,
|
|
):
|
|
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, allow_manual=True)
|
|
assert "allowManual: true" in html
|
|
assert "addDigit('7')" in html
|
|
|
|
|
|
def test_keypad_stays_when_the_recipe_cannot_be_read(
|
|
logged_in_client, mock_api_client, monkeypatch,
|
|
):
|
|
"""A recipe that failed to load must not silently take the keypad away."""
|
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
|
mock_api_client.get_station_recipes.return_value = [{"id": 3}]
|
|
mock_api_client.get.side_effect = [
|
|
TASK,
|
|
[{"id": 11, "order_index": 0}],
|
|
{"error": True, "detail": "boom"},
|
|
]
|
|
resp = logged_in_client.get("/measure/execute/11")
|
|
assert resp.status_code == 200
|
|
assert "allowManual: true" in resp.get_data(as_text=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Point 11 on the measurement screen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_operator_sees_the_description_formatted(
|
|
logged_in_client, mock_api_client, monkeypatch,
|
|
):
|
|
"""It was written for the operator, and until now only the maker saw it."""
|
|
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, allow_manual=True)
|
|
assert "prima riga<br><strong>grassetto</strong>" in html
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Point 8 - the start control knows what the recipe demands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _task_list(client, mock_api_client, recipe, session_lot=None):
|
|
# The fixture session is English; these assertions read the Italian source
|
|
# strings, so the locale is pinned rather than assumed.
|
|
with client.session_transaction() as sess:
|
|
sess["language"] = "it"
|
|
if session_lot is not None:
|
|
sess["lot_number"] = session_lot
|
|
mock_api_client.get.side_effect = [
|
|
recipe,
|
|
[{"id": 11, "title": "Task", "order_index": 0, "task_type": "measure"}],
|
|
]
|
|
resp = client.get("/measure/tasks/3")
|
|
assert resp.status_code == 200
|
|
return resp.get_data(as_text=True)
|
|
|
|
|
|
def test_avvia_does_not_start_without_the_required_lot(
|
|
logged_in_client, mock_api_client,
|
|
):
|
|
"""This page is reachable directly, so the rule has to hold here too."""
|
|
html = _task_list(
|
|
logged_in_client, mock_api_client,
|
|
{"id": 3, "code": "REC-1", "name": "Con lotto", "requires_lot": True},
|
|
)
|
|
assert "Compila prima" in html
|
|
# The way in is closed, not merely greyed: it leads back to where the lot is
|
|
# asked for. (The per-task links below belong to the Maker's own view.)
|
|
assert "/measure/select?recipe=REC-1" in html
|
|
|
|
|
|
def test_avvia_starts_once_the_lot_is_there(logged_in_client, mock_api_client):
|
|
html = _task_list(
|
|
logged_in_client, mock_api_client,
|
|
{"id": 3, "code": "REC-1", "name": "Con lotto", "requires_lot": True},
|
|
session_lot="LOT-1",
|
|
)
|
|
assert "Compila prima" not in html
|
|
assert "/measure/select?recipe=REC-1" not in html
|
|
|
|
|
|
def test_avvia_is_untouched_when_nothing_is_required(
|
|
logged_in_client, mock_api_client,
|
|
):
|
|
html = _task_list(
|
|
logged_in_client, mock_api_client,
|
|
{"id": 3, "code": "REC-1", "name": "Libera"},
|
|
)
|
|
assert "Compila prima" not in html
|
|
assert "/measure/select?recipe=REC-1" not in html
|
|
|
|
|
|
def test_selection_screen_carries_the_traceability_rules(
|
|
logged_in_client, mock_api_client, monkeypatch,
|
|
):
|
|
# Patch instead of reload()ing config/blueprints.measure: the patch is undone at
|
|
# teardown, a reload would leave ST-TEST behind for later tests (#79).
|
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
|
|
|
mock_api_client.get_station_recipes.return_value = [
|
|
{
|
|
"id": 1, "code": "REC-001", "name": "Con lotto",
|
|
"requires_lot": True, "requires_serial": False,
|
|
},
|
|
]
|
|
resp = logged_in_client.get("/measure/select")
|
|
assert resp.status_code == 200
|
|
html = resp.get_data(as_text=True)
|
|
assert "missingTraceability" in html
|
|
# The rule travels with the recipe data the cards are built from.
|
|
assert "requires_lot" in html
|