Files
cappuccino/Tests/AppKit/CPViewThemeChangeTest.j
T
David Richardson 7bc6b2d0ce Tests: Add tests for theme switching contract
* BuiltThemes.j: Load the themes in the built blends once, for every test that needs them.

* CPThemeDataTest.j: Use BuiltThemes.j.

* CPThemeSwitchTest.j: Check that a view changed to a new theme matches one made under it, that a theme change reaches every view in a window, and that a view outside a window follows the current theme when it enters one.

* CPViewThemeChangeTest.j: Check that a per-view value survives a change of theme class.
2026-11-03 12:16:16 -06:00

342 lines
8.3 KiB
Plaintext

/*
* CPViewThemeChangeTest.j
* AppKit
*
* Created by David Richardson on 31/10/2026.
* Copyright David Richardson 2026.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import <AppKit/AppKit.j>
/*
A view resolves each theme attribute from its own values, then its theme, then its
class default. When any of these changes, the view must resolve the new value and
lay itself out again. These tests build small themes in code, so they depend on no
real theme.
*/
@implementation _CPThemeChangeView : CPView
{
int layoutCount @accessors;
int themeChangeCount @accessors;
int displayRequestCount @accessors;
}
+ (CPString)defaultThemeClass
{
return @"theme-change-view";
}
+ (CPDictionary)themeAttributes
{
return @{
@"accent": @"class default",
@"margin": @"class default"
};
}
- (void)layoutSubviews
{
layoutCount++;
}
/*
needsDisplay is true only for a view with its own drawRect:, so the request is
recorded here.
*/
- (void)setNeedsDisplay:(BOOL)aFlag
{
if (aFlag)
displayRequestCount++;
[super setNeedsDisplay:aFlag];
}
- (void)objectDidChangeTheme
{
themeChangeCount++;
[super objectDidChangeTheme];
}
@end
@implementation CPViewThemeChangeTest : OJTestCase
{
CPTheme firstTheme;
CPTheme secondTheme;
CPTheme defaultTheme;
_CPThemeChangeView view;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
defaultTheme = [CPTheme defaultTheme];
firstTheme = [self themeNamed:@"theme-change-first" themeClass:nil accent:@"first"];
secondTheme = [self themeNamed:@"theme-change-second" themeClass:nil accent:@"second"];
view = [self newView];
[view setTheme:firstTheme];
[_CPDisplayServer run];
[view setLayoutCount:0];
[view setThemeChangeCount:0];
}
- (void)tearDown
{
[CPTheme setDefaultTheme:defaultTheme];
}
- (_CPThemeChangeView)newView
{
return [[_CPThemeChangeView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
}
- (CPTheme)themeNamed:(CPString)aName themeClass:(CPString)aThemeClass accent:(CPString)anAccent
{
var theme = [[CPTheme alloc] initWithName:aName],
template = [self newView];
if (aThemeClass)
[template setThemeClass:aThemeClass];
[template setValue:anAccent forThemeAttribute:@"accent"];
[theme takeThemeFromObject:template];
return theme;
}
- (id)archived:(CPView)aView
{
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:aView]];
}
- (void)assertAccent:(CPString)aValue ofView:(CPView)aView
{
[self assert:aValue equals:[aView currentValueForThemeAttribute:@"accent"]];
}
- (void)testAViewResolvesItsThemeValue
{
[self assertAccent:@"first" ofView:view];
}
- (void)testAnAttributeTheThemeDoesNotDefineUsesTheClassDefault
{
[self assert:@"class default" equals:[view currentValueForThemeAttribute:@"margin"]];
}
- (void)testAViewWithoutAThemeUsesTheClassDefault
{
[view setTheme:nil];
[self assertAccent:@"class default" ofView:view];
}
- (void)testSettingAThemeChangesTheResolvedValue
{
[view setTheme:secondTheme];
[self assertAccent:@"second" ofView:view];
}
- (void)testSettingAThemeNotifiesTheView
{
[view setTheme:secondTheme];
[self assert:1 equals:[view themeChangeCount]];
}
- (void)testSettingTheSameThemeDoesNothing
{
[view setTheme:firstTheme];
[self assert:0 equals:[view themeChangeCount]];
}
- (void)testSettingAThemeQueuesLayout
{
[view setTheme:secondTheme];
[_CPDisplayServer run];
[self assert:1 equals:[view layoutCount]];
}
- (void)testSettingAThemeRequestsDisplay
{
[view setDisplayRequestCount:0];
[view setTheme:secondTheme];
[self assertTrue:[view displayRequestCount] > 0];
}
- (void)testAThemeChangeReachesNestedViews
{
var parent = [self newView],
child = [self newView],
grandchild = [self newView];
[parent addSubview:child];
[child addSubview:grandchild];
[parent _setThemeIncludingDescendants:secondTheme];
[self assertAccent:@"second" ofView:parent];
[self assertAccent:@"second" ofView:child];
[self assertAccent:@"second" ofView:grandchild];
}
- (void)testAPerViewValueTakesPriorityOverTheTheme
{
[view setValue:@"own" forThemeAttribute:@"accent"];
[self assertAccent:@"own" ofView:view];
}
- (void)testAPerViewValueSurvivesAThemeChange
{
[view setValue:@"own" forThemeAttribute:@"accent"];
[view setTheme:secondTheme];
[self assertAccent:@"own" ofView:view];
}
- (void)testAPerViewValueQueuesLayout
{
[view setValue:@"own" forThemeAttribute:@"accent"];
[_CPDisplayServer run];
[self assert:1 equals:[view layoutCount]];
}
- (void)testSettingTheSamePerViewValueQueuesNoLayout
{
[view setValue:@"first" forThemeAttribute:@"accent"];
[_CPDisplayServer run];
[self assert:0 equals:[view layoutCount]];
}
- (void)testAStateSelectsItsValue
{
[view setValue:@"disabled" forThemeAttribute:@"accent" inState:CPThemeStateDisabled];
[self assertAccent:@"first" ofView:view];
[view setThemeState:CPThemeStateDisabled];
[self assertAccent:@"disabled" ofView:view];
[view unsetThemeState:CPThemeStateDisabled];
[self assertAccent:@"first" ofView:view];
}
- (void)testAStateChangeQueuesLayout
{
[view setThemeState:CPThemeStateDisabled];
[_CPDisplayServer run];
[self assert:1 equals:[view layoutCount]];
}
- (void)testSettingAThemeClassChangesTheResolvedValue
{
var theme = [self themeNamed:@"theme-change-class" themeClass:@"other-class" accent:@"other class"];
[view setTheme:theme];
[view setThemeClass:@"other-class"];
[self assertAccent:@"other class" ofView:view];
}
/*
A value set on one view is held apart from the values it inherits, so a change of
theme class must keep it, as a change of theme does. This fails today:
-setThemeClass: calls -_loadThemeAttributes, which replaces the view's attributes
with the set shared by every view of the class, and the view's own value is lost.
*/
- (void)testAPerViewValueSurvivesAThemeClassChange
{
[view setValue:@"own" forThemeAttribute:@"accent"];
[view setThemeClass:@"other-class"];
[self assertAccent:@"own" ofView:view];
}
- (void)testSettingAThemeClassQueuesLayout
{
[view setThemeClass:@"other-class"];
[_CPDisplayServer run];
[self assert:1 equals:[view layoutCount]];
}
- (void)testANewViewUsesTheDefaultTheme
{
[CPTheme setDefaultTheme:secondTheme];
[self assertAccent:@"second" ofView:[self newView]];
}
- (void)testChangingTheDefaultThemeLeavesAnExistingViewUnchanged
{
[CPTheme setDefaultTheme:secondTheme];
[self assertAccent:@"first" ofView:view];
}
- (void)testADecodedViewUsesTheDefaultTheme
{
[CPTheme setDefaultTheme:secondTheme];
[self assertAccent:@"second" ofView:[self archived:view]];
}
- (void)testADecodedViewKeepsAPerViewValue
{
[view setValue:@"own" forThemeAttribute:@"accent"];
[self assertAccent:@"own" ofView:[self archived:view]];
}
- (void)testADecodedViewKeepsAPerViewStateValue
{
[view setValue:@"disabled" forThemeAttribute:@"accent" inState:CPThemeStateDisabled];
var decoded = [self archived:view];
[decoded setThemeState:CPThemeStateDisabled];
[self assertAccent:@"disabled" ofView:decoded];
}
- (void)testADecodedViewKeepsItsThemeState
{
[view setThemeState:CPThemeStateDisabled];
[self assertTrue:[[self archived:view] hasThemeState:CPThemeStateDisabled]];
}
- (void)testADecodedViewKeepsItsThemeClass
{
[view setThemeClass:@"other-class"];
[self assert:@"other-class" equals:[[self archived:view] themeClass]];
}
@end