Per-type board rows with shared categories, a network device IP field, and two dark-mode dialogs
This commit is contained in:
117
tests/test_plugins/test_board_category.py
Normal file
117
tests/test_plugins/test_board_category.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Tests for notificationtypes.boardcategory (shared row on the shopfloor board).
|
||||
|
||||
The board grouped cards by display style alone, so every type set to grid landed
|
||||
in the Recertification row and every carousel type in Recognition's, under a
|
||||
heading naming someone else's type. Each type now gets a row of its own, and
|
||||
this column is the opt-in for the other direction: Change, Awareness and
|
||||
Incident share one row under a category name.
|
||||
|
||||
The grouping itself is frontend (ShopfloorDashboard.vue); what is pinned here is
|
||||
the contract it groups on - the feed must carry boardcategory per card, and the
|
||||
type API must round-trip it.
|
||||
"""
|
||||
|
||||
from plugins.notifications.models import Notification, NotificationType
|
||||
|
||||
|
||||
def _make_type(db, typename, displaystyle='grid', boardcategory=None):
|
||||
t = NotificationType(typename=typename, typecolor='#17a2b8', isactive=True,
|
||||
displaystyle=displaystyle, boardcategory=boardcategory)
|
||||
db.session.add(t)
|
||||
db.session.commit()
|
||||
return t
|
||||
|
||||
|
||||
def _make_note(db, ntype, text):
|
||||
n = Notification(notificationtypeid=ntype.notificationtypeid,
|
||||
notification=text, businessunitid=None,
|
||||
isactive=True, isshopfloor=True)
|
||||
db.session.add(n)
|
||||
db.session.commit()
|
||||
return n
|
||||
|
||||
|
||||
def _current(client):
|
||||
resp = client.get('/api/notifications/shopfloor')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
return resp.get_json()['data']['current']
|
||||
|
||||
|
||||
def test_feed_carries_the_category_per_card(client, db):
|
||||
"""The dashboard groups on this field, so it has to reach the card."""
|
||||
ntype = _make_type(db, 'Awareness', boardcategory='Alerts')
|
||||
_make_note(db, ntype, 'watch your step')
|
||||
|
||||
card = _current(client)[0]
|
||||
assert card['boardcategory'] == 'Alerts'
|
||||
assert card['typename'] == 'Awareness'
|
||||
assert card['displaystyle'] == 'grid'
|
||||
|
||||
|
||||
def test_uncategorised_type_reports_an_empty_category(client, db):
|
||||
"""Blank, not null: the dashboard falls back to the type name for a row."""
|
||||
ntype = _make_type(db, 'Recertification')
|
||||
_make_note(db, ntype, 'forklift cert due')
|
||||
|
||||
assert _current(client)[0]['boardcategory'] == ''
|
||||
|
||||
|
||||
def test_types_sharing_a_category_keep_their_own_names(client, db):
|
||||
"""Grouping is the dashboard's job; the feed must not flatten the types."""
|
||||
change = _make_type(db, 'Change', boardcategory='Alerts')
|
||||
incident = _make_type(db, 'Incident', boardcategory='Alerts')
|
||||
_make_note(db, change, 'line 3 retooling')
|
||||
_make_note(db, incident, 'press 12 down')
|
||||
|
||||
cards = _current(client)
|
||||
assert {c['typename'] for c in cards} == {'Change', 'Incident'}
|
||||
assert {c['boardcategory'] for c in cards} == {'Alerts'}
|
||||
|
||||
|
||||
def test_api_round_trips_the_category(client, db, auth_headers):
|
||||
create = client.post('/api/notifications/types',
|
||||
json={'typename': 'Awareness', 'displaystyle': 'grid',
|
||||
'boardcategory': 'Alerts'},
|
||||
headers=auth_headers)
|
||||
assert create.status_code in (200, 201), create.get_json()
|
||||
typeid = create.get_json()['data']['notificationtypeid']
|
||||
assert create.get_json()['data']['boardcategory'] == 'Alerts'
|
||||
|
||||
listed = client.get('/api/notifications/types', headers=auth_headers)
|
||||
row = next(t for t in listed.get_json()['data']
|
||||
if t['notificationtypeid'] == typeid)
|
||||
assert row['boardcategory'] == 'Alerts'
|
||||
|
||||
|
||||
def test_blank_category_stores_as_null_not_an_empty_group(client, db, auth_headers):
|
||||
"""Every uncategorised type sharing the empty string would be one big row."""
|
||||
create = client.post('/api/notifications/types',
|
||||
json={'typename': 'Solo', 'boardcategory': ' '},
|
||||
headers=auth_headers)
|
||||
assert create.status_code in (200, 201), create.get_json()
|
||||
typeid = create.get_json()['data']['notificationtypeid']
|
||||
|
||||
stored = db.session.get(NotificationType, typeid)
|
||||
assert stored.boardcategory is None
|
||||
assert create.get_json()['data']['boardcategory'] == ''
|
||||
|
||||
|
||||
def test_api_rejects_an_overlong_category(client, db, auth_headers):
|
||||
resp = client.post('/api/notifications/types',
|
||||
json={'typename': 'Bad', 'boardcategory': 'x' * 51},
|
||||
headers=auth_headers)
|
||||
|
||||
assert resp.status_code == 400
|
||||
assert 'boardcategory' in resp.get_data(as_text=True)
|
||||
|
||||
|
||||
def test_category_change_moves_the_config_version(client, db, auth_headers):
|
||||
"""Open kiosks reload on a layout change; a re-grouped board is one."""
|
||||
ntype = _make_type(db, 'Awareness')
|
||||
before = client.get('/api/notifications/shopfloor').get_json()['data']['configversion']
|
||||
|
||||
client.put(f'/api/notifications/types/{ntype.notificationtypeid}',
|
||||
json={'boardcategory': 'Alerts'}, headers=auth_headers)
|
||||
|
||||
after = client.get('/api/notifications/shopfloor').get_json()['data']['configversion']
|
||||
assert before != after
|
||||
@@ -57,6 +57,39 @@ def test_update_network_device_ipaddress_upserts(client, db, auth_headers, netwo
|
||||
assert len(comms) == 1 and comms[0].ipaddress == '10.0.0.9'
|
||||
|
||||
|
||||
def test_clearing_the_ip_takes_it_back_out(client, db, auth_headers, network_setup):
|
||||
"""Blanking the form field must clear the stored IP.
|
||||
|
||||
The upsert helper returned early on a blank value, so an address could be
|
||||
typed and never removed - invisible until the form grew an IP field.
|
||||
"""
|
||||
from shopdb.core.models import Communication
|
||||
created = client.post('/api/network', json={
|
||||
'assetnumber': 'SW-09', 'name': 'Switch', 'ipaddress': '10.0.0.5',
|
||||
}, headers=auth_headers).get_json()['data']
|
||||
devid = created['networkdevice']['networkdeviceid']
|
||||
|
||||
cleared = client.put(f'/api/network/{devid}', json={'ipaddress': ''},
|
||||
headers=auth_headers)
|
||||
assert cleared.status_code == 200, cleared.get_json()
|
||||
assert not cleared.get_json()['data']['ipaddress']
|
||||
|
||||
comms = Communication.query.filter_by(assetid=created['assetid']).all()
|
||||
assert len(comms) == 1 and comms[0].ipaddress is None
|
||||
|
||||
|
||||
def test_omitting_ipaddress_leaves_it_alone(client, db, auth_headers, network_setup):
|
||||
"""A PUT that never mentions the field must not wipe the stored IP."""
|
||||
created = client.post('/api/network', json={
|
||||
'assetnumber': 'SW-10', 'name': 'Switch', 'ipaddress': '10.0.0.7',
|
||||
}, headers=auth_headers).get_json()['data']
|
||||
devid = created['networkdevice']['networkdeviceid']
|
||||
|
||||
updated = client.put(f'/api/network/{devid}', json={'hostname': 'sw-10'},
|
||||
headers=auth_headers)
|
||||
assert updated.get_json()['data']['ipaddress'] == '10.0.0.7'
|
||||
|
||||
|
||||
def test_seed_reference_data_creates_ip_commtype(app, db):
|
||||
"""The reference-data seed now creates the IP communication type the asset
|
||||
import depends on."""
|
||||
|
||||
Reference in New Issue
Block a user