1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-14 16:42:02 +00:00

test(core): don't fetch full DebugLinkState by default

In case the main workflow is restarting after a `DebugLinkDecision`,
sending the response of `DebugLinkGetState` may get interrupted.

We are making the state fetching explicit, in order to avoid the
"restart" race condition (as described in #4401).

Following the above change, text-based layout recording is removed.

[no changelog]
This commit is contained in:
Roman Zeyde 2025-02-02 17:29:45 +02:00 committed by Roman Zeyde
parent 85bbc89eed
commit 061e71213e
22 changed files with 343 additions and 365 deletions

View File

@ -110,6 +110,8 @@ message DebugLinkGetState {
// trezor-core only - wait until current layout changes // trezor-core only - wait until current layout changes
// changed in 2.6.4: multiple wait types instead of true/false. // changed in 2.6.4: multiple wait types instead of true/false.
optional DebugWaitType wait_layout = 3 [default=IMMEDIATE]; optional DebugWaitType wait_layout = 3 [default=IMMEDIATE];
// Responds immediately with an empty `DebugLinkState` (used for client-side synchronization).
optional bool return_empty_state = 4 [default=false];
} }
/** /**

View File

@ -204,12 +204,12 @@ test_emu_persistence_ui: ## run persistence tests with UI testing
test_emu_ui: ## run ui integration tests test_emu_ui: ## run ui integration tests
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) \ $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) \
--ui=test --ui-check-missing --record-text-layout --do-master-diff \ --ui=test --ui-check-missing --do-master-diff \
--lang=$(TEST_LANG) --lang=$(TEST_LANG)
test_emu_ui_multicore: ## run ui integration tests using multiple cores test_emu_ui_multicore: ## run ui integration tests using multiple cores
$(PYTEST) -n $(MULTICORE) $(TESTPATH)/device_tests $(TESTOPTS) --timeout $(PYTEST_TIMEOUT) \ $(PYTEST) -n $(MULTICORE) $(TESTPATH)/device_tests $(TESTOPTS) --timeout $(PYTEST_TIMEOUT) \
--ui=test --ui-check-missing --record-text-layout --do-master-diff \ --ui=test --ui-check-missing --do-master-diff \
--control-emulators --model=core --random-order-seed=$(RANDOM) \ --control-emulators --model=core --random-order-seed=$(RANDOM) \
--lang=$(TEST_LANG) --lang=$(TEST_LANG)

View File

@ -268,6 +268,11 @@ if __debug__:
async def dispatch_DebugLinkGetState( async def dispatch_DebugLinkGetState(
msg: DebugLinkGetState, msg: DebugLinkGetState,
) -> DebugLinkState | None: ) -> DebugLinkState | None:
if msg.return_empty_state:
from trezor.messages import DebugLinkState
return DebugLinkState()
if msg.wait_layout == DebugWaitType.IMMEDIATE: if msg.wait_layout == DebugWaitType.IMMEDIATE:
return _state() return _state()

View File

@ -2898,11 +2898,13 @@ if TYPE_CHECKING:
class DebugLinkGetState(protobuf.MessageType): class DebugLinkGetState(protobuf.MessageType):
wait_layout: "DebugWaitType" wait_layout: "DebugWaitType"
return_empty_state: "bool"
def __init__( def __init__(
self, self,
*, *,
wait_layout: "DebugWaitType | None" = None, wait_layout: "DebugWaitType | None" = None,
return_empty_state: "bool | None" = None,
) -> None: ) -> None:
pass pass

View File

@ -64,8 +64,7 @@ if TYPE_CHECKING:
def __call__( def __call__(
self, self,
hold_ms: int | None = None, hold_ms: int | None = None,
wait: bool | None = None, ) -> "None": ...
) -> "LayoutContent": ...
InputFlowType = Generator[None, messages.ButtonRequest, None] InputFlowType = Generator[None, messages.ButtonRequest, None]
@ -416,11 +415,10 @@ def _make_input_func(
def input_func( def input_func(
self: "DebugLink", self: "DebugLink",
hold_ms: int | None = None, hold_ms: int | None = None,
wait: bool | None = None, ) -> None:
) -> LayoutContent:
__tracebackhide__ = True # for pytest # pylint: disable=W0612 __tracebackhide__ = True # for pytest # pylint: disable=W0612
decision.hold_ms = hold_ms decision.hold_ms = hold_ms
return self._decision(decision, wait=wait) self._decision(decision)
return input_func # type: ignore [Parameter name mismatch] return input_func # type: ignore [Parameter name mismatch]
@ -442,12 +440,7 @@ class DebugLink:
self.t1_screenshot_directory: Path | None = None self.t1_screenshot_directory: Path | None = None
self.t1_screenshot_counter = 0 self.t1_screenshot_counter = 0
# Optional file for saving text representation of the screen
self.screen_text_file: Path | None = None
self.last_screen_content = ""
self.waiting_for_layout_change = False self.waiting_for_layout_change = False
self.layout_dirty = True
self.input_wait_type = DebugWaitType.IMMEDIATE self.input_wait_type = DebugWaitType.IMMEDIATE
@ -477,11 +470,6 @@ class DebugLink:
assert self.model is not None assert self.model is not None
return LayoutType.from_model(self.model) return LayoutType.from_model(self.model)
def set_screen_text_file(self, file_path: Path | None) -> None:
if file_path is not None:
file_path.write_bytes(b"")
self.screen_text_file = file_path
def open(self) -> None: def open(self) -> None:
self.transport.begin_session() self.transport.begin_session()
@ -543,8 +531,19 @@ class DebugLink:
raise TrezorFailure(result) raise TrezorFailure(result)
return result return result
def read_layout(self) -> LayoutContent: def read_layout(self, wait: bool | None = None) -> LayoutContent:
return LayoutContent(self.state().tokens) """
Force waiting for the layout by setting `wait=True`. Force not waiting by
setting `wait=False` -- useful when, e.g., you are causing the next layout to be
deliberately delayed.
"""
if wait is True:
wait_type = DebugWaitType.CURRENT_LAYOUT
elif wait is False:
wait_type = DebugWaitType.IMMEDIATE
else:
wait_type = None
return LayoutContent(self.state(wait_type=wait_type).tokens)
def wait_layout(self, wait_for_external_change: bool = False) -> LayoutContent: def wait_layout(self, wait_for_external_change: bool = False) -> LayoutContent:
# Next layout change will be caused by external event # Next layout change will be caused by external event
@ -558,18 +557,12 @@ class DebugLink:
obj = self._call( obj = self._call(
messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT) messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT)
) )
self.layout_dirty = True
if isinstance(obj, messages.Failure): if isinstance(obj, messages.Failure):
raise TrezorFailure(obj) raise TrezorFailure(obj)
return LayoutContent(obj.tokens) return LayoutContent(obj.tokens)
@contextmanager @contextmanager
def wait_for_layout_change(self) -> Iterator[LayoutContent]: def wait_for_layout_change(self) -> Iterator[None]:
# set up a dummy layout content object to be yielded
layout_content = LayoutContent(
["DUMMY CONTENT, WAIT UNTIL THE END OF THE BLOCK :("]
)
# make sure some current layout is up by issuing a dummy GetState # make sure some current layout is up by issuing a dummy GetState
self.state() self.state()
@ -579,18 +572,14 @@ class DebugLink:
# allow the block to proceed # allow the block to proceed
self.waiting_for_layout_change = True self.waiting_for_layout_change = True
try: try:
yield layout_content yield
finally: finally:
self.waiting_for_layout_change = False self.waiting_for_layout_change = False
self.layout_dirty = True
# wait for the reply # wait for the reply
resp = self._read() resp = self._read()
assert isinstance(resp, messages.DebugLinkState) assert isinstance(resp, messages.DebugLinkState)
# replace contents of the yielded object with the new thing
layout_content.__init__(resp.tokens)
def reset_debug_events(self) -> None: def reset_debug_events(self) -> None:
# Only supported on TT and above certain version # Only supported on TT and above certain version
if (self.model is not models.T1B1) and not self.legacy_debug: if (self.model is not models.T1B1) and not self.legacy_debug:
@ -634,44 +623,24 @@ class DebugLink:
state = self._call(messages.DebugLinkGetState(wait_word_list=True)) state = self._call(messages.DebugLinkGetState(wait_word_list=True))
return state.reset_word return state.reset_word
def _decision( def _decision(self, decision: messages.DebugLinkDecision) -> None:
self, decision: messages.DebugLinkDecision, wait: bool | None = None """Send a debuglink decision.
) -> LayoutContent:
"""Send a debuglink decision and returns the resulting layout.
If hold_ms is set, an additional 200ms is added to account for processing If hold_ms is set, an additional 200ms is added to account for processing
delays. (This is needed for hold-to-confirm to trigger reliably.) delays. (This is needed for hold-to-confirm to trigger reliably.)
If `wait` is unset, the following wait mode is used:
- `IMMEDIATE`, when in normal tests, which never deadlocks the device, but may
return an empty layout in case the next one didn't come up immediately. (E.g.,
in SignTx flow, the device is waiting for more TxRequest/TxAck exchanges
before showing the next UI layout.)
- `CURRENT_LAYOUT`, when in tests running through a `DeviceHandler`. This mode
returns the current layout or waits for some layout to come up if there is
none at the moment. The assumption is that wirelink is communicating on
another thread and won't be blocked by waiting on debuglink.
Force waiting for the layout by setting `wait=True`. Force not waiting by
setting `wait=False` -- useful when, e.g., you are causing the next layout to be
deliberately delayed.
""" """
if not self.allow_interactions: if not self.allow_interactions:
return self.wait_layout() self.wait_layout()
return
if decision.hold_ms is not None: if decision.hold_ms is not None:
decision.hold_ms += 200 decision.hold_ms += 200
self._write(decision) self._write(decision)
self.layout_dirty = True if self.model is models.T1B1:
if wait is True: return
wait_type = DebugWaitType.CURRENT_LAYOUT # When the call below returns, we know that `decision` has been processed in Core.
elif wait is False: self._call(messages.DebugLinkGetState(return_empty_state=True))
wait_type = DebugWaitType.IMMEDIATE
else:
wait_type = self.input_wait_type
return self._snapshot_core(wait_type)
press_yes = _make_input_func(button=messages.DebugButton.YES) press_yes = _make_input_func(button=messages.DebugButton.YES)
"""Confirm current layout. See `_decision` for more details.""" """Confirm current layout. See `_decision` for more details."""
@ -698,58 +667,14 @@ class DebugLink:
) )
"""Press right button. See `_decision` for more details.""" """Press right button. See `_decision` for more details."""
def input(self, word: str, wait: bool | None = None) -> LayoutContent: def input(self, word: str) -> None:
"""Send text input to the device. See `_decision` for more details.""" """Send text input to the device. See `_decision` for more details."""
return self._decision(messages.DebugLinkDecision(input=word), wait) self._decision(messages.DebugLinkDecision(input=word))
def click( def click(self, click: Tuple[int, int], hold_ms: int | None = None) -> None:
self,
click: Tuple[int, int],
hold_ms: int | None = None,
wait: bool | None = None,
) -> LayoutContent:
"""Send a click to the device. See `_decision` for more details.""" """Send a click to the device. See `_decision` for more details."""
x, y = click x, y = click
return self._decision( self._decision(messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms))
messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms), wait
)
def _snapshot_core(
self, wait_type: DebugWaitType = DebugWaitType.IMMEDIATE
) -> LayoutContent:
"""Save text and image content of the screen to relevant directories."""
# skip the snapshot if we are on T1
if self.model is models.T1B1:
return LayoutContent([])
# take the snapshot
state = self.state(wait_type)
layout = LayoutContent(state.tokens)
if state.tokens and self.layout_dirty:
# save it, unless we already did or unless it's empty
self.save_debug_screen(layout.visible_screen())
self.layout_dirty = False
# return the layout
return layout
def save_debug_screen(self, screen_content: str) -> None:
if self.screen_text_file is None:
return
if not self.screen_text_file.exists():
self.screen_text_file.write_bytes(b"")
# Not writing the same screen twice
if screen_content == self.last_screen_content:
return
self.last_screen_content = screen_content
with open(self.screen_text_file, "a") as f:
f.write(screen_content)
f.write("\n" + 80 * "/" + "\n")
def stop(self) -> None: def stop(self) -> None:
self._write(messages.DebugLinkStop()) self._write(messages.DebugLinkStop())
@ -882,7 +807,8 @@ class DebugUI:
# Paginating (going as further as possible) and pressing Yes # Paginating (going as further as possible) and pressing Yes
if br.pages is not None: if br.pages is not None:
for _ in range(br.pages - 1): for _ in range(br.pages - 1):
self.debuglink.swipe_up(wait=True) self.debuglink.swipe_up()
if self.debuglink.model is models.T3T1: if self.debuglink.model is models.T3T1:
layout = self.debuglink.read_layout() layout = self.debuglink.read_layout()
if "PromptScreen" in layout.all_components(): if "PromptScreen" in layout.all_components():

View File

@ -4136,6 +4136,7 @@ class DebugLinkGetState(protobuf.MessageType):
1: protobuf.Field("wait_word_list", "bool", repeated=False, required=False, default=None), 1: protobuf.Field("wait_word_list", "bool", repeated=False, required=False, default=None),
2: protobuf.Field("wait_word_pos", "bool", repeated=False, required=False, default=None), 2: protobuf.Field("wait_word_pos", "bool", repeated=False, required=False, default=None),
3: protobuf.Field("wait_layout", "DebugWaitType", repeated=False, required=False, default=DebugWaitType.IMMEDIATE), 3: protobuf.Field("wait_layout", "DebugWaitType", repeated=False, required=False, default=DebugWaitType.IMMEDIATE),
4: protobuf.Field("return_empty_state", "bool", repeated=False, required=False, default=False),
} }
def __init__( def __init__(
@ -4144,10 +4145,12 @@ class DebugLinkGetState(protobuf.MessageType):
wait_word_list: Optional["bool"] = None, wait_word_list: Optional["bool"] = None,
wait_word_pos: Optional["bool"] = None, wait_word_pos: Optional["bool"] = None,
wait_layout: Optional["DebugWaitType"] = DebugWaitType.IMMEDIATE, wait_layout: Optional["DebugWaitType"] = DebugWaitType.IMMEDIATE,
return_empty_state: Optional["bool"] = False,
) -> None: ) -> None:
self.wait_word_list = wait_word_list self.wait_word_list = wait_word_list
self.wait_word_pos = wait_word_pos self.wait_word_pos = wait_word_pos
self.wait_layout = wait_layout self.wait_layout = wait_layout
self.return_empty_state = return_empty_state
class DebugLinkState(protobuf.MessageType): class DebugLinkState(protobuf.MessageType):

View File

@ -1128,6 +1128,8 @@ pub struct DebugLinkGetState {
pub wait_word_pos: ::std::option::Option<bool>, pub wait_word_pos: ::std::option::Option<bool>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetState.wait_layout) // @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetState.wait_layout)
pub wait_layout: ::std::option::Option<::protobuf::EnumOrUnknown<debug_link_get_state::DebugWaitType>>, pub wait_layout: ::std::option::Option<::protobuf::EnumOrUnknown<debug_link_get_state::DebugWaitType>>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetState.return_empty_state)
pub return_empty_state: ::std::option::Option<bool>,
// special fields // special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.debug.DebugLinkGetState.special_fields) // @@protoc_insertion_point(special_field:hw.trezor.messages.debug.DebugLinkGetState.special_fields)
pub special_fields: ::protobuf::SpecialFields, pub special_fields: ::protobuf::SpecialFields,
@ -1204,8 +1206,27 @@ impl DebugLinkGetState {
self.wait_layout = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); self.wait_layout = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v));
} }
// optional bool return_empty_state = 4;
pub fn return_empty_state(&self) -> bool {
self.return_empty_state.unwrap_or(false)
}
pub fn clear_return_empty_state(&mut self) {
self.return_empty_state = ::std::option::Option::None;
}
pub fn has_return_empty_state(&self) -> bool {
self.return_empty_state.is_some()
}
// Param is passed by value, moved
pub fn set_return_empty_state(&mut self, v: bool) {
self.return_empty_state = ::std::option::Option::Some(v);
}
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
let mut fields = ::std::vec::Vec::with_capacity(3); let mut fields = ::std::vec::Vec::with_capacity(4);
let mut oneofs = ::std::vec::Vec::with_capacity(0); let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"wait_word_list", "wait_word_list",
@ -1222,6 +1243,11 @@ impl DebugLinkGetState {
|m: &DebugLinkGetState| { &m.wait_layout }, |m: &DebugLinkGetState| { &m.wait_layout },
|m: &mut DebugLinkGetState| { &mut m.wait_layout }, |m: &mut DebugLinkGetState| { &mut m.wait_layout },
)); ));
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"return_empty_state",
|m: &DebugLinkGetState| { &m.return_empty_state },
|m: &mut DebugLinkGetState| { &mut m.return_empty_state },
));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<DebugLinkGetState>( ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<DebugLinkGetState>(
"DebugLinkGetState", "DebugLinkGetState",
fields, fields,
@ -1249,6 +1275,9 @@ impl ::protobuf::Message for DebugLinkGetState {
24 => { 24 => {
self.wait_layout = ::std::option::Option::Some(is.read_enum_or_unknown()?); self.wait_layout = ::std::option::Option::Some(is.read_enum_or_unknown()?);
}, },
32 => {
self.return_empty_state = ::std::option::Option::Some(is.read_bool()?);
},
tag => { tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
}, },
@ -1270,6 +1299,9 @@ impl ::protobuf::Message for DebugLinkGetState {
if let Some(v) = self.wait_layout { if let Some(v) = self.wait_layout {
my_size += ::protobuf::rt::int32_size(3, v.value()); my_size += ::protobuf::rt::int32_size(3, v.value());
} }
if let Some(v) = self.return_empty_state {
my_size += 1 + 1;
}
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32); self.special_fields.cached_size().set(my_size as u32);
my_size my_size
@ -1285,6 +1317,9 @@ impl ::protobuf::Message for DebugLinkGetState {
if let Some(v) = self.wait_layout { if let Some(v) = self.wait_layout {
os.write_enum(3, ::protobuf::EnumOrUnknown::value(&v))?; os.write_enum(3, ::protobuf::EnumOrUnknown::value(&v))?;
} }
if let Some(v) = self.return_empty_state {
os.write_bool(4, v)?;
}
os.write_unknown_fields(self.special_fields.unknown_fields())?; os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(()) ::std::result::Result::Ok(())
} }
@ -1305,6 +1340,7 @@ impl ::protobuf::Message for DebugLinkGetState {
self.wait_word_list = ::std::option::Option::None; self.wait_word_list = ::std::option::Option::None;
self.wait_word_pos = ::std::option::Option::None; self.wait_word_pos = ::std::option::Option::None;
self.wait_layout = ::std::option::Option::None; self.wait_layout = ::std::option::Option::None;
self.return_empty_state = ::std::option::Option::None;
self.special_fields.clear(); self.special_fields.clear();
} }
@ -1313,6 +1349,7 @@ impl ::protobuf::Message for DebugLinkGetState {
wait_word_list: ::std::option::Option::None, wait_word_list: ::std::option::Option::None,
wait_word_pos: ::std::option::Option::None, wait_word_pos: ::std::option::Option::None,
wait_layout: ::std::option::Option::None, wait_layout: ::std::option::Option::None,
return_empty_state: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(), special_fields: ::protobuf::SpecialFields::new(),
}; };
&instance &instance
@ -3650,39 +3687,40 @@ static file_descriptor_proto_data: &'static [u8] = b"\
\x01\x20\x03(\tR\x06tokens:\x02\x18\x01\"-\n\x15DebugLinkReseedRandom\ \x01\x20\x03(\tR\x06tokens:\x02\x18\x01\"-\n\x15DebugLinkReseedRandom\
\x12\x14\n\x05value\x18\x01\x20\x01(\rR\x05value\"j\n\x15DebugLinkRecord\ \x12\x14\n\x05value\x18\x01\x20\x01(\rR\x05value\"j\n\x15DebugLinkRecord\
Screen\x12)\n\x10target_directory\x18\x01\x20\x01(\tR\x0ftargetDirectory\ Screen\x12)\n\x10target_directory\x18\x01\x20\x01(\tR\x0ftargetDirectory\
\x12&\n\rrefresh_index\x18\x02\x20\x01(\r:\x010R\x0crefreshIndex\"\x91\ \x12&\n\rrefresh_index\x18\x02\x20\x01(\r:\x010R\x0crefreshIndex\"\xc6\
\x02\n\x11DebugLinkGetState\x12(\n\x0ewait_word_list\x18\x01\x20\x01(\ \x02\n\x11DebugLinkGetState\x12(\n\x0ewait_word_list\x18\x01\x20\x01(\
\x08R\x0cwaitWordListB\x02\x18\x01\x12&\n\rwait_word_pos\x18\x02\x20\x01\ \x08R\x0cwaitWordListB\x02\x18\x01\x12&\n\rwait_word_pos\x18\x02\x20\x01\
(\x08R\x0bwaitWordPosB\x02\x18\x01\x12e\n\x0bwait_layout\x18\x03\x20\x01\ (\x08R\x0bwaitWordPosB\x02\x18\x01\x12e\n\x0bwait_layout\x18\x03\x20\x01\
(\x0e29.hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType:\tIMMED\ (\x0e29.hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType:\tIMMED\
IATER\nwaitLayout\"C\n\rDebugWaitType\x12\r\n\tIMMEDIATE\x10\0\x12\x0f\n\ IATER\nwaitLayout\x123\n\x12return_empty_state\x18\x04\x20\x01(\x08:\x05\
\x0bNEXT_LAYOUT\x10\x01\x12\x12\n\x0eCURRENT_LAYOUT\x10\x02\"\x97\x04\n\ falseR\x10returnEmptyState\"C\n\rDebugWaitType\x12\r\n\tIMMEDIATE\x10\0\
\x0eDebugLinkState\x12\x16\n\x06layout\x18\x01\x20\x01(\x0cR\x06layout\ \x12\x0f\n\x0bNEXT_LAYOUT\x10\x01\x12\x12\n\x0eCURRENT_LAYOUT\x10\x02\"\
\x12\x10\n\x03pin\x18\x02\x20\x01(\tR\x03pin\x12\x16\n\x06matrix\x18\x03\ \x97\x04\n\x0eDebugLinkState\x12\x16\n\x06layout\x18\x01\x20\x01(\x0cR\
\x20\x01(\tR\x06matrix\x12'\n\x0fmnemonic_secret\x18\x04\x20\x01(\x0cR\ \x06layout\x12\x10\n\x03pin\x18\x02\x20\x01(\tR\x03pin\x12\x16\n\x06matr\
\x0emnemonicSecret\x129\n\x04node\x18\x05\x20\x01(\x0b2%.hw.trezor.messa\ ix\x18\x03\x20\x01(\tR\x06matrix\x12'\n\x0fmnemonic_secret\x18\x04\x20\
ges.common.HDNodeTypeR\x04node\x123\n\x15passphrase_protection\x18\x06\ \x01(\x0cR\x0emnemonicSecret\x129\n\x04node\x18\x05\x20\x01(\x0b2%.hw.tr\
\x20\x01(\x08R\x14passphraseProtection\x12\x1d\n\nreset_word\x18\x07\x20\ ezor.messages.common.HDNodeTypeR\x04node\x123\n\x15passphrase_protection\
\x01(\tR\tresetWord\x12#\n\rreset_entropy\x18\x08\x20\x01(\x0cR\x0creset\ \x18\x06\x20\x01(\x08R\x14passphraseProtection\x12\x1d\n\nreset_word\x18\
Entropy\x12,\n\x12recovery_fake_word\x18\t\x20\x01(\tR\x10recoveryFakeWo\ \x07\x20\x01(\tR\tresetWord\x12#\n\rreset_entropy\x18\x08\x20\x01(\x0cR\
rd\x12*\n\x11recovery_word_pos\x18\n\x20\x01(\rR\x0frecoveryWordPos\x12$\ \x0cresetEntropy\x12,\n\x12recovery_fake_word\x18\t\x20\x01(\tR\x10recov\
\n\x0ereset_word_pos\x18\x0b\x20\x01(\rR\x0cresetWordPos\x12N\n\rmnemoni\ eryFakeWord\x12*\n\x11recovery_word_pos\x18\n\x20\x01(\rR\x0frecoveryWor\
c_type\x18\x0c\x20\x01(\x0e2).hw.trezor.messages.management.BackupTypeR\ dPos\x12$\n\x0ereset_word_pos\x18\x0b\x20\x01(\rR\x0cresetWordPos\x12N\n\
\x0cmnemonicType\x12\x16\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\"\x0f\n\ \rmnemonic_type\x18\x0c\x20\x01(\x0e2).hw.trezor.messages.management.Bac\
\rDebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\x20\x01(\ kupTypeR\x0cmnemonicType\x12\x16\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\
\rR\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\x12\x12\n\ \"\x0f\n\rDebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\
\x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRead\x12\x18\ \x20\x01(\rR\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\
\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06length\x18\x02\ \x12\x12\n\x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRe\
\x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06memory\x18\ ad\x12\x18\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06leng\
\x01\x20\x01(\x0cR\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\x18\n\x07a\ th\x18\x02\x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06me\
ddress\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06memory\x18\x02\x20\ mory\x18\x01\x20\x01(\x0cR\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\
\x01(\x0cR\x06memory\x12\x14\n\x05flash\x18\x03\x20\x01(\x08R\x05flash\"\ \x18\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06memory\x18\
-\n\x13DebugLinkFlashErase\x12\x16\n\x06sector\x18\x01\x20\x01(\rR\x06se\ \x02\x20\x01(\x0cR\x06memory\x12\x14\n\x05flash\x18\x03\x20\x01(\x08R\
ctor\".\n\x14DebugLinkEraseSdCard\x12\x16\n\x06format\x18\x01\x20\x01(\ \x05flash\"-\n\x13DebugLinkFlashErase\x12\x16\n\x06sector\x18\x01\x20\
\x08R\x06format\"0\n\x14DebugLinkWatchLayout\x12\x14\n\x05watch\x18\x01\ \x01(\rR\x06sector\".\n\x14DebugLinkEraseSdCard\x12\x16\n\x06format\x18\
\x20\x01(\x08R\x05watch:\x02\x18\x01\"\x1f\n\x19DebugLinkResetDebugEvent\ \x01\x20\x01(\x08R\x06format\"0\n\x14DebugLinkWatchLayout\x12\x14\n\x05w\
s:\x02\x18\x01\"\x1a\n\x18DebugLinkOptigaSetSecMaxB=\n#com.satoshilabs.t\ atch\x18\x01\x20\x01(\x08R\x05watch:\x02\x18\x01\"\x1f\n\x19DebugLinkRes\
rezor.lib.protobufB\x12TrezorMessageDebug\x80\xa6\x1d\x01\ etDebugEvents:\x02\x18\x01\"\x1a\n\x18DebugLinkOptigaSetSecMaxB=\n#com.s\
atoshilabs.trezor.lib.protobufB\x12TrezorMessageDebug\x80\xa6\x1d\x01\
"; ";
/// `FileDescriptorProto` object which was a source for this generated file /// `FileDescriptorProto` object which was a source for this generated file

View File

@ -49,36 +49,27 @@ def get_char_category(char: str) -> PassphraseCategory:
def go_next(debug: "DebugLink") -> LayoutContent: def go_next(debug: "DebugLink") -> LayoutContent:
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
return debug.click(buttons.OK) debug.click(buttons.OK)
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
return debug.press_right() debug.press_right()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
return debug.swipe_up() debug.swipe_up()
else:
raise RuntimeError("Unknown model")
def tap_to_confirm(debug: "DebugLink") -> LayoutContent:
if debug.layout_type is LayoutType.Bolt:
return debug.read_layout()
elif debug.layout_type is LayoutType.Caesar:
return debug.read_layout()
elif debug.layout_type is LayoutType.Delizia:
return debug.click(buttons.TAP_TO_CONFIRM)
else: else:
raise RuntimeError("Unknown model") raise RuntimeError("Unknown model")
return debug.read_layout()
def go_back(debug: "DebugLink", r_middle: bool = False) -> LayoutContent: def go_back(debug: "DebugLink", r_middle: bool = False) -> LayoutContent:
if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia): if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
return debug.click(buttons.CANCEL) debug.click(buttons.CANCEL)
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
if r_middle: if r_middle:
return debug.press_middle() debug.press_middle()
else: else:
return debug.press_left() debug.press_left()
else: else:
raise RuntimeError("Unknown model") raise RuntimeError("Unknown model")
return debug.read_layout()
def navigate_to_action_and_press( def navigate_to_action_and_press(
@ -108,10 +99,10 @@ def navigate_to_action_and_press(
if steps < 0: if steps < 0:
for _ in range(-steps): for _ in range(-steps):
layout = debug.press_left() debug.press_left()
else: else:
for _ in range(steps): for _ in range(steps):
layout = debug.press_right() debug.press_right()
# Press or hold # Press or hold
debug.press_middle(hold_ms=hold_ms) debug.press_middle(hold_ms=hold_ms)
@ -125,13 +116,14 @@ def _carousel_steps(current_index: int, wanted_index: int, length: int) -> int:
def unlock_gesture(debug: "DebugLink") -> LayoutContent: def unlock_gesture(debug: "DebugLink") -> LayoutContent:
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
return debug.click(buttons.OK) debug.click(buttons.OK)
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
return debug.press_right() debug.press_right()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
return debug.click(buttons.TAP_TO_CONFIRM) debug.click(buttons.TAP_TO_CONFIRM)
else: else:
raise RuntimeError("Unknown model") raise RuntimeError("Unknown model")
return debug.read_layout()
def _get_action_index(wanted_action: str, all_actions: AllActionsType) -> int: def _get_action_index(wanted_action: str, all_actions: AllActionsType) -> int:

View File

@ -23,7 +23,8 @@ def enter_word(
if debug.layout_type is LayoutType.Delizia and not is_slip39 and len(word) > 4: if debug.layout_type is LayoutType.Delizia and not is_slip39 and len(word) > 4:
# T3T1 (delizia) BIP39 keyboard allows to "confirm" only if the word is fully written, you need to click the word to auto-complete # T3T1 (delizia) BIP39 keyboard allows to "confirm" only if the word is fully written, you need to click the word to auto-complete
debug.click(buttons.CONFIRM_WORD) debug.click(buttons.CONFIRM_WORD)
return debug.click(buttons.CONFIRM_WORD) debug.click(buttons.CONFIRM_WORD)
return debug.read_layout()
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
letter_index = 0 letter_index = 0
layout = debug.read_layout() layout = debug.read_layout()
@ -32,16 +33,20 @@ def enter_word(
while layout.find_values_by_key("letter_choices"): while layout.find_values_by_key("letter_choices"):
letter = word[letter_index] letter = word[letter_index]
while not layout.get_middle_choice() == letter: while not layout.get_middle_choice() == letter:
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
layout = debug.press_middle() debug.press_middle()
layout = debug.read_layout()
letter_index += 1 letter_index += 1
# Word choices # Word choices
while not layout.get_middle_choice() == word: while not layout.get_middle_choice() == word:
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
return debug.press_middle() debug.press_middle()
return debug.read_layout()
else: else:
raise ValueError("Unknown model") raise ValueError("Unknown model")
@ -78,7 +83,8 @@ def select_number_of_words(
coords = coords_map.get(num_of_words) coords = coords_map.get(num_of_words)
if coords is None: if coords is None:
raise ValueError("Invalid num_of_words") raise ValueError("Invalid num_of_words")
return debug.click(coords) debug.click(coords)
return debug.read_layout()
def select_caesar() -> "LayoutContent": def select_caesar() -> "LayoutContent":
# navigate to the number and confirm it # navigate to the number and confirm it
@ -86,7 +92,8 @@ def select_number_of_words(
index = word_options.index(num_of_words) index = word_options.index(num_of_words)
for _ in range(index): for _ in range(index):
debug.press_right() debug.press_right()
return debug.press_middle() debug.press_middle()
return debug.read_layout()
def select_delizia() -> "LayoutContent": def select_delizia() -> "LayoutContent":
# click the button from ValuePad # click the button from ValuePad
@ -103,13 +110,15 @@ def select_number_of_words(
coords = coords_map.get(num_of_words) coords = coords_map.get(num_of_words)
if coords is None: if coords is None:
raise ValueError("Invalid num_of_words") raise ValueError("Invalid num_of_words")
return debug.click(coords) debug.click(coords)
return debug.read_layout()
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
assert debug.read_layout().text_content() == TR.recovery__num_of_words assert debug.read_layout().text_content() == TR.recovery__num_of_words
layout = select_bolt() layout = select_bolt()
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert layout.title() == TR.word_count__title assert layout.title() == TR.word_count__title
layout = select_caesar() layout = select_caesar()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
@ -150,12 +159,15 @@ def enter_share(
assert TR.translate(before_title) in debug.read_layout().title() assert TR.translate(before_title) in debug.read_layout().title()
layout = debug.read_layout() layout = debug.read_layout()
for _ in range(layout.page_count()): for _ in range(layout.page_count()):
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
layout = debug.swipe_up() debug.swipe_up()
layout = debug.read_layout()
else: else:
assert TR.translate(before_title) in debug.read_layout().title() assert TR.translate(before_title) in debug.read_layout().title()
layout = debug.click(buttons.OK) debug.click(buttons.OK)
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components() assert "MnemonicKeyboard" in layout.all_components()
@ -236,13 +248,17 @@ def enter_seed_previous_correct(
layout = debug.read_layout() layout = debug.read_layout()
while layout.get_middle_choice() not in DELETE_BTNS: while layout.get_middle_choice() not in DELETE_BTNS:
layout = debug.press_right() debug.press_right()
layout = debug.press_middle() layout = debug.read_layout()
debug.press_middle()
layout = debug.read_layout()
for _ in range(len(bad_word)): for _ in range(len(bad_word)):
while layout.get_middle_choice() not in DELETE_BTNS: while layout.get_middle_choice() not in DELETE_BTNS:
layout = debug.press_left() debug.press_left()
layout = debug.press_middle() layout = debug.read_layout()
debug.press_middle()
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
debug.click(buttons.RECOVERY_DELETE) # Top-left debug.click(buttons.RECOVERY_DELETE) # Top-left
for _ in range(len(bad_word)): for _ in range(len(bad_word)):
@ -278,7 +294,8 @@ def prepare_enter_seed(
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
debug.press_right() debug.press_right()
debug.press_right() debug.press_right()
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components() assert "MnemonicKeyboard" in layout.all_components()

View File

@ -79,7 +79,8 @@ def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> Non
in TR.reset__title_number_of_shares + TR.words__title_threshold in TR.reset__title_number_of_shares + TR.words__title_threshold
): ):
# Special info screens # Special info screens
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert "NumberInput" in layout.all_components() assert "NumberInput" in layout.all_components()
if button == buttons.reset_minus(debug.model.internal_name): if button == buttons.reset_minus(debug.model.internal_name):
for _ in range(diff): for _ in range(diff):
@ -102,7 +103,8 @@ def read_words(debug: "DebugLink", do_htc: bool = True) -> list[str]:
layout = debug.read_layout() layout = debug.read_layout()
for _ in range(layout.page_count() - 1): for _ in range(layout.page_count() - 1):
words.extend(layout.seed_words()) words.extend(layout.seed_words())
layout = debug.swipe_up() debug.swipe_up()
layout = debug.read_layout()
assert layout is not None assert layout is not None
if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia): if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
words.extend(layout.seed_words()) words.extend(layout.seed_words())
@ -147,7 +149,8 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
] ]
wanted_word = words[word_pos - 1].lower() wanted_word = words[word_pos - 1].lower()
button_pos = btn_texts.index(wanted_word) button_pos = btn_texts.index(wanted_word)
layout = debug.click(buttons.RESET_WORD_CHECK[button_pos]) debug.click(buttons.RESET_WORD_CHECK[button_pos])
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
assert TR.regexp("reset__select_word_x_of_y_template").match(layout.subtitle()) assert TR.regexp("reset__select_word_x_of_y_template").match(layout.subtitle())
for _ in range(3): for _ in range(3):
@ -162,10 +165,12 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
] ]
wanted_word = words[word_pos - 1].lower() wanted_word = words[word_pos - 1].lower()
button_pos = btn_texts.index(wanted_word) button_pos = btn_texts.index(wanted_word)
layout = debug.click(buttons.VERTICAL_MENU[button_pos]) debug.click(buttons.VERTICAL_MENU[button_pos])
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
assert TR.reset__select_correct_word in layout.text_content() assert TR.reset__select_correct_word in layout.text_content()
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
for _ in range(3): for _ in range(3):
# "SELECT 2ND WORD" # "SELECT 2ND WORD"
# ^ # ^
@ -176,9 +181,11 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
wanted_word = words[word_pos - 1].lower() wanted_word = words[word_pos - 1].lower()
while not layout.get_middle_choice() == wanted_word: while not layout.get_middle_choice() == wanted_word:
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
layout = debug.press_middle() debug.press_middle()
layout = debug.read_layout()
def validate_mnemonics(mnemonics: list[str], expected_ems: bytes) -> None: def validate_mnemonics(mnemonics: list[str], expected_ems: bytes) -> None:

View File

@ -30,7 +30,7 @@ from .. import translations as TR
from ..device_tests.bitcoin.payment_req import make_coinjoin_request from ..device_tests.bitcoin.payment_req import make_coinjoin_request
from ..tx_cache import TxCache from ..tx_cache import TxCache
from . import recovery from . import recovery
from .common import go_next, tap_to_confirm, unlock_gesture from .common import go_next, unlock_gesture
if TYPE_CHECKING: if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink, LayoutContent from trezorlib.debuglink import DebugLink, LayoutContent
@ -71,7 +71,8 @@ def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int)
layout = go_next(debug) layout = go_next(debug)
if debug.layout_type is LayoutType.Delizia: if debug.layout_type is LayoutType.Delizia:
layout = tap_to_confirm(debug) debug.click(buttons.TAP_TO_CONFIRM)
layout = debug.read_layout()
assert layout.main_component() == "Homescreen" assert layout.main_component() == "Homescreen"
device_handler.result() device_handler.result()
@ -106,17 +107,20 @@ def test_autolock_interrupts_signing(device_handler: "BackgroundDeviceHandler"):
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK) debug.click(buttons.OK)
layout = debug.click(buttons.OK) debug.click(buttons.OK)
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content() assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content() assert "0.0039 BTC" in layout.text_content()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
debug.swipe_up() debug.swipe_up()
layout = debug.swipe_up() debug.swipe_up()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content() assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content() assert "0.0039 BTC" in layout.text_content()
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
debug.press_right() debug.press_right()
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content() assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content() assert "0.0039 BTC" in layout.text_content()
@ -158,18 +162,21 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK) debug.click(buttons.OK)
layout = debug.click(buttons.OK) debug.click(buttons.OK)
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content() assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content() assert "0.0039 BTC" in layout.text_content()
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
debug.swipe_up() debug.swipe_up()
layout = debug.swipe_up() debug.swipe_up()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content() assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content() assert "0.0039 BTC" in layout.text_content()
debug.swipe_up() debug.swipe_up()
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
debug.press_right() debug.press_right()
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content() assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content() assert "0.0039 BTC" in layout.text_content()
@ -181,17 +188,12 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa
with device_handler.client: with device_handler.client:
device_handler.client.set_filter(messages.TxAck, sleepy_filter) device_handler.client.set_filter(messages.TxAck, sleepy_filter)
# confirm transaction # confirm transaction
# In all cases we set wait=False to avoid waiting for the screen and triggering
# the layout deadlock detection. In reality there is no deadlock but the
# `sleepy_filter` delays the response by 10 secs while the layout deadlock
# timeout is 3. In this test we don't need the result of the input event so
# waiting for it is not necessary.
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK, wait=False) debug.click(buttons.OK)
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
debug.click(buttons.TAP_TO_CONFIRM, wait=False) debug.click(buttons.TAP_TO_CONFIRM)
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
debug.press_middle(wait=False) debug.press_middle()
signatures, tx = device_handler.result() signatures, tx = device_handler.result()
assert len(signatures) == 1 assert len(signatures) == 1
@ -277,7 +279,8 @@ def unlock_dry_run(debug: "DebugLink") -> "LayoutContent":
layout = go_next(debug) layout = go_next(debug)
assert "PinKeyboard" in layout.all_components() assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4) debug.input(PIN4)
layout = debug.read_layout()
assert layout is not None assert layout is not None
return layout return layout
@ -307,7 +310,8 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle
layout = unlock_gesture(debug) layout = unlock_gesture(debug)
assert "PinKeyboard" in layout.all_components() assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4) debug.input(PIN4)
layout = debug.read_layout()
assert layout is not None assert layout is not None
# we are back at homescreen # we are back at homescreen
@ -330,7 +334,8 @@ def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"):
layout = go_next(debug) layout = go_next(debug)
assert layout.main_component() == "MnemonicKeyboard" assert layout.main_component() == "MnemonicKeyboard"
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components() assert "MnemonicKeyboard" in layout.all_components()
# make sure keyboard locks # make sure keyboard locks
@ -353,31 +358,36 @@ def test_dryrun_enter_word_slowly(device_handler: "BackgroundDeviceHandler"):
recovery.select_number_of_words(debug, 20) recovery.select_number_of_words(debug, 20)
if debug.layout_type is LayoutType.Bolt: if debug.layout_type is LayoutType.Bolt:
layout = debug.click(buttons.OK) debug.click(buttons.OK)
layout = debug.read_layout()
assert layout.main_component() == "MnemonicKeyboard" assert layout.main_component() == "MnemonicKeyboard"
# type the word OCEAN slowly # type the word OCEAN slowly
for coords in buttons.type_word("ocea", is_slip39=True): for coords in buttons.type_word("ocea", is_slip39=True):
time.sleep(9) time.sleep(9)
debug.click(coords) debug.click(coords)
layout = debug.click(buttons.CONFIRM_WORD) debug.click(buttons.CONFIRM_WORD)
layout = debug.read_layout()
# should not have locked, even though we took 9 seconds to type each letter # should not have locked, even though we took 9 seconds to type each letter
assert layout.main_component() == "MnemonicKeyboard" assert layout.main_component() == "MnemonicKeyboard"
elif debug.layout_type is LayoutType.Delizia: elif debug.layout_type is LayoutType.Delizia:
layout = debug.swipe_up() debug.swipe_up()
layout = debug.read_layout()
assert layout.main_component() == "MnemonicKeyboard" assert layout.main_component() == "MnemonicKeyboard"
# type the word OCEAN slowly # type the word OCEAN slowly
for coords in buttons.type_word("ocea", is_slip39=True): for coords in buttons.type_word("ocea", is_slip39=True):
time.sleep(9) time.sleep(9)
debug.click(coords) debug.click(coords)
layout = debug.click(buttons.CONFIRM_WORD) debug.click(buttons.CONFIRM_WORD)
layout = debug.read_layout()
# should not have locked, even though we took 9 seconds to type each letter # should not have locked, even though we took 9 seconds to type each letter
assert layout.main_component() == "MnemonicKeyboard" assert layout.main_component() == "MnemonicKeyboard"
elif debug.layout_type is LayoutType.Caesar: elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right() debug.press_right()
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components() assert "MnemonicKeyboard" in layout.all_components()
# pressing middle button three times # pressing middle button three times

View File

@ -79,9 +79,10 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
# unlock by touching # unlock by touching
if debug.layout_type is LayoutType.Caesar: if debug.layout_type is LayoutType.Caesar:
layout = debug.press_right() debug.press_right()
else: else:
layout = debug.click(buttons.INFO) debug.click(buttons.INFO)
layout = debug.read_layout()
assert "PinKeyboard" in layout.all_components() assert "PinKeyboard" in layout.all_components()
debug.input("1234") debug.input("1234")

View File

@ -212,6 +212,11 @@ def _cancel_pin(debug: "DebugLink") -> None:
# TODO: implement cancel PIN for TR? # TODO: implement cancel PIN for TR?
_delete_pin(debug, 1, check=False) _delete_pin(debug, 1, check=False)
# Note: `prepare()` context manager will send a tap after PIN cancellation,
# so we make sure the lockscreen is already up to receive it -- otherwise
# the input event may get lost in the loop restart.
assert debug.read_layout().main_component() != "PinKeyboard"
def _confirm_pin(debug: "DebugLink") -> None: def _confirm_pin(debug: "DebugLink") -> None:
"""Navigate to "ENTER" and press it""" """Navigate to "ENTER" and press it"""

View File

@ -56,7 +56,8 @@ def go_through_tutorial_tr(debug: "DebugLink") -> None:
debug.press_right(hold_ms=1000) debug.press_right(hold_ms=1000)
debug.press_right() debug.press_right()
debug.press_right() debug.press_right()
layout = debug.press_middle() debug.press_middle()
layout = debug.read_layout()
assert layout.title() == TR.tutorial__title_tutorial_complete assert layout.title() == TR.tutorial__title_tutorial_complete

View File

@ -38,18 +38,17 @@ def test_tutorial_ignore_menu(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink() debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial) device_handler.run(device.show_device_tutorial)
layout = debug.read_layout() assert debug.read_layout().title() == TR.tutorial__welcome_safe5
assert layout.title() == TR.tutorial__welcome_safe5 debug.click(buttons.TAP_TO_CONFIRM)
layout = debug.click(buttons.TAP_TO_CONFIRM) assert debug.read_layout().title() == TR.tutorial__title_lets_begin
assert layout.title() == TR.tutorial__title_lets_begin debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
assert layout.title() == TR.tutorial__title_easy_navigation debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_handy_menu
assert layout.title() == TR.tutorial__title_handy_menu debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_hold
assert layout.title() == TR.tutorial__title_hold debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000) assert debug.read_layout().title() == TR.tutorial__title_well_done
assert layout.title() == TR.tutorial__title_well_done
debug.swipe_up() debug.swipe_up()
device_handler.result() device_handler.result()
@ -59,24 +58,23 @@ def test_tutorial_menu_open_close(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink() debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial) device_handler.run(device.show_device_tutorial)
layout = debug.read_layout() assert debug.read_layout().title() == TR.tutorial__welcome_safe5
assert layout.title() == TR.tutorial__welcome_safe5 debug.click(buttons.TAP_TO_CONFIRM)
layout = debug.click(buttons.TAP_TO_CONFIRM) assert debug.read_layout().title() == TR.tutorial__title_lets_begin
assert layout.title() == TR.tutorial__title_lets_begin debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
assert layout.title() == TR.tutorial__title_easy_navigation debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_handy_menu
assert layout.title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content() assert TR.tutorial__did_you_know in debug.read_layout().text_content()
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert layout.title() == TR.tutorial__title_handy_menu assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up() debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold assert debug.read_layout().title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000) debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up() debug.swipe_up()
device_handler.result() device_handler.result()
@ -86,21 +84,20 @@ def test_tutorial_menu_exit(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink() debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial) device_handler.run(device.show_device_tutorial)
layout = debug.read_layout() assert debug.read_layout().title() == TR.tutorial__welcome_safe5
assert layout.title() == TR.tutorial__welcome_safe5 debug.click(buttons.TAP_TO_CONFIRM)
layout = debug.click(buttons.TAP_TO_CONFIRM) assert debug.read_layout().title() == TR.tutorial__title_lets_begin
assert layout.title() == TR.tutorial__title_lets_begin debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
assert layout.title() == TR.tutorial__title_easy_navigation debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_handy_menu
assert layout.title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content() assert TR.tutorial__did_you_know in debug.read_layout().text_content()
layout = debug.click(buttons.VERTICAL_MENU[2]) debug.click(buttons.VERTICAL_MENU[2])
assert TR.instructions__hold_to_exit_tutorial in layout.footer() assert TR.instructions__hold_to_exit_tutorial in debug.read_layout().footer()
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000) debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up() debug.swipe_up()
device_handler.result() device_handler.result()
@ -110,28 +107,27 @@ def test_tutorial_menu_repeat(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink() debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial) device_handler.run(device.show_device_tutorial)
layout = debug.read_layout() assert debug.read_layout().title() == TR.tutorial__welcome_safe5
assert layout.title() == TR.tutorial__welcome_safe5 debug.click(buttons.TAP_TO_CONFIRM)
layout = debug.click(buttons.TAP_TO_CONFIRM) assert debug.read_layout().title() == TR.tutorial__title_lets_begin
assert layout.title() == TR.tutorial__title_lets_begin debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
assert layout.title() == TR.tutorial__title_easy_navigation debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_handy_menu
assert layout.title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content() assert TR.tutorial__did_you_know in debug.read_layout().text_content()
layout = debug.click(buttons.VERTICAL_MENU[1]) debug.click(buttons.VERTICAL_MENU[1])
assert layout.title() == TR.tutorial__title_lets_begin assert debug.read_layout().title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up() debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up() debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up() debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold assert debug.read_layout().title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000) debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up() debug.swipe_up()
device_handler.result() device_handler.result()
@ -141,29 +137,30 @@ def test_tutorial_menu_funfact(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink() debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial) device_handler.run(device.show_device_tutorial)
layout = debug.read_layout() assert debug.read_layout().title() == TR.tutorial__welcome_safe5
assert layout.title() == TR.tutorial__welcome_safe5 debug.click(buttons.TAP_TO_CONFIRM)
layout = debug.click(buttons.TAP_TO_CONFIRM) assert debug.read_layout().title() == TR.tutorial__title_lets_begin
assert layout.title() == TR.tutorial__title_lets_begin debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
assert layout.title() == TR.tutorial__title_easy_navigation debug.swipe_up()
layout = debug.swipe_up() assert debug.read_layout().title() == TR.tutorial__title_handy_menu
assert layout.title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content() assert TR.tutorial__did_you_know in debug.read_layout().text_content()
layout = debug.click(buttons.VERTICAL_MENU[0]) debug.click(buttons.VERTICAL_MENU[0])
assert layout.text_content() in TR.tutorial__first_wallet.replace("\n", " ") assert debug.read_layout().text_content() in TR.tutorial__first_wallet.replace(
"\n", " "
)
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content() assert TR.tutorial__did_you_know in debug.read_layout().text_content()
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
assert layout.title() == TR.tutorial__title_handy_menu assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up() debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold assert debug.read_layout().title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000) debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up() debug.swipe_up()
device_handler.result() device_handler.result()

View File

@ -323,7 +323,8 @@ def click_info_button_bolt(debug: "DebugLink") -> Generator[Any, Any, ButtonRequ
def click_info_button_delizia(debug: "DebugLink"): def click_info_button_delizia(debug: "DebugLink"):
"""Click Shamir backup info button and return back.""" """Click Shamir backup info button and return back."""
layout = debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)
layout = debug.read_layout()
assert "VerticalMenu" in layout.all_components() assert "VerticalMenu" in layout.all_components()
debug.click(buttons.VERTICAL_MENU[0]) debug.click(buttons.VERTICAL_MENU[0])
debug.click(buttons.CORNER_BUTTON) debug.click(buttons.CORNER_BUTTON)

View File

@ -375,7 +375,6 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: pytest.ExitCode) -
exitstatus, exitstatus,
test_ui, # type: ignore test_ui, # type: ignore
bool(session.config.getoption("ui_check_missing")), bool(session.config.getoption("ui_check_missing")),
bool(session.config.getoption("record_text_layout")),
bool(session.config.getoption("do_master_diff")), bool(session.config.getoption("do_master_diff")),
) )

View File

@ -347,10 +347,12 @@ class InputFlowShowAddressQRCode(InputFlowBase):
# really cancel # really cancel
self.debug.click(buttons.CORNER_BUTTON) self.debug.click(buttons.CORNER_BUTTON)
# menu # menu
layout = self.debug.click(buttons.CORNER_BUTTON) self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.read_layout()
while "PromptScreen" not in layout.all_components(): while "PromptScreen" not in layout.all_components():
layout = self.debug.swipe_up() self.debug.swipe_up()
layout = self.debug.read_layout()
self.debug.synchronize_at("PromptScreen") self.debug.synchronize_at("PromptScreen")
# tap to confirm # tap to confirm
self.debug.click(buttons.TAP_TO_CONFIRM) self.debug.click(buttons.TAP_TO_CONFIRM)
@ -437,14 +439,16 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
self.debug.click(buttons.CORNER_BUTTON) self.debug.click(buttons.CORNER_BUTTON)
assert "Qr" in self.all_components() assert "Qr" in self.all_components()
layout = self.debug.swipe_left() self.debug.swipe_left()
layout = self.debug.read_layout()
# address details # address details
assert "Multisig 2 of 3" in layout.screen_content() assert "Multisig 2 of 3" in layout.screen_content()
assert TR.address_details__derivation_path in layout.screen_content() assert TR.address_details__derivation_path in layout.screen_content()
# Three xpub pages with the same testing logic # Three xpub pages with the same testing logic
for xpub_num in range(3): for xpub_num in range(3):
layout = self.debug.swipe_left() self.debug.swipe_left()
layout = self.debug.read_layout()
self._assert_xpub_title(layout.title(), xpub_num) self._assert_xpub_title(layout.title(), xpub_num)
content = layout.text_content().replace(" ", "") content = layout.text_content().replace(" ", "")
assert self.xpubs[xpub_num] in content assert self.xpubs[xpub_num] in content
@ -470,18 +474,21 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
self.debug.press_right() self.debug.press_right()
assert "Qr" in self.all_components() assert "Qr" in self.all_components()
layout = self.debug.press_right() self.debug.press_right()
layout = self.debug.read_layout()
# address details # address details
# TODO: locate it more precisely # TODO: locate it more precisely
assert "Multisig 2 of 3" in layout.json_str assert "Multisig 2 of 3" in layout.json_str
# Three xpub pages with the same testing logic # Three xpub pages with the same testing logic
for xpub_num in range(3): for xpub_num in range(3):
layout = self.debug.press_right() self.debug.press_right()
layout = self.debug.read_layout()
self._assert_xpub_title(layout.title(), xpub_num) self._assert_xpub_title(layout.title(), xpub_num)
xpub_part_1 = layout.text_content().replace(" ", "") xpub_part_1 = layout.text_content().replace(" ", "")
# Press "SHOW MORE" # Press "SHOW MORE"
layout = self.debug.press_middle() self.debug.press_middle()
layout = self.debug.read_layout()
xpub_part_2 = layout.text_content().replace(" ", "") xpub_part_2 = layout.text_content().replace(" ", "")
# Go back # Go back
self.debug.press_left() self.debug.press_left()
@ -525,24 +532,25 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
# three xpub pages with the same testing logic # three xpub pages with the same testing logic
for _xpub_num in range(3): for _xpub_num in range(3):
layout = self.debug.swipe_left() self.debug.swipe_left()
layout = self.debug.swipe_left() self.debug.swipe_left()
self.debug.click(buttons.CORNER_BUTTON) self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.synchronize_at("VerticalMenu") self.debug.synchronize_at("VerticalMenu")
# menu # menu
self.debug.click(buttons.VERTICAL_MENU[2]) self.debug.click(buttons.VERTICAL_MENU[2])
# cancel # cancel
self.debug.swipe_up() self.debug.swipe_up()
# really cancel # really cancel
self.debug.click(buttons.CORNER_BUTTON) self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.synchronize_at("VerticalMenu") self.debug.synchronize_at("VerticalMenu")
# menu # menu
self.debug.click(buttons.CORNER_BUTTON) self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.synchronize_at("Paragraphs") layout = self.debug.synchronize_at("Paragraphs")
# address # address
while "PromptScreen" not in layout.all_components(): while "PromptScreen" not in layout.all_components():
layout = self.debug.swipe_up() self.debug.swipe_up()
layout = self.debug.read_layout()
self.debug.synchronize_at("PromptScreen") self.debug.synchronize_at("PromptScreen")
# tap to confirm # tap to confirm
self.debug.press_yes() self.debug.press_yes()
@ -652,7 +660,8 @@ class InputFlowShowXpubQRCode(InputFlowBase):
layout = self.debug.synchronize_at("Paragraphs") layout = self.debug.synchronize_at("Paragraphs")
# address # address
while "PromptScreen" not in layout.all_components(): while "PromptScreen" not in layout.all_components():
layout = self.debug.swipe_up() self.debug.swipe_up()
layout = self.debug.read_layout()
self.debug.synchronize_at("PromptScreen") self.debug.synchronize_at("PromptScreen")
# tap to confirm # tap to confirm
self.debug.press_yes() self.debug.press_yes()
@ -834,10 +843,12 @@ def sign_tx_go_to_info_tr(
client.debug.press_middle() client.debug.press_middle()
yield yield
layout = client.debug.press_right() client.debug.press_right()
layout = client.debug.read_layout()
screen_texts.append(layout.visible_screen()) screen_texts.append(layout.visible_screen())
layout = client.debug.press_right() client.debug.press_right()
layout = client.debug.read_layout()
screen_texts.append(layout.visible_screen()) screen_texts.append(layout.visible_screen())
client.debug.press_left() client.debug.press_left()

View File

@ -179,7 +179,8 @@ class RecoveryFlow:
self.debug.synchronize_at("VerticalMenu") self.debug.synchronize_at("VerticalMenu")
self.debug.click(buttons.VERTICAL_MENU[0]) self.debug.click(buttons.VERTICAL_MENU[0])
assert (yield).name == "abort_recovery" assert (yield).name == "abort_recovery"
layout = self.debug.swipe_up() self.debug.swipe_up()
layout = self.debug.read_layout()
assert layout.title() == TR.recovery__title_cancel_recovery assert layout.title() == TR.recovery__title_cancel_recovery
self.debug.click(buttons.TAP_TO_CONFIRM) self.debug.click(buttons.TAP_TO_CONFIRM)
else: else:

View File

@ -44,8 +44,6 @@ def screen_recording(
yield yield
return return
record_text_layout = request.config.getoption("record_text_layout")
testcase = TestCase.build(client, request) testcase = TestCase.build(client, request)
testcase.dir.mkdir(exist_ok=True, parents=True) testcase.dir.mkdir(exist_ok=True, parents=True)
@ -55,9 +53,6 @@ def screen_recording(
try: try:
client.debug.start_recording(str(testcase.actual_dir)) client.debug.start_recording(str(testcase.actual_dir))
if record_text_layout:
client.debug.set_screen_text_file(testcase.screen_text_file)
client.debug.watch_layout(True)
yield yield
finally: finally:
client.ensure_open() client.ensure_open()
@ -65,9 +60,6 @@ def screen_recording(
# Wait for response to Initialize, which gives the emulator time to catch up # Wait for response to Initialize, which gives the emulator time to catch up
# and redraw the homescreen. Otherwise there's a race condition between that # and redraw the homescreen. Otherwise there's a race condition between that
# and stopping recording. # and stopping recording.
if record_text_layout:
client.debug.set_screen_text_file(None)
client.debug.watch_layout(False)
client.init_device() client.init_device()
client.debug.stop_recording() client.debug.stop_recording()
@ -163,13 +155,12 @@ def sessionfinish(
exitstatus: pytest.ExitCode, exitstatus: pytest.ExitCode,
test_ui: str, test_ui: str,
check_missing: bool, check_missing: bool,
record_text_layout: bool,
do_master_diff: bool, do_master_diff: bool,
) -> pytest.ExitCode: ) -> pytest.ExitCode:
if not _should_write_ui_report(exitstatus): if not _should_write_ui_report(exitstatus):
return exitstatus return exitstatus
testreport.generate_reports(record_text_layout, do_master_diff) testreport.generate_reports(do_master_diff)
recents = list(TestResult.recent_results()) recents = list(TestResult.recent_results())

View File

@ -5,7 +5,6 @@ from collections import defaultdict
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import dominate
import dominate.tags as t import dominate.tags as t
from dominate.tags import ( from dominate.tags import (
a, a,
@ -31,7 +30,6 @@ from .common import REPORTS_PATH, document, generate_master_diff_report, get_dif
TESTREPORT_PATH = REPORTS_PATH / "test" TESTREPORT_PATH = REPORTS_PATH / "test"
IMAGES_PATH = TESTREPORT_PATH / "images" IMAGES_PATH = TESTREPORT_PATH / "images"
SCREEN_TEXT_FILE = TESTREPORT_PATH / "screen_text.txt"
# These two html files are referencing each other # These two html files are referencing each other
ALL_SCREENS = "all_screens.html" ALL_SCREENS = "all_screens.html"
@ -201,35 +199,6 @@ def all_unique_screens() -> Path:
return html.write(TESTREPORT_PATH, doc, ALL_UNIQUE_SCREENS) return html.write(TESTREPORT_PATH, doc, ALL_UNIQUE_SCREENS)
def screen_text_report() -> None:
"""Generate a report with text representation of all screens."""
recent_results = list(TestResult.recent_results())
# Creating both a text file (suitable for offline usage)
# and an HTML file (suitable for online usage).
with open(SCREEN_TEXT_FILE, "w") as f2:
for result in recent_results:
if not result.test.screen_text_file.exists():
continue
f2.write(f"\n{result.test.id}\n")
with open(result.test.screen_text_file, "r") as f:
for line in f.readlines():
f2.write(f"\t{line}")
doc = dominate.document(title="Screen text report")
with doc:
for result in recent_results:
if not result.test.screen_text_file.exists():
continue
with a(href=f"{ALL_SCREENS}#{result.test.id}"):
h2(result.test.id)
with open(result.test.screen_text_file, "r") as f:
for line in f.readlines():
p(line)
html.write(TESTREPORT_PATH, doc, "screen_text.html")
def differing_screens() -> None: def differing_screens() -> None:
"""Creating an HTML page showing all the unique screens that got changed.""" """Creating an HTML page showing all the unique screens that got changed."""
unique_diffs: set[tuple[str | None, str | None]] = set() unique_diffs: set[tuple[str | None, str | None]] = set()
@ -316,17 +285,13 @@ def master_index() -> Path:
return html.write(TESTREPORT_PATH, doc, "master_index.html") return html.write(TESTREPORT_PATH, doc, "master_index.html")
def generate_reports( def generate_reports(do_master_diff: bool = False) -> None:
do_screen_text: bool = False, do_master_diff: bool = False
) -> None:
"""Generate HTML reports for the test.""" """Generate HTML reports for the test."""
html.set_image_dir(IMAGES_PATH) html.set_image_dir(IMAGES_PATH)
index() index()
all_screens() all_screens()
all_unique_screens() all_unique_screens()
differing_screens() differing_screens()
if do_screen_text:
screen_text_report()
if do_master_diff: if do_master_diff:
master_diff() master_diff()
master_index() master_index()

View File

@ -11,17 +11,21 @@ def _enter_word(
) -> "LayoutContent": ) -> "LayoutContent":
typed_word = word[:4] typed_word = word[:4]
for coords in buttons.type_word(typed_word, is_slip39=is_slip39): for coords in buttons.type_word(typed_word, is_slip39=is_slip39):
debug.click(coords, wait=False) debug.click(coords)
debug.read_layout(wait=False)
return debug.click(buttons.CONFIRM_WORD, wait=True) debug.click(buttons.CONFIRM_WORD)
return debug.read_layout(wait=True)
def confirm_recovery(debug: "DebugLink") -> None: def confirm_recovery(debug: "DebugLink") -> None:
debug.click(buttons.OK) debug.click(buttons.OK)
debug.read_layout(wait=True)
def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None: def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None:
debug.click(buttons.OK) debug.click(buttons.OK)
debug.read_layout(wait=True)
# click the number # click the number
word_option_offset = 6 word_option_offset = 6
@ -31,12 +35,12 @@ def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None:
) # raises if num of words is invalid ) # raises if num of words is invalid
coords = buttons.grid34(index % 3, index // 3) coords = buttons.grid34(index % 3, index // 3)
debug.click(coords) debug.click(coords)
debug.read_layout(wait=True)
def enter_share( def enter_share(debug: "DebugLink", share: str) -> "LayoutContent":
debug: "DebugLink", share: str, is_first: bool = True debug.click(buttons.OK)
) -> "LayoutContent":
layout = debug.click(buttons.OK)
for word in share.split(" "): for word in share.split(" "):
layout = _enter_word(debug, word, is_slip39=True) _enter_word(debug, word, is_slip39=True)
return layout
return debug.read_layout(wait=True)