WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s

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.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
@@ -0,0 +1,641 @@
# -*- 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']