Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,518 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "applib/ui/recognizer/recognizer.h"
#include "applib/ui/recognizer/recognizer_impl.h"
#include "applib/ui/recognizer/recognizer_manager.h"
#include "applib/ui/recognizer/recognizer_private.h"
#include "util/size.h"
// Stubs
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_logging.h"
#include "test_recognizer_impl.h"
static bool s_manager_state_change = false;
void recognizer_manager_handle_state_change(RecognizerManager *manager, Recognizer *changed) {
s_manager_state_change = true;
}
static TestImplData s_test_impl_data;
// setup and teardown
void test_recognizer__initialize(void) {
s_test_impl_data = (TestImplData) {};
}
void test_recognizer__cleanup(void) {
}
// tests
void test_recognizer__create_with_data(void) {
int sub_data;
void *dummy = &sub_data;
RecognizerImpl s_test_impl = {
.handle_touch_event = dummy,
.cancel = dummy,
.reset = dummy
};
Recognizer *r = recognizer_create_with_data(&s_test_impl, &s_test_impl_data,
sizeof(s_test_impl_data), dummy,
&sub_data);
cl_assert(r != NULL);
cl_assert_equal_p(r->impl, &s_test_impl);
cl_assert_equal_m(r->impl_data, &s_test_impl_data, sizeof(s_test_impl_data));
cl_assert_equal_p(r->subscriber.event, dummy);
cl_assert_equal_p(r->subscriber.data, &sub_data);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_i(r->flags, 0);
cl_assert_equal_p(r->simultaneous_with_cb, NULL);
cl_assert_equal_p(r->fail_after, NULL);
cl_assert_passert(recognizer_create_with_data(NULL, &s_test_impl_data,
sizeof(s_test_impl_data), dummy,
NULL));
cl_assert_passert(recognizer_create_with_data(&s_test_impl, NULL,
sizeof(s_test_impl_data), dummy,
NULL));
cl_assert_passert(recognizer_create_with_data(&s_test_impl, &s_test_impl_data,
0, dummy, NULL));
cl_assert_equal_p(NULL, recognizer_create_with_data(&s_test_impl, &s_test_impl_data,
sizeof(s_test_impl_data), NULL, NULL));
s_test_impl.handle_touch_event = NULL;
cl_assert_passert(recognizer_create_with_data(&s_test_impl, &s_test_impl_data,
sizeof(s_test_impl_data), dummy,
NULL));
s_test_impl.handle_touch_event = dummy;
s_test_impl.reset = NULL;
cl_assert_passert(recognizer_create_with_data(&s_test_impl, &s_test_impl_data,
sizeof(s_test_impl_data), dummy,
NULL));
s_test_impl.reset = dummy;
s_test_impl.cancel = NULL;
cl_assert_passert(recognizer_create_with_data(&s_test_impl, &s_test_impl_data,
sizeof(s_test_impl_data), dummy,
NULL));
recognizer_destroy(r);
}
void test_recognizer__transition_state(void) {
RecognizerEvent event_type = -1;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, &event_type);
// Test that manager state change handler is called when not called from a touch event handler
recognizer_transition_state(r, RecognizerState_Failed);
cl_assert_equal_i(r->state, RecognizerState_Failed);
cl_assert(s_manager_state_change);
cl_assert_equal_i(event_type, -1);
r->state = RecognizerState_Possible;
s_manager_state_change = false;
recognizer_transition_state(r, RecognizerState_Completed);
cl_assert_equal_i(r->state, RecognizerState_Completed);
cl_assert(s_manager_state_change);
cl_assert_equal_i(event_type, RecognizerEvent_Completed);
s_manager_state_change = false;
r->handling_touch_event = true;
r->state = RecognizerState_Possible;
event_type = -1;
recognizer_transition_state(r, RecognizerState_Failed);
cl_assert(!s_manager_state_change);
cl_assert_equal_i(r->state, RecognizerState_Failed);
cl_assert_equal_i(event_type, -1);
// Test that invalid state transitions get caught by asserts
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Completed));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Started));
r->state = RecognizerState_Possible;
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Possible));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Updated));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Cancelled));
recognizer_transition_state(r, RecognizerState_Started);
cl_assert_equal_i(r->state, RecognizerState_Started);
cl_assert_equal_i(event_type, RecognizerEvent_Started);
cl_assert(!s_manager_state_change);
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Failed));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Possible));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Started));
recognizer_transition_state(r, RecognizerState_Updated);
cl_assert_equal_i(r->state, RecognizerState_Updated);
cl_assert_equal_i(event_type, RecognizerEvent_Updated);
cl_assert(!s_manager_state_change);
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Failed));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Possible));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Started));
event_type = -1;
recognizer_transition_state(r, RecognizerState_Updated);
cl_assert_equal_i(event_type, RecognizerEvent_Updated);
cl_assert_equal_i(r->state, RecognizerState_Updated);
cl_assert(!s_manager_state_change);
recognizer_transition_state(r, RecognizerState_Completed);
cl_assert_equal_i(event_type, RecognizerEvent_Completed);
cl_assert_equal_i(r->state, RecognizerState_Completed);
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Failed));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Possible));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Started));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Updated));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Cancelled));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Completed));
r->state = RecognizerState_Updated;
recognizer_transition_state(r, RecognizerState_Cancelled);
cl_assert_equal_i(event_type, RecognizerEvent_Cancelled);
cl_assert_equal_i(r->state, RecognizerState_Cancelled);
cl_assert(!s_manager_state_change);
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Failed));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Possible));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Started));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Updated));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Cancelled));
cl_assert_passert(recognizer_transition_state(r, RecognizerState_Completed));
r->state = RecognizerState_Started;
event_type = -1;
recognizer_transition_state(r, RecognizerState_Cancelled);
cl_assert_equal_i(event_type, RecognizerEvent_Cancelled);
cl_assert_equal_i(r->state, RecognizerState_Cancelled);
cl_assert(!s_manager_state_change);
r->state = RecognizerState_Started;
recognizer_transition_state(r, RecognizerState_Completed);
cl_assert_equal_i(event_type, RecognizerEvent_Completed);
cl_assert_equal_i(r->state, RecognizerState_Completed);
cl_assert(!s_manager_state_change);
r->state = RecognizerState_Possible;
recognizer_transition_state(r, RecognizerState_Completed);
cl_assert_equal_i(r->state, RecognizerState_Completed);
cl_assert(!s_manager_state_change);
}
void test_recognizer__set_failed(void) {
bool failed = false;
s_test_impl_data.failed = &failed;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, NULL);
recognizer_set_failed(r);
cl_assert_equal_i(r->state, RecognizerState_Failed);
cl_assert(failed);
// Failed -> Failed invalid transition
cl_assert_passert(recognizer_set_failed(r));
// (!Possible) -> Failed invalid transition
r->state = RecognizerState_Started;
cl_assert_passert(recognizer_set_failed(r));
r->state = RecognizerState_Completed;
cl_assert_passert(recognizer_set_failed(r));
}
static void prv_sub_destroy(const Recognizer *r) {
bool *destroyed = recognizer_get_user_data(r);
*destroyed = true;
}
void test_recognizer__destroy(void) {
bool impl_destroyed = false;
s_test_impl_data.destroyed = &impl_destroyed;
bool sub_destroyed = false;
Recognizer *r = test_recognizer_create(&s_test_impl_data, &sub_destroyed);
test_recognizer_enable_on_destroy();
recognizer_set_on_destroy(r, prv_sub_destroy);
// can't destroy a recognizer if it is owned
r->is_owned = true;
recognizer_destroy(r);
cl_assert_equal_b(impl_destroyed, false);
cl_assert_equal_b(sub_destroyed, false);
r->is_owned = false;
recognizer_destroy(r);
cl_assert_equal_b(impl_destroyed, true);
cl_assert_equal_b(sub_destroyed, true);
}
void test_recognizer__reset(void) {
bool reset = false;
bool cancelled = false;
s_test_impl_data.reset = &reset;
s_test_impl_data.cancelled = &cancelled;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, NULL);
recognizer_reset(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(reset, true);
cl_assert_equal_b(cancelled, false);
reset = false;
r->state = RecognizerState_Failed;
recognizer_reset(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(reset, true);
cl_assert_equal_b(cancelled, false);
reset = false;
r->state = RecognizerState_Cancelled;
recognizer_reset(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(reset, true);
cl_assert_equal_b(cancelled, false);
reset = false;
r->state = RecognizerState_Completed;
recognizer_reset(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(reset, true);
cl_assert_equal_b(cancelled, false);
reset = false;
r->state = RecognizerState_Started;
recognizer_reset(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(reset, true);
cl_assert_equal_b(cancelled, true);
reset = false;
cancelled = false;
r->state = RecognizerState_Updated;
recognizer_reset(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(reset, true);
cl_assert_equal_b(cancelled, true);
}
void test_recognizer__cancel(void) {
bool cancelled = false;
s_test_impl_data.cancelled = &cancelled;
RecognizerEvent rec_event = -1;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, &rec_event);
recognizer_cancel(r);
cl_assert_equal_i(r->state, RecognizerState_Possible);
cl_assert_equal_b(cancelled, false);
cl_assert_equal_i(rec_event, -1);
r->state = RecognizerState_Failed;
recognizer_cancel(r);
cl_assert_equal_i(r->state, RecognizerState_Failed);
cl_assert_equal_b(cancelled, false);
cl_assert_equal_i(rec_event, -1);
r->state = RecognizerState_Cancelled;
recognizer_cancel(r);
cl_assert_equal_i(r->state, RecognizerState_Cancelled);
cl_assert_equal_b(cancelled, false);
cl_assert_equal_i(rec_event, -1);
r->state = RecognizerState_Completed;
recognizer_cancel(r);
cl_assert_equal_i(r->state, RecognizerState_Completed);
cl_assert_equal_b(cancelled, false);
cl_assert_equal_i(rec_event, -1);
r->state = RecognizerState_Started;
recognizer_cancel(r);
cl_assert_equal_i(r->state, RecognizerState_Cancelled);
cl_assert_equal_b(cancelled, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Cancelled);
cancelled = false;
r->state = RecognizerState_Updated;
rec_event = -1;
recognizer_cancel(r);
cl_assert_equal_i(r->state, RecognizerState_Cancelled);
cl_assert_equal_b(cancelled, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Cancelled);
}
void test_recognizer__handle_touch_events(void) {
RecognizerEvent rec_event = -1;
TouchEvent last_touch_event = { .type = TouchEvent_Liftoff };
RecognizerState new_state;
bool updated = false;
s_test_impl_data.last_touch_event = &last_touch_event;
s_test_impl_data.new_state = &new_state;
s_test_impl_data.updated = &updated;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, &rec_event);
new_state = RecognizerState_Possible;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Touchdown });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Touchdown);
cl_assert_equal_b(updated, false);
new_state = RecognizerState_Completed;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Liftoff });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Liftoff);
cl_assert_equal_b(updated, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Completed);
r->state = RecognizerState_Possible;
updated = false;
new_state = RecognizerState_Started;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Touchdown });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Touchdown);
cl_assert_equal_b(updated, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Started);
updated = false;
new_state = RecognizerState_Updated;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_PositionUpdate });
cl_assert_equal_i(last_touch_event.type, TouchEvent_PositionUpdate);
cl_assert_equal_b(updated, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Updated);
updated = false;
new_state = RecognizerState_Cancelled;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Liftoff });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Liftoff);
cl_assert_equal_b(updated, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Cancelled);
// Should not pass touch events to recognizers that are not active
cl_assert_passert(recognizer_handle_touch_event(r, &(TouchEvent) {}));
// Should not pass null touch events
r->state = RecognizerState_Possible;
cl_assert_passert(recognizer_handle_touch_event(r, NULL));
}
void test_recognizer__handle_touch_events_fail_after(void) {
RecognizerEvent rec_event = -1;
RecognizerState new_state;
bool updated = false;
TouchEvent last_touch_event = { .type = TouchEvent_Liftoff };
s_test_impl_data.new_state = &new_state;
s_test_impl_data.updated = &updated;
s_test_impl_data.last_touch_event = &last_touch_event;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, &rec_event);
NEW_RECOGNIZER(fail) = test_recognizer_create(&s_test_impl_data, NULL);
recognizer_set_fail_after(r, fail);
new_state = RecognizerState_Completed;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Touchdown });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Liftoff);
cl_assert_equal_b(updated, false);
fail->state = RecognizerState_Failed;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Touchdown });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Touchdown);
cl_assert_equal_b(updated, true);
cl_assert_equal_i(rec_event, RecognizerEvent_Completed);
}
static bool prv_filter(const Recognizer *recognizer, const TouchEvent *touch_event) {
bool *allow = recognizer_get_user_data(recognizer);
return *allow;
}
void test_recognizer__handle_touch_events_filter_cb(void) {
RecognizerState new_state;
bool updated = false;
TouchEvent last_touch_event = { .type = TouchEvent_Liftoff };
s_test_impl_data.new_state = &new_state;
s_test_impl_data.updated = &updated;
s_test_impl_data.last_touch_event = &last_touch_event;
bool allow = false;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, &allow);
recognizer_set_touch_filter(r, prv_filter);
new_state = RecognizerState_Completed;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Touchdown });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Liftoff);
cl_assert_equal_b(updated, false);
allow = true;
recognizer_handle_touch_event(r, &(TouchEvent) { .type = TouchEvent_Touchdown });
cl_assert_equal_i(last_touch_event.type, TouchEvent_Touchdown);
cl_assert_equal_b(updated, true);
cl_assert_equal_i(r->state, new_state);
}
bool s_simultaneous = false;
static bool prv_simultaneous_with_cb(const Recognizer *recognizer,
const Recognizer *simultaneous_with) {
return s_simultaneous;
}
void test_recognizer__set_simultaneous_with(void) {
NEW_RECOGNIZER(r1) = test_recognizer_create(&s_test_impl_data, NULL);
NEW_RECOGNIZER(r2) = test_recognizer_create(&s_test_impl_data, NULL);
s_simultaneous = false;
cl_assert(!recognizer_should_evaluate_simultaneously(NULL, NULL));
cl_assert(!recognizer_should_evaluate_simultaneously(r1, NULL));
cl_assert(!recognizer_should_evaluate_simultaneously(NULL, r2));
cl_assert(!recognizer_should_evaluate_simultaneously(r1, r2));
recognizer_set_simultaneous_with(r1, prv_simultaneous_with_cb);
cl_assert(!recognizer_should_evaluate_simultaneously(NULL, NULL));
cl_assert(!recognizer_should_evaluate_simultaneously(r1, NULL));
cl_assert(!recognizer_should_evaluate_simultaneously(NULL, r2));
cl_assert(!recognizer_should_evaluate_simultaneously(r1, r2));
s_simultaneous = true;
cl_assert(!recognizer_should_evaluate_simultaneously(NULL, NULL));
cl_assert(!recognizer_should_evaluate_simultaneously(r1, NULL));
cl_assert(!recognizer_should_evaluate_simultaneously(NULL, r2));
cl_assert(recognizer_should_evaluate_simultaneously(r1, r2));
}
void test_recognizer__add_remove_list(void) {
RecognizerList list = { NULL };
NEW_RECOGNIZER(r1) = test_recognizer_create(&s_test_impl_data, NULL);
NEW_RECOGNIZER(r2) = test_recognizer_create(&s_test_impl_data, NULL);
recognizer_add_to_list(r1, &list);
recognizer_add_to_list(r2, &list);
cl_assert_equal_i(list_count(list.node), 2);
cl_assert(list_contains(list.node, &r1->node));
cl_assert(list_contains(list.node, &r2->node));
cl_assert(r1->is_owned);
cl_assert(r2->is_owned);
recognizer_remove_from_list(r1, &list);
cl_assert(!list_contains(list.node, &r1->node));
cl_assert(!r1->is_owned);
recognizer_remove_from_list(r1, &list);
cl_assert(!r1->is_owned);
}
static int s_list_idx = 0;
static bool prv_list_iterator(Recognizer *recognizer, void *context) {
const char *names[] = { "R1", "R2", "R3" };
cl_assert(s_list_idx < ARRAY_LENGTH(names));
char s[20];
snprintf(s, sizeof(s), "%s != %s", recognizer->subscriber.data, names[s_list_idx]);
cl_assert_(strcmp(recognizer->subscriber.data, names[s_list_idx++]) == 0, s);
return (s_list_idx < *((int *)context));
}
void test_recognizer__list_iterate(void) {
NEW_RECOGNIZER(r1) = test_recognizer_create(&s_test_impl_data, "R1");
NEW_RECOGNIZER(r2) = test_recognizer_create(&s_test_impl_data, "R2");
NEW_RECOGNIZER(r3) = test_recognizer_create(&s_test_impl_data, "R3");
RecognizerList list = { NULL };
recognizer_add_to_list(r1, &list);
recognizer_add_to_list(r2, &list);
recognizer_add_to_list(r3, &list);
s_list_idx = 0;
int end = 4;
recognizer_list_iterate(&list, prv_list_iterator, &end);
cl_assert_equal_i(s_list_idx, 3);
end = 2;
s_list_idx = 0;
recognizer_list_iterate(&list, prv_list_iterator, &end);
cl_assert_equal_i(s_list_idx, 2);
}

View file

