# -*- coding: utf-8 -*- """Host tests for `vibe.pa_handling` -- the Panel Analysis form's Handling section, rendered as generic blocks for the WPF page renderer. Loaded via the sibling `conftest.py` trick, same as `test_panelanalysisform.py` -- plain CPython, no Revit, no pyRevit. Covers the things the module's own docstring promises: only the fixed block vocabulary comes out, "does not apply" and "not filled in" render as different cell shapes, an unticked stage never drops or re-gates a value the user already typed, every field and matrix row carries a plain-English label and a hint, a missing `panelanalysisform` accessor degrades to a `problems` sentence instead of raising, and `values` is never mutated. """ import copy from vibe import knowledge # noqa: F401 (registers the vibe stub) from vibe import panelanalysisform as paf from vibe import pa_handling # The fixed vocabulary `pa_handling.page()` may emit. Anything else is a bug # -- the generic renderer draws an unknown `kind` as nothing, so a typo here # is a silently missing section rather than a crash, which is exactly why # this is asserted rather than assumed. _KNOWN_KINDS = set([ 'heading', 'columns', 'field', 'matrix', 'table', 'legend', 'kv', 'sentence', 'drawing', 'rule', 'space', ]) def _walk(blocks): """Flatten every block, including nested 'columns' children.""" for b in blocks: yield b if b.get('kind') == 'columns': for col in b.get('columns') or []: for sub in _walk(col): yield sub def _matrix_blocks(blocks): return [b for b in _walk(blocks) if b.get('kind') == 'matrix'] def _field_blocks(blocks): return [b for b in _walk(blocks) if b.get('kind') == 'field'] # --------------------------------------------------------------- fixtures def _some_values(): """A plausible in-progress form: a couple of fields filled, most blank.""" values = {} for key in paf.stage_field_keys(): values[key] = '' for f in paf.FIELDS: values[f['key']] = '' values['weight_lb'] = '32000' values[paf.stage_field_key('Stripping', 'include')] = 'yes' values[paf.stage_field_key('Stripping', 'load_multiplier')] = '1.4' values[paf.stage_field_key('Primary Erection', 'rigging')] = 'Auto' values[paf.stage_field_key('Primary Erection', 'sling_angle')] = '60' # A stage the user switched OFF but already typed a value into -- # exactly the case the "unticked stage keeps values" rule protects. values[paf.stage_field_key('Secondary Erection', 'include')] = 'no' values[paf.stage_field_key('Secondary Erection', 'load_multiplier')] = '2.0' return values # --------------------------------------------------------------- vocabulary def test_every_block_kind_is_known(): # `problems` is expected to be non-empty here -- see the module # docstring's list of gaps (no centroid, no top-edge-lifter field, no # dunnage field, no lifting-configuration drawing source). Those are # honest, permanent gaps, not bugs; this test only checks the block # vocabulary itself. blocks, _problems = pa_handling.page(_some_values()) for b in _walk(blocks): assert 'kind' in b assert b['kind'] in _KNOWN_KINDS, b def test_empty_values_still_renders_known_kinds_only(): values = dict((k, '') for k in paf.stage_field_keys()) for f in paf.FIELDS: values[f['key']] = '' blocks, _problems = pa_handling.page(values) for b in _walk(blocks): assert b['kind'] in _KNOWN_KINDS, b def test_page_never_raises_on_none_values(): # page() must degrade, never raise, even with nothing handed in. blocks, problems = pa_handling.page(None) assert isinstance(blocks, list) assert isinstance(problems, list) # ---------------------------------------------------- not-applicable vs blank def test_stage_matrix_shape(): blocks, _problems = pa_handling.page(_some_values()) matrices = _matrix_blocks(blocks) assert len(matrices) == 1 m = matrices[0] assert m['columns'] == list(paf.STAGES) assert len(m['rows']) == len(paf.STAGE_OPTIONS) for row in m['rows']: assert len(row['cells']) == len(paf.STAGES) def test_erection_only_option_is_na_for_stripping_and_transportation(): blocks, _problems = pa_handling.page(_some_values()) m = _matrix_blocks(blocks)[0] columns = m['columns'] rigging_row = next(r for r in m['rows'] if r['cells'] is not None and 'rigging configuration' in r['label'].lower()) for stage_name in ('Stripping', 'Transportation'): idx = columns.index(stage_name) assert rigging_row['cells'][idx] == {'na': True} for stage_name in ('Primary Erection', 'Secondary Erection'): idx = columns.index(stage_name) cell = rigging_row['cells'][idx] assert cell != {'na': True} assert cell is not None assert 'editor' in cell def test_na_cell_and_unfilled_cell_render_differently(): blocks, _problems = pa_handling.page(_some_values()) m = _matrix_blocks(blocks)[0] columns = m['columns'] rigging_row = next(r for r in m['rows'] if 'rigging configuration' in r['label'].lower()) load_mult_row = next(r for r in m['rows'] if 'load multiplier' in r['label'].lower()) na_cell = rigging_row['cells'][columns.index('Stripping')] # Load Multiplier applies to every stage, so Stripping's cell there is # an ordinary (currently unfilled, from the caller's point of view) # editable cell -- shape must differ from the NA cell above. unfilled_cell = load_mult_row['cells'][columns.index('Transportation')] assert na_cell == {'na': True} assert 'na' not in unfilled_cell assert unfilled_cell['editor'] == 'num' def test_include_row_never_na(): blocks, _problems = pa_handling.page(_some_values()) m = _matrix_blocks(blocks)[0] include_row = next(r for r in m['rows'] if 'include' in r['label'].lower()) for cell in include_row['cells']: assert cell != {'na': True} assert cell['editor'] == 'check' # ------------------------------------------- unticked stage keeps its values def test_excluded_stage_cell_still_present_and_editable(): """Secondary Erection is switched off in `_some_values()` but its load_multiplier was typed in; the matrix must still offer that cell as an ordinary editable cell (never NA, never dropped) -- the caller's values dict is what actually decides required-ness at validate() time, not this block, and the cell key must still be there for the renderer to read/write against.""" values = _some_values() blocks, _problems = pa_handling.page(values) m = _matrix_blocks(blocks)[0] columns = m['columns'] load_mult_row = next(r for r in m['rows'] if 'load multiplier' in r['label'].lower()) cell = load_mult_row['cells'][columns.index('Secondary Erection')] assert cell != {'na': True} assert cell is not None assert cell['key'] == paf.stage_field_key('Secondary Erection', 'load_multiplier') # And the value the user typed is still sitting in `values`, untouched. assert values[cell['key']] == '2.0' def test_values_dict_is_never_mutated(): values = _some_values() before = copy.deepcopy(values) pa_handling.page(values) assert values == before # --------------------------------------------------------- labels and hints def test_every_field_block_has_label_and_hint(): blocks, _problems = pa_handling.page(_some_values()) for f in _field_blocks(blocks): assert f.get('label'), f assert f.get('hint'), f def test_every_matrix_row_has_label_and_hint(): blocks, _problems = pa_handling.page(_some_values()) m = _matrix_blocks(blocks)[0] for row in m['rows']: assert row.get('label'), row # 'include' is the only row Eriksson gives a very short help line # to, but every STAGE_OPTIONS row does carry one -- assert it rides # through rather than getting dropped in translation. assert row.get('hint'), row def test_symbol_rides_beside_label_not_instead_of_it(): blocks, _problems = pa_handling.page(_some_values()) m = _matrix_blocks(blocks)[0] load_mult_row = next(r for r in m['rows'] if 'load multiplier' in r['label'].lower()) # The symbol (x self-weight) must be ADDITIONAL to the plain words, per # the em-dash join `_row_label` uses -- never a bare symbol standing in # for the label. assert load_mult_row['label'].lower().startswith('load multiplier') assert u'—' in load_mult_row['label'] # em dash # --------------------------------------------------- missing-accessor degrade def test_missing_stage_accessor_degrades_to_problem_not_exception(): # Build the values dict BEFORE removing STAGES -- `stage_field_keys()` # itself reads the module-global `STAGES`, so this must not depend on # the attribute we are about to take away. values = _some_values() had_stages = hasattr(paf, 'STAGES') saved = getattr(paf, 'STAGES', None) try: if had_stages: delattr(paf, 'STAGES') blocks, problems = pa_handling.page(values) assert isinstance(blocks, list) assert any('STAGES' in p for p in problems) # The rest of the page still renders around the hole. assert any(b.get('kind') == 'heading' and b.get('text') == 'Handling' for b in blocks) finally: if had_stages: paf.STAGES = saved def test_missing_stage_field_key_degrades_to_problem(): values = _some_values() had = hasattr(paf, 'stage_field_key') saved = getattr(paf, 'stage_field_key', None) try: if had: delattr(paf, 'stage_field_key') blocks, problems = pa_handling.page(values) assert any('stage_field_key' in p for p in problems) assert not _matrix_blocks(blocks) finally: if had: paf.stage_field_key = saved def test_missing_weight_field_degrades_to_problem(): had = 'FIELDS' in vars(paf) saved = paf.FIELDS try: paf.FIELDS = [f for f in paf.FIELDS if f['key'] != 'weight_lb'] blocks, problems = pa_handling.page(_some_values()) assert any('weight_lb' in p for p in problems) # kv block still renders (weight row falls back to the "not # entered" sentence), it does not vanish or raise. kvs = [b for b in _walk(blocks) if b['kind'] == 'kv'] assert kvs finally: if had: paf.FIELDS = saved def test_no_top_edge_lifter_or_dunnage_field_is_a_known_gap(): """These are genuinely absent from panelanalysisform today (only face_lifters / edge_lifters exist) -- confirm the page says so rather than inventing a field or silently going blank.""" blocks, problems = pa_handling.page(_some_values()) assert any('top edge lifters' in p for p in problems) assert any('dunnage' in p for p in problems) sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence'] assert 'Design contains no top edge lifters.' in sentences assert 'Design contains no dunnage entered.' in sentences # --------------------------------------------------------------- real keys def test_matrix_cell_keys_match_panelanalysisform_stage_field_key(): blocks, _problems = pa_handling.page(_some_values()) m = _matrix_blocks(blocks)[0] for opt, row in zip(paf.STAGE_OPTIONS, m['rows']): opt_key = opt[0] for stage, cell in zip(m['columns'], row['cells']): if cell is None or cell.get('na'): continue assert cell['key'] == paf.stage_field_key(stage, opt_key) def test_load_factor_and_lifter_fields_use_real_keys(): blocks, _problems = pa_handling.page(_some_values()) keys = set(f['key'] for f in _field_blocks(blocks)) assert 'weight_lb' in keys assert 'self_weight_factor' in keys assert 'face_lifters' in keys assert 'edge_lifters' in keys def test_drawing_blocks_name_real_sibling_sources_only(): blocks, _problems = pa_handling.page(_some_values()) drawings = [b for b in _walk(blocks) if b['kind'] == 'drawing'] sources = set(d['source'] for d in drawings) # 'plan' and 'weight_diagram' are the two panelsketch entry points the # sibling agent added; pa_handling must never invent a third. assert sources <= set(['plan', 'weight_diagram']) assert 'plan' in sources for d in drawings: assert d.get('caption') assert isinstance(d.get('w'), int) assert isinstance(d.get('h'), int) def test_legend_groups_match_documented_vocabulary(): blocks, _problems = pa_handling.page(_some_values()) legends = [b for b in _walk(blocks) if b['kind'] == 'legend'] assert len(legends) == 1 names = [g[0] for g in legends[0]['groups']] assert names == ['Shapes', 'Fills'] def test_import_failure_degrades_cleanly(monkeypatch): import vibe.pa_handling as mod def _boom(): return None, 'vibe.panelanalysisform could not be imported (boom)' monkeypatch.setattr(mod, '_import_paf', _boom) blocks, problems = mod.page(_some_values()) assert problems == ['vibe.panelanalysisform could not be imported (boom)'] assert any(b['kind'] == 'heading' for b in blocks)