mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-03 13:49:45 +00:00
Protects themed drawing by verifying that the states selecting theme values remain correct as views enter and leave windows and as window status changes. * CPViewThemeStateTest: Verifies the key window, first responder and appearance states across nested views, including views added, removed and moved between windows; the key and main states of the window view; and the order of window move notifications.
409 lines
12 KiB
Plaintext
409 lines
12 KiB
Plaintext
/*
|
|
* CPViewThemeStateTest.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 theme value is chosen by the state of a view, so a view with the wrong state
|
|
draws wrongly with a correct theme. These tests check that the states that depend
|
|
on a view's surroundings stay correct as views enter and leave windows and as
|
|
windows change. The states are the key window state, the first responder state
|
|
and the appearance states. Only a window's own window view has the main window
|
|
state.
|
|
|
|
These tests run with no theme loaded. They check the states, and not what a theme
|
|
does with them.
|
|
*/
|
|
|
|
@implementation _CPThemeStateRecordingView : CPView
|
|
{
|
|
CPArray calls @accessors;
|
|
BOOL hadKeyStateOnArrival @accessors;
|
|
}
|
|
|
|
- (id)initWithFrame:(CGRect)aFrame
|
|
{
|
|
self = [super initWithFrame:aFrame];
|
|
|
|
if (self)
|
|
calls = [];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)acceptsFirstResponder
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)viewWillMoveToWindow:(CPWindow)aWindow
|
|
{
|
|
calls.push({ @"call": @"willMove", @"from": [self window], @"to": aWindow });
|
|
}
|
|
|
|
- (void)viewDidMoveToWindow
|
|
{
|
|
hadKeyStateOnArrival = [self hasThemeState:CPThemeStateKeyWindow];
|
|
calls.push({ @"call": @"didMove", @"window": [self window] });
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation CPViewThemeStateTest : OJTestCase
|
|
{
|
|
CPWindow firstWindow;
|
|
CPWindow secondWindow;
|
|
_CPThemeStateRecordingView parentView;
|
|
_CPThemeStateRecordingView childView;
|
|
}
|
|
|
|
- (void)setUp
|
|
{
|
|
// This will init the global var CPApp which are used internally in the AppKit
|
|
[[CPApplication alloc] init];
|
|
|
|
firstWindow = [self newWindow];
|
|
secondWindow = [self newWindow];
|
|
|
|
parentView = [[_CPThemeStateRecordingView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
|
childView = [[_CPThemeStateRecordingView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)];
|
|
|
|
[parentView addSubview:childView];
|
|
}
|
|
|
|
- (CPWindow)newWindow
|
|
{
|
|
return [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 200.0, 200.0) styleMask:CPTitledWindowMask];
|
|
}
|
|
|
|
- (void)assertKeyState:(BOOL)shouldHaveState
|
|
{
|
|
[self assert:shouldHaveState equals:[parentView hasThemeState:CPThemeStateKeyWindow] message:@"parent view"];
|
|
[self assert:shouldHaveState equals:[childView hasThemeState:CPThemeStateKeyWindow] message:@"child view"];
|
|
}
|
|
|
|
- (void)assertFirstResponderState:(BOOL)shouldHaveState
|
|
{
|
|
[self assert:shouldHaveState equals:[parentView hasThemeState:CPThemeStateFirstResponder] message:@"parent view"];
|
|
[self assert:shouldHaveState equals:[childView hasThemeState:CPThemeStateFirstResponder] message:@"child view"];
|
|
}
|
|
|
|
- (void)clearCalls
|
|
{
|
|
[parentView setCalls:[]];
|
|
[childView setCalls:[]];
|
|
}
|
|
|
|
// MARK: Key window state
|
|
|
|
- (void)testAViewOutsideAWindowHasNoKeyState
|
|
{
|
|
[self assertKeyState:NO];
|
|
}
|
|
|
|
- (void)testBecomingKeyReachesNestedViews
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[self assertKeyState:NO];
|
|
|
|
[firstWindow makeKeyWindow];
|
|
[self assertKeyState:YES];
|
|
}
|
|
|
|
- (void)testResigningKeyReachesNestedViews
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeKeyWindow];
|
|
[firstWindow resignKeyWindow];
|
|
|
|
[self assertKeyState:NO];
|
|
}
|
|
|
|
- (void)testAnotherWindowBecomingKeyClearsTheState
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeKeyWindow];
|
|
[secondWindow makeKeyWindow];
|
|
|
|
[self assertKeyState:NO];
|
|
}
|
|
|
|
- (void)testAViewAddedToAKeyWindowGainsTheState
|
|
{
|
|
[firstWindow makeKeyWindow];
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
|
|
[self assertKeyState:YES];
|
|
}
|
|
|
|
- (void)testAViewAddedBelowAViewInAKeyWindowGainsTheState
|
|
{
|
|
var lateView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeKeyWindow];
|
|
[childView addSubview:lateView];
|
|
|
|
[self assertTrue:[lateView hasThemeState:CPThemeStateKeyWindow]];
|
|
}
|
|
|
|
- (void)testAViewRemovedFromAKeyWindowLosesTheState
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeKeyWindow];
|
|
[parentView removeFromSuperview];
|
|
|
|
[self assertKeyState:NO];
|
|
}
|
|
|
|
- (void)testMovingToANonKeyWindowClearsTheState
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeKeyWindow];
|
|
[[secondWindow contentView] addSubview:parentView];
|
|
|
|
[self assertKeyState:NO];
|
|
}
|
|
|
|
- (void)testMovingToAKeyWindowSetsTheState
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[secondWindow makeKeyWindow];
|
|
[[secondWindow contentView] addSubview:parentView];
|
|
|
|
[self assertKeyState:YES];
|
|
}
|
|
|
|
- (void)testANewContentViewGainsTheState
|
|
{
|
|
[firstWindow makeKeyWindow];
|
|
[firstWindow setContentView:parentView];
|
|
|
|
[self assertKeyState:YES];
|
|
}
|
|
|
|
- (void)testTheStateIsCorrectInViewDidMoveToWindow
|
|
{
|
|
[firstWindow makeKeyWindow];
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
|
|
[self assertTrue:[parentView hadKeyStateOnArrival]];
|
|
[self assertTrue:[childView hadKeyStateOnArrival]];
|
|
}
|
|
|
|
// MARK: Window view
|
|
|
|
/*
|
|
A window must be visible to become main, and ordering a window front needs a DOM.
|
|
So this test sends becomeMainWindow directly, and it does not cover makeMainWindow.
|
|
*/
|
|
- (void)testTheWindowViewFollowsKeyAndMainStatus
|
|
{
|
|
var windowView = [firstWindow _windowView];
|
|
|
|
[firstWindow makeKeyWindow];
|
|
[firstWindow becomeMainWindow];
|
|
|
|
[self assertTrue:[windowView hasThemeState:CPThemeStateKeyWindow]];
|
|
[self assertTrue:[windowView hasThemeState:CPThemeStateMainWindow]];
|
|
|
|
[firstWindow resignKeyWindow];
|
|
[firstWindow resignMainWindow];
|
|
|
|
[self assertFalse:[windowView hasThemeState:CPThemeStateKeyWindow]];
|
|
[self assertFalse:[windowView hasThemeState:CPThemeStateMainWindow]];
|
|
}
|
|
|
|
// MARK: First responder state
|
|
|
|
- (void)testBecomingFirstResponderReachesNestedViews
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeFirstResponder:parentView];
|
|
|
|
[self assertFirstResponderState:YES];
|
|
}
|
|
|
|
- (void)testResigningFirstResponderReachesNestedViews
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeFirstResponder:parentView];
|
|
[firstWindow makeFirstResponder:nil];
|
|
|
|
[self assertFirstResponderState:NO];
|
|
}
|
|
|
|
- (void)testAViewAddedBelowTheFirstResponderGainsTheState
|
|
{
|
|
var lateView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeFirstResponder:parentView];
|
|
[childView addSubview:lateView];
|
|
|
|
[self assertTrue:[lateView hasThemeState:CPThemeStateFirstResponder]];
|
|
}
|
|
|
|
- (void)testRemovingTheFirstResponderClearsTheState
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[firstWindow makeFirstResponder:parentView];
|
|
[parentView removeFromSuperview];
|
|
|
|
[self assertFirstResponderState:NO];
|
|
[self assertFalse:[firstWindow firstResponder] === parentView];
|
|
}
|
|
|
|
// MARK: Window notifications
|
|
|
|
- (void)testEnteringAWindowNotifiesEachViewInOrder
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
|
|
[parentView, childView].forEach(function(aView)
|
|
{
|
|
var calls = [aView calls];
|
|
|
|
[self assert:2 equals:calls.length];
|
|
[self assert:@"willMove" equals:calls[0].call];
|
|
[self assert:firstWindow equals:calls[0].to];
|
|
[self assertNull:calls[0].from];
|
|
[self assert:@"didMove" equals:calls[1].call];
|
|
[self assert:firstWindow equals:calls[1].window];
|
|
});
|
|
}
|
|
|
|
- (void)testLeavingAWindowNotifiesEachView
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[self clearCalls];
|
|
[parentView removeFromSuperview];
|
|
|
|
[parentView, childView].forEach(function(aView)
|
|
{
|
|
var calls = [aView calls];
|
|
|
|
[self assert:2 equals:calls.length];
|
|
[self assertNull:calls[0].to];
|
|
[self assert:firstWindow equals:calls[0].from];
|
|
[self assertNull:calls[1].window];
|
|
});
|
|
}
|
|
|
|
- (void)testMovingBetweenWindowsNotifiesEachView
|
|
{
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[self clearCalls];
|
|
[[secondWindow contentView] addSubview:parentView];
|
|
|
|
[parentView, childView].forEach(function(aView)
|
|
{
|
|
var calls = [aView calls];
|
|
|
|
[self assert:2 equals:calls.length];
|
|
[self assert:firstWindow equals:calls[0].from];
|
|
[self assert:secondWindow equals:calls[0].to];
|
|
[self assert:secondWindow equals:calls[1].window];
|
|
});
|
|
}
|
|
|
|
/*
|
|
Cocoa also sends these notifications when a view stays in its window and only its
|
|
position in the view hierarchy changes.
|
|
*/
|
|
- (void)testMovingWithinAWindowNotifiesEachView
|
|
{
|
|
var otherView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
[[firstWindow contentView] addSubview:parentView];
|
|
[[firstWindow contentView] addSubview:otherView];
|
|
[self clearCalls];
|
|
[otherView addSubview:parentView];
|
|
|
|
[parentView, childView].forEach(function(aView)
|
|
{
|
|
var calls = [aView calls];
|
|
|
|
[self assert:2 equals:calls.length];
|
|
[self assert:@"willMove" equals:calls[0].call];
|
|
[self assert:firstWindow equals:calls[0].to];
|
|
[self assert:@"didMove" equals:calls[1].call];
|
|
[self assert:firstWindow equals:calls[1].window];
|
|
});
|
|
}
|
|
|
|
// MARK: Appearance states
|
|
|
|
- (void)testEffectiveAppearanceComesFromTheNearestAncestor
|
|
{
|
|
var appearance = [CPAppearance appearanceNamed:CPAppearanceNameVibrantDark];
|
|
|
|
[parentView setAppearance:appearance];
|
|
|
|
[self assert:appearance equals:[childView effectiveAppearance]];
|
|
}
|
|
|
|
- (void)testOwnAppearanceOverridesAnAncestor
|
|
{
|
|
var appearance = [CPAppearance appearanceNamed:CPAppearanceNameVibrantLight];
|
|
|
|
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
|
[childView setAppearance:appearance];
|
|
|
|
[self assert:appearance equals:[childView effectiveAppearance]];
|
|
}
|
|
|
|
- (void)testAppearanceStateReachesNestedViews
|
|
{
|
|
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
|
[_CPDisplayServer run];
|
|
|
|
[self assertTrue:[parentView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
|
[self assertTrue:[childView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
|
}
|
|
|
|
- (void)testChangingAppearanceReplacesTheState
|
|
{
|
|
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
|
[_CPDisplayServer run];
|
|
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantLight]];
|
|
[_CPDisplayServer run];
|
|
|
|
[self assertFalse:[childView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
|
[self assertTrue:[childView hasThemeState:CPThemeStateAppearanceVibrantLight]];
|
|
}
|
|
|
|
- (void)testAViewAddedBelowAnAppearanceGainsItsState
|
|
{
|
|
var lateView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
|
[_CPDisplayServer run];
|
|
[childView addSubview:lateView];
|
|
[lateView setNeedsLayout];
|
|
[_CPDisplayServer run];
|
|
|
|
[self assertTrue:[lateView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
|
}
|
|
|
|
@end
|