@ -0,0 +1,112 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "test_recognizer_impl.h"
#include "clar_asserts.h"
#include "applib/ui/recognizer/recognizer.h"
#include "applib/ui/recognizer/recognizer_impl.h"
static const int TEST_PATTERN = 0xA5A5A5A5;
static RecognizerImpl s_test_impl;
static void prv_handle_touch_event(Recognizer *recognizer, const TouchEvent *touch_event) {
TestImplData *data = recognizer_get_impl_data(recognizer, &s_test_impl);
cl_assert_equal_i(data->test, TEST_PATTERN);
if (data->last_touch_event) {
*data->last_touch_event = *touch_event;
}
if (data->handled) {
*data->handled = true;
}
if(data->new_state && (*data->new_state != RecognizerState_Possible)) {
cl_assert(data->updated);
cl_assert_equal_p(*data->updated, false);
*data->updated = true;
recognizer_transition_state(recognizer, *data->new_state);
}
}
static bool prv_cancel(Recognizer *recognizer) {
TestImplData *data = recognizer_get_impl_data(recognizer, &s_test_impl);
cl_assert_equal_i(data->test, TEST_PATTERN);
if (data->cancelled) {
cl_assert_equal_b(*data->cancelled, false);
*data->cancelled = true;
}
return true;
}
static void prv_reset(Recognizer *recognizer) {
TestImplData *data = recognizer_get_impl_data(recognizer, &s_test_impl);
cl_assert_equal_i(data->test, TEST_PATTERN);
if (data->reset) {
cl_assert_equal_b(*data->reset, false);
*data->reset = true;
}
}
static void prv_on_destroy(Recognizer *recognizer) {
TestImplData *data = recognizer_get_impl_data(recognizer, &s_test_impl);
cl_assert_equal_i(data->test, TEST_PATTERN);
if (data->destroyed) {
cl_assert_equal_b(*data->destroyed, false);
*data->destroyed = true;
}
}
static void prv_on_fail(Recognizer *recognizer) {
TestImplData *data = recognizer_get_impl_data(recognizer, &s_test_impl);
cl_assert_equal_i(data->test, TEST_PATTERN);
if (data->failed) {
cl_assert_equal_b(*data->failed, false);
*data->failed = true;
}
}
static void prv_sub_event_handler(const Recognizer *recognizer, RecognizerEvent event) {
RecognizerEvent *event_type = recognizer_get_user_data(recognizer);
if (event_type) {
*event_type = event;
}
}
Recognizer *test_recognizer_create(TestImplData *test_impl_data, void *user_data) {
s_test_impl = (RecognizerImpl) {
.handle_touch_event = prv_handle_touch_event,
.cancel = prv_cancel,
.reset = prv_reset,
.on_fail = prv_on_fail
};
test_impl_data->test = TEST_PATTERN;
return recognizer_create_with_data(&s_test_impl, test_impl_data,
sizeof(*test_impl_data), prv_sub_event_handler,
user_data);
}
void test_recognizer_enable_on_destroy(void) {
s_test_impl.on_destroy = prv_on_destroy;
}
void test_recognizer_destroy(Recognizer **recognizer) {
recognizer_destroy(*recognizer);
}
void *test_recognizer_get_data(Recognizer *recognizer) {
return recognizer_get_impl_data(recognizer, &s_test_impl);
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/ui/recognizer/recognizer.h"
#include <stdbool.h>
#include <stdint.h>
#define NEW_RECOGNIZER(r) \
Recognizer *r __attribute__ ((__cleanup__(test_recognizer_destroy)))
typedef struct TestImplData {
int test;
bool *destroyed;
bool *cancelled;
bool *reset;
bool *handled;
bool *updated;
bool *failed;
TouchEvent *last_touch_event;
RecognizerState *new_state;
} TestImplData;
Recognizer *test_recognizer_create(TestImplData *test_impl_data, void *user_data);
void test_recognizer_destroy(Recognizer **recognizer);
void test_recognizer_enable_on_destroy(void);

View file

@ -0,0 +1,943 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "applib/ui/layer.h"
#include "applib/ui/window.h"
#include "applib/ui/recognizer/recognizer.h"
#include "applib/ui/recognizer/recognizer_impl.h"
#include "applib/ui/recognizer/recognizer_list.h"
#include "applib/ui/recognizer/recognizer_manager.h"
#include "applib/ui/recognizer/recognizer_private.h"
#include "util/size.h"
// Stubs
#include "stubs_app_state.h"
#include "stubs_gbitmap.h"
#include "stubs_graphics.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_process_manager.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
#include "test_recognizer_impl.h"
static RecognizerList *s_app_list;
static Layer *s_active_layer;
static RecognizerManager *s_manager;
static TestImplData s_test_impl_data;
RecognizerList *app_state_get_recognizer_list(void) {
return s_app_list;
}
RecognizerList *window_get_recognizer_list(Window *window) {
if (!window) {
return NULL;
}
return layer_get_recognizer_list(&window->layer);
}
RecognizerManager *window_get_recognizer_manager(Window *window) {
return s_manager;
}
struct Layer* window_get_root_layer(const Window *window) {
if (!window) {
return NULL;
}
return &((Window *)window)->layer;
}
// Override find layer function so we don't have to muck around with points and layer bounds (also
// this process can change and this test will keep on working)
Layer *layer_find_layer_containing_point(const Layer *node, const GPoint *point) {
return s_active_layer;
}
typedef struct RecognizerHandled {
ListNode node;
int idx;
} RecognizerHandled;
static ListNode *s_recognizers_handled;
static ListNode *s_recognizers_reset;
static bool prv_simultaneous_with_cb(const Recognizer *recognizer,
const Recognizer *simultaneous_with) {
return true;
}
static void prv_handle_touch_event (Recognizer *recognizer, const TouchEvent *touch_event) {
}
static bool prv_cancel(Recognizer *recognizer) {
return false;
}
static void prv_reset (Recognizer *recognizer) {
}
static RecognizerImpl s_dummy_impl;
static void prv_clear_recognizers_processed(ListNode **list) {
ListNode *node = *list;
while (node) {
ListNode *next = list_pop_head(node);
free(node);
node = next;
}
*list = NULL;
}
static void prv_compare_recognizers_processed(int indices[], uint32_t count, ListNode **list) {
printf(list == &s_recognizers_handled ? "Handle touch: " : "");
printf(list == &s_recognizers_reset ? "Reset: " : "");
printf("{ ");
for (uint32_t i = 0; i < count; i++) {
printf("%d, ", indices[i]);
}
printf("}");
ListNode *node = *list;
int list_num = list_count(node);
bool failed = list_num != count;
if (failed) {
count = list_num;
}
if (!failed) {
for (uint32_t i = 0; (i < count) && !failed; i++) {
failed = (indices[i] != ((RecognizerHandled *)node)->idx);
node = list_get_next(node);
}
}
if (failed) {
node = *list;
printf(" != { ");
for (uint32_t i = 0; i < count; i++) {
printf("%d, ", ((RecognizerHandled *)node)->idx);
node = list_get_next(node);
}
printf("}");
}
printf("\n");
cl_assert(!failed);
prv_clear_recognizers_processed(list);
}
static void prv_sub_event_handler(const Recognizer *recognizer, RecognizerEvent event) {
}
// setup and teardown
void test_recognizer_manager__initialize(void) {
s_test_impl_data = (TestImplData){};
s_app_list = NULL;
s_active_layer = NULL;
s_manager = NULL;
s_dummy_impl = (RecognizerImpl) {
.handle_touch_event = prv_handle_touch_event,
.cancel = prv_cancel,
.reset = prv_reset,
};
}
void test_recognizer_manager__cleanup(void) {
prv_clear_recognizers_processed(&s_recognizers_handled);
prv_clear_recognizers_processed(&s_recognizers_reset);
}
static void prv_store_recognizer_idx(Recognizer *recognizer, ListNode **list) {
int *idx = recognizer_get_impl_data(recognizer, &s_dummy_impl);
if (idx) {
RecognizerHandled *rec = malloc(sizeof(RecognizerHandled));
cl_assert(rec);
*rec = (RecognizerHandled){ .idx = *idx };
*list = list_get_head(list_append(*list, &rec->node));
}
}
static bool prv_handle_dummy_touch_event(Recognizer *recognizer, void *unused) {
prv_store_recognizer_idx(recognizer, &s_recognizers_handled);
return true;
}
static Recognizer **prv_create_recognizers(int count) {
Recognizer **recognizers = malloc(sizeof(Recognizer*) * count);
cl_assert(recognizers);
for (int i = 0; i < count; i++) {
recognizers[i] = recognizer_create_with_data(&s_dummy_impl, &i,
sizeof(i), prv_sub_event_handler,
NULL);
cl_assert(recognizers[i]);
}
return recognizers;
}
static void prv_destroy_recognizers(Recognizer **recognizers, int count) {
for (int i = 0; i < count; i++) {
recognizer_destroy(recognizers[i]);
}
free(recognizers);
}
// tests
bool prv_process_all_recognizers(RecognizerManager *manager,
RecognizerListIteratorCb iter_cb, void *context);
void test_recognizer_manager__process_all_recognizers(void) {
const int k_rec_count = 7;
Recognizer **recognizers = prv_create_recognizers(k_rec_count);
RecognizerManager manager;
recognizer_manager_init(&manager);
// ensure this runs without crashing even if there are no recognizer lists
prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL);
RecognizerList app_list = {};
s_app_list = &app_list;
Window window = {};
layer_init(&window.layer, &GRectZero);
manager.window = &window;
Layer layer_a, layer_b, layer_c;
layer_init(&layer_a, &GRectZero);
layer_init(&layer_b, &GRectZero);
layer_init(&layer_c, &GRectZero);
layer_add_child(&window.layer, &layer_a);
layer_add_child(&layer_a, &layer_b);
layer_add_child(&window.layer, &layer_c);
manager.active_layer = &layer_c;
// ensure that this runs without crashing even if all the lists are empty
prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL);
// One recognizer attached to the active layer
recognizer_add_to_list(recognizers[0], &layer_c.recognizer_list);
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {0}, 1, &s_recognizers_handled);
// Two recognizers attached to the active layer - processed in order that they were added
recognizer_add_to_list(recognizers[1], &layer_c.recognizer_list);
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {0, 1}, 2, &s_recognizers_handled);
// Recognizers that attached to layers other than the active layer and its ancestors will not be
// processed
recognizer_add_to_list(recognizers[2], &layer_a.recognizer_list);
recognizer_add_to_list(recognizers[3], &layer_a.recognizer_list);
recognizer_add_to_list(recognizers[4], &layer_b.recognizer_list);
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {0, 1}, 2, &s_recognizers_handled);
// Recognizers attached to children of active layer will not be evaluated
manager.active_layer = &layer_a;
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {2, 3}, 2, &s_recognizers_handled);
// Recognizers attached to active layer will be processed before those attached to their ancestors
manager.active_layer = &layer_b;
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {4, 2, 3}, 3, &s_recognizers_handled);
// Recognizers attached to window processed before layer recognizers
recognizer_add_to_list(recognizers[5], window_get_recognizer_list(&window));
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {5, 4, 2, 3}, 4, &s_recognizers_handled);
// Recognizers attached to app processed before window and layer recognizers
recognizer_add_to_list(recognizers[6], &app_list);
cl_assert(prv_process_all_recognizers(&manager, prv_handle_dummy_touch_event, NULL));
prv_compare_recognizers_processed((int[]) {6, 5, 4, 2, 3}, 5, &s_recognizers_handled);
prv_destroy_recognizers(recognizers, k_rec_count);
}
bool prv_dispatch_touch_event(Recognizer *recognizer, void *context);
void test_recognizer_manager__dispatch_touch_event(void) {
bool handled = false;
s_test_impl_data.handled = &handled;
NEW_RECOGNIZER(r) = test_recognizer_create(&s_test_impl_data, NULL);
// Copied from recognizer_manager.c
TouchEvent t;
struct ProcessTouchCtx {
Recognizer *triggered;
const TouchEvent *touch_event;
} ctx = { .triggered = NULL, .touch_event = &t };
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(handled);
cl_assert(!ctx.triggered);
handled = false;
// Recognizer should not get a touch event when it is in inactive states
r->state = RecognizerState_Failed;
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(!handled);
cl_assert(!ctx.triggered);
r->state = RecognizerState_Cancelled;
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(!handled);
cl_assert(!ctx.triggered);
r->state = RecognizerState_Completed;
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(!handled);
cl_assert(!ctx.triggered);
r->state = RecognizerState_Started;
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(handled);
cl_assert_equal_p(ctx.triggered, r);
ctx.triggered = NULL;
handled = false;
r->state = RecognizerState_Updated;
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(handled);
cl_assert_equal_p(ctx.triggered, r);
handled = false;
ctx.triggered = NULL;
NEW_RECOGNIZER(s) = test_recognizer_create(&s_test_impl_data, NULL);
s->state = RecognizerState_Started;
r->state = RecognizerState_Possible;
ctx.triggered = s;
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(!handled);
recognizer_set_simultaneous_with(r, prv_simultaneous_with_cb);
cl_assert(prv_dispatch_touch_event(r, &ctx));
cl_assert(handled);
cl_assert_equal_p(ctx.triggered, s);
}
bool prv_fail_recognizer(Recognizer *recognizer, void *context);
void test_recognizer_manager__fail_recognizer(void) {
NEW_RECOGNIZER(r1) = test_recognizer_create(&s_test_impl_data, NULL);
NEW_RECOGNIZER(r2) = test_recognizer_create(&s_test_impl_data, NULL);
r2->state = RecognizerState_Started;
// copied from recognizer_manager.c
struct FailRecognizerCtx {
Recognizer *triggered;
bool recognizers_active;
} ctx = { .triggered = r2, .recognizers_active = false };
cl_assert(prv_fail_recognizer(r2, &ctx));
cl_assert_equal_i(r2->state, RecognizerState_Started);
cl_assert(!ctx.recognizers_active);
ctx.recognizers_active = false;
r1->state = RecognizerState_Possible;
cl_assert(prv_fail_recognizer(r1, &ctx));
cl_assert_equal_i(r1->state, RecognizerState_Failed);
cl_assert(!ctx.recognizers_active);
// Make sure that we don't try to fail a recognizer twice (causing an assert)
cl_assert(prv_fail_recognizer(r1, &ctx));
cl_assert_equal_i(r1->state, RecognizerState_Failed);
r1->state = RecognizerState_Possible;
recognizer_set_simultaneous_with(r1, prv_simultaneous_with_cb);
cl_assert(prv_fail_recognizer(r1, &ctx));
cl_assert_equal_i(r1->state, RecognizerState_Possible);
cl_assert(ctx.recognizers_active);
}
void prv_cancel_layer_tree_recognizers(RecognizerManager *manager, Layer *top_layer,
Layer *bottom_layer);
static void prv_set_all_states(Recognizer **recognizers, int count, RecognizerState state) {
for(int i = 0; i < count; i++) {
recognizers[i]->state = state;
}
}
void test_recognizer_manager__cancel_layer_tree_recognizers(void) {
const int k_rec_count = 4;
Recognizer **recognizers = prv_create_recognizers(k_rec_count);
Window window = {};
layer_init(&window.layer, &GRectZero);
Layer *root = &window.layer;
RecognizerManager manager;
recognizer_manager_init(&manager);
manager.window = &window;
Layer layer_a, layer_b, layer_c;
layer_init(&layer_a, &GRectZero);
layer_init(&layer_b, &GRectZero);
layer_init(&layer_c, &GRectZero);
layer_add_child(root, &layer_a);
layer_add_child(root, &layer_b);
layer_add_child(&layer_a, &layer_c);
recognizer_add_to_list(recognizers[0], window_get_recognizer_list(&window));
recognizer_add_to_list(recognizers[1], &layer_a.recognizer_list);
recognizer_add_to_list(recognizers[2], &layer_b.recognizer_list);
recognizer_add_to_list(recognizers[3], &layer_c.recognizer_list);
prv_set_all_states(recognizers, k_rec_count, RecognizerState_Started);
// Layer C's recognizers reset when layer A becomes the new active layer
manager.active_layer = &layer_c;
prv_cancel_layer_tree_recognizers(&manager, &layer_a, &layer_c);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Cancelled);
// Layer C's and layer A's recognizers get reset when layer B becomes the new active layer
prv_set_all_states(recognizers, k_rec_count, RecognizerState_Started);
prv_cancel_layer_tree_recognizers(&manager, &layer_b, &layer_c);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Cancelled);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Cancelled);
// Layer C's and layer A's recognizers get cancelled when there is no new active layer
prv_set_all_states(recognizers, k_rec_count, RecognizerState_Started);
prv_cancel_layer_tree_recognizers(&manager, NULL, &layer_c);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Cancelled);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Cancelled);
// If recognizers are in the possible state, they will be failed, rather than cancelled
prv_set_all_states(recognizers, k_rec_count, RecognizerState_Possible);
prv_cancel_layer_tree_recognizers(&manager, NULL, &layer_c);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Failed);
}
static RecognizerState s_next_state = RecognizerStateCount;
static int s_idx_to_change = -1;
static void prv_handle_touch_event_test(Recognizer *recognizer, const TouchEvent *touch_event) {
int *idx = recognizer_get_impl_data(recognizer, &s_dummy_impl);
prv_store_recognizer_idx(recognizer, &s_recognizers_handled);
if ((s_idx_to_change >= 0) && (*idx == s_idx_to_change)) {
recognizer_transition_state(recognizer, s_next_state);
s_idx_to_change = -1;
s_next_state = RecognizerStateCount;
}
}
static void prv_reset_test(Recognizer *recognizer) {
prv_store_recognizer_idx(recognizer, &s_recognizers_reset);
}
void test_recognizer_manager__handle_touch_event(void) {
const int k_rec_count = 5;
s_dummy_impl.handle_touch_event = prv_handle_touch_event_test;
s_dummy_impl.reset = prv_reset_test;
Recognizer **recognizers = prv_create_recognizers(k_rec_count);
RecognizerList app_list = {};
s_app_list = &app_list;
Window window = {};
layer_init(&window.layer, &GRectZero);
Layer *root = &window.layer;
RecognizerManager manager;
recognizer_manager_init(&manager);
manager.window = &window;
Layer layer_a, layer_b, layer_c;
layer_init(&layer_a, &GRectZero);
layer_init(&layer_b, &GRectZero);
layer_init(&layer_c, &GRectZero);
layer_add_child(root, &layer_a);
layer_add_child(root, &layer_b);
layer_add_child(&layer_a, &layer_c);
recognizer_add_to_list(recognizers[0], window_get_recognizer_list(&window));
recognizer_add_to_list(recognizers[1], &layer_a.recognizer_list);
recognizer_add_to_list(recognizers[2], &layer_b.recognizer_list);
recognizer_add_to_list(recognizers[3], &layer_c.recognizer_list);
recognizer_add_to_list(recognizers[4], s_app_list);
s_active_layer = &layer_c;
TouchEvent e = { .type = TouchEvent_PositionUpdate };
// No active recognizers because manager is waiting for a touchdown event
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_handled);
// Touchdown event occurs, active layer is found and all applicable recognizers receive events
// while none have started recognizing
e.type = TouchEvent_Touchdown;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_handled);
cl_assert_equal_p(manager.active_layer, &layer_c);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// All recognizers receive events while none have started recognizing
e.type = TouchEvent_PositionUpdate;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_handled);
cl_assert_equal_p(manager.active_layer, &layer_c);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// Same as above. Different event type
e.type = TouchEvent_Liftoff;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_handled);
cl_assert_equal_p(manager.active_layer, &layer_c);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// Layer A recognizer's gesture starts to be recognized. All other recognizers failed
e.type = TouchEvent_Touchdown;
s_next_state = RecognizerState_Started;
s_idx_to_change = 3;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3}, 3, &s_recognizers_handled);
cl_assert_equal_p(manager.active_layer, &layer_c);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// Only layer A recognizer's gesture receives touch events
e.type = TouchEvent_PositionUpdate;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {3}, 1, &s_recognizers_handled);
cl_assert_equal_p(manager.active_layer, &layer_c);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// Layer A recognizer's gesture updates. Only that recognizer receives touch events
e.type = TouchEvent_Liftoff;
s_next_state = RecognizerState_Updated;
s_idx_to_change = 3;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {3}, 1, &s_recognizers_handled);
cl_assert_equal_p(manager.active_layer, &layer_c);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Updated);
// Layer A recognizer's gesture completes and all recognizers are reset
e.type = TouchEvent_Liftoff;
s_next_state = RecognizerState_Completed;
s_idx_to_change = 3;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {3}, 1, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// Layer A recognizer's gesture does not complete because there is no active layer until a
// touchdown occurs
e.type = TouchEvent_PositionUpdate;
s_next_state = RecognizerState_Completed;
s_idx_to_change = 3;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
// Layer A's recognizer's gesture completes immediately. All recognizers receive the touch event
// because Layer A's recognizers receive the touch events last. All recognizers in the chain are
// reset
e.type = TouchEvent_Touchdown;
s_next_state = RecognizerState_Completed;
s_idx_to_change = 1;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
// The app's recognizer's gesture completes immediately. Only the app's recognizer sees the touch
// events. All recognizers in the chain are reset
e.type = TouchEvent_Touchdown;
s_next_state = RecognizerState_Completed;
s_idx_to_change = 4;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4}, 1, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
// Layer C recognizer starts recognizing a gesture, failing other recognizers
e.type = TouchEvent_Touchdown;
s_next_state = RecognizerState_Started;
s_idx_to_change = 1;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// A second touchdown event occurs while recognizers are active. A different layer is touched, so
// the active recognizers on non-touched layers in the tree are cancelled
s_active_layer = &layer_b;
e.type = TouchEvent_Touchdown;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 2}, 3, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) {4, 0, 2}, 3, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Cancelled);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// Window recognizer becomes triggered
e.type = TouchEvent_PositionUpdate;
s_next_state = RecognizerState_Started;
s_idx_to_change = 0;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0 }, 2, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[2]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// Another layer in a separate branch becomes active while a window recognizer is triggered
e.type = TouchEvent_Touchdown;
s_active_layer = &layer_a;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) { 0 }, 1, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Cancelled); // was already cancelled
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// A child layer of the active layer becomes active when a window recognizer is triggered
e.type = TouchEvent_Touchdown;
s_active_layer = &layer_c;
recognizers[3]->state = RecognizerState_Possible;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) { 0 }, 1, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Cancelled);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// A touchdown occurs where no layers are touched while a window recognizer is active
e.type = TouchEvent_Touchdown;
s_active_layer = NULL;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) { 0 }, 1, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Started);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Cancelled);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Failed);
// Touchdown occurs, Window recognizer completes, active layer becomes non-null
e.type = TouchEvent_Touchdown;
s_next_state = RecognizerState_Completed;
s_idx_to_change = 0;
s_active_layer = &layer_a;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) { 0 }, 1, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) { 4, 0, 1 }, 3, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_p(manager.triggered, NULL);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// A touchdown occurs where no layers are touched
s_active_layer = NULL;
e.type = TouchEvent_Touchdown;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0}, 2, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_p(manager.triggered, NULL);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// A touchdown occurs and the active layer goes from non-null to null. All layer recognizers get
// reset. All recognizers remain in the possible state.
s_active_layer = &layer_a;
e.type = TouchEvent_Touchdown;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 1}, 3, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) {1}, 1, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// A touchdown occurs and a child of the previous active recognizer becomes the active layer. The
// child is reset. All recognizers remain in the possible state.
s_active_layer = &layer_c;
e.type = TouchEvent_Touchdown;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 3, 1}, 4, &s_recognizers_handled);
prv_compare_recognizers_processed((int[]) {3}, 1, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
// A touchdown occurs and the parent of the previous active recognizer becomes the active layer.
// No recognizers are reset and all recognizers remain in the possible state. The child is failed.
s_active_layer = &layer_a;
e.type = TouchEvent_Touchdown;
recognizer_manager_handle_touch_event(&e, &manager);
prv_compare_recognizers_processed((int[]) {4, 0, 1}, 3, &s_recognizers_handled);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_i(recognizers[0]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[1]->state, RecognizerState_Possible);
cl_assert_equal_i(recognizers[3]->state, RecognizerState_Failed);
cl_assert_equal_i(recognizers[4]->state, RecognizerState_Possible);
prv_destroy_recognizers(recognizers, k_rec_count);
}
void test_recognizer_manager__deregister_recognizer(void) {
NEW_RECOGNIZER(r1) = test_recognizer_create(&s_test_impl_data, NULL);
NEW_RECOGNIZER(r2) = test_recognizer_create(&s_test_impl_data, NULL);
Window window = {};
layer_init(&window.layer, &GRectZero);
Layer *root = &window.layer;
RecognizerManager manager;
recognizer_manager_init(&manager);
Layer layer_a;
layer_init(&layer_a, &GRectZero);
layer_add_child(root, &layer_a);
manager.window = &window;
manager.active_layer = &layer_a;
recognizer_add_to_list(r1, &layer_a.recognizer_list);
recognizer_add_to_list(r2, &layer_a.recognizer_list);
RecognizerManager manager2;
recognizer_set_manager(r1, &manager2);
recognizer_manager_deregister_recognizer(&manager, r1);
cl_assert_equal_p(manager.active_layer, &layer_a);
cl_assert_equal_p(recognizer_get_manager(r1), &manager2);
recognizer_set_manager(r1, &manager);
recognizer_manager_deregister_recognizer(&manager, r1);
cl_assert(!recognizer_get_manager(r1));
cl_assert_equal_p(manager.active_layer, &layer_a);
recognizer_set_manager(r1, &manager);
r1->state = RecognizerState_Started;
r2->state = RecognizerState_Failed;
manager.triggered = r1;
manager.state = RecognizerManagerState_RecognizersTriggered;
recognizer_manager_deregister_recognizer(&manager, r1);
cl_assert(!manager.triggered);
cl_assert(!manager.active_layer);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_i(r2->state, RecognizerState_Possible);
cl_assert(!recognizer_get_manager(r1));
recognizer_set_manager(r1, &manager);
r1->state = RecognizerState_Possible;
r2->state = RecognizerState_Started;
manager.active_layer = &layer_a;
manager.triggered = r2;
manager.state = RecognizerManagerState_RecognizersTriggered;
recognizer_manager_deregister_recognizer(&manager, r1);
cl_assert_equal_p(manager.triggered, r2);
cl_assert_equal_p(manager.active_layer, &layer_a);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(r2->state, RecognizerState_Started);
cl_assert(!recognizer_get_manager(r1));
recognizer_set_manager(r1, &manager);
recognizer_set_simultaneous_with(r2, prv_simultaneous_with_cb);
r1->state = RecognizerState_Started;
r2->state = RecognizerState_Started;
manager.triggered = r1;
manager.state = RecognizerManagerState_RecognizersTriggered;
recognizer_manager_deregister_recognizer(&manager, r1);
cl_assert_equal_p(manager.triggered, r2);
cl_assert_equal_p(manager.active_layer, &layer_a);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_i(r2->state, RecognizerState_Started);
cl_assert(!recognizer_get_manager(r1));
}
void test_recognizer_manager__handle_state_change(void) {
const int k_rec_count = 2;
s_dummy_impl.handle_touch_event = prv_handle_touch_event_test;
s_dummy_impl.reset = prv_reset_test;
Recognizer **r = prv_create_recognizers(k_rec_count);
Window window = {};
layer_init(&window.layer, &GRectZero);
Layer *root = &window.layer;
RecognizerManager manager;
recognizer_manager_init(&manager);
Layer layer_a;
layer_init(&layer_a, &GRectZero);
layer_add_child(root, &layer_a);
manager.window = &window;
manager.active_layer = &layer_a;
manager.state = RecognizerManagerState_RecognizersActive;
recognizer_add_to_list(r[0], &layer_a.recognizer_list);
recognizer_add_to_list(r[1], &layer_a.recognizer_list);
recognizer_set_manager(r[0], &manager);
recognizer_set_manager(r[1], &manager);
r[0]->state = RecognizerState_Failed;
recognizer_manager_handle_state_change(&manager, r[0]);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersActive);
cl_assert_equal_p(manager.active_layer, &layer_a);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
r[1]->state = RecognizerState_Failed;
recognizer_manager_handle_state_change(&manager, r[1]);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_p(manager.active_layer, NULL);
prv_compare_recognizers_processed((int []) { 0, 1 }, 2, &s_recognizers_reset);
manager.active_layer = &layer_a;
manager.state = RecognizerManagerState_RecognizersActive;
manager.triggered = NULL;
r[0]->state = RecognizerState_Started;
r[1]->state = RecognizerState_Possible;
recognizer_manager_handle_state_change(&manager, r[0]);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_p(manager.triggered, r[0]);
cl_assert_equal_p(manager.active_layer, &layer_a);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(r[0]->state, RecognizerState_Started);
cl_assert_equal_i(r[1]->state, RecognizerState_Failed);
r[0]->state = RecognizerState_Updated;
recognizer_manager_handle_state_change(&manager, r[0]);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_p(manager.triggered, r[0]);
cl_assert_equal_p(manager.active_layer, &layer_a);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
r[0]->state = RecognizerState_Completed;
recognizer_manager_handle_state_change(&manager, r[0]);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_p(manager.triggered, NULL);
cl_assert_equal_p(manager.active_layer, NULL);
prv_compare_recognizers_processed((int []) { 0, 1 }, 2, &s_recognizers_reset);
cl_assert_equal_i(r[0]->state, RecognizerState_Possible);
cl_assert_equal_i(r[1]->state, RecognizerState_Possible);
r[0]->state = RecognizerState_Completed;
manager.active_layer = &layer_a;
manager.state = RecognizerManagerState_RecognizersActive;
manager.triggered = NULL;
recognizer_manager_handle_state_change(&manager, r[0]);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_p(manager.triggered, NULL);
cl_assert_equal_p(manager.active_layer, NULL);
prv_compare_recognizers_processed((int []) { 0, 1 }, 2, &s_recognizers_reset);
cl_assert_equal_i(r[0]->state, RecognizerState_Possible);
cl_assert_equal_i(r[1]->state, RecognizerState_Possible);
r[0]->state = RecognizerState_Cancelled;
manager.active_layer = &layer_a;
manager.state = RecognizerManagerState_RecognizersActive;
manager.triggered = r[0];
recognizer_manager_handle_state_change(&manager, r[0]);
cl_assert_equal_i(manager.state, RecognizerManagerState_WaitForTouchdown);
cl_assert_equal_p(manager.triggered, NULL);
cl_assert_equal_p(manager.active_layer, NULL);
prv_compare_recognizers_processed((int []) { 0, 1 }, 2, &s_recognizers_reset);
recognizer_set_simultaneous_with(r[0], prv_simultaneous_with_cb);
r[0]->state = RecognizerState_Started;
r[1]->state = RecognizerState_Completed;
manager.active_layer = &layer_a;
manager.state = RecognizerManagerState_RecognizersTriggered;
manager.triggered = r[0];
recognizer_manager_handle_state_change(&manager, r[1]);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_p(manager.triggered, r[0]);
cl_assert_equal_p(manager.active_layer, &layer_a);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(r[0]->state, RecognizerState_Started);
cl_assert_equal_i(r[1]->state, RecognizerState_Completed);
recognizer_set_simultaneous_with(r[0], prv_simultaneous_with_cb);
r[0]->state = RecognizerState_Started;
r[1]->state = RecognizerState_Completed;
manager.active_layer = &layer_a;
manager.state = RecognizerManagerState_RecognizersTriggered;
manager.triggered = r[1];
recognizer_manager_handle_state_change(&manager, r[1]);
cl_assert_equal_i(manager.state, RecognizerManagerState_RecognizersTriggered);
cl_assert_equal_p(manager.triggered, r[0]);
cl_assert_equal_p(manager.active_layer, &layer_a);
prv_compare_recognizers_processed(NULL, 0, &s_recognizers_reset);
cl_assert_equal_i(r[0]->state, RecognizerState_Started);
cl_assert_equal_i(r[1]->state, RecognizerState_Completed);
}

View file

@ -0,0 +1,17 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob = " src/fw/applib/ui/recognizer/recognizer.c" \
" tests/fw/ui/recognizer/test_recognizer_impl.c",
test_sources_ant_glob = "test_recognizer.c")
clar(ctx,
sources_ant_glob = " src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/recognizer/recognizer.c" \
" src/fw/applib/ui/recognizer/recognizer_manager.c" \
" tests/fw/ui/recognizer/test_recognizer_impl.c",
defines = ['CAPABILITY_HAS_TOUCHSCREEN'],
test_sources_ant_glob = "test_recognizer_manager.c")
# vim:filetype=python

View file

