Files
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

642 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Tests for `vibe.pa_criteria.page` -- the Design Criteria section layout.
No Revit and no WPF renderer are available here (see `conftest.py`'s
docstring for why `vibe` imports this way), so this suite cannot check that
the page LOOKS right -- nobody has run this inside Revit and this file does
not claim otherwise. What it can and does check mechanically:
* `page()` never raises, on an empty dict, a filled dict, `None` values,
`None` reads, or a `values` object that is not even a dict.
* every block returned is a well-formed member of the block vocabulary
`page()`'s docstring promises (`heading`, `columns`, `field`, `kv`,
`sentence`, `table`, `rule`, `space`) -- an unknown `kind` renders as
nothing on the real form, so a typo here would be a silently missing
block that only this test would catch.
* every `field` block carries a non-empty, plain-English `label` and a
non-empty `hint` -- the owner's whole complaint about the old form was
that a field carried only its engineering symbol.
* a field key with no backing entry in `panelanalysisform.FIELDS` still
renders (an empty editor), and is named in `problems` -- never silently
dropped.
* `values` is never mutated by a call to `page()`.
* the block list is stable: calling `page()` twice on the same input
produces the same output.
"""
import copy
import sys
from pathlib import Path
TESTS_DIR = Path(__file__).resolve().parent
if str(TESTS_DIR) not in sys.path:
sys.path.insert(0, str(TESTS_DIR))
from vibe import pa_criteria as pc # noqa: E402
KNOWN_KINDS = set(['heading', 'columns', 'field', 'kv', 'sentence', 'table',
'lc_table', 'rule', 'space'])
KNOWN_EDITORS = set(['text', 'num', 'check', 'choice'])
def _walk(blocks):
"""Yield every block in the tree, 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 _assert_well_formed(blocks):
for b in blocks:
assert isinstance(b, dict), 'block is not a dict: {0!r}'.format(b)
kind = b.get('kind')
assert kind in KNOWN_KINDS, 'unknown block kind {0!r}'.format(kind)
if kind == 'heading':
assert isinstance(b['text'], str) and b['text'].strip()
assert b['level'] in (1, 2, 3)
elif kind == 'columns':
assert isinstance(b['columns'], list) and b['columns']
for col in b['columns']:
assert isinstance(col, list)
_assert_well_formed(col)
elif kind == 'field':
assert isinstance(b['key'], str) and b['key']
assert isinstance(b['label'], str) and b['label'].strip(), (
'field {0!r} has no plain-English label'.format(b['key']))
assert isinstance(b['hint'], str) and b['hint'].strip(), (
'field {0!r} has no hint'.format(b['key']))
assert b['symbol'] is None or isinstance(b['symbol'], str)
assert isinstance(b['required'], bool)
assert b['editor'] in KNOWN_EDITORS, (
'field {0!r} has unknown editor {1!r}'
.format(b['key'], b['editor']))
if b['editor'] == 'choice':
assert b['choices'], (
'choice field {0!r} has no choices'.format(b['key']))
else:
assert b['choices'] is None
elif kind == 'kv':
assert isinstance(b['rows'], list)
for row in b['rows']:
assert len(row) == 2
elif kind == 'sentence':
assert isinstance(b['text'], str) and b['text'].strip()
elif kind == 'table':
assert isinstance(b['headers'], list) and b['headers']
assert isinstance(b['rows'], list)
for row in b['rows']:
assert len(row) == len(b['headers']), (
'table row {0!r} does not match headers {1!r}'
.format(row, b['headers']))
for cell in row:
if isinstance(cell, dict):
# `_render_table` grew support for ANY editor kind in
# a cell, not just a tick box (see its own comment,
# "ANY editor kind, not just 'check'") -- that is what
# let Resistance Factors move from a squeezed
# `columns`-of-`field`s to a `table` of live `num`
# editors on 2026-09-04 (WPA655). A cell dict is no
# longer only ever a checkbox.
assert cell.get('editor') in KNOWN_EDITORS
assert cell.get('key')
else:
assert isinstance(cell, str)
elif kind == 'lc_table':
# The load-combination grid: same row/header contract as `table`,
# plus a `name` naming the table and a `factors` list the renderer
# needs to build an added row's cells. Unlike `table`, a cell may
# carry ANY editor kind -- the factors are editable now.
assert isinstance(b['name'], str) and b['name']
assert isinstance(b['headers'], list) and b['headers']
assert isinstance(b['factors'], list) and b['factors']
assert isinstance(b['rows'], list)
for row in b['rows']:
assert len(row) == len(b['headers']), (
'lc_table row {0!r} does not match headers {1!r}'
.format(row, b['headers']))
for cell in row:
if isinstance(cell, dict):
assert cell.get('editor') in KNOWN_EDITORS
assert cell.get('key')
else:
assert isinstance(cell, str)
elif kind == 'rule':
assert set(b.keys()) == set(['kind'])
elif kind == 'space':
assert isinstance(b['h'], int)
def _field_keys(blocks):
return [b['key'] for b in _walk(blocks) if b.get('kind') == 'field']
# --------------------------------------------------------------------- tests
def test_empty_values_never_raises_and_is_well_formed():
blocks, problems = pc.page({})
assert isinstance(blocks, list) and blocks
assert isinstance(problems, list)
_assert_well_formed(blocks)
def test_none_values_and_none_reads_never_raises():
blocks, problems = pc.page(None, None)
assert isinstance(blocks, list) and blocks
_assert_well_formed(blocks)
def test_non_dict_values_degrades_rather_than_raising():
blocks, problems = pc.page('not a dict')
assert isinstance(blocks, list) and blocks
_assert_well_formed(blocks)
def test_filled_values_never_raises_and_is_well_formed():
values = {
'fc_psi': '6000', 'fci_psi': '3500', 'unit_weight_pcf': '150',
'concrete_type': 'Normalweight', 'rebar_fy_ksi': '60',
'rebar_es_ksi': '29000', 'ps_fpu_ksi': '270', 'strand_ep_ksi': '28500',
'ps_loss_method': 'PCI', 'ps_loss_humidity_pct': '75',
'design_criteria.rvalue_calc_enabled': 'yes',
'design_criteria.min_reinf_checked': 'true',
}
blocks, problems = pc.page(values, {'mark': 'sample_mark',
'design_number': 'PNL1.0'})
assert isinstance(blocks, list) and blocks
_assert_well_formed(blocks)
def test_values_dict_is_never_mutated():
values = {'fc_psi': '6000'}
before = copy.deepcopy(values)
pc.page(values, {'mark': 'sample_mark'})
assert values == before
def test_reads_dict_is_never_mutated():
reads = {'mark': 'sample_mark', 'design_number': 'PNL1.0'}
before = copy.deepcopy(reads)
pc.page({}, reads)
assert reads == before
def test_block_list_is_stable_for_the_same_input():
values = {'fc_psi': '6000', 'unit_weight_pcf': '150'}
reads = {'mark': 'sample_mark'}
a, pa = pc.page(values, reads)
b, pb = pc.page(values, reads)
assert a == b
assert pa == pb
def test_missing_key_degrades_to_a_field_and_names_a_problem():
blocks, problems = pc.page({})
keys = set(_field_keys(blocks))
# A key this file invented because Eriksson's page has no home for it
# yet in panelanalysisform.FIELDS.
assert 'design_criteria.analysis_beta_d' in keys
joined = ' '.join(problems)
assert 'design_criteria.analysis_beta_d' in joined
def test_existing_form_key_is_not_reported_as_missing():
blocks, problems = pc.page({})
joined = ' '.join(problems)
# fc_psi is a real, long-standing panelanalysisform.FIELDS key.
assert "'fc_psi'" not in joined or 'fc_psi) has no backing' not in joined
for p in problems:
assert 'fc_psi)' not in p
def test_every_field_key_appears_at_most_once():
blocks, _problems = pc.page({})
keys = _field_keys(blocks)
dupes = [k for k in set(keys) if keys.count(k) > 1]
assert not dupes, 'field key(s) rendered more than once: {0}'.format(dupes)
def test_section_heading_is_present():
blocks, _problems = pc.page({})
headings = [b['text'] for b in blocks if b.get('kind') == 'heading']
assert 'Design Criteria' in headings
assert headings[0] == 'Design Criteria'
def test_prime_notation_used_not_ascii_apostrophe():
blocks, _problems = pc.page({})
for b in _walk(blocks):
if b.get('kind') != 'field':
continue
symbol = b.get('symbol') or ''
# f'c / f'ci style ASCII apostrophes must never appear; the real
# prime character (U+2032) is what the renderer turns into a
# subscript.
assert "f'c" not in symbol and "f'ci" not in symbol
if u"f′" in symbol or u"E′" in symbol:
assert True # sanity: the prime character is reachable at all
def test_load_combination_tables_have_include_checkboxes():
blocks, problems = pc.page({})
tables = [b for b in _walk(blocks) if b.get('kind') == 'lc_table']
assert len(tables) == 2
assert set(t['name'] for t in tables) == set(['strength', 'service'])
for t in tables:
assert t['headers'][-1] == 'Include'
for row in t['rows']:
cell = row[-1]
assert isinstance(cell, dict) and cell['editor'] == 'check'
assert cell['key'].startswith(
'load_combo.{0}.'.format(t['name']))
assert cell['key'].endswith('.include')
# The Include ticks used to be reported as having "no backing fields yet".
# They do now (`panelanalysisform.load_combo_include_key`, pre-ticked by
# `prefill`), so that problem is gone rather than standing stale.
joined = ' '.join(problems)
assert 'no backing fields yet' not in joined
def test_load_combination_factor_cells_are_editable_and_keyed():
"""Owner, 2026-09-03: the ACI factors are the user's to change. Every
factor cell must be a live numeric editor with a key of its own, not the
plain printed text the old `table` block carried."""
blocks, _problems = pc.page({})
tables = [b for b in _walk(blocks) if b.get('kind') == 'lc_table']
assert tables
for t in tables:
assert t['factors'] == ['D', 'L', 'R', 'W', 'E', 'T', 'H']
for row in t['rows']:
rid = row[0]
assert isinstance(rid, str) and rid
for i, col in enumerate(t['factors']):
cell = row[i + 1]
assert isinstance(cell, dict), (
'factor cell {0} of row {1} is not an editor'
.format(col, rid))
assert cell['editor'] == 'num'
assert cell['key'] == 'load_combo.{0}.{1}.{2}'.format(
t['name'], rid, col)
def test_load_combination_defaults_match_the_form_modules_table():
"""The local ACI tuples are a fallback for an unimportable sibling module
(see `_combo_table`); they must not drift from the live copy."""
from vibe import panelanalysisform as paf
assert pc._STRENGTH_COMBOS == paf.LOAD_COMBO_ROWS['strength']
assert pc._SERVICE_COMBOS == paf.LOAD_COMBO_ROWS['service']
def test_a_user_added_row_shows_up_in_the_table():
"""A row the user added exists only as keys in `values`; the table has to
rebuild it from there or the row would vanish on the next render."""
values = {'load_combo.strength.u1.include': 'yes',
'load_combo.strength.u1.D': '1.05'}
blocks, _problems = pc.page(values)
strength = [b for b in _walk(blocks)
if b.get('kind') == 'lc_table' and b['name'] == 'strength'][0]
ids = [row[0] for row in strength['rows']]
assert ids[-1] == 'u1'
assert len(ids) == 9 # the eight ACI rows, then the user's
def test_soil_blocks_are_sentences_not_editors():
blocks, problems = pc.page({})
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert any('no exterior soil loads' in s for s in sentences)
assert any('no interior soil loads' in s for s in sentences)
assert any('soil loads are not collected' in p for p in problems)
def test_prestress_loss_group_is_a_sentence_when_blank():
blocks, _problems = pc.page({})
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert any('No prestress loss assumptions' in s for s in sentences)
# And its fields must NOT be rendered while blank -- shown only once
# something in the group is filled in.
keys = set(_field_keys(blocks))
assert 'ps_loss_method' not in keys
def test_prestress_loss_group_becomes_fields_when_filled():
blocks, _problems = pc.page({'ps_loss_method': 'PCI'})
keys = set(_field_keys(blocks))
assert 'ps_loss_method' in keys
assert 'ps_loss_humidity_pct' in keys
def test_filling_one_group_field_does_not_file_a_phantom_problem_for_others():
# Regression guard: `_group_or_sentence` must not evaluate the
# filled-in branch (and thus call `F(key)`, which files a "missing"
# problem) when the sentence branch is the one actually shown.
_blocks, problems_blank = pc.page({})
joined = ' '.join(problems_blank)
assert 'ps_loss_method' not in joined
assert 'ps_loss_humidity_pct' not in joined
def test_rvalue_and_min_reinforcement_default_sentences_present():
blocks, _problems = pc.page({})
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert any('R-value is not being calculated' in s for s in sentences)
assert any('Minimum steel is not being checked' in s for s in sentences)
def test_concrete_grid_has_release_and_inplace_strength_fields():
blocks, _problems = pc.page({})
keys = set(_field_keys(blocks))
assert 'fci_psi' in keys
assert 'fc_psi' in keys
assert 'conc_eci_psi' in keys
assert 'conc_ec_psi' in keys
def test_ksi_reconciliation_hint_on_psi_keys():
blocks, _problems = pc.page({})
for b in _walk(blocks):
if b.get('kind') == 'field' and b['key'] in ('fc_psi', 'fci_psi'):
assert 'ksi' in b['hint'].lower()
def test_required_fields_match_panelanalysisform_where_it_exists():
from vibe import panelanalysisform as pf
blocks, _problems = pc.page({})
required_here = dict((b['key'], b['required'])
for b in _walk(blocks) if b.get('kind') == 'field')
for f in pf.FIELDS:
key = f['key']
if key in required_here:
assert required_here[key] == f['required'], (
'required flag mismatch for {0!r}'.format(key))
def test_panel_header_kv_shown_only_when_reads_present():
blocks_no_reads, _ = pc.page({}, {})
kvs_no_reads = [b for b in blocks_no_reads if b.get('kind') == 'kv']
blocks_with_reads, _ = pc.page({}, {'mark': 'sample_mark'})
kvs_with_reads = [b for b in blocks_with_reads if b.get('kind') == 'kv']
assert len(kvs_with_reads) >= len(kvs_no_reads) + 1
def test_page_import_survives_a_broken_panelanalysisform(monkeypatch):
"""Simulates the sibling agent's file being mid-edit / unimportable.
`_existing_keys()` is the one function that reaches for
`panelanalysisform`; monkeypatching it directly to behave the way it
would if the import raised (returning `None`) exercises the same
"could not judge" path `page()` must degrade through, without depending
on the interpreter's module-cache mechanics for a package (`vibe`)
that other tests in this same process have already imported
`panelanalysisform` onto as an attribute.
"""
monkeypatch.setattr(pc, '_existing_keys', lambda: None)
blocks, problems = pc.page({})
assert isinstance(blocks, list) and blocks
assert any('could not read panelanalysisform' in p for p in problems)
def test_choice_field_has_choices():
blocks, _problems = pc.page({})
for b in _walk(blocks):
if b.get('kind') == 'field' and b['key'] == 'concrete_type':
assert b['editor'] == 'choice'
assert 'Normalweight' in b['choices']
# ------------------------------------------------ 2026-09-04 dropdown pass
#
# Owner: "很多东西应该是选项,而不是让用户填写" -- governing code, ASCE
# chapters and the rest of the enumerated Design Criteria settings become
# pick-lists. Vocabulary is pinned exactly because `vibe.pa_defaults` (a
# sibling module) writes these values verbatim.
def test_design_spec_is_a_choice_field():
blocks, _problems = pc.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['design_spec']
assert f['editor'] == 'choice'
assert f['choices'] == ['ACI 318-19', 'ACI 318-14', 'ACI 318-11']
def test_design_criteria_settings_fields_are_choice_editors():
blocks, _problems = pc.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
expected = {
'design_criteria.analysis_beta_d': ['Non-Sway', 'Sway'],
'design_criteria.cracking_method': ['Mast Method', 'Uncracked',
'Cracked'],
'design_criteria.flexure_method': ['Whitney Stress Block',
'Strain Compatibility'],
'design_criteria.flexure_cracking_check': ['None', '1.2 Mcr',
'1.33 Mu'],
'design_criteria.deflection_cantilever_limit': ['L/75', 'L/120',
'L/150'],
}
for key, choices in expected.items():
f = fields[key]
assert f['editor'] == 'choice', '{0} is not a choice editor'.format(
key)
assert f['choices'] == choices
def test_yes_no_design_criteria_fields_stay_checkboxes():
blocks, _problems = pc.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
for key in (
'design_criteria.deflection_add_bow',
'design_criteria.deflection_check_cantilever',
'design_criteria.rvalue_calc_enabled',
'design_criteria.min_reinf_checked',
'design_criteria.stress_failure_if_cracked',
'design_criteria.stress_ignore_lateral_ecc',
'design_criteria.stress_use_principal_axis',
'design_criteria.shear_treat_as_column',
'design_criteria.axial_ignore_tensile_failures',
'design_criteria.axial_ignore_buckling_failures',
):
assert fields[key]['editor'] == 'check', key
assert fields[key]['choices'] is None
def test_cracking_assumption_now_renders_as_a_choice_field():
"""`cracking_assumption` is a real `panelanalysisform.FIELDS` key with a
Cracked/Uncracked vocabulary (and a `PREFILL_DEFAULTS` entry) that no
page module surfaced before this change -- added beside `second_order`
in Structural Analysis so a pre-filled value has somewhere to be seen
and edited."""
blocks, _problems = pc.page({})
keys = set(_field_keys(blocks))
assert 'cracking_assumption' in keys
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['cracking_assumption']
assert f['editor'] == 'choice'
assert f['choices'] == ['Cracked', 'Uncracked']
# ------------------------------------------------ 2026-09-04 headless review
#
# WPA655's rendered Design Criteria tab was reviewed by eye and five findings
# were filed; these tests pin the fix for each. See the module docstring's
# "UPDATE 2026-09-04" note for the Resistance Factors layout change and the
# `ps_loss_humidity_pct` comment above the Prestress Losses block.
def _table_blocks(blocks):
"""Every `table`-kind block anywhere in the tree, `columns` descended."""
return [b for b in _walk(blocks) if b.get('kind') == 'table']
def _table_cell_keys(blocks):
"""Every editor key embedded in a `table` cell. These live inside
`table.rows`, not as `field` blocks -- `_field_keys` cannot see them,
which is exactly why the Resistance Factors phi editors need their own
check rather than reusing that helper."""
keys = []
for t in _table_blocks(blocks):
for row in t.get('rows') or []:
for cell in row:
if isinstance(cell, dict) and cell.get('key'):
keys.append(cell['key'])
return keys
_PHI_KEYS = ('phi_flexure', 'design_criteria.phi_tension_transfer',
'design_criteria.phi_compression', 'phi_shear',
'design_criteria.phi_shear_transfer')
def test_every_resistance_factor_phi_has_a_visible_table_editor():
"""Finding 1: the old two-column `Developed | Transferring` `columns` of
`field` blocks squeezed every phi editor to zero width. All five phi
keys must now show up as live editors somewhere on the page."""
blocks, _problems = pc.page({})
keys = set(_table_cell_keys(blocks))
for key in _PHI_KEYS:
assert key in keys, '{0} has no visible editor'.format(key)
def test_resistance_factor_phi_cells_are_live_num_editors_not_text():
blocks, _problems = pc.page({})
cells_by_key = {}
for t in _table_blocks(blocks):
for row in t.get('rows') or []:
for cell in row:
if isinstance(cell, dict) and cell.get('key') in _PHI_KEYS:
cells_by_key[cell['key']] = cell
assert set(cells_by_key) == set(_PHI_KEYS)
for key, cell in cells_by_key.items():
assert cell['editor'] == 'num', '{0} is not a num editor'.format(key)
def test_resistance_factors_table_marks_compression_transferring_na():
"""The one phi that genuinely does not apply (compression once the
strand has fully transferred) stays a plain text cell, not an editor --
same discipline as the old `sentence`, just inside the new table."""
blocks, _problems = pc.page({})
tables = [t for t in _table_blocks(blocks) if t['headers'][0] == u'φ']
assert len(tables) == 1
rows = dict((row[0], row) for row in tables[0]['rows'])
assert 'Compression' in rows
na_cell = rows['Compression'][2] # the 'Transferring' column
assert isinstance(na_cell, str)
assert 'not applicable' in na_cell.lower()
def test_resistance_factor_missing_backing_field_problems_still_filed():
"""Moving the three `design_criteria.phi_*` keys into table cells must
not silently drop the existing 'no backing field yet' bookkeeping --
`_cell()` routes every key through `F()` first specifically to keep it."""
_blocks, problems = pc.page({})
joined = ' '.join(problems)
for key in ('design_criteria.phi_tension_transfer',
'design_criteria.phi_compression',
'design_criteria.phi_shear_transfer'):
assert key in joined, '{0} missing-field problem was dropped'.format(
key)
def test_mesh_material_fields_are_collected():
"""Finding 2: `mesh_fy_ksi` / `mesh_es_ksi` exist in
`panelanalysisform.FIELDS` (80 ksi / 29000 ksi defaults) but this page
used to render only a 'not collected yet' sentence for Mesh -- now shown
the same way Rebar's fy / Es are."""
blocks, problems = pc.page({})
keys = set(_field_keys(blocks))
assert 'mesh_fy_ksi' in keys
assert 'mesh_es_ksi' in keys
joined = ' '.join(problems)
# `_field()`'s missing-backing-field message quotes the key inside
# parens -- "(mesh_fy_ksi)" -- and must not appear, since both keys are
# real `panelanalysisform.FIELDS` entries.
assert '(mesh_fy_ksi)' not in joined
assert '(mesh_es_ksi)' not in joined
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert not any('No mesh material properties' in s for s in sentences)
def test_inplace_tension_sqrt_row_present_above_modulus_of_rupture():
"""Finding 3: the uncracked in-place tension limit
(`inplace_tension_sqrt`, 7.5 sqrt(f'c)) is a real `panelanalysisform`
key this page never surfaced. All three In-Place Stress Limits rows
must be present and named the way Eriksson prints them."""
blocks, _problems = pc.page({})
keys = set(_field_keys(blocks))
assert 'inplace_tension_sqrt' in keys
assert 'design_criteria.inplace_modulus_rupture_sqrt_fc' in keys
assert 'inplace_comp_ratio' in keys
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
assert fields['inplace_tension_sqrt']['label'] == u'Tension, uncracked'
assert (fields['design_criteria.inplace_modulus_rupture_sqrt_fc']['label']
== u'Tension, transition (modulus of rupture)')
assert fields['inplace_comp_ratio']['label'] == u'Compression'
def test_inplace_tension_sqrt_precedes_modulus_of_rupture_in_col_b():
"""Order matters, not just presence -- the uncracked tension row belongs
above the modulus-of-rupture row, as on Eriksson's own printout."""
blocks, _problems = pc.page({})
field_keys = _field_keys(blocks)
i_tension = field_keys.index('inplace_tension_sqrt')
i_rupture = field_keys.index(
'design_criteria.inplace_modulus_rupture_sqrt_fc')
assert i_tension < i_rupture
def test_prestress_loss_percent_symbol_field_still_present_and_editable():
"""Finding 4: the '%' unit on `ps_loss_humidity_pct` must stay attached
to a live editor after the group's fields moved from one wide column
into a 2x2 layout -- only the layout changed, not the field itself."""
blocks, _problems = pc.page({'ps_loss_method': 'PCI'})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['ps_loss_humidity_pct']
assert f['symbol'] == '%'
assert f['editor'] == 'num'
def test_sway_condition_field_is_a_choice_editor():
"""Finding 6: keep the label, make sure the choice editor is used."""
blocks, _problems = pc.page({})
fields = dict((b['key'], b) for b in _walk(blocks)
if b.get('kind') == 'field')
f = fields['design_criteria.analysis_beta_d']
assert f['label'] == u'Sway condition for 2nd-order analysis'
assert f['symbol'] == u'βd'
assert f['editor'] == 'choice'
assert f['choices'] == ['Non-Sway', 'Sway']