@ -0,0 +1,526 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/action_menu_hierarchy.h"
#include "applib/ui/action_menu_layer.h"
#include "applib/ui/action_menu_window.h"
#include "applib/ui/action_menu_window_private.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/content_indicator_private.h"
#include "apps/system_apps/settings/settings_notifications_private.h"
#include "resource/resource.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/graphics.h"
#include "util/hash.h"
#include "util/math.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
static GContext s_ctx;
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_spi_flash.h"
#include "../../fixtures/load_test_resources.h"
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_click.h"
#include "stubs_heap.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscall_internal.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
int16_t interpolate_int16(int32_t normalized, int16_t from, int16_t to) {
return to;
}
AnimationProgress animation_timing_scaled(AnimationProgress time_normalized,
AnimationProgress interval_start,
AnimationProgress interval_end) {
return interval_end;
}
int64_t interpolate_moook(int32_t normalized, int64_t from, int64_t to) {
return to;
}
uint32_t interpolate_moook_duration() {
return 0;
}
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static FrameBuffer *fb = NULL;
static GBitmap *s_dest_bitmap;
// To easily render multiple windows in a single canvas, we'll use an 8-bit bitmap for color
// displays (including round), but we can use the native format for black and white displays (1-bit)
#define CANVAS_GBITMAP_FORMAT PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBITMAP_NATIVE_FORMAT)
// Overrides same function in graphics.c; we need to do this so we can pass in the GBitmapFormat
// we need to use for the unit test output canvas instead of relying on GBITMAP_NATIVE_FORMAT, which
// wouldn't work for Spalding since it uses GBitmapFormat8BitCircular
GBitmap* graphics_capture_frame_buffer(GContext *ctx) {
PBL_ASSERTN(ctx);
return graphics_capture_frame_buffer_format(ctx, CANVAS_GBITMAP_FORMAT);
}
// Overrides same function in graphics.c; we need to do this so we can release the framebuffer we're
// using even though its format doesn't match GBITMAP_NATIVE_FORMAT (see comment for mocked
// graphics_capture_frame_buffer() above)
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
PBL_ASSERTN(ctx);
ctx->lock = false;
framebuffer_dirty_all(ctx->parent_framebuffer);
return true;
}
void test_action_menu_window__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, fb);
framebuffer_clear(fb);
// Setup resources
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false /* is_next */);
resource_init();
}
void test_action_menu_window__cleanup(void) {
free(fb);
gbitmap_destroy(s_dest_bitmap);
s_dest_bitmap = NULL;
}
// Helpers
//////////////////////
static void prv_action_menu_did_close_cb(ActionMenu *action_menu,
const ActionMenuItem *item,
void *context) {
ActionMenuLevel *root_level = action_menu_get_root_level(action_menu);
action_menu_hierarchy_destroy(root_level, NULL, NULL);
}
static void prv_noop_action_callback(ActionMenu *action_menu, const ActionMenuItem *action,
void *context) {
// Do nothing
}
// From action_menu_layer.c, needed to scroll the action menu layer to the point of interest
void prv_set_selected_index(ActionMenuLayer *aml, int selected_index, bool animated);
typedef enum {
ActionMenuLayerLongLabelScrollingAnimationState_Top,
ActionMenuLayerLongLabelScrollingAnimationState_Middle,
ActionMenuLayerLongLabelScrollingAnimationState_Bottom,
ActionMenuLayerLongLabelScrollingAnimationStateCount
} ActionMenuLayerLongLabelScrollingAnimationState;
void prv_set_cell_offset(void *subject, int16_t value);
static void prv_update_cell_for_long_label_scrolling_animation_state(
ActionMenuLayer *aml, ActionMenuLayerLongLabelScrollingAnimationState state) {
const bool item_animation_is_valid = (aml && aml->item_animation.animation);
if (item_animation_is_valid) {
ActionMenuItemAnimation *item_animation = &aml->item_animation;
int16_t new_cell_origin_y = 0;
switch (state) {
case ActionMenuLayerLongLabelScrollingAnimationState_Top:
new_cell_origin_y = item_animation->bottom_offset_y;
break;
case ActionMenuLayerLongLabelScrollingAnimationState_Middle:
new_cell_origin_y = (item_animation->top_offset_y + item_animation->bottom_offset_y) / 2;
break;
case ActionMenuLayerLongLabelScrollingAnimationState_Bottom:
new_cell_origin_y = item_animation->top_offset_y;
break;
default:
return;
}
prv_set_cell_offset(aml, new_cell_origin_y);
}
}
static void prv_render_action_menu_window(const ActionMenuLevel *root_level,
unsigned int selected_index,
ActionMenuLayerLongLabelScrollingAnimationState state,
unsigned int additional_crumbs) {
ActionMenuConfig config = {
.root_level = root_level,
.colors.background = GColorChromeYellow,
.did_close = prv_action_menu_did_close_cb,
};
ActionMenu *action_menu_window = app_action_menu_open(&config);
// Set the window on screen so its window handlers will be called
window_set_on_screen(&action_menu_window->window, true, true);
// Scroll down to the selected index
ActionMenuData *data = window_get_user_data(&action_menu_window->window);
data->view_model.num_dots += additional_crumbs;
data->crumbs_layer.level += additional_crumbs;
prv_set_selected_index(&data->action_menu_layer, selected_index, false /* animated */);
// Render the window so that we set the state of the cells again now that we've scrolled
window_render(&action_menu_window->window, &s_ctx);
// Update the animation state of the selected cell
prv_update_cell_for_long_label_scrolling_animation_state(&data->action_menu_layer, state);
// Render the window (for real this time)!
window_render(&action_menu_window->window, &s_ctx);
}
#define GRID_CELL_PADDING 5
typedef void (*RenderCallback)(SettingsContentSize content_size, const ActionMenuLevel *root_level,
unsigned int selected_index, unsigned int additional_crumbs);
static void prv_prepare_canvas_and_render_for_each_size(RenderCallback callback,
const ActionMenuLevel *root_level,
unsigned int selected_index,
unsigned int num_rows,
unsigned int additional_crumbs) {
gbitmap_destroy(s_dest_bitmap);
const unsigned int num_columns = SettingsContentSizeCount;
const int16_t bitmap_width = (DISP_COLS * num_columns) + (GRID_CELL_PADDING * (num_columns + 1));
const int16_t bitmap_height = (num_rows == 1) ? DISP_ROWS :
((DISP_ROWS * num_rows) + (GRID_CELL_PADDING * (num_rows + 1)));
const GSize bitmap_size = GSize(bitmap_width, bitmap_height);
s_dest_bitmap = gbitmap_create_blank(bitmap_size, CANVAS_GBITMAP_FORMAT);
s_ctx.dest_bitmap = *s_dest_bitmap;
s_ctx.draw_state.clip_box.size = bitmap_size;
s_ctx.draw_state.drawing_box.size = bitmap_size;
// Fill the bitmap with pink (on color) or white (on b&w) so it's easier to see errors
memset(s_dest_bitmap->addr, PBL_IF_COLOR_ELSE(GColorShockingPinkARGB8, GColorWhiteARGB8),
s_dest_bitmap->row_size_bytes * s_dest_bitmap->bounds.size.h);
for (SettingsContentSize content_size = 0; content_size < SettingsContentSizeCount;
content_size++) {
system_theme_set_content_size(settings_content_size_to_preferred_size(content_size));
callback(content_size, root_level, selected_index, additional_crumbs);
}
}
static void prv_render_action_menus_static(
SettingsContentSize content_size, const ActionMenuLevel *root_level,
unsigned int selected_index, unsigned int additional_crumbs) {
const int16_t x_offset = GRID_CELL_PADDING + (content_size * (GRID_CELL_PADDING + DISP_COLS));
s_ctx.draw_state.drawing_box.origin = GPoint(x_offset, 0);
prv_render_action_menu_window(root_level, selected_index,
ActionMenuLayerLongLabelScrollingAnimationState_Top,
additional_crumbs);
}
static void prv_render_action_menus_animated(
SettingsContentSize content_size, const ActionMenuLevel *root_level,
unsigned int selected_index, unsigned int additional_crumbs) {
const int16_t x_offset = GRID_CELL_PADDING + (content_size * (GRID_CELL_PADDING + DISP_COLS));
for (ActionMenuLayerLongLabelScrollingAnimationState animation_state = 0;
animation_state < ActionMenuLayerLongLabelScrollingAnimationStateCount;
animation_state++) {
const int16_t y_offset = GRID_CELL_PADDING +
(animation_state * (GRID_CELL_PADDING + DISP_ROWS));
s_ctx.draw_state.drawing_box.origin = GPoint(x_offset, y_offset);
prv_render_action_menu_window(root_level, selected_index, animation_state,
additional_crumbs);
}
}
void prv_prepare_canvas_and_render_action_menus_static(const ActionMenuLevel *root_level,
unsigned int selected_index,
unsigned int additional_crumbs) {
prv_prepare_canvas_and_render_for_each_size(
prv_render_action_menus_static, root_level, selected_index, 1 /* num_rows */,
additional_crumbs);
}
void prv_prepare_canvas_and_render_action_menus_animated(const ActionMenuLevel *root_level,
unsigned int selected_index) {
prv_prepare_canvas_and_render_for_each_size(
prv_render_action_menus_animated, root_level, selected_index,
ActionMenuLayerLongLabelScrollingAnimationStateCount,
0 /* additional_crumbs */);
}
// Tests
//////////////////////
void test_action_menu_window__wide_display_mode_with_just_titles(void) {
ActionMenuLevel *root_level = action_menu_level_create(3);
action_menu_level_add_action(root_level,
"I will text back",
prv_noop_action_callback,
NULL);
action_menu_level_add_action(root_level,
"Sorry, I can't talk right now, call me back at a later time",
prv_noop_action_callback,
NULL);
action_menu_level_add_action(root_level,
"I will call back",
prv_noop_action_callback,
NULL);
const unsigned int selected_index = 1;
prv_prepare_canvas_and_render_action_menus_animated(root_level, selected_index);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__thin_display_mode_with_emoji(void) {
// Copied from prv_create_emoji_level_from_action() in timeline_actions.c; it wouldn't work that
// well to just make the array T_STATIC in that function because we need to know its length too
static const char* thin_values[] = {
"😃",
"😉",
"😂",
"😍",
"😘",
"\xe2\x9d\xa4",
"😇",
"😎",
"😛",
"😟",
"😩",
"😭",
"😴",
"😐",
"😯",
"👍",
"👎",
"👌",
"💩",
"🎉",
"🍺",
};
ActionMenuLevel *root_level = action_menu_level_create(ARRAY_LENGTH(thin_values));
action_menu_level_set_display_mode(root_level, ActionMenuLevelDisplayModeThin);
for (size_t i = 0; i < ARRAY_LENGTH(thin_values); i++) {
action_menu_level_add_action(root_level, thin_values[i], prv_noop_action_callback, NULL);
}
const unsigned int selected_index = 0;
prv_prepare_canvas_and_render_action_menus_static(root_level, selected_index, 0);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__thin_display_mode_two_row(void) {
static const char* thin_values[] = { "a", "b", "c", "d", "e" };
ActionMenuLevel *root_level = action_menu_level_create(ARRAY_LENGTH(thin_values));
action_menu_level_set_display_mode(root_level, ActionMenuLevelDisplayModeThin);
for (size_t i = 0; i < ARRAY_LENGTH(thin_values); i++) {
action_menu_level_add_action(root_level, thin_values[i], prv_noop_action_callback, NULL);
}
const unsigned int selected_index = 4;
prv_prepare_canvas_and_render_action_menus_static(root_level, selected_index, 0);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__thin_display_mode_one_row(void) {
static const char* thin_values[] = { "Y", "N" };
ActionMenuLevel *root_level = action_menu_level_create(ARRAY_LENGTH(thin_values));
action_menu_level_set_display_mode(root_level, ActionMenuLevelDisplayModeThin);
for (size_t i = 0; i < ARRAY_LENGTH(thin_values); i++) {
action_menu_level_add_action(root_level, thin_values[i], prv_noop_action_callback, NULL);
}
const unsigned int selected_index = 1;
prv_prepare_canvas_and_render_action_menus_static(root_level, selected_index, 0);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__thin_display_mode_one_item(void) {
static const char* thin_values[] = { "Y" };
ActionMenuLevel *root_level = action_menu_level_create(ARRAY_LENGTH(thin_values));
action_menu_level_set_display_mode(root_level, ActionMenuLevelDisplayModeThin);
for (size_t i = 0; i < ARRAY_LENGTH(thin_values); i++) {
action_menu_level_add_action(root_level, thin_values[i], prv_noop_action_callback, NULL);
}
const unsigned int selected_index = 0;
prv_prepare_canvas_and_render_action_menus_static(root_level, selected_index, 0);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__wide_display_mode_with_chevron(void) {
ActionMenuLevel *root_level = action_menu_level_create(3);
ActionMenuLevel *voice_level = action_menu_level_create(1);
action_menu_level_add_action(voice_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, voice_level, "Voice");
ActionMenuLevel *template_level = action_menu_level_create(1);
action_menu_level_add_action(template_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, template_level, "Template");
ActionMenuLevel *emoji_level = action_menu_level_create(1);
action_menu_level_add_action(emoji_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, emoji_level, "Emoji");
const unsigned int selected_index = 1;
prv_prepare_canvas_and_render_action_menus_static(root_level, selected_index, 0);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__wide_display_mode_with_chevron_and_long_labels(void) {
ActionMenuLevel *root_level = action_menu_level_create(3);
ActionMenuLevel *voice_level = action_menu_level_create(1);
action_menu_level_add_action(voice_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, voice_level, "I will text back");
ActionMenuLevel *template_level = action_menu_level_create(1);
action_menu_level_add_action(template_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, template_level,
"Sorry, I can't talk right now, call me back at a later time");
ActionMenuLevel *emoji_level = action_menu_level_create(1);
action_menu_level_add_action(emoji_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, emoji_level, "I will call back");
const unsigned int selected_index = 1;
prv_prepare_canvas_and_render_action_menus_animated(root_level, selected_index);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__wide_display_mode_with_chevron_and_long_labels_hyphenated(void) {
ActionMenuLevel *root_level = action_menu_level_create(3);
ActionMenuLevel *voice_level = action_menu_level_create(1);
action_menu_level_add_action(voice_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, voice_level, "Dismiss");
ActionMenuLevel *template_level = action_menu_level_create(1);
action_menu_level_add_action(template_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, template_level,
"Reply to HUBERT BLAINE WOLFESCHLEGELSTEINHAUSENBERGERDORFF");
ActionMenuLevel *emoji_level = action_menu_level_create(1);
action_menu_level_add_action(emoji_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, emoji_level, "Open on phone");
const unsigned int selected_index = 1;
prv_prepare_canvas_and_render_action_menus_animated(root_level, selected_index);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_action_menu_window__wide_display_mode_with_separator(void) {
ActionMenuLevel *root_level = action_menu_level_create(3);
action_menu_level_add_action(root_level,
"Change Time",
prv_noop_action_callback,
NULL);
action_menu_level_add_action(root_level,
"Change Days",
prv_noop_action_callback,
NULL);
ActionMenuLevel *snooze_level = action_menu_level_create(1);
action_menu_level_add_action(snooze_level,
"This won't be seen",
prv_noop_action_callback,
NULL);
action_menu_level_add_child(root_level, snooze_level, "Snooze Delay");
root_level->separator_index = root_level->num_items - 1;
const unsigned int selected_index = 1;
prv_prepare_canvas_and_render_action_menus_static(root_level, selected_index, 1);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}

3753
tests/fw/ui/test_animation.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,136 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/animation_interpolate.h"
#include "applib/ui/animation.h"
#include "util/size.h"
#include "clar.h"
#include "stubs/stubs_logging.h"
#include "stubs/stubs_passert.h"
InterpolateInt64Function s_animation_private_current_interpolate_override;
InterpolateInt64Function animation_private_current_interpolate_override(void) {
return s_animation_private_current_interpolate_override;
}
void test_animation_interpolate__initialize(void) {
s_animation_private_current_interpolate_override = NULL;
}
void test_animation_interpolate__override_is_null(void) {
cl_assert_equal_i(-10000, interpolate_int16(0, -10000, 10000));
cl_assert_equal_i(10000, interpolate_int16(ANIMATION_NORMALIZED_MAX, -10000, 10000));
}
static AnimationProgress s_override_progress;
static int64_t s_override_from;
static int64_t s_override_to;
int64_t prv_override_capture_args(AnimationProgress p, int64_t a, int64_t b) {
s_override_progress = p;
s_override_from = a;
s_override_to = b;
return 1;
}
void test_animation_interpolate__override_gets_called(void) {
s_animation_private_current_interpolate_override = prv_override_capture_args;
cl_assert_equal_i(1, interpolate_int16(2, 3, 4));
cl_assert_equal_i(2, s_override_progress);
cl_assert_equal_i(3, s_override_from);
cl_assert_equal_i(4, s_override_to);
}
int64_t prv_override_times_two(AnimationProgress p, int64_t a, int64_t b) {
return interpolate_int64_linear(p, a, b) * 2;
}
void test_animation_interpolate__override_gets_clipped(void) {
s_animation_private_current_interpolate_override = prv_override_times_two;
cl_assert_equal_i(INT16_MIN, interpolate_int16(0, -20000, 20000));
cl_assert_equal_i(INT16_MAX, interpolate_int16(ANIMATION_NORMALIZED_MAX, -20000, 20000));
}
void test_animation_interpolate__moook(void) {
const int expected[] = {-20000, -19999, -19980, 20004, 20002, 20001, 20000};
const int num_frames = ARRAY_LENGTH(expected);
for (int i = 0; i < num_frames; i++) {
printf("frame: %d\n", i);
cl_assert_equal_i(interpolate_moook((i * ANIMATION_NORMALIZED_MAX) / num_frames,
-20000, 20000), expected[i]);
}
}
void test_animation_interpolate__moook_in(void) {
const int expected[] = {-20000, -19999, -19980};
const int num_frames = ARRAY_LENGTH(expected);
for (int i = 0; i < num_frames; i++) {
printf("frame: %d\n", i);
cl_assert_equal_i(interpolate_moook_in_only((i * ANIMATION_NORMALIZED_MAX) / num_frames,
-20000, 20000), expected[i]);
}
cl_assert_equal_i(interpolate_moook_in_only(ANIMATION_NORMALIZED_MAX,
-20000, 20000), 20000);
}
void test_animation_interpolate__moook_out(void) {
const int expected[] = {20004, 20002, 20001, 20000};
const int num_frames = ARRAY_LENGTH(expected);
for (int i = 0; i < num_frames; i++) {
printf("frame: %d\n", i);
cl_assert_equal_i(interpolate_moook_out((i * ANIMATION_NORMALIZED_MAX) / num_frames,
-20000, 20000, 0, true), expected[i]);
}
}
void test_animation_interpolate__moook_soft(void) {
const int32_t moook_num_soft_frames = 3;
cl_assert_equal_i(-20000, interpolate_moook_soft(0, -20000, 20000, moook_num_soft_frames));
// mid frame is closer to end due to more end frames
cl_assert_equal_i(6676, interpolate_moook_soft(ANIMATION_NORMALIZED_MAX / 2, -20000,
20000, moook_num_soft_frames));
cl_assert_equal_i(20000, interpolate_moook_soft(ANIMATION_NORMALIZED_MAX, -20000,
20000, moook_num_soft_frames));
}
static const int32_t s_custom_moook_in[] = {0, 2, 8};
static const int32_t s_custom_moook_out[] = {21, 9, 3, 0};
static const MoookConfig s_custom_moook = {
.frames_in = s_custom_moook_in,
.num_frames_in = ARRAY_LENGTH(s_custom_moook_in),
.frames_out = s_custom_moook_out,
.num_frames_out = ARRAY_LENGTH(s_custom_moook_out),
.num_frames_mid = 3,
};
void test_animation_interpolate__moook_custom(void) {
cl_assert_equal_i(330, interpolate_moook_custom_duration(&s_custom_moook));
cl_assert_equal_i(-20000, interpolate_moook_custom(0, -20000, 20000, &s_custom_moook));
// mid frame is closer to end due to more end frames
cl_assert_equal_i(6683, interpolate_moook_custom(ANIMATION_NORMALIZED_MAX / 2, -20000, 20000,
&s_custom_moook));
cl_assert_equal_i(20000, interpolate_moook_custom(ANIMATION_NORMALIZED_MAX, -20000, 20000,
&s_custom_moook));
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "pebble_asserts.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/animation.h"
// stubs
#include "stubs_logging.h"
#include "stubs_passert.h"
// tests
void test_animation_timing__scaled(void) {
AnimationProgress third = ANIMATION_NORMALIZED_MAX / 3;
AnimationProgress half = ANIMATION_NORMALIZED_MAX / 2;
AnimationProgress two_third = ANIMATION_NORMALIZED_MAX * 2 / 3;
AnimationProgress four_third = ANIMATION_NORMALIZED_MAX * 4 / 3;
AnimationProgress (*f)(AnimationProgress time_normalized,
AnimationProgress interval_start,
AnimationProgress interval_end) = animation_timing_scaled;
cl_assert_equal_i(-half, f(-half, 0, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(0, f(0, 0, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(third, f(third, 0, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(two_third, f(two_third, 0, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(four_third, f(four_third, 0, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(-65535, f(-half, 0, half));
cl_assert_equal_i(0, f(0, 0, half));
cl_assert_equal_i(43690, f(third, 0, half));
cl_assert_equal_i(87381, f(two_third, 0, half));
cl_assert_equal_i(174762, f(four_third, 0, half));
cl_assert_equal_i(-131066, f(-half, half, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(-65533, f(0, half, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(-21843, f(third, half, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(21845, f(two_third, half, ANIMATION_NORMALIZED_MAX));
cl_assert_equal_i(109224, f(four_third, half, ANIMATION_NORMALIZED_MAX));
}

View file

@ -0,0 +1,298 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/content_indicator_private.h"
#include "util/buffer.h"
// Fakes
////////////////////////////////////
#include "fake_app_timer.h"
#include "fake_content_indicator.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_gpath.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
extern void prv_content_indicator_update_proc(Layer *layer, GContext *ctx);
// Helpers
////////////////////////////////////
static Layer s_content_indicator_dummy_layer;
static LayerUpdateProc s_content_indicator_dummy_layer_update_proc;
ContentIndicatorConfig helper_get_dummy_config(void) {
return (ContentIndicatorConfig) {
.layer = &s_content_indicator_dummy_layer,
.times_out = false,
.alignment = GAlignLeft,
.colors = {
.foreground = GColorGreen,
.background = GColorRed
}
};
}
void helper_check_buffer_for_content_indicator(size_t index, ContentIndicator *content_indicator) {
ContentIndicatorsBuffer *content_indicators_buffer = content_indicator_get_current_buffer();
Buffer *buffer = &content_indicators_buffer->buffer;
ContentIndicator **content_indicators = (ContentIndicator**)buffer->data;
cl_assert_equal_p(content_indicators[index], content_indicator);
}
void helper_check_configs_for_equality(ContentIndicatorConfig a, ContentIndicatorConfig b) {
cl_assert_equal_p(a.layer, b.layer);
cl_assert_equal_b(a.times_out, b.times_out);
cl_assert(a.alignment == b.alignment);
cl_assert(a.colors.foreground.argb == b.colors.foreground.argb);
cl_assert(a.colors.background.argb == b.colors.background.argb);
}
// Setup
/////////////////////////////////
void test_content_indicator__initialize(void) {
// Initialize the static buffer of content indicators
content_indicator_init_buffer(content_indicator_get_current_buffer());
// Reset the dummy layer's fields
memset(&s_content_indicator_dummy_layer, 0, sizeof(Layer));
}
void test_content_indicator__cleanup(void) {
}
// Tests
////////////////////////////////////
void test_content_indicator__create_should_add_to_buffer(void) {
ContentIndicator *content_indicator;
for (size_t i = 0; i < CONTENT_INDICATOR_BUFFER_SIZE; i++) {
content_indicator = content_indicator_create();
cl_assert(content_indicator);
helper_check_buffer_for_content_indicator(i, content_indicator);
}
// Creating more content indicators than the buffer can hold should return NULL
cl_assert_equal_p(content_indicator_create(), NULL);
}
void test_content_indicator__init_should_add_to_buffer(void) {
ContentIndicator content_indicator;
for (size_t i = 0; i < CONTENT_INDICATOR_BUFFER_SIZE; i++) {
content_indicator_init(&content_indicator);
helper_check_buffer_for_content_indicator(i, &content_indicator);
}
// Initializing more content indicators than the buffer can hold should assert
cl_assert_passert(content_indicator_init(&content_indicator));
}
void test_content_indicator__deinit_should_remove_from_buffer(void) {
ContentIndicatorsBuffer *content_indicators_buffer = content_indicator_get_current_buffer();
Buffer *buffer = &content_indicators_buffer->buffer;
ContentIndicator content_indicator;
size_t bytes_written = 0;
for (size_t i = 0; i < CONTENT_INDICATOR_BUFFER_SIZE; i++) {
content_indicator_init(&content_indicator);
bytes_written += sizeof(ContentIndicator *);
cl_assert_equal_i(buffer->bytes_written, bytes_written);
}
for (size_t i = 0; i < CONTENT_INDICATOR_BUFFER_SIZE; i++) {
content_indicator_deinit(&content_indicator);
bytes_written -= sizeof(ContentIndicator *);
cl_assert_equal_i(buffer->bytes_written, bytes_written);
}
}
void test_content_indicator__configuring_should_configure(void) {
ContentIndicator content_indicator;
content_indicator_init(&content_indicator);
ContentIndicatorDirectionData *direction_data = content_indicator.direction_data;
// Test setting a dummy configuration for a direction
const ContentIndicatorConfig dummy_config = helper_get_dummy_config();
const ContentIndicatorDirection direction = ContentIndicatorDirectionUp;
dummy_config.layer->update_proc = s_content_indicator_dummy_layer_update_proc;
cl_assert(content_indicator_configure_direction(&content_indicator, direction, &dummy_config));
helper_check_configs_for_equality(dummy_config, direction_data[direction].config);
// Should save a reference to the config layer's update proc
cl_assert_equal_p(dummy_config.layer->update_proc, direction_data->original_update_proc);
cl_assert_equal_p(direction_data->original_update_proc,
s_content_indicator_dummy_layer_update_proc);
}
void test_content_indicator__configuring_different_directions_with_same_layer_should_fail(void) {
ContentIndicator content_indicator;
content_indicator_init(&content_indicator);
// Setting a dummy configuration for a direction should return true
const ContentIndicatorConfig dummy_config = helper_get_dummy_config();
dummy_config.layer->update_proc = s_content_indicator_dummy_layer_update_proc;
cl_assert(content_indicator_configure_direction(&content_indicator,
ContentIndicatorDirectionUp,
&dummy_config));
// Using the same dummy configuration (which has the same layer) to configure a different
// direction should fail
cl_assert(!content_indicator_configure_direction(&content_indicator,
ContentIndicatorDirectionDown,
&dummy_config));
}
void test_content_indicator__setting_content_available_should_update_layer_update_proc(void) {
ContentIndicator content_indicator;
content_indicator_init(&content_indicator);
ContentIndicatorDirectionData *direction_data = content_indicator.direction_data;
const ContentIndicatorConfig dummy_config = helper_get_dummy_config();
const ContentIndicatorDirection direction = ContentIndicatorDirectionUp;
dummy_config.layer->update_proc = s_content_indicator_dummy_layer_update_proc;
cl_assert(content_indicator_configure_direction(&content_indicator, direction, &dummy_config));
cl_assert_equal_p(dummy_config.layer->update_proc, direction_data->original_update_proc);
cl_assert_equal_p(direction_data->original_update_proc,
s_content_indicator_dummy_layer_update_proc);
// Setting content available should switch the layer's update proc to draw an arrow
content_indicator_set_content_available(&content_indicator, direction, true);
cl_assert_equal_p(dummy_config.layer->update_proc, prv_content_indicator_update_proc);
// Setting content unavailable should revert the layer's update proc
content_indicator_set_content_available(&content_indicator, direction, false);
cl_assert_equal_p(dummy_config.layer->update_proc, direction_data->original_update_proc);
cl_assert_equal_p(direction_data->original_update_proc,
s_content_indicator_dummy_layer_update_proc);
}
void test_content_indicator__creating_for_scroll_layer(void) {
ScrollLayer scroll_layer;
ContentIndicator *content_indicator = content_indicator_get_or_create_for_scroll_layer(
&scroll_layer);
cl_assert(content_indicator);
// Should save a reference to the scroll layer
cl_assert_equal_p(content_indicator->scroll_layer, &scroll_layer);
// Should retrieve the same content indicator with the same scroll layer
ContentIndicator *content_indicator2 = content_indicator_get_or_create_for_scroll_layer(
&scroll_layer);
cl_assert(content_indicator2);
// Should save a reference to the scroll layer
cl_assert_equal_p(content_indicator2->scroll_layer, &scroll_layer);
cl_assert_equal_p(content_indicator2, content_indicator);
// Should retrieve a different content indicator for a different scroll layer
ScrollLayer scroll_layer2;
ContentIndicator *content_indicator3 = content_indicator_get_or_create_for_scroll_layer(
&scroll_layer2);
cl_assert(content_indicator3);
// Should save a reference to the scroll layer
cl_assert_equal_p(content_indicator3->scroll_layer, &scroll_layer2);
cl_assert(content_indicator3 != content_indicator);
}
void test_content_indicator__should_only_be_created_for_scroll_layer_upon_client_access(void) {
ContentIndicatorsBuffer *content_indicators_buffer = content_indicator_get_current_buffer();
Buffer *buffer = &content_indicators_buffer->buffer;
// At the start of the test, the buffer should be empty
cl_assert(buffer_is_empty(buffer));
ScrollLayer scroll_layer;
// Trying to access the ContentIndicator for this ScrollLayer should return NULL because we
// haven't tried to access it as the client yet
cl_assert_equal_p(content_indicator_get_for_scroll_layer(&scroll_layer), NULL);
// And the buffer should still be empty
cl_assert(buffer_is_empty(buffer));
// Now we try to access it as the client, which should actually create the ContentIndicator
ContentIndicator *content_indicator = content_indicator_get_or_create_for_scroll_layer(
&scroll_layer);
cl_assert(content_indicator);
// The ContentIndicator should have a reference to the ScrollLayer
cl_assert_equal_p(content_indicator->scroll_layer, &scroll_layer);
// The buffer should now hold the newly created ContentIndicator
cl_assert(buffer->bytes_written == sizeof(ContentIndicator *));
// Finally, calling content_indicator_get_for_scroll_layer() again should return the same
// ContentIndicator
ContentIndicator *content_indicator2 = content_indicator_get_for_scroll_layer(&scroll_layer);
cl_assert(content_indicator2);
cl_assert_equal_p(content_indicator2, content_indicator);
// The buffer should still only hold the single ContentIndicator
cl_assert(buffer->bytes_written == sizeof(ContentIndicator *));
}
void test_content_indicator__pass_null_config_to_reset_direction_data(void) {
ContentIndicator content_indicator;
content_indicator_init(&content_indicator);
ContentIndicatorDirectionData *direction_data = content_indicator.direction_data;
const ContentIndicatorConfig dummy_config = helper_get_dummy_config();
const ContentIndicatorDirection direction = ContentIndicatorDirectionUp;
dummy_config.layer->update_proc = s_content_indicator_dummy_layer_update_proc;
cl_assert(content_indicator_configure_direction(&content_indicator, direction, &dummy_config));
cl_assert_equal_p(dummy_config.layer->update_proc, direction_data->original_update_proc);
cl_assert_equal_p(direction_data->original_update_proc,
s_content_indicator_dummy_layer_update_proc);
// Setting content available should switch the layer's update proc
content_indicator_set_content_available(&content_indicator, direction, true);
cl_assert_equal_p(dummy_config.layer->update_proc, prv_content_indicator_update_proc);
// Direction data should be emptied and layer's update proc should return to original when NULL
// config is passed
cl_assert(content_indicator_configure_direction(&content_indicator, direction, NULL));
cl_assert_equal_p(dummy_config.layer->update_proc, s_content_indicator_dummy_layer_update_proc);
cl_assert(!direction_data[direction].config.layer);
// Setting content available should not change layer update proc without reconfiguring.
content_indicator_set_content_available(&content_indicator, direction, true);
cl_assert_equal_p(dummy_config.layer->update_proc, s_content_indicator_dummy_layer_update_proc);
}
void test_content_indicator__re_configure_direction(void) {
ContentIndicator content_indicator;
content_indicator_init(&content_indicator);
const ContentIndicatorConfig dummy_config = helper_get_dummy_config();
const ContentIndicatorDirection up = ContentIndicatorDirectionUp;
const ContentIndicatorDirection down = ContentIndicatorDirectionDown;
cl_assert(content_indicator_configure_direction(&content_indicator, up, &dummy_config));
// re-configure with the same direction, should be a success
cl_assert(content_indicator_configure_direction(&content_indicator, up, &dummy_config));
// re-configure with a different same direction, should fail
cl_assert(!content_indicator_configure_direction(&content_indicator, down, &dummy_config));
}

View file

@ -0,0 +1,187 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "util/graphics.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
// Fakes
/////////////////////
#include "fixtures/load_test_resources.h"
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_app_state.h"
#include "stubs_bootbits.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static FrameBuffer *fb;
static GContext s_ctx;
static FontInfo s_font_info;
static GBitmap *s_dest_bitmap;
void test_emoji_fonts__initialize(void) {
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
memset(&s_font_info, 0, sizeof(s_font_info));
resource_init();
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, fb);
framebuffer_clear(fb);
}
void test_emoji_fonts__cleanup(void) {
free(fb);
fb = NULL;
gbitmap_destroy(s_dest_bitmap);
s_dest_bitmap = NULL;
}
// Helpers
//////////////////////
static char s_emoji_string[] =
"😄😃😀😊☺😉😍😘😚😗😙😜😝😛😳😁😔😌😒😞😣😢😂😭😥😪😰😅😓😩😫😨😱"
"😠😡😤😖😆😋😷😎😴😵😲😟😧😈👿😮😬😐😕😯😶😇😏😑😺😸😻😽😼🙀😿😹😾💩"
"👍👎👌👊✊✌👋✋👐👆👇👉👈🙌🙏☝👏💛💙💜💚❤💔💗💓💕💖💞💘💋🐥🎉💩🍻🍺"
"💪🔥🐵🙈→►★🎤🎥📷🎵🎁";
static void prv_render_text(GContext *ctx, const char *text, GFont const font, const GRect *box,
const GTextOverflowMode overflow_mode, const GTextAlignment alignment,
GTextLayoutCacheRef layout, bool render) {
if (render) {
graphics_draw_text(ctx, text, font, *box, overflow_mode, alignment, layout);
} else {
graphics_text_layout_get_max_used_size(ctx, text, font, *box, overflow_mode, alignment,
layout);
}
}
static GSize prv_draw_emoji(GContext *ctx, const GRect *bounds, GFont font, bool render) {
TextLayoutExtended layout = {
.line_spacing_delta = 2, // Give some space for the larger emojis
};
graphics_context_set_text_color(ctx, GColorBlack);
prv_render_text(ctx, s_emoji_string, font, bounds, GTextOverflowModeWordWrap,
GTextAlignmentLeft, (GTextLayoutCacheRef)&layout, true /* render */);
return layout.max_used_size;
}
void prv_prepare_canvas(GSize bitmap_size, GColor background_color) {
gbitmap_destroy(s_dest_bitmap);
s_dest_bitmap = gbitmap_create_blank(bitmap_size, GBitmapFormat8Bit);
s_ctx.dest_bitmap = *s_dest_bitmap;
s_ctx.draw_state.clip_box.size = bitmap_size;
s_ctx.draw_state.drawing_box.size = bitmap_size;
memset(s_dest_bitmap->addr, background_color.argb,
s_dest_bitmap->row_size_bytes * s_dest_bitmap->bounds.size.h);
}
void prv_prepare_canvas_and_render_emoji(ResourceId font_handle) {
// Calculate canvas size necessary
cl_assert(text_resources_init_font(0, font_handle, 0, &s_font_info));
const GPoint margin = { 10, 10 }; // Canvas margins
const int max_width = 300; // Canvas width, choose any visually pleasing width
const int max_height = 2000; // Large size protected from overflow and automatically truncated
const GSize max_size = { max_width, max_height };
// FIXME: PBL-34261 graphics_text_layout_get_max_used_size reports a max of 192px in a unit test
// with the default aplite framebuffer size. Work around this issue by creating a larger canvas.
prv_prepare_canvas(max_size, GColorWhite);
const GRect max_draw_frame = grect_inset_internal((GRect){ .size = max_size },
margin.x, margin.y);
const GSize used_size = prv_draw_emoji(&s_ctx, &max_draw_frame, &s_font_info,
false /* render */);
// Resize the canvas to the used size.
const GSize canvas_size = { max_width, used_size.h + 2 * margin.y };
prv_prepare_canvas(canvas_size, GColorWhite);
// Create the new canvas
const GRect draw_frame = grect_inset_internal((GRect){ .size = canvas_size },
margin.x, margin.y);
prv_draw_emoji(&s_ctx, &draw_frame, &s_font_info, true /* render */);
}
// Tests
//////////////////////
void test_emoji_fonts__gothic_14_emoji(void) {
prv_prepare_canvas_and_render_emoji(RESOURCE_ID_GOTHIC_14_EMOJI);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_emoji_fonts__gothic_18_emoji(void) {
prv_prepare_canvas_and_render_emoji(RESOURCE_ID_GOTHIC_18_EMOJI);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_emoji_fonts__gothic_24_emoji(void) {
prv_prepare_canvas_and_render_emoji(RESOURCE_ID_GOTHIC_24_EMOJI);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_emoji_fonts__gothic_28_emoji(void) {
prv_prepare_canvas_and_render_emoji(RESOURCE_ID_GOTHIC_28_EMOJI);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}

View file

@ -0,0 +1,176 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/content_indicator_private.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window_private.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "shell/system_theme.h"
#include "util/graphics.h"
#include "util/hash.h"
#include "util/math.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
static GContext s_ctx;
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_click.h"
#include "stubs_heap.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscall_internal.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_vibes.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
AnimationProgress animation_timing_scaled(AnimationProgress time_normalized,
AnimationProgress interval_start,
AnimationProgress interval_end) {
return interval_end;
}
AnimationProgress animation_timing_curve(AnimationProgress time_normalized, AnimationCurve curve) {
return time_normalized;
}
KinoReel *kino_reel_scale_segmented_create(KinoReel *from_reel, bool take_ownership,
GRect screen_frame) {
return NULL;
}
void kino_reel_scale_segmented_set_deflate_effect(KinoReel *reel, int16_t expand) {}
bool kino_reel_scale_segmented_set_delay_by_distance(KinoReel *reel, GPoint target) {
return false;
}
uint16_t time_ms(time_t *tloc, uint16_t *out_ms) { return 0; }
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static FrameBuffer *fb = NULL;
void test_expandable_dialog__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
// Must use System init mode to enable orphan avoidance algorithm
const GContextInitializationMode context_init_mode = GContextInitializationMode_System;
graphics_context_init(&s_ctx, fb, context_init_mode);
framebuffer_clear(fb);
// Setup resources
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
}
void test_expandable_dialog__cleanup(void) {
free(fb);
}
// Helpers
//////////////////////
void prv_manual_scroll(ScrollLayer *scroll_layer, int8_t dir);
void prv_push_and_render_expandable_dialog(ExpandableDialog *expandable_dialog,
uint32_t num_times_to_scroll_down) {
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_INFINITE /* no timeout */);
window_set_on_screen(&dialog->window, true, true);
for (uint32_t i = 0; i < num_times_to_scroll_down; i++) {
scroll_layer_scroll(&expandable_dialog->scroll_layer, ScrollDirectionDown,
false /* animated */);
}
window_render(&dialog->window, &s_ctx);
}
// Tests
//////////////////////
void test_expandable_dialog__dismiss_tutorial_portuguese_orphan(void) {
const char* tutorial_msg = "Remova rapidamente todas as notificações ao segurar o botão Select "
"durante 2 segundos a partir de qualquer notificação.";
ExpandableDialog *expandable_dialog = expandable_dialog_create_with_params(
"Dismiss First Use", RESOURCE_ID_QUICK_DISMISS, tutorial_msg,
gcolor_legible_over(GColorLightGray), GColorLightGray, NULL,
RESOURCE_ID_ACTION_BAR_ICON_CHECK, NULL);
// Scroll down to the last page where we will observe the orphan avoidance effect
const uint32_t num_times_to_scroll_down = 2;
prv_push_and_render_expandable_dialog(expandable_dialog, num_times_to_scroll_down);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}

104
tests/fw/ui/test_jumboji.c Normal file
View file

@ -0,0 +1,104 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "services/normal/timeline/notification_jumboji_table.h"
#include "services/normal/timeline/notification_layout.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_attribute.h"
#include "stubs_clock.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_kino_layer.h"
#include "stubs_layer.h"
#include "stubs_layout_node.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pin_db.h"
#include "stubs_resources.h"
#include "stubs_shell_prefs.h"
#include "stubs_text_node.h"
#include "stubs_timeline_item.h"
#include "stubs_timeline_resources.h"
// Statics
////////////////////////////////////
static const EmojiEntry s_emoji_table[] = JUMBOJI_TABLE(EMOJI_ENTRY);
// Tests
//////////////////////
void test_jumboji__jumboji_table(void) {
for (unsigned int i = 0; i < ARRAY_LENGTH(s_emoji_table); i++) {
const EmojiEntry *emoji = &s_emoji_table[i];
const Codepoint codepoint = utf8_peek_codepoint((utf8_t *)emoji->string, NULL);
if (emoji->codepoint != codepoint) {
printf(" ENTRY(\"%s\", 0x%05x, %s)\n", emoji->string, (unsigned int)codepoint,
emoji->resource_name);
}
cl_check(emoji->codepoint == codepoint);
}
}
ResourceId prv_get_emoji_icon_by_string(const EmojiEntry *table, const char *str);
void test_jumboji__jumboji_detection(void) {
// NULL is not an emoji
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, NULL) == INVALID_RESOURCE);
// Empty string is not an emoji
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "") == INVALID_RESOURCE);
// Single emoji is detected
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "😀") ==
RESOURCE_ID_EMOJI_BIG_OPEN_SMILE_LARGE);
// Leading whitespace is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, " 😀") ==
RESOURCE_ID_EMOJI_BIG_OPEN_SMILE_LARGE);
// Trailing whitespace is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "😀 ") ==
RESOURCE_ID_EMOJI_BIG_OPEN_SMILE_LARGE);
// Leading and trailing whitespace is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, " 😀 ") ==
RESOURCE_ID_EMOJI_BIG_OPEN_SMILE_LARGE);
// Double emoji is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "😀😂") == INVALID_RESOURCE);
// LTR indicator is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "\u200E😂") ==
RESOURCE_ID_EMOJI_LAUGHING_WITH_TEARS_LARGE);
// Zero-width-no-break at the end is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "😂\uFEFF") ==
RESOURCE_ID_EMOJI_LAUGHING_WITH_TEARS_LARGE);
// Skin tone modifier is ignored
cl_check(prv_get_emoji_icon_by_string(s_emoji_table, "👍\xf0\x9f\x8f\xbe") ==
RESOURCE_ID_EMOJI_THUMBS_UP_LARGE);
}

View file

@ -0,0 +1,412 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/kino/kino_player.h"
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/kino/kino_reel_gbitmap.h"
#include "applib/ui/kino/kino_reel_gbitmap_sequence.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/kino/kino_reel_pdcs.h"
#include "applib/ui/kino/kino_reel_custom.h"
#include "clar.h"
// Fakes
////////////////////////////////////
#include "fake_resource_syscalls.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_applib_resource.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_gpath.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_resources.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {}
typedef uint16_t ResourceId;
const uint8_t *resource_get_builtin_bytes(ResAppNum app_num, uint32_t resource_id,
uint32_t *num_bytes_out) { return NULL; }
typedef struct TestReelData {
uint32_t elapsed_ms;
uint32_t duration_ms;
} TestReelData;
static int s_num_destructor_calls;
static void prv_destructor(KinoReel *reel) {
s_num_destructor_calls++;
free(kino_reel_custom_get_data(reel));
}
static uint32_t prv_elapsed_getter(KinoReel *reel) {
return ((TestReelData*)kino_reel_custom_get_data(reel))->elapsed_ms;
}
static bool prv_elapsed_setter(KinoReel *reel, uint32_t elapsed_ms) {
((TestReelData*)kino_reel_custom_get_data(reel))->elapsed_ms = elapsed_ms;
return true;
}
static uint32_t prv_duration_getter(KinoReel *reel) {
return ((TestReelData*)kino_reel_custom_get_data(reel))->duration_ms;
}
static struct TestReelData *test_reel_data;
static KinoReelImpl *test_reel_impl = NULL;
static KinoReel *test_reel = NULL;
static KinoPlayer *test_player = NULL;
// Setup
void test_kino_player__initialize(void) {
test_reel_data = malloc(sizeof(TestReelData));
memset(test_reel_data, 0, sizeof(TestReelData));
s_num_destructor_calls = 0;
test_reel_impl = malloc(sizeof(KinoReelImpl));
*test_reel_impl = (KinoReelImpl) {
.destructor = prv_destructor,
.set_elapsed = prv_elapsed_setter,
.get_elapsed = prv_elapsed_getter,
.get_duration = prv_duration_getter
};
test_reel = kino_reel_custom_create(test_reel_impl, test_reel_data);
cl_assert(test_reel != NULL);
test_player = malloc(sizeof(KinoPlayer));
memset(test_player, 0, sizeof(KinoPlayer));
kino_player_set_reel(test_player, test_reel, true);
}
// Teardown
void test_kino_player__cleanup(void) {
kino_reel_destroy(test_reel);
}
// Tests
////////////////////////////////////
extern void prv_play_animation_update(Animation *animation, const AnimationProgress normalized);
void test_kino_player__finite_animation_finite_reel_foward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
kino_player_play(test_player);
animation_set_elapsed(test_player->animation, 1234); // intentionally bad value
prv_play_animation_update(test_player->animation, ANIMATION_NORMALIZED_MAX * 20 / 300);
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__create_finite_animation_finite_reel_foward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_elapsed(animation, 1234); // intentionally bad value
prv_play_animation_update(animation, ANIMATION_NORMALIZED_MAX * 20 / 300);
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__finite_animation_finite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
kino_player_play(test_player);
animation_set_reverse(test_player->animation, true);
animation_set_elapsed(test_player->animation, 1234); // intentionally bad value
prv_play_animation_update(test_player->animation,
ANIMATION_NORMALIZED_MAX - ANIMATION_NORMALIZED_MAX * 20 / 300);
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 300 - 20);
}
void test_kino_player__create_finite_animation_finite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_reverse(animation, true);
animation_set_elapsed(animation, 1234); // intentionally bad value
prv_play_animation_update(animation,
ANIMATION_NORMALIZED_MAX - ANIMATION_NORMALIZED_MAX * 20 / 300);
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 300 - 20);
}
void test_kino_player__finite_animation_infinite_reel_forward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
kino_player_play(test_player);
animation_set_elapsed(test_player->animation, 20);
animation_set_duration(test_player->animation, 300);
prv_play_animation_update(test_player->animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__create_finite_animation_infinite_reel_forward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_elapsed(animation, 20);
animation_set_duration(animation, 300);
prv_play_animation_update(animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__infinite_animation_finite_reel_forward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
kino_player_play(test_player);
animation_set_elapsed(test_player->animation, 20);
animation_set_duration(test_player->animation, ANIMATION_DURATION_INFINITE);
prv_play_animation_update(test_player->animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__create_infinite_animation_finite_reel_forward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_elapsed(animation, 20);
animation_set_duration(animation, ANIMATION_DURATION_INFINITE);
prv_play_animation_update(animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__infinite_animation_infinite_reel_forward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
kino_player_play(test_player);
animation_set_elapsed(test_player->animation, 20);
animation_set_duration(test_player->animation, ANIMATION_DURATION_INFINITE);
prv_play_animation_update(test_player->animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__create_infinite_animation_infinite_reel_forward(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_elapsed(animation, 20);
animation_set_duration(animation, ANIMATION_DURATION_INFINITE);
prv_play_animation_update(animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 20);
}
void test_kino_player__infinite_animation_finite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
kino_player_play(test_player);
animation_set_reverse(test_player->animation, true);
animation_set_duration(test_player->animation, ANIMATION_DURATION_INFINITE);
animation_set_elapsed(test_player->animation, 20);
prv_play_animation_update(test_player->animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 300 - 20);
}
void test_kino_player__create_infinite_animation_finite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = 300;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_reverse(animation, true);
animation_set_duration(animation, ANIMATION_DURATION_INFINITE);
animation_set_elapsed(animation, 20);
prv_play_animation_update(animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), 300 - 20);
}
void test_kino_player__finite_animation_infinite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
kino_player_play(test_player);
animation_set_reverse(test_player->animation, true);
animation_set_duration(test_player->animation, 300);
animation_set_elapsed(test_player->animation, 20);
prv_play_animation_update(test_player->animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), ANIMATION_DURATION_INFINITE);
}
void test_kino_player__create_finite_animation_infinite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_reverse(animation, true);
animation_set_duration(animation, 300);
animation_set_elapsed(animation, 20);
prv_play_animation_update(animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), ANIMATION_DURATION_INFINITE);
}
void test_kino_player__infinite_animation_infinite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
kino_player_play(test_player);
animation_set_reverse(test_player->animation, true);
animation_set_duration(test_player->animation, ANIMATION_DURATION_INFINITE);
animation_set_elapsed(test_player->animation, 20);
prv_play_animation_update(test_player->animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), ANIMATION_DURATION_INFINITE);
}
void test_kino_player__create_infinite_animation_infinite_reel_reverse(void) {
// Choose duration and elapsed to have clean division for
// ANIMATION_NORMALIZED_MAX * elapsed / duration = whole_number
test_reel_data->duration_ms = ANIMATION_DURATION_INFINITE;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
animation_set_reverse(animation, true);
animation_set_duration(animation, ANIMATION_DURATION_INFINITE);
animation_set_elapsed(animation, 20);
prv_play_animation_update(animation, 0); // intentionally bad value
cl_assert_equal_i(kino_reel_get_elapsed(test_reel), ANIMATION_DURATION_INFINITE);
}
void test_kino_player__create_animation_is_immutable(void) {
test_reel_data->duration_ms = 300;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
cl_assert_equal_i(animation_is_immutable(animation), true);
}
void test_kino_player__create_animation_is_unscheduled_by_play_pause_rewind(void) {
test_reel_data->duration_ms = 300;
Animation *animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
// Play plays a new animation
cl_assert_equal_i(animation_is_scheduled(animation), true);
kino_player_play(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
// Create play animation unschedules previous animation
animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
// Pause unschedules the current animation
cl_assert_equal_i(animation_is_scheduled(animation), true);
kino_player_pause(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation = (Animation *)kino_player_create_play_animation(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
animation_schedule(animation);
// Rewind unschedules the current animation
cl_assert_equal_i(animation_is_scheduled(animation), true);
kino_player_rewind(test_player);
cl_assert_equal_i(animation_is_scheduled(animation), false);
}
void test_kino_player__set_reel_calls_destructor(void) {
kino_player_set_reel(test_player, test_reel, true);
cl_assert_equal_p(test_player->reel, test_reel);
cl_assert_equal_i(s_num_destructor_calls, 0);
kino_player_set_reel(test_player, NULL, true);
cl_assert_equal_p(test_player->reel, NULL);
cl_assert_equal_i(s_num_destructor_calls, 1);
kino_player_set_reel(test_player, NULL, true);
cl_assert_equal_p(test_player->reel, NULL);
cl_assert_equal_i(s_num_destructor_calls, 1);
test_reel = NULL;
}
void test_kino_player__set_reel_does_not_call_destructor(void) {
test_player->owns_reel = false;
kino_player_set_reel(test_player, test_reel, false);
cl_assert_equal_p(test_player->reel, test_reel);
cl_assert_equal_i(s_num_destructor_calls, 0);
kino_player_set_reel(test_player, NULL, true);
cl_assert_equal_p(test_player->reel, NULL);
cl_assert_equal_i(s_num_destructor_calls, 0);
}

View file

@ -0,0 +1,196 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/kino/kino_reel_custom.h"
#include "applib/ui/kino/kino_reel_gbitmap.h"
#include "applib/ui/kino/kino_reel_gbitmap_sequence.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/kino/kino_reel_pdcs.h"
#include "util/graphics.h"
#include "clar.h"
// Fakes
////////////////////////////////////
#include "fake_resource_syscalls.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_gpath.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_resources.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
void framebuffer_clear(FrameBuffer* f) {}
void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {}
typedef uint16_t ResourceId;
const uint8_t *resource_get_builtin_bytes(ResAppNum app_num, uint32_t resource_id,
uint32_t *num_bytes_out) { return NULL; }
// Helper Functions
////////////////////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/8bit/test_framebuffer.h"
static FrameBuffer *fb = NULL;
// Setup
void test_kino_reel__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
fb->size = (GSize) {DISP_COLS, DISP_ROWS};
}
// Teardown
void test_kino_reel__cleanup(void) {
free(fb);
}
// Tests
////////////////////////////////////
void test_kino_reel__resource_gbitmap(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Test loading GBitmap Kino Reel
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_kino_reel__resource_gbitmap.pbi");
cl_assert(resource_id != UINT32_MAX);
KinoReel *kino_reel = kino_reel_create_with_resource(resource_id);
cl_assert(kino_reel);
cl_assert(kino_reel_get_type(kino_reel) == KinoReelTypeGBitmap);
cl_assert_equal_i(kino_reel_get_data_size(kino_reel), 2308);
kino_reel_draw(kino_reel, &ctx, GPointZero);
}
void test_kino_reel__resource_gbitmap_sequence(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Test loading GBitmap Sequence Kino Reel
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_kino_reel__resource_gbitmap_sequence.apng");
cl_assert(resource_id != UINT32_MAX);
KinoReel *kino_reel = kino_reel_create_with_resource(resource_id);
cl_assert(kino_reel);
cl_assert(kino_reel_get_type(kino_reel) == KinoReelTypeGBitmapSequence);
// We expect the default 0 because get_data_size hasn't been implemented for GBitmapSequence
cl_assert_equal_i(kino_reel_get_data_size(kino_reel), 0);
kino_reel_draw(kino_reel, &ctx, GPointZero);
}
void test_kino_reel__resource_pdci(void) {
// Test loading PDCI Kino Reel
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_kino_reel__resource_pdci.pdc");
cl_assert(resource_id != UINT32_MAX);
KinoReel *kino_reel = kino_reel_create_with_resource(resource_id);
cl_assert(kino_reel);
cl_assert(kino_reel_get_type(kino_reel) == KinoReelTypePDCI);
cl_assert_equal_i(kino_reel_get_data_size(kino_reel), 192);
}
void test_kino_reel__resource_pdcs(void) {
// Test loading PDCS Kino Reel
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_kino_reel__resource_pdcs.pdc");
cl_assert(resource_id != UINT32_MAX);
KinoReel *kino_reel = kino_reel_create_with_resource(resource_id);
cl_assert(kino_reel);
cl_assert(kino_reel_get_type(kino_reel) == KinoReelTypePDCS);
cl_assert_equal_i(kino_reel_get_data_size(kino_reel), 356);
}
void test_kino_reel__verify_pdci_get_list(void) {
// Test loading PDCI Kino Reel
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_kino_reel__resource_pdci.pdc");
cl_assert(resource_id != UINT32_MAX);
KinoReel *kino_reel = kino_reel_create_with_resource(resource_id);
cl_assert(kino_reel);
cl_assert(kino_reel_get_type(kino_reel) == KinoReelTypePDCI);
// Verify that list retrieved from kino_reel same as from command_image
GDrawCommandList *list = kino_reel_get_gdraw_command_list(kino_reel);
GDrawCommandList *list_direct = gdraw_command_image_get_command_list(
kino_reel_get_gdraw_command_image(kino_reel));
cl_assert(list == list_direct && list != NULL);
}
void test_kino_reel__verify_pdcs_get_list(void) {
// Test loading PDCI Kino Reel
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_kino_reel__resource_pdcs.pdc");
cl_assert(resource_id != UINT32_MAX);
KinoReel *kino_reel = kino_reel_create_with_resource(resource_id);
cl_assert(kino_reel);
cl_assert(kino_reel_get_type(kino_reel) == KinoReelTypePDCS);
kino_reel_set_elapsed(kino_reel, 0);
GDrawCommandList *list1 = kino_reel_get_gdraw_command_list(kino_reel);
GDrawCommandList *list1_direct = gdraw_command_frame_get_command_list(
gdraw_command_sequence_get_frame_by_elapsed(
kino_reel_get_gdraw_command_sequence(kino_reel), 0));
cl_assert(list1 != NULL);
cl_assert(list1 == list1_direct);
// Test that after elapsed, frame has changed and new list is correct
kino_reel_set_elapsed(kino_reel, 100);
GDrawCommandList *list2 = kino_reel_get_gdraw_command_list(kino_reel);
GDrawCommandList *list2_direct = gdraw_command_frame_get_command_list(
gdraw_command_sequence_get_frame_by_elapsed(
kino_reel_get_gdraw_command_sequence(kino_reel), 100));
cl_assert(list2 != NULL);
cl_assert(list2 != list1);
cl_assert(list2 == list2_direct);
}
static KinoReelProcessor s_dummy_processor;
static void prv_dummy_impl_draw_processed(KinoReel *reel, GContext *ctx, GPoint offset,
KinoReelProcessor *processor) {
cl_assert_equal_p(processor, &s_dummy_processor);
}
void test_kino_reel__draw_processed(void) {
// Calling kino_reel_draw_processed() should pass the processor to the .draw_processed function
const KinoReelImpl dummy_impl = (KinoReelImpl) {
.draw_processed = prv_dummy_impl_draw_processed,
};
KinoReel *kino_reel = kino_reel_custom_create(&dummy_impl, NULL);
GContext ctx;
kino_reel_draw_processed(kino_reel, &ctx, GPointZero, &s_dummy_processor);
}

433
tests/fw/ui/test_layer.c Normal file
View file

@ -0,0 +1,433 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/layer.h"
#include "applib/ui/layer_private.h"
#include "util/size.h"
#include "clar.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_bitblt.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_gbitmap.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_unobstructed_area.h"
// Setup
////////////////////////////////////
void test_layer__initialize(void) {
}
void test_layer__cleanup(void) {
}
GDrawState graphics_context_get_drawing_state(GContext *ctx) {
return (GDrawState) { 0 };
}
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
return false;
}
void graphics_context_set_drawing_state(GContext *ctx, GDrawState draw_state) {
}
void window_schedule_render(struct Window *window) {
}
void recognizer_destroy(Recognizer *recognizer) {}
void recognizer_add_to_list(Recognizer *recognizer, RecognizerList *list) {}
void recognizer_remove_from_list(Recognizer *recognizer, RecognizerList *list) {}
RecognizerManager *window_get_recognizer_manager(Window *window) { return NULL; }
bool recognizer_list_iterate(RecognizerList *list, RecognizerListIteratorCb iter_cb,
void *context) {
return false;
}
void recognizer_manager_register_recognizer(RecognizerManager *manager, Recognizer *recognizer) {}
void recognizer_manager_deregister_recognizer(RecognizerManager *manager, Recognizer *recognizer) {}
// Tests
////////////////////////////////////
void test_layer__add_child_and_remove_from_parent(void) {
Layer parent, child_a, child_b, child_c, grand_child_a;
Layer *layers[] = {&parent, &child_a, &child_b, &child_c, &grand_child_a};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
// Create this hierarchy:
// This hits first_child and next_sibling add code paths.
//
// +-parent
// |
// '->child_a->child_b->child_c
// |
// '->grand_child_a
//
cl_assert(parent.first_child == NULL);
layer_add_child(&parent, &child_a);
cl_assert(parent.first_child == &child_a);
layer_add_child(&parent, &child_b);
cl_assert(parent.first_child == &child_a);
cl_assert(child_a.next_sibling == &child_b);
cl_assert(child_a.parent == &parent);
cl_assert(child_b.parent == &parent);
layer_add_child(&parent, &child_c);
cl_assert(child_c.parent == &parent);
cl_assert(child_b.next_sibling == &child_c);
layer_add_child(&child_a, &grand_child_a);
cl_assert(grand_child_a.parent == &child_a);
// Remove non-first-child (child_b):
//
// +-parent
// |
// '->child_a->child_c
// |
// '->grand_child_a
//
// +-child_b
//
layer_remove_from_parent(&child_b);
cl_assert(child_b.parent == NULL);
cl_assert(child_b.next_sibling == NULL);
cl_assert(parent.first_child == &child_a);
cl_assert(child_a.next_sibling == &child_c);
cl_assert(grand_child_a.parent == &child_a);
cl_assert(child_c.parent == &parent);
// Remove first-child (child_a):
//
// +-parent
// |
// '->child_c
//
// +-child_a
// |
// '->grand_child_a
//
layer_remove_from_parent(&child_a);
cl_assert(parent.first_child == &child_c);
cl_assert(child_c.parent == &parent);
cl_assert(child_a.parent == NULL);
cl_assert(child_a.next_sibling == NULL);
cl_assert(grand_child_a.parent == &child_a);
// Return early when (parent->paren == NULL):
layer_remove_from_parent(&parent);
}
void test_layer__remove_child_layers(void) {
Layer parent, child_a, child_b;
Layer *layers[] = {&parent, &child_a, &child_b};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
// Create this hierarchy:
//
// +-parent
// |
// '->child_a->child_b
//
layer_add_child(&parent, &child_a);
layer_add_child(&parent, &child_b);
layer_remove_child_layers(&parent);
cl_assert(child_a.parent == NULL);
cl_assert(child_a.next_sibling == NULL);
cl_assert(child_b.parent == NULL);
cl_assert(parent.first_child == NULL);
}
void test_layer__insert_below(void) {
Layer parent, child_a, child_b, child_c;
Layer *layers[] = {&parent, &child_a, &child_b, &child_c};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
// Create this hierarchy:
//
// +-parent
// |
// '->child_a
//
layer_add_child(&parent, &child_a);
// Insert child_b below child_b (first_child code path):
//
// +-parent
// |
// '->child_b->child_a
//
layer_insert_below_sibling(&child_b, &child_a);
cl_assert(child_b.parent == &parent);
cl_assert(child_b.next_sibling == &child_a);
cl_assert(child_a.next_sibling == NULL);
// Insert child_c below child_a (next_sibling code path):
//
// +-parent
// |
// '->child_b->child_c->child_a
//
layer_insert_below_sibling(&child_c, &child_a);
cl_assert(parent.first_child == &child_b);
cl_assert(child_b.next_sibling == &child_c);
cl_assert(child_c.parent == &parent);
cl_assert(child_c.next_sibling == &child_a);
cl_assert(child_a.next_sibling == NULL);
}
void test_layer__insert_above(void) {
Layer parent, child_a, child_b, child_c;
Layer *layers[] = {&parent, &child_a, &child_b, &child_c};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
// Create this hierarchy:
//
// +-parent
// |
// '->child_b
//
layer_add_child(&parent, &child_b);
// Insert child_a above child_b (first_child code path):
//
// +-parent
// |
// '->child_b->child_a
//
layer_insert_above_sibling(&child_a, &child_b);
cl_assert(child_b.parent == &parent);
cl_assert(child_b.next_sibling == &child_a);
cl_assert(child_a.next_sibling == NULL);
// Insert child_c above child_b (next_sibling code path):
//
// +-parent
// |
// '->child_b->child_c->child_a
//
layer_insert_above_sibling(&child_c, &child_b);
cl_assert(parent.first_child == &child_b);
cl_assert(child_b.next_sibling == &child_c);
cl_assert(child_c.parent == &parent);
cl_assert(child_c.next_sibling == &child_a);
cl_assert(child_a.next_sibling == NULL);
}
void test_layer__traverse(void) {
Layer *stack[5];
uint8_t current_stack = 0;
Layer *a = layer_create(GRectZero);
Layer *aa = layer_create(GRectZero);
Layer *aaa = layer_create(GRectZero);
Layer *aaaa = layer_create(GRectZero);
Layer *ab = layer_create(GRectZero);
Layer *b = layer_create(GRectZero);
layer_add_child(a, aa);
layer_add_child(aa, aaa);
layer_add_child(aaa, aaaa);
layer_add_child(a, ab);
a->next_sibling = b;
stack[0] = a;
// go to child if possible
Layer *actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack,
true);
cl_assert_equal_p(aa, actual);
cl_assert_equal_i(1, current_stack);
cl_assert_equal_p(a, stack[0]);
cl_assert_equal_p(aa, stack[1]);
// go to child if possible
actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack, true);
cl_assert_equal_p(aaa, actual);
cl_assert_equal_i(2, current_stack);
cl_assert_equal_p(a, stack[0]);
cl_assert_equal_p(aa, stack[1]);
cl_assert_equal_p(aaa, stack[2]);
// go to child if possible
actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack, true);
cl_assert_equal_p(aaaa, actual);
cl_assert_equal_i(3, current_stack);
cl_assert_equal_p(a, stack[0]);
cl_assert_equal_p(aa, stack[1]);
cl_assert_equal_p(aaa, stack[2]);
cl_assert_equal_p(aaaa, stack[3]);
// go back two levels and then to sibling
actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack, true);
cl_assert_equal_p(ab, actual);
cl_assert_equal_i(1, current_stack);
cl_assert_equal_p(a, stack[0]);
cl_assert_equal_p(ab, stack[1]);
// go back one level and then to sibling
actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack, true);
cl_assert_equal_p(b, actual);
cl_assert_equal_i(0, current_stack);
cl_assert_equal_p(b, stack[0]);
// no more siblings on root level
actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack, true);
cl_assert_equal_p(NULL, actual);
cl_assert_equal_i(0, current_stack);
// do not descend
stack[0] = a;
current_stack = 0;
actual = __layer_tree_traverse_next__test_accessor(stack, ARRAY_LENGTH(stack), &current_stack, false);
cl_assert_equal_p(b, actual);
cl_assert_equal_i(0, current_stack);
cl_assert_equal_p(b, stack[0]);
// test limited stack size (go to sibling instead of child)
stack[0] = a;
current_stack = 0;
actual = __layer_tree_traverse_next__test_accessor(stack, 1, &current_stack, true);
cl_assert_equal_p(b, actual);
cl_assert_equal_i(0, current_stack);
cl_assert_equal_p(b, stack[0]);
// test limited stack size (go to sibling of parent instead of child)
stack[0] = a;
stack[1] = aa;
stack[2] = aaa;
current_stack = 2;
actual = __layer_tree_traverse_next__test_accessor(stack, 3, &current_stack, true);
cl_assert_equal_p(ab, actual);
cl_assert_equal_i(1, current_stack);
cl_assert_equal_p(a, stack[0]);
cl_assert_equal_p(ab, stack[1]);
// not necessary to free memory on unit tests
}
void test_layer__is_ancestor(void) {
Layer parent, child_a, child_b, child_c;
Layer *layers[] = {&parent, &child_a, &child_b, &child_c};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
layer_add_child(&parent, &child_a);
cl_assert(layer_is_descendant(&child_a, &parent));
cl_assert(!layer_is_descendant(&parent, &child_a));
cl_assert(!layer_is_descendant(&child_b, &parent));
layer_add_child(&parent, &child_b);
cl_assert(layer_is_descendant(&child_b, &parent));
cl_assert(!layer_is_descendant(&child_b, &child_a));
cl_assert(!layer_is_descendant(&child_c, &child_a));
layer_add_child(&child_a, &child_c);
cl_assert(layer_is_descendant(&child_c, &child_a));
cl_assert(layer_is_descendant(&child_c, &parent));
cl_assert(!layer_is_descendant(&child_c, &child_b));
}
void test_layer__find_layer_contains_point(void) {
Layer parent, child_a, child_b, child_c, child_d, child_e, child_f;
Layer *layers[] = {&parent, &child_a, &child_b, &child_c, &child_d, &child_e, &child_f};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
layer_set_frame(&parent, &GRect(0, 0, 20, 20));
layer_set_frame(&child_a, &GRect(0, 0, 10, 10));
layer_set_frame(&child_b, &GRect(2, 2, 6, 6));
layer_set_frame(&child_c, &GRect(10, 10, 10, 10));
layer_set_frame(&child_d, &GRect(2, 2, 6, 6));
layer_set_frame(&child_e, &GRect(10, 10, 10, 10));
layer_set_frame(&child_f, &GRect(-10, -10, 40, 40));
layer_add_child(&parent, &child_a);
cl_assert_equal_p(layer_find_layer_containing_point(&child_a, &GPoint(11, 11)), NULL);
cl_assert_equal_p(layer_find_layer_containing_point(&child_a, &GPoint(10, 10)), NULL);
cl_assert_equal_p(layer_find_layer_containing_point(&child_a, &GPoint(9, 9)), &child_a);
cl_assert_equal_p(layer_find_layer_containing_point(&child_a, &GPoint(0, 0)), &child_a);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(9, 9)), &child_a);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(10, 10)), &parent);
layer_add_child(&child_a, &child_f);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(9, 9)), &child_f);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(1, 1)), &child_f);
// child layers are subject to their parents' bounds as well as their own (parent layers clip the
// bounds of child layers)
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(15, 15)), &parent);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(-5, -5)), NULL);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(21, 21)), NULL);
layer_remove_from_parent(&child_f);
layer_add_child(&parent, &child_b);
layer_add_child(&parent, &child_c);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(9, 9)), &child_a);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(6, 6)), &child_b);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(15, 15)), &child_c);
layer_add_child(&child_a, &child_d);
layer_add_child(&child_c, &child_e);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(9, 9)), &child_a);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(6, 6)), &child_d);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(15, 15)), &child_e);
}
static bool prv_override_layer_contains_point(const Layer *layer, const GPoint *point) {
return true;
}
void test_layer__find_layer_contains_point_override_layer_contains_point(void) {
Layer parent, child_a, child_b;
Layer *layers[] = {&parent, &child_a, &child_b};
for (int i = 0; i < ARRAY_LENGTH(layers); ++i) {
layer_init(layers[i], &GRectZero);
}
layer_set_frame(&parent, &GRect(0, 0, 20, 20));
layer_set_frame(&child_a, &GRect(0, 0, 10, 10));
layer_set_frame(&child_b, &GRect(2, 2, 6, 6));
layer_add_child(&parent, &child_a);
layer_add_child(&child_a, &child_b);
layer_set_contains_point_override(&child_b, prv_override_layer_contains_point);
cl_assert_equal_p(layer_find_layer_containing_point(&child_b, &GPoint(9, 9)), &child_b);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(9, 9)), &child_b);
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(0, 0)), &child_b);
// outside the bounds of child a, so child b is not found
cl_assert_equal_p(layer_find_layer_containing_point(&parent, &GPoint(15, 15)), &parent);
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/layer.h"
#include "applib/ui/layer_private.h"
#include "clar.h"
#include "pebble_asserts.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_bitblt.h"
#include "stubs_gbitmap.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_unobstructed_area.h"
GDrawState graphics_context_get_drawing_state(GContext *ctx) {
return (GDrawState) { 0 };
}
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
return false;
}
void graphics_context_set_drawing_state(GContext *ctx, GDrawState draw_state) {
}
void window_schedule_render(struct Window *window) {
}
static bool s_process_manager_compiled_with_legacy2_sdk;
bool process_manager_compiled_with_legacy2_sdk(void) {
return s_process_manager_compiled_with_legacy2_sdk;
}
// Setup
////////////////////////////////////
void test_layer_rect__initialize(void) {
s_process_manager_compiled_with_legacy2_sdk = false;
}
void test_layer_rect__cleanup(void) {
}
// Tests
////////////////////////////////////
void test_layer_rect__2_x_extend_shrink(void) {
Layer l;
s_process_manager_compiled_with_legacy2_sdk = true;
layer_init(&l, &GRect(10, 20, 30, 40));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 30, 40));
// expands
layer_set_frame(&l, &GRect(10, 20, 300, 400));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 300, 400));
// only expands .h, keeps .w
layer_set_frame(&l, &GRect(10, 20, 200, 500));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 300, 500));
}
void test_layer_rect__3_x_sync_if_applicable(void) {
Layer l;
s_process_manager_compiled_with_legacy2_sdk = false;
layer_init(&l, &GRect(10, 20, 30, 40));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 30, 40));
// expands
layer_set_frame(&l, &GRect(10, 20, 300, 400));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 300, 400));
// keeps size in sync
layer_set_frame(&l, &GRect(10, 20, 200, 500));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 200, 500));
// act as 2.x once bounds.origin is different from (0, 0)
layer_set_bounds(&l, &GRect(1, 1, 200, 500));
layer_set_frame(&l, &GRect(10, 20, 100, 600));
cl_assert_equal_grect(l.bounds, GRect(1, 1, 200, 599));
// act as 2.x once bounds.size isn't same as frame.size
layer_set_bounds(&l, &GRect(0, 0, 150, 600));
layer_set_frame(&l, &GRect(10, 20, 100, 700));
cl_assert_equal_grect(l.bounds, GRect(0, 0, 150, 700));
}

View file

@ -0,0 +1,460 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "pebble_asserts.h"
#include "applib/ui/menu_layer.h"
#include "applib/ui/content_indicator_private.h"
// Stubs
/////////////////////
#include "stubs_app_state.h"
#include "stubs_graphics.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_ui_window.h"
#include "stubs_process_manager.h"
#include "stubs_unobstructed_area.h"
// Fakes
////////////////////////
//#include "fake_gbitmap_png.c"
GDrawState graphics_context_get_drawing_state(GContext* ctx) {
return (GDrawState){};
}
void graphics_context_set_drawing_state(GContext* ctx, GDrawState draw_state) {}
void graphics_context_set_fill_color(GContext* ctx, GColor color){}
Layer* inverter_layer_get_layer(InverterLayer *inverter_layer) {
return &inverter_layer->layer;
}
void inverter_layer_init(InverterLayer *inverter, const GRect *frame) {}
void window_long_click_subscribe(ButtonId button_id, uint16_t delay_ms,
ClickHandler down_handler, ClickHandler up_handler) {}
void window_single_click_subscribe(ButtonId button_id, ClickHandler handler) {}
void window_single_repeating_click_subscribe(ButtonId button_id, uint16_t repeat_interval_ms,
ClickHandler handler) {}
void window_set_click_config_provider_with_context(Window *window,
ClickConfigProvider click_config_provider,
void *context) {}
void window_set_click_context(ButtonId button_id, void *context) {}
void content_indicator_destroy_for_scroll_layer(ScrollLayer *scroll_layer) {}
ContentIndicator s_content_indicator;
ContentIndicator *content_indicator_get_for_scroll_layer(ScrollLayer *scroll_layer) {
return &s_content_indicator;
}
ContentIndicator *content_indicator_get_or_create_for_scroll_layer(ScrollLayer *scroll_layer) {
return &s_content_indicator;
}
static bool s_content_available[NumContentIndicatorDirections];
void content_indicator_set_content_available(ContentIndicator *content_indicator,
ContentIndicatorDirection direction,
bool available) {
s_content_available[direction] = available;
}
void graphics_context_set_compositing_mode(GContext* ctx, GCompOp mode) {}
void graphics_draw_bitmap_in_rect(GContext *ctx, const GBitmap *bitmap, const GRect *rect){}
int16_t menu_cell_basic_cell_height(void) {
return 44;
}
// Tests
//////////////////////
static uint16_t s_num_rows;
void test_menu_layer__initialize(void) {
s_num_rows = 10;
}
void test_menu_layer__cleanup(void) {
}
static void prv_draw_row(GContext* ctx,
const Layer *cell_layer,
MenuIndex *cell_index,
void *callback_context) {}
static uint16_t prv_get_num_rows(struct MenuLayer *menu_layer,
uint16_t section_index,
void *callback_context) {
return s_num_rows;
}
void test_menu_layer__test_set_selected_classic(void) {
MenuLayer l;
menu_layer_init(&l, &GRect(10, 10, 180, 180));
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks){
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
});
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
cl_assert_equal_i(0, scroll_layer_get_content_offset(&l.scroll_layer).y);
menu_layer_set_selected_index(&l, MenuIndex(0, 1), MenuRowAlignTop, false);
cl_assert_equal_i(1, menu_layer_get_selected_index(&l).row);
const int16_t basic_cell_height = menu_cell_basic_cell_height();
cl_assert_equal_i(basic_cell_height, l.selection.y);
cl_assert_equal_i(-basic_cell_height,
scroll_layer_get_content_offset(&l.scroll_layer).y);
}
void test_menu_layer__test_set_selected_center_focused(void) {
MenuLayer l;
const int height = 180;
menu_layer_init(&l, &GRect(10, 10, height, 180));
menu_layer_set_center_focused(&l, true);
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks){
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
});
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
const int16_t basic_cell_height = menu_cell_basic_cell_height();
const int row0_vertically_centered = (height - basic_cell_height)/2;
cl_assert_equal_i(row0_vertically_centered, scroll_layer_get_content_offset(&l.scroll_layer).y);
menu_layer_set_selected_index(&l, MenuIndex(0, 1), MenuRowAlignTop, false);
cl_assert_equal_i(1, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(basic_cell_height, l.selection.y);
const int y_center_of_row_1 = basic_cell_height + basic_cell_height / 2;
const int row1_vertically_centered = height / 2 - y_center_of_row_1;
cl_assert_equal_i(row1_vertically_centered, scroll_layer_get_content_offset(&l.scroll_layer).y);
}
void test_menu_layer__test_set_selection_animation(void) {
MenuLayer l;
const int height = 180;
menu_layer_init(&l, &GRect(10, 10, height, 180));
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks){
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
});
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
// Test disabled first
l.selection_animation_disabled = true;
menu_layer_set_selected_index(&l, MenuIndex(0, 1), MenuRowAlignTop, true);
cl_assert_equal_i(1, menu_layer_get_selected_index(&l).row);
cl_assert(!l.animation.animation);
// Test enabled
l.selection_animation_disabled = false;
menu_layer_set_selected_index(&l, MenuIndex(0, 0), MenuRowAlignTop, true);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert(l.animation.animation);
}
int16_t prv_get_row_height_depending_on_selection_state(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *callback_context) {
MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
bool is_selected = menu_index_compare(&selected_index, cell_index) == 0;
return is_selected ? MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT : menu_cell_basic_cell_height();
}
void test_menu_layer__default_ignores_row_height_for_selection(void) {
MenuLayer l;
const int height = 180;
menu_layer_init(&l, &GRect(10, 10, height, 180));
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks){
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
.get_cell_height = prv_get_row_height_depending_on_selection_state,
});
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
cl_assert_equal_i(0, scroll_layer_get_content_offset(&l.scroll_layer).y);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
const int FOCUSED = MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT;
const int NORMAL = menu_cell_basic_cell_height();
cl_assert_equal_i(FOCUSED, l.selection.h);
menu_layer_set_selected_index(&l, MenuIndex(0, 2), MenuRowAlignNone, false);
cl_assert(menu_layer_get_center_focused(&l) == false);
// non-center-focus behavior: don't ask adjust for changed height of row(0,0)
cl_assert_equal_i(FOCUSED + 1 * NORMAL, l.selection.y);
// also non-center-focus behavior: don't update selected_index before asking row (0,1) for height
cl_assert_equal_i(NORMAL, l.selection.h);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
// in general, the default behavior does not handle changes in row height correctly
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, false);
cl_assert_equal_i(2 * FOCUSED + NORMAL, l.selection.y);
cl_assert_equal_i(NORMAL, l.selection.h);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
// totally wrong
menu_layer_set_selected_next(&l, true, MenuRowAlignNone, false);
cl_assert_equal_i(2 * FOCUSED, l.selection.y);
cl_assert_equal_i(NORMAL, l.selection.h);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
// WTF?!
menu_layer_set_selected_index(&l, MenuIndex(0, 1), MenuRowAlignNone, false);
cl_assert_equal_i(2 * FOCUSED - NORMAL, l.selection.y);
cl_assert_equal_i(NORMAL, l.selection.h);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
}
void test_menu_layer__center_focused_respects_row_height_for_selection(void) {
MenuLayer l;
const int height = 180;
menu_layer_init(&l, &GRect(10, 10, height, 180));
menu_layer_set_center_focused(&l, true);
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks){
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
.get_cell_height = prv_get_row_height_depending_on_selection_state,
});
const int FOCUSED = MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT;
const int NORMAL = menu_cell_basic_cell_height();
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
const int row0_vertically_centered = (height - FOCUSED)/2;
cl_assert_equal_i(row0_vertically_centered, scroll_layer_get_content_offset(&l.scroll_layer).y);
cl_assert_equal_i(FOCUSED, l.selection.h);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
menu_layer_set_selected_index(&l, MenuIndex(0, 2), MenuRowAlignNone, false);
// new center-focus behavior: adjust for changed row sizes depending on focused row
cl_assert(menu_layer_get_center_focused(&l) == true);
cl_assert_equal_i(2 * NORMAL, l.selection.y);
cl_assert_equal_i(NORMAL - FOCUSED, scroll_layer_get_content_offset(&l.scroll_layer).y);
cl_assert_equal_i(FOCUSED, l.selection.h);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, false);
cl_assert_equal_i(3 * NORMAL, l.selection.y);
cl_assert_equal_i(-FOCUSED, scroll_layer_get_content_offset(&l.scroll_layer).y);
cl_assert_equal_i(FOCUSED, l.selection.h);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
menu_layer_set_selected_next(&l, true, MenuRowAlignNone, false);
cl_assert_equal_i(2 * NORMAL, l.selection.y);
cl_assert_equal_i(NORMAL - FOCUSED, scroll_layer_get_content_offset(&l.scroll_layer).y);
cl_assert_equal_i(FOCUSED, l.selection.h);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
menu_layer_set_selected_index(&l, MenuIndex(0, 1), MenuRowAlignNone, false);
cl_assert_equal_i(1 * NORMAL, l.selection.y);
cl_assert_equal_i(2 * NORMAL - FOCUSED, scroll_layer_get_content_offset(&l.scroll_layer).y);
cl_assert_equal_i(FOCUSED, l.selection.h);
cl_assert_equal_b(false, s_content_available[ContentIndicatorDirectionUp]);
cl_assert_equal_b(true, s_content_available[ContentIndicatorDirectionDown]);
}
static void prv_skip_odd_rows(struct MenuLayer *menu_layer,
MenuIndex *new_index,
MenuIndex old_index,
void *callback_context) {
if (new_index->row == 1) {
new_index->row = 2;
}
if (new_index->row == 3) {
new_index->row = 4;
}
}
void test_menu_layer__center_focused_handles_skipped_rows(void) {
MenuLayer l;
menu_layer_init(&l, &GRect(10, 10, DISP_COLS, DISP_ROWS));
menu_layer_set_center_focused(&l, true);
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks) {
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
.selection_will_change = prv_skip_odd_rows,
});
menu_layer_reload_data(&l);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, false);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
cl_assert_equal_i(2, menu_layer_get_selected_index(&l).row);
const int16_t basic_cell_height = menu_cell_basic_cell_height();
cl_assert_equal_i(2 * basic_cell_height, l.selection.y);
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, false);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
cl_assert_equal_i(4, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(4 * basic_cell_height, l.selection.y);
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, false);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
cl_assert_equal_i(5, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(5 * basic_cell_height, l.selection.y);
menu_layer_set_selected_next(&l, true, MenuRowAlignNone, false);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
cl_assert_equal_i(4, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(4 * basic_cell_height, l.selection.y);
}
void test_menu_layer__center_focused_handles_skipped_rows_animated(void) {
MenuLayer l;
menu_layer_init(&l, &GRect(10, 10, DISP_COLS, DISP_ROWS));
menu_layer_set_center_focused(&l, true);
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks) {
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
.selection_will_change = prv_skip_odd_rows,
});
menu_layer_reload_data(&l);
const int16_t basic_cell_height = menu_cell_basic_cell_height();
const int initial_scroll_offset = (DISP_ROWS - basic_cell_height) / 2;
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0, l.selection.y);
cl_assert_equal_i(initial_scroll_offset, l.scroll_layer.content_sublayer.bounds.origin.y);
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, true);
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).section);
// these values are unchanged until the animation updates them
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0 * basic_cell_height, l.selection.y);
cl_assert_equal_i(initial_scroll_offset, l.scroll_layer.content_sublayer.bounds.origin.y);
// in this test setup, we can directly cast an animation to AnimationPrivate
AnimationPrivate *ap = (AnimationPrivate *) l.animation.animation;
const AnimationImplementation *const impl = ap->implementation;
impl->update(l.animation.animation, ANIMATION_NORMALIZED_MAX / 10);
// still unchanged
cl_assert_equal_i(0, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(0 * basic_cell_height, l.selection.y);
cl_assert_equal_i(initial_scroll_offset, l.scroll_layer.content_sublayer.bounds.origin.y);
// and updated
impl->update(l.animation.animation, ANIMATION_NORMALIZED_MAX * 9 / 10);
cl_assert_equal_i(2, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(2 * basic_cell_height, l.selection.y);
cl_assert_equal_i(initial_scroll_offset - 2 * basic_cell_height,
l.scroll_layer.content_sublayer.bounds.origin.y);
animation_unschedule(l.animation.animation);
menu_layer_set_selected_next(&l, false, MenuRowAlignNone, true);
// these values are unchanged until the animation updates them
cl_assert_equal_i(2, menu_layer_get_selected_index(&l).row);
cl_assert_equal_i(2 * basic_cell_height, l.selection.y);
cl_assert_equal_i(initial_scroll_offset - 2 * basic_cell_height,
l.scroll_layer.content_sublayer.bounds.origin.y);
}
static MenuLayer s_menu_layer_hierarchy;
static void prv_menu_cell_is_part_of_hierarchy_draw_row(GContext* ctx,
const Layer *cell_layer,
MenuIndex *cell_index,
void *callback_context) {
cl_assert_equal_p(cell_layer->window, s_menu_layer_hierarchy.scroll_layer.layer.window);
cl_assert_equal_p(cell_layer->parent, &s_menu_layer_hierarchy.scroll_layer.content_sublayer);
const GPoint actual = layer_convert_point_to_screen(cell_layer, GPointZero);
const GPoint expected = layer_convert_point_to_screen(&s_menu_layer_hierarchy.scroll_layer.layer,
GPoint(0, cell_index->row * 44));
cl_assert_equal_gpoint(actual, expected);
}
int prv_num_sublayers(const Layer *l) {
int result = 0;
Layer *child = l->first_child;
while (l) {
l = l->next_sibling;
result++;
}
return result;
}
void test_menu_layer__menu_cell_is_part_of_hierarchy(void) {
menu_layer_init(&s_menu_layer_hierarchy, &GRect(10, 10, 100, 180));
Layer *layer = &s_menu_layer_hierarchy.scroll_layer.content_sublayer;
// two layers (inverter + shadow)
cl_assert_equal_i(2, prv_num_sublayers(layer));
menu_layer_set_callbacks(&s_menu_layer_hierarchy, NULL, &(MenuLayerCallbacks){
.draw_row = prv_menu_cell_is_part_of_hierarchy_draw_row,
.get_num_rows = prv_get_num_rows,
});
menu_layer_reload_data(&s_menu_layer_hierarchy);
GContext ctx = {};
cl_assert_equal_i(2, prv_num_sublayers(layer));
layer->update_proc(layer, &ctx);
cl_assert_equal_i(2, prv_num_sublayers(layer));
}
void test_menu_layer__center_focused_updates_height_on_reload(void) {
MenuLayer l;
const int height = DISP_ROWS;
menu_layer_init(&l, &GRect(10, 10, height, DISP_COLS));
menu_layer_set_center_focused(&l, true);
s_num_rows = 3;
menu_layer_set_callbacks(&l, NULL, &(MenuLayerCallbacks) {
.draw_row = prv_draw_row,
.get_num_rows = prv_get_num_rows,
.get_cell_height = prv_get_row_height_depending_on_selection_state,
});
menu_layer_set_center_focused(&l, true);
menu_layer_reload_data(&l);
const int focused_height = MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT;
// focus last row
menu_layer_set_selected_index(&l, MenuIndex(0, s_num_rows - 1), MenuRowAlignNone, false);
cl_assert_equal_i(focused_height, l.selection.h);
s_num_rows--;
cl_assert_equal_i(2, s_num_rows);
menu_layer_reload_data(&l);
cl_assert_equal_i(s_num_rows - 1, l.selection.index.row);
cl_assert_equal_i(focused_height, l.selection.h);
s_num_rows--;
cl_assert_equal_i(1, s_num_rows);
menu_layer_reload_data(&l);
cl_assert_equal_i(s_num_rows - 1, l.selection.index.row);
cl_assert_equal_i(focused_height, l.selection.h);
}

View file

@ -0,0 +1,417 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/menu_layer.h"
#include "applib/ui/status_bar_layer.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "util/graphics.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
// Fakes
/////////////////////
#include "fake_resource_syscalls.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
static bool s_cell_is_highlighted = false;
// TODO PBL-23041: When round MenuLayer animations are enabled, we need a "is_selected" function
bool menu_cell_layer_is_highlighted(const Layer *cell_layer) {
return s_cell_is_highlighted;
}
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_app_state.h"
#include "stubs_bootbits.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_content_indicator.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
void window_long_click_subscribe(ButtonId button_id, uint16_t delay_ms, ClickHandler down_handler,
ClickHandler up_handler) {}
void window_set_click_config_provider_with_context(Window *window,
ClickConfigProvider click_config_provider,
void *context) {}
void window_set_click_context(ButtonId button_id, void *context) {}
void window_single_click_subscribe(ButtonId button_id, ClickHandler handler) {}
void window_single_repeating_click_subscribe(ButtonId button_id, uint16_t repeat_interval_ms,
ClickHandler handler) {}
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static FrameBuffer *fb;
static GContext s_ctx;
static GBitmap *s_dest_bitmap;
static GBitmap s_tictoc_icon_bitmap;
static GBitmap s_smart_alarm_icon_bitmap;
static void prv_initialize_icons(void) {
gbitmap_init_with_resource(&s_tictoc_icon_bitmap, RESOURCE_ID_MENU_ICON_TICTOC_WATCH);
gbitmap_init_with_resource(&s_smart_alarm_icon_bitmap, RESOURCE_ID_SMART_ALARM_ICON_BLACK);
}
void test_menu_layer_system_cells__initialize(void) {
process_manager_set_compiled_with_legacy2_sdk(false);
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, fb);
framebuffer_clear(fb);
// Setup resources
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false /* is_next */);
resource_init();
prv_initialize_icons();
}
void test_menu_layer_system_cells__cleanup(void) {
free(fb);
fb = NULL;
gbitmap_deinit(&s_tictoc_icon_bitmap);
gbitmap_deinit(&s_smart_alarm_icon_bitmap);
gbitmap_destroy(s_dest_bitmap);
s_dest_bitmap = NULL;
}
// Helpers
//////////////////////
// This struct represents the data for a column of the grid of our resulting test image
typedef struct {
const char *title_font;
const char *subtitle_font;
const char *value_font;
} MenuLayerSystemCellTestColumnData;
static const MenuLayerSystemCellTestColumnData s_menu_system_basic_cell_test_column_data[] = {
{ NULL, NULL, NULL }, // Use the default fonts
};
static const MenuLayerSystemCellTestColumnData s_menu_system_cell_layer_test_column_data[] = {
{ FONT_KEY_GOTHIC_24_BOLD, FONT_KEY_GOTHIC_14, FONT_KEY_GOTHIC_24_BOLD },
{ FONT_KEY_GOTHIC_14, FONT_KEY_GOTHIC_24_BOLD, FONT_KEY_GOTHIC_14 },
};
// This struct represents the data for a row of the grid of our resulting test image
typedef struct {
char *title;
char *subtitle;
char *value;
GBitmap *icon;
MenuCellLayerIconAlign icon_align;
bool icon_subbitmap;
GBoxModel *icon_box_model;
int horizontal_inset;
bool icon_form_fit;
} MenuLayerSystemCellTestRowData;
#define DEFAULT_ICON_ALIGN PBL_IF_RECT_ELSE(MenuCellLayerIconAlign_Left, MenuCellLayerIconAlign_Top)
static const MenuLayerSystemCellTestRowData s_menu_system_cell_test_row_data[] = {
{"Star Wars", NULL, NULL, NULL, DEFAULT_ICON_ALIGN},
{"The Lord of the Rings", "The Fellowship of the Ring", NULL, NULL, DEFAULT_ICON_ALIGN},
{"The Lord of the Rings", NULL, NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN},
{"The Matrix", "Revolutions", NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN},
{"8:00 AM", "Weekdays", "OFF", NULL, DEFAULT_ICON_ALIGN},
{"8:00 AM", "Weekdays", NULL, &s_tictoc_icon_bitmap, MenuCellLayerIconAlign_Right},
{"8:00 AM", "Weekdays", "OFF", &s_smart_alarm_icon_bitmap, MenuCellLayerIconAlign_TopLeft,
false, &(GBoxModel){ .offset = { 0, 5 }, .margin = { 6, 0 }}, PBL_IF_ROUND_ELSE(-6, 0), true},
{"The Lord of the Rings", NULL, NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN, true},
{"The Matrix", "Revolutions", NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN, true},
};
// We render all of the row data's for each of the following heights
static const int16_t s_menu_system_cell_test_row_heights[] = {
#if PBL_ROUND
MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT,
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT,
#endif
0, //!< Will be interpreted as "use menu_cell_basic_cell_height()"
MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT,
MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT,
};
#define GRID_CELL_PADDING 5
static int16_t prv_get_row_height_for_index(int i) {
return (s_menu_system_cell_test_row_heights[i] == 0) ? menu_cell_basic_cell_height() :
s_menu_system_cell_test_row_heights[i];
}
static int16_t prv_calculate_overall_grid_height(void) {
int16_t sum = 0;
const unsigned int num_distinct_rows = ARRAY_LENGTH(s_menu_system_cell_test_row_data);
const unsigned int num_row_heights = ARRAY_LENGTH(s_menu_system_cell_test_row_heights);
for (int i = 0; i < num_row_heights; i++) {
sum += GRID_CELL_PADDING + prv_get_row_height_for_index(i);
}
return (sum * num_distinct_rows) + GRID_CELL_PADDING;
}
typedef enum MenuCellType {
MenuCellType_Basic,
MenuCellType_BasicCustom,
MenuCellType_CellLayer,
} MenuCellType;
//! Allows testing other cell drawing functions using MenuCellLayerConfig
static void prv_menu_cell_draw_dispatch(GContext *ctx, Layer *cell_layer, MenuCellType cell_type,
const MenuCellLayerConfig *config, bool icon_subbitmap) {
GRect old_icon_bounds = GRectZero;
if (config->icon) {
old_icon_bounds = config->icon->bounds;
if (icon_subbitmap) {
gpoint_add_eq(&config->icon->bounds.origin, GPoint(4, 4));
config->icon->bounds.size.w -= 8;
config->icon->bounds.size.h -= 8;
}
}
switch (cell_type) {
case MenuCellType_Basic:
// These should be ignored, we want to make sure they are!
menu_cell_basic_draw(ctx, cell_layer, config->title, config->subtitle, config->icon);
break;
case MenuCellType_BasicCustom:
menu_cell_basic_draw_custom(
ctx, cell_layer, config->title_font, config->title, config->value_font, config->value,
config->subtitle_font, config->subtitle, config->icon,
(config->icon_align == MenuCellLayerIconAlign_Right), config->overflow_mode);
break;
case MenuCellType_CellLayer:
menu_cell_layer_draw(ctx, cell_layer, config);
break;
}
if (config->icon) {
config->icon->bounds = old_icon_bounds;
}
}
static void prv_draw_cell(MenuCellType cell_type, const GRect *cell_bounds,
const MenuLayerSystemCellTestRowData *row_data,
const MenuLayerSystemCellTestColumnData *column_data, bool is_selected) {
s_ctx.draw_state.drawing_box.origin = cell_bounds->origin;
const GRect cell_frame = GRect(0, 0, cell_bounds->size.w, cell_bounds->size.h);
const GColor background_color = is_selected ? GColorCobaltBlue : GColorWhite;
graphics_context_set_fill_color(&s_ctx, background_color);
graphics_fill_rect(&s_ctx, &cell_frame);
const GColor foreground_color = is_selected ? GColorWhite : GColorBlack;
graphics_context_set_text_color(&s_ctx, foreground_color);
graphics_context_set_tint_color(&s_ctx, foreground_color);
graphics_context_set_stroke_color(&s_ctx, foreground_color);
Layer cell_layer;
layer_init(&cell_layer, &cell_frame);
cell_layer.is_highlighted = is_selected;
s_cell_is_highlighted = is_selected;
const MenuCellLayerConfig config = {
.title_font = column_data->title_font ? fonts_get_system_font(column_data->title_font) : NULL,
.subtitle_font = column_data->subtitle_font ? fonts_get_system_font(column_data->subtitle_font) : NULL,
.value_font = column_data->value_font ? fonts_get_system_font(column_data->value_font) : NULL,
.title = row_data->title,
.subtitle = row_data->subtitle,
.value = row_data->value,
.icon = row_data->icon,
.icon_align = row_data->icon_align,
.icon_box_model = row_data->icon_box_model,
.icon_form_fit = row_data->icon_form_fit,
.horizontal_inset = row_data->horizontal_inset,
.overflow_mode = GTextOverflowModeFill,
};
prv_menu_cell_draw_dispatch(&s_ctx, &cell_layer, cell_type, &config, row_data->icon_subbitmap);
}
void prv_prepare_canvas_and_render_cells(MenuCellType cell_type, int16_t cell_width,
const MenuLayerSystemCellTestColumnData *columns,
unsigned int num_columns, bool is_legacy2) {
gbitmap_destroy(s_dest_bitmap);
const unsigned int num_row_heights = ARRAY_LENGTH(s_menu_system_cell_test_row_heights);
const unsigned int num_rows = ARRAY_LENGTH(s_menu_system_cell_test_row_data);
// Multiply num_columns * 2 to account for drawing both focused and unfocused side-by-side
const int num_columns_accounting_for_focused_unfocused = num_columns * 2;
const int16_t bitmap_width = (cell_width * num_columns_accounting_for_focused_unfocused) +
(GRID_CELL_PADDING * (num_columns_accounting_for_focused_unfocused + 1));
const int16_t bitmap_height = prv_calculate_overall_grid_height();
const GSize bitmap_size = GSize(bitmap_width, bitmap_height);
s_dest_bitmap = gbitmap_create_blank(bitmap_size,
PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBitmapFormat1Bit));
s_ctx.dest_bitmap = *s_dest_bitmap;
s_ctx.draw_state.clip_box.size = bitmap_size;
s_ctx.draw_state.drawing_box.size = bitmap_size;
// Fill the bitmap with pink so it's easier to see errors
memset(s_dest_bitmap->addr, GColorShockingPinkARGB8,
s_dest_bitmap->row_size_bytes * s_dest_bitmap->bounds.size.h);
process_manager_set_compiled_with_legacy2_sdk(is_legacy2);
int16_t y_offset = 0;
for (unsigned int row_index = 0; row_index < num_rows; row_index++) {
const MenuLayerSystemCellTestRowData row_data =
s_menu_system_cell_test_row_data[row_index];
for (unsigned int row_height_index = 0; row_height_index < num_row_heights;
row_height_index++) {
y_offset += GRID_CELL_PADDING;
const int16_t row_height = prv_get_row_height_for_index(row_height_index);
for (unsigned int column_index = 0; column_index < num_columns; column_index++) {
const MenuLayerSystemCellTestColumnData column_data = columns[column_index];
int16_t x_offset = GRID_CELL_PADDING + column_index * ((GRID_CELL_PADDING + cell_width) * 2);
GRect cell_bounds = GRect(x_offset, y_offset, cell_width, row_height);
prv_draw_cell(cell_type, &cell_bounds, &row_data, &column_data, true);
cell_bounds.origin.x += cell_width + GRID_CELL_PADDING;
prv_draw_cell(cell_type, &cell_bounds, &row_data, &column_data, false);
}
y_offset += row_height;
}
}
process_manager_set_compiled_with_legacy2_sdk(false);
}
// Tests
//////////////////////
void test_menu_layer_system_cells__basic_cell_width_144_legacy2(void) {
#if PLATFORM_TINTIN || PLATFORM_SILK
// NOTE: The generated bitmap will look really funky because it's rendering 8bit gbitmaps as
// 1bit due to the legacy2 check in gbitmap_get_format. This is normal and expected.
const int16_t cell_width = 144;
prv_prepare_canvas_and_render_cells(
MenuCellType_Basic, cell_width,
s_menu_system_basic_cell_test_column_data,
ARRAY_LENGTH(s_menu_system_basic_cell_test_column_data),
/* is_legacy2 */ true);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_menu_layer_system_cells__basic_cell_width_144(void) {
const int16_t cell_width = 144;
prv_prepare_canvas_and_render_cells(
MenuCellType_Basic, cell_width,
s_menu_system_basic_cell_test_column_data,
ARRAY_LENGTH(s_menu_system_basic_cell_test_column_data),
/* is_legacy2 */ false);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_menu_layer_system_cells__basic_custom_cell_width_144(void) {
const int16_t cell_width = 144;
prv_prepare_canvas_and_render_cells(
MenuCellType_BasicCustom, cell_width,
s_menu_system_cell_layer_test_column_data,
ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data),
/* is_legacy2 */ false);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_menu_layer_system_cells__cell_width_32(void) {
#if PBL_ROUND
const int16_t cell_width = 32;
prv_prepare_canvas_and_render_cells(
MenuCellType_CellLayer, cell_width,
s_menu_system_cell_layer_test_column_data,
ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data),
/* is_legacy2 */ false);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_menu_layer_system_cells__cell_width_100(void) {
const int16_t cell_width = 100;
prv_prepare_canvas_and_render_cells(
MenuCellType_CellLayer, cell_width,
s_menu_system_cell_layer_test_column_data,
ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data),
/* is_legacy2 */ false);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_menu_layer_system_cells__cell_width_144(void) {
const int16_t cell_width = 144;
prv_prepare_canvas_and_render_cells(
MenuCellType_CellLayer, cell_width,
s_menu_system_cell_layer_test_column_data,
ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data),
/* is_legacy2 */ false);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_menu_layer_system_cells__cell_width_180(void) {
const int16_t cell_width = 180;
prv_prepare_canvas_and_render_cells(
MenuCellType_CellLayer, cell_width,
s_menu_system_cell_layer_test_column_data,
ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data),
/* is_legacy2 */ false);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}

View file

@ -0,0 +1,461 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "applib/preferred_content_size.h"
#include "applib/ui/window_private.h"
#include "apps/system_apps/settings/settings_notifications_private.h"
#include "popups/notifications/notification_window.h"
#include "popups/notifications/notification_window_private.h"
#include "resource/timeline_resource_ids.auto.h"
#include "services/normal/timeline/notification_layout.h"
#include "util/trig.h"
#include <stdio.h>
// Stubs
/////////////////////
#include "stubs_action_menu.h"
#include "stubs_alarm_layout.h"
#include "stubs_alerts.h"
#include "stubs_analytics.h"
#include "stubs_ancs_filtering.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_app_window_stack.h"
#include "stubs_bluetooth_persistent_storage.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_calendar_layout.h"
#include "stubs_click.h"
#include "stubs_content_indicator.h"
#include "stubs_dialog.h"
#include "stubs_do_not_disturb.h"
#include "stubs_event_loop.h"
#include "stubs_event_service_client.h"
#include "stubs_evented_timer.h"
#include "stubs_generic_layout.h"
#include "stubs_health_layout.h"
#include "stubs_heap.h"
#include "stubs_i18n.h"
#include "stubs_ios_notif_pref_db.h"
#include "stubs_layer.h"
#include "stubs_light.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_menu_cell_layer.h"
#include "stubs_modal_manager.h"
#include "stubs_mutex.h"
#include "stubs_notification_storage.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_peek_layer.h"
#include "stubs_pin_db.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_regular_timer.h"
#include "stubs_reminder_db.h"
#include "stubs_reminders.h"
#include "stubs_serial.h"
#include "stubs_session.h"
#include "stubs_shell_prefs.h"
#include "stubs_simple_dialog.h"
#include "stubs_sleep.h"
#include "stubs_sports_layout.h"
#include "stubs_stringlist.h"
#include "stubs_syscall_internal.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_time.h"
#include "stubs_timeline.h"
#include "stubs_timeline_actions.h"
#include "stubs_timeline_item.h"
#include "stubs_timeline_layer.h"
#include "stubs_timeline_peek.h"
#include "stubs_vibes.h"
#include "stubs_weather_layout.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
int16_t interpolate_int16(int32_t normalized, int16_t from, int16_t to) {
return to;
}
uint32_t interpolate_uint32(int32_t normalized, uint32_t from, uint32_t to) {
return to;
}
int64_t interpolate_moook(int32_t normalized, int64_t from, int64_t to) {
return to;
}
uint32_t interpolate_moook_duration() {
return 0;
}
int64_t interpolate_moook_soft(int32_t normalized, int64_t from, int64_t to,
int32_t num_frames_mid) {
return to;
}
uint32_t interpolate_moook_soft_duration(int32_t num_frames_mid) {
return 0;
}
// Fakes
/////////////////////
#include "fake_animation.h"
#include "fake_app_state.h"
#include "fake_content_indicator.h"
#include "fake_graphics_context.h"
#include "fake_spi_flash.h"
#include "../../fixtures/load_test_resources.h"
typedef struct NotificationWindowTestData {
uint32_t icon_id;
const char *app_name;
const char *title;
const char *subtitle;
const char *location_name;
const char *body;
const char *timestamp;
const char *reminder_timestamp;
GColor primary_color;
GColor background_color;
bool show_notification_timestamp;
bool is_reminder;
struct {
AttributeList attr_list;
TimelineItem timeline_item;
} statics;
} NotificationWindowTestData;
static NotificationWindowTestData s_test_data;
void clock_get_since_time(char *buffer, int buf_size, time_t timestamp) {
if (buffer && s_test_data.timestamp) {
strncpy(buffer, s_test_data.timestamp, (size_t)buf_size);
buffer[buf_size - 1] = '\0';
}
}
void clock_get_until_time(char *buffer, int buf_size, time_t timestamp, int max_relative_hrs) {
if (buffer && s_test_data.reminder_timestamp) {
strncpy(buffer, s_test_data.reminder_timestamp, (size_t)buf_size);
buffer[buf_size - 1] = '\0';
}
}
void clock_copy_time_string(char *buffer, uint8_t buf_size) {
if (buffer) {
strncpy(buffer, "12:00 PM", buf_size);
buffer[buf_size - 1] = '\0';
}
}
//! This function overrides the implementation in swap_layer.c as a way of providing the data
//! we want to display in each notification
LayoutLayer *prv_get_layout_handler(SwapLayer *swap_layer, int8_t rel_position,
void *context) {
// Only support one layout at a time for now
if (rel_position != 0) {
return NULL;
}
NotificationWindowData *data = context;
AttributeList *attr_list = &s_test_data.statics.attr_list;
attribute_list_add_resource_id(attr_list, AttributeIdIconTiny, s_test_data.icon_id);
if (s_test_data.app_name) {
attribute_list_add_cstring(attr_list, AttributeIdAppName, s_test_data.app_name);
}
if (s_test_data.title) {
attribute_list_add_cstring(attr_list, AttributeIdTitle, s_test_data.title);
}
if (s_test_data.subtitle) {
attribute_list_add_cstring(attr_list, AttributeIdSubtitle, s_test_data.subtitle);
}
if (s_test_data.location_name) {
attribute_list_add_cstring(attr_list, AttributeIdLocationName, s_test_data.location_name);
}
if (s_test_data.body) {
attribute_list_add_cstring(attr_list, AttributeIdBody, s_test_data.body);
}
if (!gcolor_is_invisible(s_test_data.primary_color)) {
attribute_list_add_uint8(attr_list, AttributeIdPrimaryColor, s_test_data.primary_color.argb);
}
if (!gcolor_is_invisible(s_test_data.background_color)) {
attribute_list_add_uint8(attr_list, AttributeIdBgColor, s_test_data.background_color.argb);
}
s_test_data.statics.timeline_item = (TimelineItem) {
.header = (CommonTimelineItemHeader) {
.layout = LayoutIdNotification,
.type = s_test_data.is_reminder ? TimelineItemTypeReminder : TimelineItemTypeNotification,
},
.attr_list = *attr_list,
};
TimelineItem *item = &s_test_data.statics.timeline_item;
NotificationLayoutInfo layout_info = (NotificationLayoutInfo) {
.item = item,
.show_notification_timestamp = s_test_data.show_notification_timestamp,
};
const LayoutLayerConfig config = {
.frame = &data->window.layer.bounds,
.attributes = &item->attr_list,
.mode = LayoutLayerModeCard,
.app_id = &data->notification_app_id,
.context = &layout_info,
};
return notification_layout_create(&config);
}
static void prv_property_animation_grect_update(Animation *animation,
const AnimationProgress progress) {
PropertyAnimationPrivate *property_animation = (PropertyAnimationPrivate *)animation;
if (property_animation) {
layer_set_frame(property_animation->subject, &property_animation->values.to.grect);
}
}
static const PropertyAnimationImplementation s_frame_layer_implementation = {
.base.update = prv_property_animation_grect_update,
.accessors = {
.setter.grect = (const GRectSetter)layer_set_frame_by_value,
.getter.grect = (const GRectGetter)layer_get_frame_by_value,
},
};
//! Overrides the stub in stubs_animation.c to provide the proper plumbing for scrolling
PropertyAnimation *property_animation_create_layer_frame(
struct Layer *layer, GRect *from_frame, GRect *to_frame) {
PropertyAnimationPrivate *animation = (PropertyAnimationPrivate *)
property_animation_create(&s_frame_layer_implementation, layer, from_frame, to_frame);
if (from_frame) {
animation->values.from.grect = *from_frame;
PropertyAnimationImplementation *impl =
(PropertyAnimationImplementation *)animation->animation.implementation;
impl->accessors.setter.grect(animation->subject, animation->values.from.grect);
}
if (to_frame) {
animation->values.to.grect = *to_frame;
}
return (PropertyAnimation *)animation;
}
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
// To easily render multiple windows in a single canvas, we'll use an 8-bit bitmap for color
// displays (including round), but we can use the native format for black and white displays (1-bit)
#define CANVAS_GBITMAP_FORMAT PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBITMAP_NATIVE_FORMAT)
// Overrides same function in graphics.c; we need to do this so we can pass in the GBitmapFormat
// we need to use for the unit test output canvas instead of relying on GBITMAP_NATIVE_FORMAT, which
// wouldn't work for Spalding since it uses GBitmapFormat8BitCircular
GBitmap* graphics_capture_frame_buffer(GContext *ctx) {
PBL_ASSERTN(ctx);
return graphics_capture_frame_buffer_format(ctx, CANVAS_GBITMAP_FORMAT);
}
// Overrides same function in graphics.c; we need to do this so we can release the framebuffer we're
// using even though its format doesn't match GBITMAP_NATIVE_FORMAT (see comment for mocked
// graphics_capture_frame_buffer() above)
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
PBL_ASSERTN(ctx);
ctx->lock = false;
framebuffer_dirty_all(ctx->parent_framebuffer);
return true;
}
void test_notification_window__initialize(void) {
fake_app_state_init();
load_system_resources_fixture();
attribute_list_destroy_list(&s_test_data.statics.attr_list);
s_test_data = (NotificationWindowTestData) {};
}
void test_notification_window__cleanup(void) {
}
// Helpers
//////////////////////
extern NotificationWindowData s_notification_window_data;
//! Static function in swap_layer.c used to scroll
void prv_attempt_scroll(SwapLayer *swap_layer, ScrollDirection direction, bool is_repeating);
static void prv_render_notification_window(unsigned int num_down_scrolls) {
Window *window = &s_notification_window_data.window;
// Set the window on screen so its load/appear handlers will be called
window_set_on_screen(window, true, true);
// Trigger a reload of the NotificationWindow's SwapLayer so it will be updated with the content
// in s_test_data
swap_layer_reload_data(&s_notification_window_data.swap_layer);
// Scroll down the specified number of times
SwapLayer *swap_layer = &s_notification_window_data.swap_layer;
for (int i = 0; i < num_down_scrolls; i++) {
prv_attempt_scroll(swap_layer, ScrollDirectionDown, false /* is_repeating */);
fake_animation_complete(swap_layer->animation);
swap_layer->animation = NULL;
}
// Force the display of the action button
layer_set_hidden(&s_notification_window_data.action_button_layer, false);
// Render the window
window_render(window, fake_graphics_context_get_context());
}
//! @note This must be a multiple of 8 so that we are word-aligned when using a 1-bit bitmap.
#define GRID_CELL_PADDING 8
static void prv_prepare_canvas_and_render_notification_windows(unsigned int num_down_scrolls) {
// Initialize the notification window module before rendering anything
notification_window_init(false /* is_modal */);
const unsigned int num_columns = SettingsContentSizeCount;
const unsigned int num_rows = num_down_scrolls + 1;
const int16_t bitmap_width = (DISP_COLS * num_columns) + (GRID_CELL_PADDING * (num_columns + 1));
const int16_t bitmap_height =
(int16_t)((num_rows == 1) ? DISP_ROWS :
((DISP_ROWS * num_rows) + (GRID_CELL_PADDING * (num_rows + 1))));
const GSize bitmap_size = GSize(bitmap_width, bitmap_height);
GBitmap *canvas_bitmap = gbitmap_create_blank(bitmap_size, CANVAS_GBITMAP_FORMAT);
PBL_ASSERTN(canvas_bitmap);
GContext *ctx = fake_graphics_context_get_context();
ctx->dest_bitmap = *canvas_bitmap;
// We modify the bitmap's data pointer below so save a reference to the original here
uint8_t *saved_bitmap_addr = ctx->dest_bitmap.addr;
const uint8_t bitdepth = gbitmap_get_bits_per_pixel(ctx->dest_bitmap.info.format);
// Fill the bitmap with pink (on color) or white (on b&w) so it's easier to see errors
const GColor out_of_bounds_color = PBL_IF_COLOR_ELSE(GColorShockingPink, GColorWhite);
memset(canvas_bitmap->addr, out_of_bounds_color.argb,
canvas_bitmap->row_size_bytes * canvas_bitmap->bounds.size.h);
for (int settings_content_size = 0; settings_content_size < SettingsContentSizeCount;
settings_content_size++) {
const PreferredContentSize content_size =
settings_content_size_to_preferred_size((SettingsContentSize)settings_content_size);
system_theme_set_content_size(content_size);
const int16_t x_offset =
(int16_t)(GRID_CELL_PADDING + (settings_content_size * (GRID_CELL_PADDING + DISP_COLS)));
for (int down_scrolls = 0; down_scrolls <= num_down_scrolls; down_scrolls++) {
const int16_t y_offset =
(int16_t)((num_rows == 1) ? 0 :
GRID_CELL_PADDING + (down_scrolls * (GRID_CELL_PADDING + DISP_ROWS)));
// Set the GContext bitmap's data pointer to the position in the larger bitmap where we
// want to draw this particular notification window
ctx->dest_bitmap.addr =
saved_bitmap_addr + (y_offset * ctx->dest_bitmap.row_size_bytes) +
(x_offset * bitdepth / 8);
prv_render_notification_window((unsigned int)down_scrolls);
// On Round we end up drawing outside the visible screen bounds, so let's draw a circle where
// those bounds are to help us visualize each copy of the screen
#if PBL_ROUND
graphics_context_set_fill_color(ctx, GColorBlack);
graphics_fill_radial(ctx, DISP_FRAME, GOvalScaleModeFitCircle, 1, 0, TRIG_MAX_ANGLE);
#endif
}
}
// Restore the bitmap's original data pointer
ctx->dest_bitmap.addr = saved_bitmap_addr;
}
// Tests
//////////////////////
void test_notification_window__title_body(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_FACEBOOK_MESSENGER,
.title = "Henry Levak",
.body = "Nu, Shara. Where are my designs, blat?",
.show_notification_timestamp = true,
.timestamp = "Just now",
.background_color = GColorPictonBlue,
};
const unsigned int num_down_scrolls =
PBL_IF_RECT_ELSE((PreferredContentSizeDefault < PreferredContentSizeLarge) ? 1 : 0, 0);
prv_prepare_canvas_and_render_notification_windows(num_down_scrolls);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_notification_window__title_subtitle_body(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_INBOX,
.title = "Henry Levak",
.subtitle = "Henry Levak sent you a 1-1 message",
.body = "Good morning to you my friend!",
.background_color = GColorRed,
};
prv_prepare_canvas_and_render_notification_windows(PBL_IF_RECT_ELSE(2, 1) /* num_down_scrolls */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_notification_window__reminder(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_REMINDER,
.title = "Feed Humphrey",
.location_name = "RWC Office",
.body = "Only the best!",
.reminder_timestamp = "In 15 minutes",
.is_reminder = true,
};
const unsigned int num_down_scrolls =
(PreferredContentSizeDefault >= PreferredContentSizeLarge) ? 0 : 1;
prv_prepare_canvas_and_render_notification_windows(num_down_scrolls);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_notification_window__body_icon(void) {
s_test_data = (NotificationWindowTestData) {
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_HANGOUTS,
.title = "Kevin Conley",
.subtitle = (PreferredContentSizeDefault >= PreferredContentSizeLarge) ? "New mail!" : NULL,
.body = "",
.background_color = GColorIslamicGreen,
};
prv_prepare_canvas_and_render_notification_windows(0 /* num_down_scrolls */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}

View file

@ -0,0 +1,228 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/option_menu_window.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "clar.h"
// Fakes
/////////////////////
#include "fake_app_state.h"
#include "fake_content_indicator.h"
#include "fake_graphics_context.h"
#include "fixtures/load_test_resources.h"
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_animation_timing.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_click.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_event_service_client.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_unobstructed_area.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
// Setup and Teardown
////////////////////////////////////
typedef struct OptionMenuTestData {
OptionMenu option_menu;
} OptionMenuTestData;
OptionMenuTestData s_data;
void test_option_menu_window__initialize(void) {
fake_app_state_init();
load_system_resources_fixture();
s_data = (OptionMenuTestData) {};
rtc_set_time(3 * SECONDS_PER_DAY);
}
void test_option_menu_window__cleanup(void) {
}
// Helpers
//////////////////////
typedef struct MenuItemConfig {
const char *title;
const char *subtitle;
} MenuItemConfig;
typedef struct MenuConfig {
OptionMenuCallbacks callbacks;
const char *title;
size_t num_items;
MenuItemConfig *items;
OptionMenuContentType content_type;
bool icons_enabled;
} MenuConfig;
static uint16_t prv_menu_get_num_rows(OptionMenu *option_menu, void *context) {
return ((MenuConfig *)context)->num_items;
}
static void prv_menu_draw_row(OptionMenu *option_menu, GContext *ctx, const Layer *cell_layer,
const GRect *cell_frame, uint32_t row, bool selected, void *context) {
MenuConfig *data = context;
option_menu_system_draw_row(option_menu, ctx, cell_layer, cell_frame, data->items[row].title,
selected, context);
}
static void prv_create_menu_and_render(MenuConfig *config) {
option_menu_init(&s_data.option_menu);
const OptionMenuConfig option_menu_config = {
.title = config->title ?: "Option Menu",
.content_type = config->content_type,
.status_colors = { GColorWhite, GColorBlack },
.highlight_colors = { PBL_IF_COLOR_ELSE(GColorCobaltBlue, GColorBlack), GColorWhite },
.icons_enabled = config->icons_enabled,
};
option_menu_configure(&s_data.option_menu, &option_menu_config);
const OptionMenuCallbacks callbacks = {
.draw_row = config->callbacks.draw_row ?: prv_menu_draw_row,
.get_num_rows = config->callbacks.get_num_rows ?: prv_menu_get_num_rows,
.get_cell_height = config->callbacks.get_cell_height ?: NULL,
};
option_menu_set_callbacks(&s_data.option_menu, &callbacks, config);
window_set_on_screen(&s_data.option_menu.window, true, true);
window_render(&s_data.option_menu.window, fake_graphics_context_get_context());
}
// Tests
//////////////////////
// These tests test all permutations on all platforms even if the combination on the particular
// platform was not designed and thus does not appear pleasant. Make sure you are looking at the
// combination relevant to your use case when examining the unit test output of Option Menu.
void prv_create_menu_and_render_long_title(bool icons_enabled, const char *title,
bool special_height) {
prv_create_menu_and_render(&(MenuConfig) {
.title = title,
.content_type = special_height ? OptionMenuContentType_DoubleLine :
OptionMenuContentType_Default,
.num_items = 3,
.items = (MenuItemConfig[]) {
{
.title = "Allow All Notifications",
}, {
.title = "Allow Phone Calls Only",
}, {
.title = "Mute All Notifications",
}
},
.icons_enabled = icons_enabled,
});
}
void test_option_menu_window__long_title_default_height(void) {
prv_create_menu_and_render_long_title(false /* icons_enabled */, "Default Height",
false /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_option_menu_window__long_title_default_height_icons(void) {
prv_create_menu_and_render_long_title(true /* icons_enabled */, "Default Height",
false /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_option_menu_window__long_title_special_height(void) {
prv_create_menu_and_render_long_title(false /* icons_enabled */, "Special Height",
true /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_option_menu_window__long_title_special_height_icons(void) {
prv_create_menu_and_render_long_title(true /* icons_enabled */, "Special Height",
true /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void prv_create_menu_and_render_short_title(bool icons_enabled, const char *title,
bool special_height) {
prv_create_menu_and_render(&(MenuConfig) {
.title = title,
.content_type = special_height ? OptionMenuContentType_SingleLine :
OptionMenuContentType_Default,
.num_items = 3,
.items = (MenuItemConfig[]) {
{
.title = "Smaller",
}, {
.title = "Default",
}, {
.title = "Larger",
}
},
.icons_enabled = icons_enabled,
});
}
void test_option_menu_window__short_title_default_height(void) {
prv_create_menu_and_render_short_title(false /* icons_enabled */, "Default Height",
false /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_option_menu_window__short_title_default_height_icons(void) {
prv_create_menu_and_render_short_title(true /* icons_enabled */, "Default Height",
false /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_option_menu_window__short_title_special_height(void) {
prv_create_menu_and_render_short_title(false /* icons_enabled */, "Special Height",
true /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_option_menu_window__short_title_special_height_icons(void) {
prv_create_menu_and_render_short_title(true /* icons_enabled */, "Special Height",
true /* special_height */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}

View file

@ -0,0 +1,214 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/scroll_layer.h"
#include "clar.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_content_indicator.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_unobstructed_area.h"
#define DEFAULT_SCROLL_HEIGHT 32
// Stubs
////////////////////////////////////
static GRect s_graphics_draw_bitmap_in_rect__rect = GRectZero;
void graphics_draw_bitmap_in_rect(GContext* ctx, const GBitmap *src_bitmap, const GRect *rect) {
s_graphics_draw_bitmap_in_rect__rect = *rect;
}
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {return false;}
void window_schedule_render(struct Window *window) {}
void window_set_click_config_provider_with_context(
struct Window *window, ClickConfigProvider click_config_provider, void *context) {}
void window_set_click_context(ButtonId button_id, void *context) {}
void window_single_repeating_click_subscribe(
ButtonId button_id, uint16_t repeat_interval_ms, ClickHandler handler) {}
// Internal definitions
////////////////////////////////////
extern bool prv_scroll_layer_is_paging_enabled(ScrollLayer *scroll_layer);
extern void prv_scroll_layer_set_content_offset_internal(ScrollLayer *scroll_layer, GPoint offset);
extern uint16_t prv_scroll_layer_get_paging_height(ScrollLayer *scroll_layer);
// Setup
////////////////////////////////////
void test_scroll_layer__initialize(void) {}
void test_scroll_layer__cleanup(void) {}
// Tests
////////////////////////////////////
void test_scroll_layer__enable_paging(void) {
GRect scroll_bounds = GRect(0,0,180,180);
ScrollLayer *scroll_layer = scroll_layer_create(scroll_bounds);
// Verify paging is disabled by default
cl_assert_equal_b(false, prv_scroll_layer_is_paging_enabled(scroll_layer));
// Verify shadow_layer is not hidden when paging disabled
cl_assert_equal_b(false, scroll_layer_get_shadow_hidden(scroll_layer));
scroll_layer_set_paging(scroll_layer, true);
// Verify paging is enabled
cl_assert_equal_b(true, prv_scroll_layer_is_paging_enabled(scroll_layer));
// Verify shadow_layer is hidden now that paging enabled
cl_assert_equal_b(true, scroll_layer_get_shadow_hidden(scroll_layer));
// verify disable paging works
scroll_layer_set_paging(scroll_layer, false);
cl_assert_equal_b(false, prv_scroll_layer_is_paging_enabled(scroll_layer));
// verify shadow layer is hidden on paging disabled
cl_assert_equal_b(true, scroll_layer_get_shadow_hidden(scroll_layer));
}
void test_scroll_layer__paging_vs_shadow_bits(void) {
ScrollLayer *scroll_layer = scroll_layer_create(GRect(0,0,180,180));
// Validate that paging_disabled is same position as shadow clips
scroll_layer->shadow_sublayer.clips = true;
cl_assert_equal_b(true, scroll_layer->paging.paging_disabled);
cl_assert_equal_b(false, scroll_layer->paging.shadow_hidden);
scroll_layer->shadow_sublayer.clips = false;
cl_assert_equal_b(false, scroll_layer->paging.paging_disabled);
cl_assert_equal_b(false, scroll_layer->paging.shadow_hidden);
// Validate that shadow_hidden is same position as layer hidden in shadow sublayer
scroll_layer->shadow_sublayer.hidden = true;
cl_assert_equal_b(false, scroll_layer->paging.paging_disabled);
cl_assert_equal_b(true, scroll_layer->paging.shadow_hidden);
scroll_layer->shadow_sublayer.hidden = false;
cl_assert_equal_b(false, scroll_layer->paging.paging_disabled);
cl_assert_equal_b(false, scroll_layer->paging.shadow_hidden);
}
void test_scroll_layer__scrolling(void) {
GRect scroll_bounds = GRect(0,0,180,180);
ScrollLayer *scroll_layer = scroll_layer_create(scroll_bounds);
GSize content_size = GSize(180, 2000);
scroll_layer_set_content_size(scroll_layer, content_size);
int32_t scroll_height = DEFAULT_SCROLL_HEIGHT;
int32_t offset = 0;
for (offset = 0; offset < content_size.h - scroll_bounds.size.h; offset += scroll_height) {
// scroll offset for scroll down is negative, so invert offset.y
cl_assert_equal_i(offset, -((int32_t)scroll_layer_get_content_offset(scroll_layer).y));
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
}
// can only scroll to content offset == content_size.h - bounds.size.h
// so the last scroll from the above loop is expected to have stopped short
cl_assert(offset > -((int32_t)scroll_layer_get_content_offset(scroll_layer).y));
}
void test_scroll_layer__paging_with_scroll(void) {
ScrollLayer *scroll_layer = scroll_layer_create(GRect(0,0,180,180));
int16_t page_height = 0;
page_height = scroll_layer->layer.frame.size.h;
scroll_layer_set_paging(scroll_layer, true);
cl_assert_equal_i(page_height, prv_scroll_layer_get_paging_height(scroll_layer));
// paging should force < page_height offsets to ceil of modulo page height
scroll_layer_set_content_size(scroll_layer, GSize(180, 2000));
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
// scroll offset for scroll down is negative, so invert offset.y
cl_assert_equal_i(page_height, -((int32_t)scroll_layer_get_content_offset(scroll_layer).y));
}
void test_scroll_layer__paging_last_pages_content(void) {
uint16_t page_height = 86;
ScrollLayer *scroll_layer = scroll_layer_create(GRect(0,0,180,page_height));
// validate enable paging works for paging height
scroll_layer_set_paging(scroll_layer, true);
cl_assert_equal_i(page_height, prv_scroll_layer_get_paging_height(scroll_layer));
int pages = 2;
int offset = 0;
// setup content size to be slightly more than 2 pages
GSize content_size = GSize(180, page_height * pages + 10);
scroll_layer_set_content_size(scroll_layer, content_size);
// paging should force full contents of last page to show
// so content size rounded up to the next modulo of page_height
cl_assert_equal_i(offset, -scroll_layer_get_content_offset(scroll_layer).y);
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
offset += page_height;
cl_assert_equal_i(offset, -scroll_layer_get_content_offset(scroll_layer).y);
// we expect to scroll to the end of content padded to the last full page
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
offset += page_height;
cl_assert_equal_i(offset, -scroll_layer_get_content_offset(scroll_layer).y);
cl_assert_equal_i(page_height * pages, -scroll_layer_get_content_offset(scroll_layer).y);
// once the last full page of content has been displayed
// another scroll down shouldn't advance the offset
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
cl_assert_equal_i(page_height * pages, -scroll_layer_get_content_offset(scroll_layer).y);
}
void test_scroll_layer__fullscreen_paging(void) {
GRect scroll_bounds = GRect(0,0,180,180);
ScrollLayer *scroll_layer = scroll_layer_create(scroll_bounds);
int16_t page_height = scroll_bounds.size.h;
scroll_layer_set_paging(scroll_layer, true);
cl_assert_equal_i(page_height, prv_scroll_layer_get_paging_height(scroll_layer));
int pages = 22;
int offset = 0;
// setup content size to be slightly more than the pages
GSize content_size = GSize(scroll_bounds.size.w, page_height * pages + 24);
scroll_layer_set_content_size(scroll_layer, content_size);
// paging should force full contents of last page to show
// so content size rounded up to the next modulo of page_height
cl_assert_equal_i(offset, -scroll_layer_get_content_offset(scroll_layer).y);
// we expect to scroll to the end of content padded to the last full page
for (int i = 0; i < pages; i++) {
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
offset += page_height;
cl_assert_equal_i(offset, -scroll_layer_get_content_offset(scroll_layer).y);
}
// once the last full page of content has been displayed
// another scroll down shouldn't advance the offset
scroll_layer_scroll(scroll_layer, ScrollDirectionDown, false);
cl_assert_equal_i(page_height * pages, -scroll_layer_get_content_offset(scroll_layer).y);
}

View file

@ -0,0 +1,238 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/time_range_selection_window.h"
#include "applib/ui/time_selection_window.h"
#include "applib/ui/app_window_stack.h"
#include "apps/system_apps/settings/settings_notifications_private.h"
#include "resource/resource.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/graphics.h"
#include "util/hash.h"
#include "util/math.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Fakes
/////////////////////
#include "fake_graphics_context.h"
#include "fake_spi_flash.h"
#include "../../fixtures/load_test_resources.h"
static GContext s_ctx;
void clock_get_time_tm(struct tm* time_tm) {
rtc_get_time_tm(time_tm);
}
static FrameBuffer *fb = NULL;
static GBitmap *s_dest_bitmap;
// To easily render multiple windows in a single canvas, we'll use an 8-bit bitmap for color
// displays (including round), but we can use the native format for black and white displays (1-bit)
#define CANVAS_GBITMAP_FORMAT PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBITMAP_NATIVE_FORMAT)
// Overrides same function in graphics.c; we need to do this so we can pass in the GBitmapFormat
// we need to use for the unit test output canvas instead of relying on GBITMAP_NATIVE_FORMAT, which
// wouldn't work for Spalding since it uses GBitmapFormat8BitCircular
GBitmap* graphics_capture_frame_buffer(GContext *ctx) {
PBL_ASSERTN(ctx);
return graphics_capture_frame_buffer_format(ctx, CANVAS_GBITMAP_FORMAT);
}
// Overrides same function in graphics.c; we need to do this so we can release the framebuffer we're
// using even though its format doesn't match GBITMAP_NATIVE_FORMAT (see comment for mocked
// graphics_capture_frame_buffer() above)
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
PBL_ASSERTN(ctx);
ctx->lock = false;
framebuffer_dirty_all(ctx->parent_framebuffer);
return true;
}
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_animation_timing.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_click.h"
#include "stubs_heap.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_menu_cell_layer.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscall_internal.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_text_layer_flow.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
// Setup and Teardown
////////////////////////////////////
void test_selection_windows__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, fb);
framebuffer_clear(fb);
load_system_resources_fixture();
}
void test_selection_windows__cleanup(void) {
free(fb);
if (s_dest_bitmap) {
gbitmap_destroy(s_dest_bitmap);
}
s_dest_bitmap = NULL;
}
// Helpers
//////////////////////
#define GRID_CELL_PADDING 5
typedef void (*RenderCallback)(GContext *ctx, SettingsContentSize content_size);
static void prv_prepare_canvas_and_render_for_each_size(RenderCallback callback) {
GContext *ctx = &s_ctx;
const unsigned int num_columns = SettingsContentSizeCount;
const int16_t bitmap_width = (DISP_COLS * num_columns) + (GRID_CELL_PADDING * (num_columns + 1));
const int16_t bitmap_height = DISP_ROWS;
const GSize bitmap_size = GSize(bitmap_width, bitmap_height);
s_dest_bitmap = gbitmap_create_blank(bitmap_size, CANVAS_GBITMAP_FORMAT);
ctx->dest_bitmap = *s_dest_bitmap;
ctx->draw_state.clip_box.size = bitmap_size;
ctx->draw_state.drawing_box.size = bitmap_size;
// Fill the bitmap with pink (on color) or white (on b&w) so it's easier to see errors
memset(s_dest_bitmap->addr, PBL_IF_COLOR_ELSE(GColorShockingPinkARGB8, GColorWhiteARGB8),
s_dest_bitmap->row_size_bytes * s_dest_bitmap->bounds.size.h);
for (SettingsContentSize content_size = 0; content_size < SettingsContentSizeCount;
content_size++) {
system_theme_set_content_size(settings_content_size_to_preferred_size(content_size));
callback(ctx, content_size);
}
}
void time_selection_window_set_to_current_time(TimeSelectionWindowData *date_time_window);
void time_selection_window_configure(TimeSelectionWindowData *time_selection_window,
const TimeSelectionWindowConfig *config);
void time_selection_window_init(TimeSelectionWindowData *time_selection_window,
const TimeSelectionWindowConfig *config);
#define SELECTION_COLOR PBL_IF_COLOR_ELSE(GColorJaegerGreen, GColorBlack)
static const TimeSelectionWindowConfig s_time_config = {
.label = "Time Config",
.color = SELECTION_COLOR,
.range = {
.update = true,
.text = "Range text yo!",
.duration_m = 30,
.enabled = true,
},
};
static void prv_render_time_selection_window(GContext *ctx, SettingsContentSize content_size) {
const int16_t x_offset = GRID_CELL_PADDING + (content_size * (GRID_CELL_PADDING + DISP_COLS));
ctx->draw_state.drawing_box.origin = GPoint(x_offset, 0);
TimeSelectionWindowData selection_window;
time_selection_window_init(&selection_window, &s_time_config);
selection_window.time_data.hour = 16;
selection_window.time_data.minute = 20;
time_selection_window_configure(&selection_window, &s_time_config);
// Set the window on screen so its window handlers will be called
window_set_on_screen(&selection_window.window, true, true);
window_render(&selection_window.window, ctx);
}
static void prv_render_time_range_selection_window(GContext *ctx,
SettingsContentSize content_size) {
const int16_t x_offset = GRID_CELL_PADDING + (content_size * (GRID_CELL_PADDING + DISP_COLS));
ctx->draw_state.drawing_box.origin = GPoint(x_offset, 0);
TimeRangeSelectionWindowData selection_window;
time_range_selection_window_init(&selection_window, SELECTION_COLOR, NULL, NULL);
selection_window.from.hour = 16;
selection_window.from.minute = 20;
selection_window.to.hour = 18;
selection_window.to.minute = 9;
// Set the window on screen so its window handlers will be called
window_set_on_screen(&selection_window.window, true, true);
window_render(&selection_window.window, ctx);
}
// Tests
//////////////////////
void test_selection_windows__time_selection_window(void) {
prv_prepare_canvas_and_render_for_each_size(prv_render_time_selection_window);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_selection_windows__time_range_selection_window(void) {
prv_prepare_canvas_and_render_for_each_size(prv_render_time_range_selection_window);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}

View file

@ -0,0 +1,287 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/content_indicator_private.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window_private.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/graphics.h"
#include "util/hash.h"
#include "util/math.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
static GContext s_ctx;
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_buffer.h"
#include "stubs_click.h"
#include "stubs_heap.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscall_internal.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_vibes.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
AnimationProgress animation_timing_scaled(AnimationProgress time_normalized,
AnimationProgress interval_start,
AnimationProgress interval_end) {
return interval_end;
}
KinoReel *kino_reel_scale_segmented_create(KinoReel *from_reel, bool take_ownership,
GRect screen_frame) {
return NULL;
}
void kino_reel_scale_segmented_set_deflate_effect(KinoReel *reel, int16_t expand) {}
bool kino_reel_scale_segmented_set_delay_by_distance(KinoReel *reel, GPoint target) {
return false;
}
// Helper Functions
/////////////////////
#include "../graphics/test_graphics.h"
#include "../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static FrameBuffer *fb = NULL;
static GBitmap *s_dest_bitmap;
// To easily render multiple windows in a single canvas, we'll use an 8-bit bitmap for color
// displays (including round), but we can use the native format for black and white displays (1-bit)
#define CANVAS_GBITMAP_FORMAT PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBITMAP_NATIVE_FORMAT)
// Overrides same function in graphics.c; we need to do this so we can pass in the GBitmapFormat
// we need to use for the unit test output canvas instead of relying on GBITMAP_NATIVE_FORMAT, which
// wouldn't work for Spalding since it uses GBitmapFormat8BitCircular
GBitmap* graphics_capture_frame_buffer(GContext *ctx) {
PBL_ASSERTN(ctx);
return graphics_capture_frame_buffer_format(ctx, CANVAS_GBITMAP_FORMAT);
}
// Overrides same function in graphics.c; we need to do this so we can release the framebuffer we're
// using even though its format doesn't match GBITMAP_NATIVE_FORMAT (see comment for mocked
// graphics_capture_frame_buffer() above)
bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
PBL_ASSERTN(ctx);
ctx->lock = false;
framebuffer_dirty_all(ctx->parent_framebuffer);
return true;
}
void test_simple_dialog__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, fb);
framebuffer_clear(fb);
// Setup resources
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false /* is_next */);
resource_init();
}
void test_simple_dialog__cleanup(void) {
free(fb);
gbitmap_destroy(s_dest_bitmap);
s_dest_bitmap = NULL;
}
// Helpers
//////////////////////
void prv_push_and_render_simple_dialog(Dialog *dialog) {
gbitmap_destroy(s_dest_bitmap);
const int16_t bitmap_width = DISP_COLS;
const int16_t bitmap_height = DISP_ROWS;
const GSize bitmap_size = GSize(bitmap_width, bitmap_height);
s_dest_bitmap = gbitmap_create_blank(bitmap_size, CANVAS_GBITMAP_FORMAT);
s_ctx.dest_bitmap = *s_dest_bitmap;
s_ctx.draw_state.clip_box.size = bitmap_size;
s_ctx.draw_state.drawing_box.size = bitmap_size;
// Fill the bitmap with pink (on color) or white (on b&w) so it's easier to see errors
memset(s_dest_bitmap->addr, PBL_IF_COLOR_ELSE(GColorShockingPinkARGB8, GColorWhiteARGB8),
s_dest_bitmap->row_size_bytes * s_dest_bitmap->bounds.size.h);
window_set_on_screen(&dialog->window, true, true);
window_render(&dialog->window, &s_ctx);
}
// Tests
//////////////////////
void test_simple_dialog__watchface_crashed(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Watchface crashed");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_set_text(dialog, "Watchface is not responding");
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_LARGE);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_INFINITE /* no timeout */);
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__battery_charged(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Battery Status");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_set_text(dialog, "Fully Charged");
dialog_set_background_color(dialog, GColorKellyGreen);
dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_FULL_LARGE);
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__battery_warning(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Battery Status");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
uint32_t percent = 20;
dialog_set_background_color(dialog, GColorRed);
char buffer[25];
snprintf(buffer, sizeof(buffer), "%d%% Battery", percent);
dialog_set_text(dialog, buffer);
dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_LOW_LARGE);
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__ping(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Ping");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_set_background_color(dialog, GColorCobaltBlue);
dialog_set_text_color(dialog, GColorWhite);
dialog_set_text(dialog, "Ping");
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__alarm_snooze(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Alarm Snooze");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *snooze_text = "Snooze delay set to %d minutes";
char snooze_buf[64];
snprintf(snooze_buf, sizeof(snooze_buf), snooze_text, 10);
dialog_set_text(dialog, snooze_buf);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_CONFIRMATION_LARGE);
dialog_set_background_color(dialog, GColorJaegerGreen);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_DEFAULT);
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__alarm_deleted(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Alarm Deleted");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_set_text(dialog, "Alarm Deleted");
dialog_set_icon(dialog, RESOURCE_ID_RESULT_SHREDDED_LARGE);
dialog_set_background_color(dialog, GColorJaegerGreen);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_DEFAULT);
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__calendar_unmute(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Calendar Unmute");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_set_text(dialog, "Calendar Unmuted");
dialog_set_icon(dialog, RESOURCE_ID_RESULT_MUTE_LARGE);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_DEFAULT);
prv_push_and_render_simple_dialog(dialog);
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_simple_dialog__does_text_fit(void) {
const bool use_status_bar = true;
const GSize icon_size = timeline_resources_get_gsize(TimelineResourceSizeLarge);
char *msg = "1 line error";
bool text_fits = simple_dialog_does_text_fit(msg, DISP_FRAME.size, icon_size, use_status_bar);
cl_assert(text_fits);
msg = "This error fits on all of our displays";
text_fits = simple_dialog_does_text_fit(msg, DISP_FRAME.size, icon_size, use_status_bar);
cl_assert(text_fits);
msg = "This error is too long for rect displays";
text_fits = simple_dialog_does_text_fit(msg, DISP_FRAME.size, icon_size, use_status_bar);
PBL_IF_RECT_ELSE(cl_assert(!text_fits), cl_assert(text_fits));
msg = "This error is too long to fit on any display shape :(";
text_fits = simple_dialog_does_text_fit(msg, DISP_FRAME.size, icon_size, use_status_bar);
cl_assert(!text_fits);
}

View file

@ -0,0 +1,93 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/ui/status_bar_layer.h"
#include "util/list.h"
#include "resource/resource_ids.auto.h"
#include "resource/resource.h"
#include "clar.h"
// Fakes
////////////////////////////////////
#include "fake_fonts.h"
// Stubs
////////////////////////////////////
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_applib_resource.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_event_service_client.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
#include "stubs_window_stack.h"
// Stubs
////////////////////////////////////
GContext *graphics_context_get_current_context(void) {
return NULL;
}
// Setup
////////////////////////////////////
ResourceCallbackHandle resource_watch(ResAppNum app_num, uint32_t resource_id,
ResourceChangedCallback callback, void* data) {
return (ResourceCallbackHandle) { 0 };
}
// Helpers
////////////////////////////////////
#define cl_assert_status_bar_height(status_bar) \
do { \
cl_assert(status_bar.layer.frame.size.h == STATUS_BAR_LAYER_HEIGHT); \
cl_assert(status_bar.layer.bounds.size.h == STATUS_BAR_LAYER_HEIGHT); \
} while (0);
// Tests
////////////////////////////////////
//! The height of the status bar should always be locked to STATUS_BAR_LAYER_HEIGHT.
//! Make sure that after marking dirty, it is always reset to STATUS_BAR_LAYER_HEIGHT.
void test_status_bar_layer__modify_height(void) {
StatusBarLayer status_bar;
status_bar_layer_init(&status_bar);
cl_assert_status_bar_height(status_bar);
GRect frame = status_bar.layer.frame;
GRect bounds = status_bar.layer.bounds;
frame.size.h = STATUS_BAR_LAYER_HEIGHT - 5;
layer_set_frame(&status_bar.layer, &frame);
cl_assert_status_bar_height(status_bar);
bounds.size.h = STATUS_BAR_LAYER_HEIGHT + 5;
layer_set_bounds(&status_bar.layer, &bounds);
cl_assert_status_bar_height(status_bar);
}

View file

@ -0,0 +1,187 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "applib/ui/text_layer.h"
// Stubs
/////////////////////
#include "stubs_app_state.h"
#include "stubs_graphics.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_process_manager.h"
#include "stubs_system_theme.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
GFont fonts_get_system_font(const char *font_key) {
return NULL;
}
void graphics_context_set_fill_color(GContext* ctx, GColor color){}
void graphics_context_set_text_color(GContext* ctx, GColor color){}
// Fakes
////////////////////////
#define MOCKED_CREATED_LAYOUT (GTextLayoutCacheRef)123456
void graphics_text_layout_cache_init(GTextLayoutCacheRef *layout_cache) {
*layout_cache = MOCKED_CREATED_LAYOUT;
}
void graphics_text_layout_cache_deinit(GTextLayoutCacheRef *layout_cache) {}
void graphics_text_layout_set_line_spacing_delta(GTextLayoutCacheRef layout, int16_t delta) {}
int16_t graphics_text_layout_get_line_spacing_delta(const GTextLayoutCacheRef layout) {
return 0;
}
GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text,
GFont const font, const GRect box,
const GTextOverflowMode overflow_mode,
const GTextAlignment alignment,
GTextLayoutCacheRef layout) {
return GSizeZero;
}
typedef struct {
struct {
GTextLayoutCacheRef layout;
} disable_text_flow;
struct {
GTextLayoutCacheRef layout;
uint8_t inset;
} enable_text_flow;
struct {
GTextLayoutCacheRef layout;
} disable_paging;
struct {
GTextLayoutCacheRef layout;
GPoint origin;
GRect paging;
} enable_paging;
} MockValues;
static MockValues s_actual;
void graphics_text_attributes_restore_default_text_flow(GTextLayoutCacheRef layout) {
s_actual.disable_text_flow.layout = layout;
}
void graphics_text_attributes_enable_screen_text_flow(GTextLayoutCacheRef layout, uint8_t inset) {
s_actual.enable_text_flow.layout = layout;
s_actual.enable_text_flow.inset = inset;
}
void graphics_text_attributes_restore_default_paging(GTextLayoutCacheRef layout) {
s_actual.disable_paging.layout = layout;
}
void graphics_text_attributes_enable_paging(GTextLayoutCacheRef layout,
GPoint content_origin_on_screen, GRect paging_on_screen) {
s_actual.enable_paging.layout = layout;
s_actual.enable_paging.origin = content_origin_on_screen;
s_actual.enable_paging.paging = paging_on_screen;
}
#define MOCKED_PAGING_ORIGIN GPoint(1 ,2)
#define MOCKED_PAGING_PAGE GRect(3, 4, 5, 6)
bool s_text_layer_calc_text_flow_paging_values_result;
bool text_layer_calc_text_flow_paging_values(const TextLayer *text_layer,
GPoint *content_origin_on_screen,
GRect *page_rect_on_screen) {
*content_origin_on_screen = MOCKED_PAGING_ORIGIN;
*page_rect_on_screen = MOCKED_PAGING_PAGE;
return s_text_layer_calc_text_flow_paging_values_result;
}
// Tests
//////////////////////
#define cl_assert_mocks_called(expected) \
do { \
cl_assert_equal_p((expected).disable_text_flow.layout, s_actual.disable_text_flow.layout); \
cl_assert_equal_p((expected).enable_text_flow.layout, s_actual.enable_text_flow.layout); \
cl_assert_equal_i((expected).enable_text_flow.inset, s_actual.enable_text_flow.inset); \
cl_assert_equal_p((expected).disable_paging.layout, s_actual.disable_paging.layout); \
cl_assert_equal_p((expected).enable_paging.layout, s_actual.enable_paging.layout); \
cl_assert_equal_i((expected).enable_paging.origin.x, s_actual.enable_paging.origin.x); \
cl_assert_equal_i((expected).enable_paging.origin.y, s_actual.enable_paging.origin.y); \
cl_assert_equal_i((expected).enable_paging.paging.origin.x, s_actual.enable_paging.paging.origin.x); \
cl_assert_equal_i((expected).enable_paging.paging.origin.y, s_actual.enable_paging.paging.origin.y); \
cl_assert_equal_i((expected).enable_paging.paging.size.w, s_actual.enable_paging.paging.size.w); \
cl_assert_equal_i((expected).enable_paging.paging.size.h, s_actual.enable_paging.paging.size.h); \
} while(0)
Window window;
TextLayer text_layer;
void test_text_layer__initialize(void) {
s_actual = (MockValues){};
window = (Window){};
text_layer_init(&text_layer, &GRect(10, 20, 30, 40));
s_text_layer_calc_text_flow_paging_values_result = true;
}
void test_text_layer__cleanup(void) {
}
void test_text_layer__enable_text_flow_does_nothing_outside_view_hierarchy(void) {
text_layer_enable_screen_text_flow_and_paging(&text_layer, 8);
// nothing called
cl_assert_mocks_called((MockValues){});
cl_assert(text_layer.layout_cache == NULL);
}
void test_text_layer__enable_text_flow(void) {
text_layer.layer.window = &window;
text_layer_enable_screen_text_flow_and_paging(&text_layer, 8);
cl_assert_mocks_called(((MockValues){
.enable_text_flow.layout = text_layer.layout_cache,
.enable_text_flow.inset = 8,
.enable_paging.layout = text_layer.layout_cache,
.enable_paging.origin = MOCKED_PAGING_ORIGIN,
.enable_paging.paging = MOCKED_PAGING_PAGE,
}));
cl_assert(text_layer.layout_cache == MOCKED_CREATED_LAYOUT);
}
void test_text_layer__enable_text_flow_requires_successful_calc_for_paging(void) {
text_layer.layer.window = &window;
s_text_layer_calc_text_flow_paging_values_result = false;
text_layer_enable_screen_text_flow_and_paging(&text_layer, 8);
cl_assert_mocks_called(((MockValues){
.enable_text_flow.layout = text_layer.layout_cache,
.enable_text_flow.inset = 8,
}));
cl_assert(text_layer.layout_cache == MOCKED_CREATED_LAYOUT);
}
void test_text_layer__disable_text_flow(void) {
text_layer.layout_cache = MOCKED_CREATED_LAYOUT;
text_layer_restore_default_text_flow_and_paging(&text_layer);
cl_assert_mocks_called(((MockValues){
.disable_paging.layout = text_layer.layout_cache,
.disable_text_flow.layout = text_layer.layout_cache,
}));
}

View file

@ -0,0 +1,120 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "pebble_asserts.h"
#include "applib/ui/text_layer_flow.h"
#include "applib/ui/scroll_layer.h"
// Stubs
/////////////////////
#include "stubs_app_state.h"
#include "stubs_fonts.h"
#include "stubs_graphics.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_ui_window.h"
#include "stubs_process_manager.h"
#include "stubs_system_theme.h"
#include "stubs_text_layout.h"
#include "stubs_unobstructed_area.h"
void graphics_context_set_fill_color(GContext* ctx, GColor color){}
void graphics_context_set_text_color(GContext* ctx, GColor color){}
// Fakes
////////////////////////
Layer *scroll_layer_is_instance_value;
bool scroll_layer_is_instance(const Layer *layer) {
return scroll_layer_is_instance_value == layer;
}
TextLayer text_layer;
Window window;
void test_text_layer_flow__initialize(void) {
window = (Window){.layer.window = &window};
text_layer_init(&text_layer, &GRect(10, 20, 30, 40));
text_layer.layer.window = &window;
scroll_layer_is_instance_value = NULL;
}
void test_text_layer_flow__cleanup(void) {
}
void test_text_layer_flow__return_value_handling(void) {
GPoint origin;
GRect page;
cl_assert(false == text_layer_calc_text_flow_paging_values(&text_layer, &origin, &page));
layer_add_child(&window.layer, &text_layer.layer);
cl_assert(true == text_layer_calc_text_flow_paging_values(&text_layer, &origin, &page));
cl_assert(true == text_layer_calc_text_flow_paging_values(&text_layer, &origin, NULL));
cl_assert(true == text_layer_calc_text_flow_paging_values(&text_layer, NULL, &page));
cl_assert(true == text_layer_calc_text_flow_paging_values(&text_layer, NULL, NULL));
cl_assert(false == text_layer_calc_text_flow_paging_values(NULL, NULL, NULL));
cl_assert_equal_gpoint(origin, text_layer.layer.frame.origin);
cl_assert_equal_gpoint(page.origin, origin);
cl_assert_equal_gsize(page.size, GSize(text_layer.layer.frame.size.w,
TEXT_LAYER_FLOW_DEFAULT_PAGING_HEIGHT));
}
void test_text_layer_flow__paging_container(void) {
Layer container;
layer_init(&container, &GRect(30, 40, 100, 10));
layer_add_child(&window.layer, &container);
layer_add_child(&container, &text_layer.layer);
GPoint origin;
GRect page;
scroll_layer_is_instance_value = NULL;
cl_assert(true == text_layer_calc_text_flow_paging_values(&text_layer, &origin, &page));
// text_layer's absolute coordinate as
cl_assert_equal_gpoint(origin, GPoint(40, 60));
// text_layer's absolute coordinate as there's no container
cl_assert_equal_gpoint(page.origin, GPoint(40, 60));
cl_assert_equal_gsize(page.size, GSize(30, TEXT_LAYER_FLOW_DEFAULT_PAGING_HEIGHT));
scroll_layer_is_instance_value = &container;
cl_assert(true == text_layer_calc_text_flow_paging_values(&text_layer, &origin, &page));
cl_assert_equal_gpoint(origin, GPoint(40, 60));
// containers's absolute coordinate
cl_assert_equal_gpoint(page.origin, GPoint(30, 40));
cl_assert_equal_gsize(page.size, GSize(100, 10));
}
void test_text_layer_flow__no_overflow_on_default_page_height(void) {
// first, make sure that grect_max_y itself overflows
cl_assert(grect_get_max_y(&(GRect){.origin.y = 1, .size.h = INT16_MAX}) < 0);
text_layer.layer.frame.origin.y = 1;
layer_add_child(&window.layer, &text_layer.layer);
GPoint origin;
GRect page;
cl_assert(text_layer_calc_text_flow_paging_values(&text_layer, &origin, &page));
cl_assert_equal_i(page.origin.y, text_layer.layer.frame.origin.y);
// must not overflow
cl_assert(grect_get_max_y(&page) > 0);
}

View file

@ -0,0 +1,462 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/graphics/bitblt.h"
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window_private.h"
#include "popups/timeline/peek_private.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "util/buffer.h"
#include "util/graphics.h"
#include "util/hash.h"
#include "util/math.h"
#include "util/size.h"
#include "util/trig.h"
#include "clar.h"
#include <stdio.h>
// Fakes
/////////////////////
#include "fake_rtc.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
void clock_get_until_time(char *buffer, int buf_size, time_t timestamp, int max_relative_hrs) {
snprintf(buffer, buf_size, "In 5 minutes");
}
// Stubs
/////////////////////
#include "stubs_activity.h"
#include "stubs_analytics.h"
#include "stubs_animation_timing.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_cron.h"
#include "stubs_event_loop.h"
#include "stubs_i18n.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_menu_cell_layer.h"
#include "stubs_modal_manager.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_pin_db.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_property_animation.h"
#include "stubs_scroll_layer.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_timeline_event.h"
#include "stubs_timeline_layer.h"
#include "stubs_unobstructed_area.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
// Helper Functions
/////////////////////
#include "fw/graphics/test_graphics.h"
#include "fw/graphics/util.h"
static GContext s_ctx;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
static bool s_is_watchface_running;
bool app_manager_is_watchface_running(void) {
return s_is_watchface_running;
}
// Setup and Teardown
////////////////////////////////////
static FrameBuffer s_fb;
static GBitmap *s_dest_bitmap;
void test_timeline_peek__initialize(void) {
// Setup time
TimezoneInfo tz_info = {
.tm_zone = "UTC",
};
time_util_update_timezone(&tz_info);
rtc_set_timezone(&tz_info);
rtc_set_time(SECONDS_PER_DAY);
// We start time out at 5pm on Jan 1, 2015 for all of these tests
struct tm time_tm = {
// Thursday, Jan 1, 2015, 5pm
.tm_hour = 17,
.tm_mday = 1,
.tm_year = 115
};
const time_t utc_sec = mktime(&time_tm);
fake_rtc_init(0 /* initial_ticks */, utc_sec);
// Setup graphics context
framebuffer_init(&s_fb, &DISP_FRAME.size);
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Initialize peek
s_is_watchface_running = true;
timeline_peek_init();
}
void test_timeline_peek__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_render_layer(Layer *layer, const GRect *box, bool use_screen) {
gbitmap_destroy(s_dest_bitmap);
const GRect *drawing_box = use_screen ? &DISP_FRAME : box;
const GSize bitmap_size = drawing_box->size;
s_dest_bitmap = gbitmap_create_blank(bitmap_size, GBITMAP_NATIVE_FORMAT);
s_ctx.dest_bitmap = *s_dest_bitmap;
s_ctx.draw_state.clip_box.size = bitmap_size;
s_ctx.draw_state.drawing_box = *drawing_box;
layer_render_tree(layer, &s_ctx);
if (use_screen) {
GBitmap *screen_bitmap = s_dest_bitmap;
screen_bitmap->bounds = (GRect) { gpoint_neg(box->origin), box->size };
s_dest_bitmap = gbitmap_create_blank(box->size, PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBitmapFormat1Bit));
bitblt_bitmap_into_bitmap(s_dest_bitmap, screen_bitmap, GPointZero, GCompOpAssign,
GColorClear);
gbitmap_destroy(screen_bitmap);
}
}
typedef struct TimelinePeekItemConfig {
time_t timestamp;
const char *title;
const char *subtitle;
TimelineResourceId icon;
int num_concurrent;
} TimelinePeekItemConfig;
static TimelineItem *prv_set_timeline_item(const TimelinePeekItemConfig *config, bool animated) {
TimelineItem *item = NULL;
const time_t now = rtc_get_time();
const time_t timestamp = config ? (config->timestamp ?: now) : now;
if (config) {
AttributeList list;
attribute_list_init_list(3 /* num_attributes */, &list);
attribute_list_add_cstring(&list, AttributeIdTitle, config->title);
if (config->subtitle) {
attribute_list_add_cstring(&list, AttributeIdSubtitle, config->subtitle);
}
attribute_list_add_uint32(&list, AttributeIdIconPin, config->icon);
item = timeline_item_create_with_attributes(timestamp, MINUTES_PER_HOUR,
TimelineItemTypePin, LayoutIdGeneric,
&list, NULL);
attribute_list_destroy_list(&list);
}
timeline_peek_set_item(item, timestamp >= now, config ? config->num_concurrent : 0,
false /* first */, animated);
return item;
}
static void prv_render_timeline_peek(const TimelinePeekItemConfig *config) {
TimelineItem *item = prv_set_timeline_item(config, false /* animated */);
// Force timeline peek to be visible
timeline_peek_set_visible(true, false /* animated */);
TimelinePeek *peek = timeline_peek_get_peek();
const Layer *layer = &peek->layout_layer;
// For text flow, the whole screen is needed. Render the screen, then reduce to the layer.
const bool use_screen = PBL_IF_ROUND_ELSE(true, false);
prv_render_layer(&peek->window.layer,
&(GRect) { gpoint_neg(layer->frame.origin), layer->frame.size },
use_screen);
timeline_item_destroy(item);
}
// Visual Layout Tests
//////////////////////
void test_timeline_peek__peek(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "CoreUX Design x Eng",
.subtitle = "ConfRM-Missile Command",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
.num_concurrent = 0,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_newline(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "NY 3\nSF 12",
.subtitle = "Bottom of\nthe 9th",
.icon = TIMELINE_RESOURCE_TIMELINE_BASEBALL,
.num_concurrent = 1,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_title_only_newline(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "NY 3\nSF 12",
.icon = TIMELINE_RESOURCE_TIMELINE_BASEBALL,
.num_concurrent = 1,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_concurrent_1(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "NY 3 - SF 12",
.subtitle = "Bottom of the 9th",
.icon = TIMELINE_RESOURCE_TIMELINE_BASEBALL,
.num_concurrent = 1,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_concurrent_2(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "Stock for party 🍺",
.subtitle = "Pebble Pad on Park",
.icon = TIMELINE_RESOURCE_NOTIFICATION_REMINDER,
.num_concurrent = 2,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_concurrent_2_max(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = ":parrot: :parrot:",
.subtitle = ":parrot: :parrot: :parrot:",
.icon = TIMELINE_RESOURCE_GENERIC_CONFIRMATION,
.num_concurrent = 3,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_title_only(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "Trash up the Place 🔥",
.icon = TIMELINE_RESOURCE_TIDE_IS_HIGH,
.num_concurrent = 0,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_title_only_concurrent_1(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "No Watch No Life",
.icon = TIMELINE_RESOURCE_DAY_SEPARATOR,
.num_concurrent = 1,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_title_only_concurrent_2(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.title = "OMG I think the text fits!",
.icon = TIMELINE_RESOURCE_GENERIC_WARNING,
.num_concurrent = 2,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
void test_timeline_peek__peek_in_5_minutes(void) {
prv_render_timeline_peek(&(TimelinePeekItemConfig) {
.timestamp = rtc_get_time() + (5 * SECONDS_PER_MINUTE),
.title = "Stock for party 🍺",
.subtitle = "Pebble Pad on Park",
.icon = TIMELINE_RESOURCE_NOTIFICATION_REMINDER,
.num_concurrent = 2,
});
cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE));
}
// Visibility Tests
//////////////////////
void test_timeline_peek__peek_visibility(void) {
prv_set_timeline_item(NULL, false /* animated */);
TimelinePeek *peek = timeline_peek_get_peek();
const Layer *layer = &peek->layout_layer;
// Normally it is animated, but for this unit test, we don't request `animated`
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Peek service shows the peek UI. Not animated for this unit test.
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
.title = "CoreUX Design x Eng",
.subtitle = "ConfRM-Missile Command",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
.num_concurrent = 0,
}, false /* animated */);
// Peek should now be on-screen.
cl_assert(layer->frame.origin.y < DISP_ROWS);
// Peek service hides the peek UI. Not animated for this unit test.
prv_set_timeline_item(NULL, false /* animated */);
// Peek should now be off-screen.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
}
void test_timeline_peek__peek_visible_to_hidden_outside_of_watchface(void) {
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
.title = "CoreUX Design x Eng",
.subtitle = "ConfRM-Missile Command",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
.num_concurrent = 0,
}, false /* animated */);
TimelinePeek *peek = timeline_peek_get_peek();
const Layer *layer = &peek->layout_layer;
// Normally it is animated, but for this unit test, we don't request `animated`
cl_assert(layer->frame.origin.y < DISP_ROWS);
// Transition away from the watchface
s_is_watchface_running = false;
timeline_peek_set_visible(false, false /* animated */);
// For simplicity, the implementation also moves the layer even though it is not necessary.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Peek service hides the peek UI using the animated code path.
prv_set_timeline_item(NULL, true /* animated */);
// This time we set the item to NULL, not just request invisibility. Since we're not in the
// watchface, even though `animated` was requested, it should immediately move the position.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Transition back to the watchface
s_is_watchface_running = true;
timeline_peek_set_visible(true, false /* animated */);
// Peek should be visible again, but it should still be off-screen.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
timeline_item_destroy(item);
}
void test_timeline_peek__peek_hidden_to_visible_outside_of_watchface(void) {
prv_set_timeline_item(NULL, false /* animated */);
TimelinePeek *peek = timeline_peek_get_peek();
const Layer *layer = &peek->layout_layer;
// Normally it is animated, but for this unit test, we don't request `animated`
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Transition away from the watchface
s_is_watchface_running = false;
timeline_peek_set_visible(false, false /* animated */);
// For simplicity, the implementation also moves the layer even though it is not necessary.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Peek service shows the peek UI using the animated code path.
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
.title = "CoreUX Design x Eng",
.subtitle = "ConfRM-Missile Command",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
.num_concurrent = 0,
}, true /* animated */);
// Since we're not in the watchface, the peek remains off-screen.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Transition back to the watchface
s_is_watchface_running = true;
timeline_peek_set_visible(true, false /* animated */);
// Peek should be visible again and now on-screen.
cl_assert(layer->frame.origin.y < DISP_ROWS);
timeline_item_destroy(item);
}
void test_timeline_peek__peek_visible_leaving_and_entering_watchface(void) {
TimelineItem *item = prv_set_timeline_item(&(TimelinePeekItemConfig) {
.title = "CoreUX Design x Eng",
.subtitle = "ConfRM-Missile Command",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
.num_concurrent = 0,
}, false /* animated */);
TimelinePeek *peek = timeline_peek_get_peek();
const Layer *layer = &peek->layout_layer;
// Normally it is animated, but for this unit test, we don't request `animated`
cl_assert(layer->frame.origin.y < DISP_ROWS);
// Transition away from the watchface
s_is_watchface_running = false;
timeline_peek_set_visible(false, false /* animated */);
// For simplicity, the implementation also moves the layer even though it is not necessary.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Transition back to the watchface
s_is_watchface_running = true;
timeline_peek_set_visible(true, false /* animated */);
// Peek should be visible again and on-screen.
cl_assert(layer->frame.origin.y < DISP_ROWS);
timeline_item_destroy(item);
}
void test_timeline_peek__peek_hidden_leaving_and_entering_watchface(void) {
prv_set_timeline_item(NULL, true /* animated */);
TimelinePeek *peek = timeline_peek_get_peek();
const Layer *layer = &peek->layout_layer;
// Peek should be off-screen.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Transition away from the watchface
s_is_watchface_running = false;
timeline_peek_set_visible(false, false /* animated */);
// Peek should be hidden and off-screen.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
// Transition back to the watchface
s_is_watchface_running = true;
timeline_peek_set_visible(true, false /* animated */);
// Peek should be visible again, but it should still be off-screen.
cl_assert(layer->frame.origin.y >= DISP_ROWS);
}

File diff suppressed because it is too large Load diff

462
tests/fw/ui/wscript Normal file
View file

@ -0,0 +1,462 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob = "src/fw/applib/graphics/gtypes.c "
"src/fw/applib/graphics/graphics_private_raw.c "
"tests/fakes/fake_gbitmap_png.c "
"src/fw/applib/ui/layer.c ",
test_sources_ant_glob="test_layer.c",
defines=['CAPABILITY_HAS_TOUCHSCREEN'])
clar(ctx,
sources_ant_glob = "src/fw/applib/graphics/graphics_private_raw.c "
"src/fw/applib/graphics/gtypes.c "
"tests/fakes/fake_gbitmap_png.c "
"src/fw/applib/ui/layer.c ",
test_sources_ant_glob="test_layer_rect.c")
clar(ctx,
sources_ant_glob = "src/fw/applib/graphics/gtypes.c "
"tests/stubs/stubs_animation.c "
"tests/fakes/fake_gbitmap_png.c "
"src/fw/applib/ui/animation_interpolate.c "
"src/fw/applib/ui/layer.c "
"src/fw/applib/ui/shadows.c "
"src/fw/applib/ui/scroll_layer.c ",
test_sources_ant_glob="test_scroll_layer.c")
clar(ctx,
sources_ant_glob = \
" src/fw/applib/graphics/8_bit/framebuffer.c"
" src/fw/applib/graphics/framebuffer.c"
" src/fw/applib/graphics/bitblt.c"
" src/fw/applib/graphics/8_bit/bitblt_private.c"
" src/fw/applib/graphics/gbitmap.c"
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/gtypes.c"
" src/fw/applib/graphics/graphics.c"
" src/fw/applib/graphics/graphics_private.c"
" src/fw/applib/graphics/graphics_private_raw.c"
" src/fw/applib/graphics/graphics_circle.c"
" src/fw/applib/graphics/graphics_line.c"
" src/fw/applib/graphics/text_layout.c"
" src/fw/applib/graphics/utf8.c"
" src/fw/applib/graphics/text_render.c"
" src/fw/applib/graphics/text_resources.c"
" src/fw/applib/fonts/codepoint.c"
" tests/fakes/fake_clock.c"
" tests/fakes/fake_fonts.c"
" tests/fakes/fake_gbitmap_png.c"
" src/fw/applib/ui/layer.c"
" src/fw/applib/ui/status_bar_layer.c",
test_sources_ant_glob="test_status_bar_layer.c",
defines=['FONT_KEY_GOTHIC_14="RESOURCE_ID_GOTHIC_14"'])
clar(ctx,
sources_ant_glob = "src/fw/applib/ui/animation.c "
"src/fw/applib/ui/animation_interpolate.c "
"src/fw/applib/ui/animation_timing.c "
"src/fw/applib/ui/property_animation.c "
"src/fw/applib/legacy2/ui/animation_legacy2.c "
"src/fw/applib/legacy2/ui/property_animation_legacy2.c "
"src/fw/applib/graphics/gtypes.c "
"src/fw/applib/graphics/gcolor_definitions.c "
"src/fw/applib/ui/layer.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_gbitmap_png.c "
"src/fw/services/common/animation_service.c",
defines=['DUMA_DISABLED'], # PBL-18358 Invalid memory read access
test_sources_ant_glob = "test_animation.c")
clar(ctx,
sources_ant_glob = "src/fw/applib/ui/animation_interpolate.c ",
test_sources_ant_glob = "test_animation_interpolate.c")
clar(ctx,
sources_ant_glob = "src/fw/applib/ui/animation_timing.c",
test_sources_ant_glob = "test_animation_timing.c")
clar(ctx,
sources_ant_glob = "src/fw/applib/graphics/gtypes.c"
" src/fw/applib/ui/layer.c"
" src/fw/applib/ui/kino/kino_reel.c"
" src/fw/applib/ui/kino/kino_reel_pdci.c"
" src/fw/applib/ui/kino/kino_reel_pdcs.c"
" src/fw/applib/ui/kino/kino_reel_gbitmap.c"
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c"
" src/fw/applib/ui/kino/kino_reel_custom.c"
" src/fw/applib/graphics/gbitmap.c"
" src/fw/applib/graphics/gbitmap_sequence.c"
" src/fw/applib/graphics/gbitmap_png.c"
" src/fw/applib/vendor/uPNG/upng.c"
" src/fw/applib/vendor/tinflate/tinflate.c"
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/gdraw_command.c"
" src/fw/applib/graphics/gdraw_command_list.c"
" src/fw/applib/graphics/gdraw_command_image.c"
" src/fw/applib/graphics/gdraw_command_frame.c"
" src/fw/applib/graphics/gdraw_command_sequence.c"
" tests/fakes/fake_resource_syscalls.c"
" tests/fakes/fake_applib_resource.c"
" tests/fakes/fake_rtc.c",
defines = ctx.env.test_image_defines,
test_sources_ant_glob = "test_kino_reel.c")
clar(ctx,
sources_ant_glob = "src/fw/applib/graphics/gtypes.c"
" src/fw/applib/graphics/gbitmap.c"
" src/fw/applib/ui/layer.c"
" src/fw/applib/ui/animation_interpolate.c"
" src/fw/applib/ui/kino/kino_player.c"
" src/fw/applib/ui/kino/kino_reel.c"
" src/fw/applib/ui/kino/kino_reel_pdci.c"
" src/fw/applib/ui/kino/kino_reel_pdcs.c"
" src/fw/applib/ui/kino/kino_reel_gbitmap.c"
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c"
" src/fw/applib/ui/kino/kino_reel_custom.c"
" src/fw/applib/graphics/gbitmap_sequence.c"
" src/fw/applib/graphics/gbitmap_png.c"
" src/fw/applib/vendor/uPNG/upng.c"
" src/fw/applib/vendor/tinflate/tinflate.c"
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/gdraw_command.c"
" src/fw/applib/graphics/gdraw_command_list.c"
" src/fw/applib/graphics/gdraw_command_image.c"
" src/fw/applib/graphics/gdraw_command_frame.c"
" src/fw/applib/graphics/gdraw_command_sequence.c"
" tests/stubs/stubs_animation.c"
" tests/fakes/fake_resource_syscalls.c"
" tests/fakes/fake_rtc.c",
defines = ctx.env.test_image_defines,
test_sources_ant_glob = "test_kino_player.c")
clar(ctx,
sources_ant_glob =
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/gtypes.c"
" src/fw/applib/ui/app_window_stack.c"
" src/fw/applib/ui/layer.c"
" src/fw/applib/ui/window.c"
" src/fw/applib/ui/window_manager.c"
" src/fw/applib/ui/window_stack.c"
" src/fw/kernel/ui/modals/modal_manager.c"
" tests/fakes/fake_animation.c"
" tests/fakes/fake_events.c"
" tests/stubs/stubs_click.c",
test_sources_ant_glob = "test_window_stack.c",
defines=['SCREEN_COLOR_DEPTH_BITS=8'],
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = "src/fw/applib/graphics/gtypes.c"
" src/fw/util/buffer.c"
" src/fw/applib/ui/layer.c"
" src/fw/applib/ui/content_indicator.c",
test_sources_ant_glob = "test_content_indicator.c")
clar(ctx,
sources_ant_glob = " " \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/applib/ui/shadows.c" \
" src/fw/applib/ui/animation_interpolate.c "
" src/fw/applib/ui/animation_timing.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gtypes.c" \
" tests/stubs/stubs_animation.c",
test_sources_ant_glob = "test_menu_layer.c")
menu_layer_system_cell_rendering_sources = \
" src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/bitblt_private.c" \
" src/fw/applib/graphics/bitblt.c" \
" src/fw/applib/graphics/framebuffer.c" \
" src/fw/applib/graphics/gbitmap.c" \
" src/fw/applib/graphics/gbitmap_png.c" \
" src/fw/applib/graphics/gbitmap_sequence.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gdraw_command.c" \
" src/fw/applib/graphics/gdraw_command_frame.c" \
" src/fw/applib/graphics/gdraw_command_image.c" \
" src/fw/applib/graphics/gdraw_command_list.c" \
" src/fw/applib/graphics/gdraw_command_sequence.c" \
" src/fw/applib/graphics/gpath.c" \
" src/fw/applib/graphics/graphics.c" \
" src/fw/applib/graphics/graphics_bitmap.c" \
" src/fw/applib/graphics/graphics_circle.c" \
" src/fw/applib/graphics/graphics_line.c" \
" src/fw/applib/graphics/graphics_private.c" \
" src/fw/applib/graphics/graphics_private_raw.c" \
" src/fw/applib/graphics/gtransform.c" \
" src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/graphics/perimeter.c" \
" src/fw/applib/graphics/text_layout.c" \
" src/fw/applib/graphics/text_render.c" \
" src/fw/applib/graphics/text_resources.c" \
" src/fw/applib/graphics/utf8.c" \
" src/fw/applib/ui/kino/kino_reel.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c" \
" src/fw/applib/ui/kino/kino_reel_pdci.c" \
" src/fw/applib/ui/kino/kino_reel_pdcs.c" \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/menu_layer_system_cells.c" \
" src/fw/board/displays/display_spalding.c" \
" src/fw/drivers/flash/flash_crc.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/flash_region/flash_region.c" \
" src/fw/resource/resource.c" \
" src/fw/resource/resource_storage.c" \
" src/fw/resource/resource_storage_builtin.c" \
" src/fw/resource/resource_storage_file.c" \
" src/fw/resource/resource_storage_flash.c" \
" src/fw/services/normal/filesystem/app_file.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/system/hexdump.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" src/fw/applib/vendor/tinflate/tinflate.c" \
" src/fw/applib/vendor/uPNG/upng.c" \
" tests/fakes/fake_applib_resource.c" \
" tests/fakes/fake_display.c" \
" tests/fakes/fake_gbitmap_get_data_row.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c" \
" tests/fixtures/resources/builtin_resources.auto.c" \
" tests/fixtures/resources/pfs_resource_table.c" \
" tests/stubs/stubs_animation.c" \
" tests/stubs/stubs_system_theme.c"
clar(ctx,
sources_ant_glob = menu_layer_system_cell_rendering_sources + \
" src/fw/shell/system_theme.c" \
" tests/fakes/fake_fonts.c" \
" tests/fixtures/resources/timeline_resource_table.auto.c",
test_sources_ant_glob = "test_menu_layer_system_cells.c",
defines=ctx.env.test_image_defines,
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk', 'robert'])
clar(ctx,
sources_ant_glob = menu_layer_system_cell_rendering_sources + \
" src/fw/applib/ui/action_menu_hierarchy.c" \
" src/fw/applib/ui/action_menu_layer.c" \
" src/fw/applib/ui/action_menu_window.c" \
" src/fw/applib/ui/crumbs_layer.c" \
" src/fw/applib/ui/window.c" \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/inverter_layer.c" \
" src/fw/applib/ui/content_indicator.c" \
" tests/fakes/fake_fonts.c" \
" src/fw/shell/system_theme.c" \
" src/fw/applib/ui/shadows.c",
test_sources_ant_glob = "test_action_menu_window.c",
defines=ctx.env.test_image_defines,
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['tintin', 'snowy', 'spalding', 'robert'])
clar(ctx,
sources_ant_glob=(
menu_layer_system_cell_rendering_sources + " " +
" src/fw/applib/ui/action_button.c"
" src/fw/applib/ui/kino/kino_layer.c"
" src/fw/applib/ui/kino/kino_player.c"
" src/fw/applib/ui/scroll_layer.c"
" src/fw/applib/ui/shadows.c"
" src/fw/applib/ui/status_bar_layer.c"
" src/fw/applib/ui/window.c"
" src/fw/apps/system_apps/timeline/text_node.c"
" src/fw/popups/notifications/notification_window.c"
" src/fw/popups/notifications/notifications_presented_list.c"
" src/fw/services/normal/timeline/attribute.c"
" src/fw/services/normal/timeline/layout_layer.c"
" src/fw/services/normal/timeline/layout_node.c"
" src/fw/services/normal/timeline/notification_layout.c"
" src/fw/services/normal/timeline/swap_layer.c"
" src/fw/services/normal/timeline/timeline_layout.c"
" src/fw/services/normal/timeline/timeline_resources.c"
" src/fw/shell/system_theme.c"
" tests/fakes/fake_animation.c"
" tests/fakes/fake_fonts.c"
" tests/fakes/fake_graphics_context.c"
" tests/fixtures/resources/timeline_resource_table.auto.c"
" tests/stubs/stubs_clock.c"
),
test_sources_ant_glob = "test_notification_window.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['silk', 'snowy', 'spalding', 'robert'])
clar(ctx,
sources_ant_glob = menu_layer_system_cell_rendering_sources + \
" src/fw/applib/ui/dialogs/simple_dialog.c" \
" src/fw/applib/ui/dialogs/dialog.c" \
" src/fw/applib/ui/dialogs/dialog_private.c" \
" src/fw/applib/ui/animation_interpolate.c" \
" src/fw/applib/ui/kino/kino_layer.c" \
" src/fw/applib/ui/kino/kino_player.c" \
" src/fw/applib/ui/kino/kino_reel_custom.c" \
" src/fw/applib/ui/kino/kino_reel/transform.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/text_layer_flow.c" \
" src/fw/applib/ui/window.c" \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/shadows.c"
" src/fw/applib/ui/inverter_layer.c" \
" src/fw/applib/ui/content_indicator.c" \
" tests/fakes/fake_fonts.c",
test_sources_ant_glob = "test_simple_dialog.c",
defines=ctx.env.test_image_defines,
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['tintin', 'snowy', 'spalding'])
clar(ctx,
sources_ant_glob = menu_layer_system_cell_rendering_sources + \
" src/fw/applib/fonts/fonts.c",
test_sources_ant_glob = "test_emoji_fonts.c",
defines=ctx.env.test_image_defines,
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = menu_layer_system_cell_rendering_sources + \
" src/fw/applib/ui/action_bar_layer.c" \
" src/fw/applib/ui/animation_interpolate.c" \
" src/fw/applib/ui/content_indicator.c" \
" src/fw/applib/ui/dialogs/dialog.c" \
" src/fw/applib/ui/dialogs/dialog_private.c" \
" src/fw/applib/ui/dialogs/expandable_dialog.c" \
" src/fw/applib/ui/inverter_layer.c" \
" src/fw/applib/ui/kino/kino_layer.c" \
" src/fw/applib/ui/kino/kino_player.c" \
" src/fw/applib/ui/kino/kino_reel/transform.c" \
" src/fw/applib/ui/kino/kino_reel_custom.c" \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/shadows.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/text_layer_flow.c" \
" src/fw/applib/ui/window.c" \
" tests/fakes/fake_fonts.c",
test_sources_ant_glob = "test_expandable_dialog.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=["dummy_board"],
platforms=["spalding"])
clar(ctx,
sources_ant_glob = " " \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gtypes.c", \
test_sources_ant_glob = "test_text_layer.c")
clar(ctx,
sources_ant_glob=(
menu_layer_system_cell_rendering_sources +
" src/fw/applib/ui/date_time_selection_window_private.c"
" src/fw/applib/ui/selection_layer.c"
" src/fw/applib/ui/text_layer.c"
" src/fw/applib/ui/time_range_selection_window.c"
" src/fw/applib/ui/time_selection_window.c"
" src/fw/applib/ui/window.c"
" src/fw/shell/system_theme.c"
" src/fw/util/date.c"
" tests/fakes/fake_clock.c"
" tests/fakes/fake_fonts.c"
),
test_sources_ant_glob="test_selection_windows.c",
defines=ctx.env.test_image_defines,
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['tintin', 'snowy', 'spalding', 'robert'])
clar(ctx,
sources_ant_glob = " " \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/text_layer_flow.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gtypes.c", \
test_sources_ant_glob = "test_text_layer_flow.c")
clar(ctx,
sources_ant_glob = " src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/graphics/utf8.c" \
" src/fw/services/normal/timeline/notification_layout.c",
test_sources_ant_glob = "test_jumboji.c")
clar(ctx,
sources_ant_glob=(
menu_layer_system_cell_rendering_sources + " " +
"src/fw/applib/ui/animation_interpolate.c "
"src/fw/applib/ui/kino/kino_layer.c "
"src/fw/applib/ui/kino/kino_player.c "
"src/fw/applib/ui/text_layer.c "
"src/fw/applib/ui/text_layer_flow.c "
"src/fw/applib/ui/window.c "
"src/fw/apps/system_apps/timeline/text_node.c "
"src/fw/popups/timeline/peek.c "
"src/fw/services/normal/timeline/attribute.c "
"src/fw/services/normal/timeline/attribute_group.c "
"src/fw/services/normal/timeline/attributes_actions.c "
"src/fw/services/normal/timeline/generic_layout.c "
"src/fw/services/normal/timeline/item.c "
"src/fw/services/normal/timeline/layout_layer.c "
"src/fw/services/normal/timeline/layout_node.c "
"src/fw/services/normal/timeline/timeline_layout.c "
"src/fw/services/normal/timeline/timeline_resources.c "
"src/fw/shell/system_theme.c "
"src/fw/util/stringlist.c "
"src/fw/util/time/time.c "
"tests/fakes/fake_clock.c "
"tests/fakes/fake_fonts.c "
"tests/fixtures/resources/timeline_resource_table.auto.c "
"tests/stubs/stubs_app_manager.c "
"tests/stubs/stubs_clock.c "
"tests/stubs/stubs_timeline_layout.c "
"tests/stubs/stubs_timeline_peek.c "
),
test_sources_ant_glob="test_timeline_peek.c",
defines=ctx.env.test_image_defines + ["CAPABILITY_HAS_TIMELINE_PEEK=1",
"USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk', 'robert'])
clar(ctx,
sources_ant_glob=(
menu_layer_system_cell_rendering_sources + " " +
"src/fw/applib/ui/animation_interpolate.c "
"src/fw/applib/ui/content_indicator.c "
"src/fw/applib/ui/inverter_layer.c "
"src/fw/applib/ui/menu_layer.c "
"src/fw/applib/ui/option_menu_window.c "
"src/fw/applib/ui/scroll_layer.c "
"src/fw/applib/ui/shadows.c "
"src/fw/applib/ui/status_bar_layer.c "
"src/fw/applib/ui/window.c "
"src/fw/shell/system_theme.c "
"tests/fakes/fake_clock.c "
"tests/fakes/fake_fonts.c "
"tests/fakes/fake_graphics_context.c "
),
test_sources_ant_glob="test_option_menu_window.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk', 'robert'])
# vim:filetype=vim