Merge pull request #3351 from cappuccino/main-testing

Protect canonical AppKit and Foundation APIs with compatibility tests

Add regression and drop-in compatibility test suites to preserve canonical AppKit and Foundation public contracts and archived behavior, incorporating immediate fixes for defects exposed by the improved tests.
This commit is contained in:
David Richardson authored and GitHub committed 2026-11-02 17:38:50 -06:00
commit 2afea4f8ee
48 files changed
+16269 -129

No files matched your search

+82
View File
@@ -414,3 +414,85 @@ var secondItem = [[CPAccordionViewItem alloc] initWithIdentifier:@"secondSection
}
@end
var CPAccordionViewItemIdentifierKey = @"CPAccordionViewItemIdentifierKey",
CPAccordionViewItemViewKey = @"CPAccordionViewItemViewKey",
CPAccordionViewItemLabelKey = @"CPAccordionViewItemLabelKey";
@implementation CPAccordionViewItem (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super init];
if (self)
{
_identifier = [aCoder decodeObjectForKey:CPAccordionViewItemIdentifierKey];
_view = [aCoder decodeObjectForKey:CPAccordionViewItemViewKey];
_label = [aCoder decodeObjectForKey:CPAccordionViewItemLabelKey];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeObject:_identifier forKey:CPAccordionViewItemIdentifierKey];
[aCoder encodeObject:_view forKey:CPAccordionViewItemViewKey];
[aCoder encodeObject:_label forKey:CPAccordionViewItemLabelKey];
}
@end
var CPAccordionViewItemsKey = @"CPAccordionViewItemsKey",
CPAccordionViewItemHeaderPrototypeKey = @"CPAccordionViewItemHeaderPrototypeKey",
CPAccordionViewExpandedItemIndexesKey = @"CPAccordionViewExpandedItemIndexesKey";
@implementation CPAccordionView (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
_items = [];
_itemViews = [];
_expandedItemIndexes = [CPIndexSet indexSet];
[self setItemHeaderPrototype:[aCoder decodeObjectForKey:CPAccordionViewItemHeaderPrototypeKey]];
var items = [aCoder decodeObjectForKey:CPAccordionViewItemsKey],
expandedItemIndexes = [aCoder decodeObjectForKey:CPAccordionViewExpandedItemIndexesKey];
items.forEach(function(anItem)
{
[self addItem:anItem];
});
items.forEach(function(anItem, anIndex)
{
if (![expandedItemIndexes containsIndex:anIndex])
[self collapseItemAtIndex:anIndex];
});
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
// The item views are recreated on decode from the items, so they are not encoded.
var subviews = [self subviews];
[_itemViews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[super encodeWithCoder:aCoder];
[self setSubviews:subviews];
[aCoder encodeObject:_items forKey:CPAccordionViewItemsKey];
[aCoder encodeObject:_itemHeaderPrototype forKey:CPAccordionViewItemHeaderPrototypeKey];
[aCoder encodeObject:_expandedItemIndexes forKey:CPAccordionViewExpandedItemIndexesKey];
}
@end
+5
View File
@@ -125,6 +125,11 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
// MARK: -
// MARK: Implementation
- (CPString)name
{
return _name;
}
- (BOOL)isEqual:(id)anObject
{
if (![anObject isKindOfClass:CPAppearance])
+12 -5
View File
@@ -869,6 +869,11 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
// We're coming from elsewhere
_titleView = [aCoder decodeObjectForKey:CPBoxTitleViewKey];
// The archiver encodes these views after encodeWithCoder: returns, when they
// are subviews again. They are added back below.
_boxView._superview = nil;
_titleView._superview = nil;
}
_contentView = [aCoder decodeObjectForKey:CPBoxContentViewKey];
@@ -877,7 +882,9 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
// FIXME: (see encodeWithCoder to understand why this is not possible)
// FIXME: we fix this by hand. This is horrible so please find a structural solution !
if (_contentView)
var contentViewNeedsBoxView = _contentView && [_contentView superview] !== _boxView;
if (contentViewNeedsBoxView)
_contentView._superview = nil;
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
@@ -885,7 +892,7 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
[_boxView setAutoresizesSubviews:YES];
[self setAutoresizesSubviews:YES];
if (_contentView)
if (contentViewNeedsBoxView)
[_boxView setSubviews:@[_contentView]];
[self addSubview:_boxView];
@@ -945,9 +952,9 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
[aCoder encodeInt:_borderType forKey:CPBoxBorderTypeKey];
[aCoder encodeObject:_title forKey:CPBoxTitleKey];
[aCoder encodeInt:_titlePosition forKey:CPBoxTitlePositionKey];
[aCoder encodeConditionalObject:_contentView forKey:CPBoxContentViewKey];
[aCoder encodeConditionalObject:_titleView forKey:CPBoxTitleViewKey];
[aCoder encodeConditionalObject:_boxView forKey:CPBoxBoxViewKey];
[aCoder encodeObject:_contentView forKey:CPBoxContentViewKey];
[aCoder encodeObject:_titleView forKey:CPBoxTitleViewKey];
[aCoder encodeObject:_boxView forKey:CPBoxBoxViewKey];
}
@end
+2
View File
@@ -202,6 +202,8 @@ CPButtonImageOffset = 3.0;
[self setBezelStyle:CPRoundRectBezelStyle];
[self setBordered:YES];
_value = CPOffState;
[self _init];
}
+5
View File
@@ -390,6 +390,11 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
_rightButtons = [aCoder decodeObjectForKey:CPButtonBarRightButtonsKey] || [];
_hasResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasResizeControlKey];
_resizeControlIsLeftAligned = [aCoder decodeBoolForKey:CPButtonBarResizeControlIsLeftAlignedKey];
var buttons = [_buttons arrayByAddingObjectsFromArray:_rightButtons];
for (var i = [buttons count] - 1; i >= 0; i--)
[buttons[i] addObserver:self forKeyPath:@"hidden" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil];
}
return self;
+11 -4
View File
@@ -1581,7 +1581,9 @@ function SHOULD_SELECT_ITEM(anOutlineView, anItem)
_retargedChildIndex = theIndex;
_shouldRetargetChildIndex = YES;
// set CPTableView's _retargetedDropRow based on retargetedItem and retargetedChildIndex
// set CPTableView's _retargetedDropOperation and _retargetedDropRow based on retargetedItem and retargetedChildIndex
_retargetedDropOperation = (_retargedChildIndex === CPOutlineViewDropOnItemIndex) ? CPTableViewDropOn : CPTableViewDropAbove;
var retargetedItemInfo = (_retargetedItem != nil) ? _itemInfosForItems[[_retargetedItem UID]] : _rootItemInfo;
if (_retargedChildIndex === [retargetedItemInfo.children count])
@@ -2330,12 +2332,17 @@ var CPOutlineViewIndentationPerLevelKey = @"CPOutlineViewIndentationPerLevelKey"
_outlineTableColumn = [aCoder decodeObjectForKey:CPOutlineViewOutlineTableColumnKey];
_indentationPerLevel = [aCoder decodeFloatForKey:CPOutlineViewIndentationPerLevelKey];
_outlineViewDataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey];
_outlineViewDelegate = [aCoder decodeObjectForKey:CPOutlineViewDelegateKey];
[super setDataSource:self];
[super setDelegate:self];
// The setters record which optional methods each object implements.
var dataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey];
if (dataSource)
[self setDataSource:dataSource];
[self setDelegate:[aCoder decodeObjectForKey:CPOutlineViewDelegateKey]];
[self _updateIsViewBased];
}
+3
View File
@@ -79,6 +79,9 @@ CPPopUpButtonStatePullsDown = CPThemeState("pulls-down");
if (self)
{
// The value of a pop up button is its selected index, and not a button state.
_value = nil;
[self selectItemAtIndex:CPNotFound];
_preferredEdge = CPMaxYEdge;
+6
View File
@@ -464,6 +464,12 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
return [super isOpaque] && [_cancelButton isOpaque] && [_searchButton isOpaque];
}
- (void)setObjectValue:(id)anObject
{
[super setObjectValue:anObject];
[self _updateCancelButtonVisibility];
}
- (void)_updateCancelButtonVisibility
{
[_cancelButton setHidden:([[self stringValue] length] === 0)];
+30
View File
@@ -52,6 +52,14 @@
}
/* @ignore */
/*!
Initializes a shadow with no offset or blur, and a black color at one third opacity.
*/
- (id)init
{
return [self _initWithOffset:CGSizeMakeZero() blurRadius:0.0 color:[CPColor colorWithWhite:0.0 alpha:1.0 / 3.0]];
}
- (id)_initWithOffset:(CGSize)anOffset blurRadius:(float)aBlurRadius color:(CPColor)aColor
{
self = [super init];
@@ -82,3 +90,25 @@
}
@end
var CPShadowOffsetKey = @"CPShadowOffsetKey",
CPShadowBlurRadiusKey = @"CPShadowBlurRadiusKey",
CPShadowColorKey = @"CPShadowColorKey";
@implementation CPShadow (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
return [self _initWithOffset:[aCoder decodeSizeForKey:CPShadowOffsetKey]
blurRadius:[aCoder decodeFloatForKey:CPShadowBlurRadiusKey]
color:[aCoder decodeObjectForKey:CPShadowColorKey]];
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeSize:_offset forKey:CPShadowOffsetKey];
[aCoder encodeFloat:_blurRadius forKey:CPShadowBlurRadiusKey];
[aCoder encodeObject:_color forKey:CPShadowColorKey];
}
@end
+4 -1
View File
@@ -221,7 +221,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
if (_dividerStyle === CPSplitViewDividerStyleThin)
return [self currentValueForThemeAttribute:@"pane-divider-color"];
return [CPColor colorWithPatternImage:[self currentValueForThemeAttribute:(_isVertical ? @"vertical-divider-color" : @"horizontal-divider-color")]];
// A theme may define no divider image, and a pattern needs one.
var image = [self currentValueForThemeAttribute:(_isVertical ? @"vertical-divider-color" : @"horizontal-divider-color")];
return image ? [CPColor colorWithPatternImage:image] : nil;
}
/*!
+2
View File
@@ -322,6 +322,8 @@ var CPStepperMinValue = @"CPStepperMinValue",
_valueWraps = [aCoder decodeBoolForKey:CPStepperValueWraps];
_autorepeat = [aCoder decodeBoolForKey:CPStepperAutorepeat];
// The archive holds the buttons as subviews, and _init creates them again.
[self setSubviews:@[]];
[self _init];
[self setEnabled:[self isEnabled]];
}
+11 -4
View File
@@ -826,9 +826,14 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
var items = [aCoder decodeObjectForKey:CPTabViewItemsKey] || [CPArray array];
[self _insertTabViewItems:items atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [items count])] canUpdateSelectedTab:NO];
[self setDelegate:[aCoder decodeObjectForKey:CPTabViewDelegateKey]];
// Inserting the items selected the first one. This is done before there is a
// delegate, because decoding is not a selection made by the user.
var selectedItem = [aCoder decodeObjectForKey:CPTabViewSelectedItemKey];
_selectedTabViewItem = [aCoder decodeObjectForKey:CPTabViewSelectedItemKey];
if (selectedItem)
[self _selectTabViewItemAtIndex:[self indexOfTabViewItem:selectedItem]];
[self setDelegate:[aCoder decodeObjectForKey:CPTabViewDelegateKey]];
_type = [aCoder decodeIntForKey:CPTabViewTypeKey];
}
@@ -866,15 +871,17 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
- (void)encodeWithCoder:(CPCoder)aCoder
{
// Don't bother to encode the CPBox. We will recreate it on decode and its content view is already
// Don't bother to encode the CPBox or the tabs. We will recreate them on decode and its content view is already
// stored by the tab view item. Not encoding _box makes the resulting archive smaller and reduces
// the surface for decoding bugs (of which we've had many in tab view).
var subviews = [self subviews];
[_box removeFromSuperview];
[_tabs removeFromSuperview];
[super encodeWithCoder:aCoder];
[self setSubviews:subviews];
[aCoder encodeObject:_items forKey:CPTabViewItemsKey];
// nib2cib stores its items in _items. Otherwise the items are the segments of _tabs.
[aCoder encodeObject:_items || [self items] forKey:CPTabViewItemsKey];
[aCoder encodeConditionalObject:_selectedTabViewItem forKey:CPTabViewSelectedItemKey];
+4 -3
View File
@@ -6269,14 +6269,15 @@ var CPTableViewDataSourceKey = @"CPTableViewDataSourceKey",
_headerView = [aCoder decodeObjectForKey:CPTableViewHeaderViewKey];
_cornerView = [aCoder decodeObjectForKey:CPTableViewCornerViewKey];
[self setDataSource:[aCoder decodeObjectForKey:CPTableViewDataSourceKey]];
[self setDelegate:[aCoder decodeObjectForKey:CPTableViewDelegateKey]];
[self _init];
if ([aCoder containsValueForKey:CPTableViewArchivedReusableViewsKey])
_archivedDataViews = [aCoder decodeObjectForKey:CPTableViewArchivedReusableViewsKey];
// Setting the data source reloads the table, which needs the state set by _init.
[self setDataSource:[aCoder decodeObjectForKey:CPTableViewDataSourceKey]];
[self setDelegate:[aCoder decodeObjectForKey:CPTableViewDelegateKey]];
[self _updateIsViewBased];
[self viewWillMoveToSuperview:[self superview]];
+6 -1
View File
@@ -2273,7 +2273,8 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
CPTextFieldPlaceholderStringKey = "CPTextFieldPlaceholderStringKey",
CPTextFieldUsesSingleLineMode = "CPTextFieldUsesSingleLineMode",
CPTextFieldWraps = "CPTextFieldWraps",
CPTextFieldScrolls = "CPTextFieldScrolls";
CPTextFieldScrolls = "CPTextFieldScrolls",
CPTextFieldDelegateKey = "CPTextFieldDelegateKey";
@implementation CPTextField (CPCoding)
@@ -2303,6 +2304,8 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
[self _setUsesSingleLineMode:[aCoder decodeBoolForKey:CPTextFieldUsesSingleLineMode]];
[self _setWraps:[aCoder decodeBoolForKey:CPTextFieldWraps]];
[self _setScrolls:[aCoder decodeBoolForKey:CPTextFieldScrolls]];
[self setDelegate:[aCoder decodeObjectForKey:CPTextFieldDelegateKey]];
[self updateTrackingAreas];
}
@@ -2332,6 +2335,8 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
[aCoder encodeBool:_usesSingleLineMode forKey:CPTextFieldUsesSingleLineMode];
[aCoder encodeBool:_wraps forKey:CPTextFieldWraps];
[aCoder encodeBool:_scrolls forKey:CPTextFieldScrolls];
[aCoder encodeConditionalObject:_delegate forKey:CPTextFieldDelegateKey];
}
@end
+9
View File
@@ -1741,8 +1741,17 @@ var CPTokenFieldTokenizingCharacterSetKey = "CPTokenFieldTokenizingCharacterSe
_completionDelay = [aCoder decodeDoubleForKey:CPTokenFieldCompletionDelayKey] || [[self class] defaultCompletionDelay];
_buttonType = [aCoder decodeIntForKey:CPTokenFieldButtonTypeKey] || CPTokenFieldDisclosureButtonType;
// The archive holds the token scroll view as a subview, and _init creates it again.
[self setSubviews:@[]];
[self _init];
// The tokens were created by setObjectValue: before the scroll view existed.
var contentView = [_tokenScrollView documentView],
tokens = [self _tokens];
for (var i = 0, count = [tokens count]; i < count; i++)
[contentView addSubview:tokens[i]];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
+1 -2
View File
@@ -121,7 +121,6 @@ CPVisualEffectStateInactive = 2;
- (void)_setEffectEnabled:(BOOL)shouldEnable
{
var dark = [[self appearance] isEqual:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]],
prop = CPBrowserStyleProperty("backdrop-filter"),
color = (dark ? [CPColor colorWithHexString:@"1e1e1e"] : [CPColor whiteColor]),
finalColor = shouldEnable ? [color colorWithAlphaComponent:0.6] : color;
@@ -129,7 +128,7 @@ CPVisualEffectStateInactive = 2;
if (CPDOMAvailable)
{
self._DOMElement.style[prop] = shouldEnable ? "blur(30px)" : nil;
self._DOMElement.style[CPBrowserStyleProperty("backdrop-filter")] = shouldEnable ? "blur(30px)" : nil;
}
}
+1
View File
@@ -63,3 +63,4 @@ task ("Theme", [$BUILD_CJS_CAPPUCCINO_APPKIT], function()
task ("build", ["AppKit", $BUILD_CJS_CAPPUCCINO_APPKIT, "Theme"]);
CLOBBER.include($BUILD_CJS_CAPPUCCINO_APPKIT);
CLOBBER.include(path.join(__dirname, "Build"));
+31
View File
@@ -137,3 +137,34 @@
}
@end
var CPMapTableKeysKey = @"CPMapTableKeysKey",
CPMapTableObjectsKey = @"CPMapTableObjectsKey";
@implementation CPMapTable (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [self init];
if (self)
{
var keys = [aCoder decodeObjectForKey:CPMapTableKeysKey],
objects = [aCoder decodeObjectForKey:CPMapTableObjectsKey];
keys.forEach(function(aKey, anIndex)
{
[self setObject:objects[anIndex] forKey:aKey];
});
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeObject:Array.from(_map.keys()) forKey:CPMapTableKeysKey];
[aCoder encodeObject:Array.from(_map.values()) forKey:CPMapTableObjectsKey];
}
@end
+32 -1
View File
@@ -526,7 +526,21 @@
type = CPBetweenPredicateOperatorType;
}
else
CPRaiseParseError(self, @"comparison predicate");
{
/*
A selector such as isKindOfClass: is a custom comparison operator. As in
Cocoa, a custom selector comparison has no modifier.
*/
var selector = [self parseCustomSelector];
if (!selector || modifier !== CPDirectPredicateModifier)
CPRaiseParseError(self, @"comparison predicate");
right = [self parseExpression];
p = [CPComparisonPredicate predicateWithLeftExpression:left rightExpression:right customSelector:selector];
return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p] : p;
}
if ([self scanString:@"[cd]" intoString:NULL])
{
@@ -552,6 +566,23 @@
return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p]:p;
}
/*
Scans a one-argument selector, such as isKindOfClass:, and returns it. Returns nil
and leaves the scan location unchanged when there is none.
*/
- (SEL)parseCustomSelector
{
var location = [self scanLocation],
name = [self scanCharactersFromSet:[CPCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"]];
if (name && [self scanString:@":" intoString:NULL])
return CPSelectorFromString(name + ":");
[self setScanLocation:location];
return nil;
}
- (CPExpression)parseExpression
{
return [self parseBinaryExpression];
+10 -2
View File
@@ -320,7 +320,7 @@ function _systemTimeZoneFromRuntime()
{
CPData _data @accessors(property=data, readonly);
CPInteger _secondsFromGMT @accessors(property=secondFromGMT, readonly);
CPString _abbreviation @accessors(property=abbreviation, readonly);
CPString _abbreviation;
CPString _name @accessors(property=name, readonly);
// Set only for zones created with +timeZoneForSecondsFromGMT:, which per
@@ -738,6 +738,14 @@ function _systemTimeZoneFromRuntime()
Returns nil if the date is nil
@return the abbreviation
*/
/*!
Returns the abbreviation for the current date, as abbreviationForDate: does.
*/
- (CPString)abbreviation
{
return [self abbreviationForDate:[CPDate date]];
}
- (CPString)abbreviationForDate:(CPDate)date
{
if (!date)
@@ -856,7 +864,7 @@ function _systemTimeZoneFromRuntime()
*/
- (CPString)description
{
return [CPString stringWithFormat:@"%s (%s) offset %i", _name, _abbreviation, [self secondsFromGMT]];
return [CPString stringWithFormat:@"%s (%s) offset %i", _name, [self abbreviation], [self secondsFromGMT]];
}
+1
View File
@@ -70,3 +70,4 @@ filedir ($BUILD_CJS_FOUNDATION, ["Foundation"], function()
task ("build", ["Foundation", $BUILD_CJS_FOUNDATION]);
CLOBBER.include($BUILD_CJS_FOUNDATION);
CLOBBER.include(path.join(__dirname, "Build"));
+114
View File
@@ -0,0 +1,114 @@
/*
* CPBoxTest.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 box that is archived and read back must keep its content view, its title and
its internal views, and the content view must remain inside the box.
*/
@implementation CPBoxTest : OJTestCase
{
CPBox box;
CPView contentView;
Function logWarning;
CPArray warnings;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
warnings = [];
logWarning = CPLog.warn;
CPLog.warn = function(aMessage)
{
warnings.push(String(aMessage));
};
box = [[CPBox alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
contentView = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 60.0, 40.0)];
[contentView addSubview:[[CPView alloc] initWithFrame:CGRectMake(3.0, 5.0, 20.0, 10.0)]];
[box setContentView:contentView];
}
- (void)tearDown
{
CPLog.warn = logWarning;
}
- (CPBox)archivedBox
{
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:box]];
}
- (void)testArchivingKeepsTheContentView
{
var decodedContentView = [[self archivedBox] contentView];
[self assertNotNull:decodedContentView];
[self assert:1 equals:[[decodedContentView subviews] count]];
[self assertTrue:CGRectEqualToRect([contentView frame], [decodedContentView frame])];
}
- (void)testArchivingKeepsTheContentViewInsideTheBox
{
var decodedBox = [self archivedBox],
boxView = [[decodedBox contentView] superview];
[self assertNotNull:boxView];
[self assert:decodedBox equals:[boxView superview]];
[self assertTrue:[[decodedBox subviews] containsObject:boxView]];
}
- (void)testArchivingLeavesTheBoxUnchanged
{
var subviews = [box subviews];
[self archivedBox];
[self assert:subviews equals:[box subviews]];
[self assert:contentView equals:[box contentView]];
}
- (void)testArchivingKeepsTheTitle
{
[box setTitle:@"Title"];
[box setTitlePosition:CPAtTop];
var decodedBox = [self archivedBox];
[self assert:@"Title" equals:[decodedBox title]];
[self assert:CPAtTop equals:[decodedBox titlePosition]];
}
- (void)testUntitledBoxDecodesWithoutAWarning
{
[self archivedBox];
[self assert:[] equals:warnings];
}
@end
+152
View File
@@ -0,0 +1,152 @@
/*
* CPButtonBarTest.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>
/*
CPButtonBar has no Cocoa counterpart. It is the strip of add, remove and action
buttons that applications place below a table or outline view, and those buttons
are often the only way a person can change the list above them.
These tests run with no theme loaded. A button bar has to work whatever a theme
chooses to define, so the class itself must supply usable buttons.
*/
@implementation CPButtonBarTest : OJTestCase
{
CPWindow theWindow;
CPButtonBar buttonBar;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 26.0)];
[[theWindow contentView] addSubview:buttonBar];
}
- (void)assertButtonHasStandardSize:(CPView)aButton
{
[self assert:35.0 equals:CGRectGetWidth([aButton frame])];
[self assert:25.0 equals:CGRectGetHeight([aButton frame])];
}
- (void)testPlusButtonHasTheStandardSize
{
[self assertButtonHasStandardSize:[CPButtonBar plusButton]];
}
- (void)testMinusButtonHasTheStandardSize
{
[self assertButtonHasStandardSize:[CPButtonBar minusButton]];
}
- (void)testActionPopupButtonHasTheStandardSize
{
[self assertButtonHasStandardSize:[CPButtonBar actionPopupButton]];
}
/*
A button must take up space in the bar and stay inside it, or a person cannot
click it. The first button starts one point before the bar, so that its border
falls outside the bar's edge.
*/
- (void)testLayoutPlacesEachButtonInsideTheBar
{
var buttons = [[CPButtonBar plusButton], [CPButtonBar minusButton], [CPButtonBar actionPopupButton]];
[buttonBar setButtons:buttons];
[buttonBar layoutSubviews];
buttons.forEach(function(aButton)
{
[self assert:buttonBar equals:[aButton superview]];
[self assertTrue:CGRectGetWidth([aButton frame]) > 0.0];
[self assertTrue:CGRectGetMinX([aButton frame]) >= -1.0];
[self assertTrue:CGRectGetMaxX([aButton frame]) <= CGRectGetWidth([buttonBar bounds])];
});
}
/*
Adjacent buttons share a one-point border, and they overlap by no more than that.
*/
- (void)testLayoutPlacesButtonsInOrderSharingABorder
{
var plusButton = [CPButtonBar plusButton],
minusButton = [CPButtonBar minusButton];
[buttonBar setButtons:[plusButton, minusButton]];
[buttonBar layoutSubviews];
[self assert:CGRectGetMaxX([plusButton frame]) - 1.0 equals:CGRectGetMinX([minusButton frame])];
}
/*
A hidden button leaves no gap, and the buttons after it move up.
*/
- (void)testHidingAButtonClosesTheGap
{
var plusButton = [CPButtonBar plusButton],
minusButton = [CPButtonBar minusButton];
[buttonBar setButtons:[plusButton, minusButton]];
[buttonBar layoutSubviews];
var firstOrigin = CGRectGetMinX([plusButton frame]);
[plusButton setHidden:YES];
[buttonBar layoutSubviews];
[self assert:firstOrigin equals:CGRectGetMinX([minusButton frame])];
}
- (void)testRightButtonsAreAlignedToTheRightEdge
{
var button = [CPButtonBar plusButton];
[buttonBar setRightButtons:[button]];
[buttonBar layoutSubviews];
[self assert:buttonBar equals:[button superview]];
[self assertTrue:CGRectGetWidth([button frame]) > 0.0];
[self assertTrue:CGRectGetMaxX([button frame]) >= CGRectGetWidth([buttonBar bounds]) - 1.0];
}
/*
Applications add their own buttons to a bar, so any button is acceptable, not
only those the class creates.
*/
- (void)testAnApplicationButtonCanBeAdded
{
var button = [[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 35.0, 25.0)];
[buttonBar setButtons:[button]];
[buttonBar layoutSubviews];
[self assert:[button] equals:[buttonBar buttons]];
[self assert:buttonBar equals:[button superview]];
}
@end
+6
View File
@@ -104,6 +104,12 @@
[self assertTrue:wasClicked];
}
- (void)testNewButtonValueIsOff
{
[self assert:CPOffState equals:[button objectValue] message:@"a new button should be off"];
[self assert:CPOffState equals:[button state] message:@"state should be CPOffState"];
}
- (void)testSetObjectValue
{
[button setObjectValue:CPOnState];
+356
View File
@@ -0,0 +1,356 @@
/*
* CPObjectArchivingTest.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>
/*
Views hold and share objects that are not views, and those objects are archived
with them. An object that is archived and read back must keep the properties that
an application reads. DropInArchiveTest.j and CPViewDecodedBehaviorTest.j cover
the views.
*/
@implementation CPObjectArchivingTest : OJTestCase
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
}
- (id)archived:(id)anObject
{
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]];
}
// MARK: Foundation collections and values
- (void)testAnArrayKeepsItsObjects
{
[self assert:[@"One", 2, YES] equals:[self archived:[@"One", 2, YES]]];
}
- (void)testADictionaryKeepsItsEntries
{
var dictionary = @{ @"one": @"first", @"two": 2 };
[self assert:dictionary equals:[self archived:dictionary]];
}
- (void)testASetKeepsItsObjects
{
var set = [CPSet setWithArray:[@"One", @"Two"]];
[self assert:set equals:[self archived:set]];
}
- (void)testAnIndexSetKeepsItsIndexes
{
var indexes = [CPMutableIndexSet indexSetWithIndexesInRange:CPMakeRange(2, 3)];
[indexes addIndex:9];
[self assert:indexes equals:[self archived:indexes]];
}
- (void)testAnIndexPathKeepsItsIndexes
{
var indexPath = [CPIndexPath indexPathWithIndexes:[1, 2, 3] length:3];
[self assert:indexPath equals:[self archived:indexPath]];
}
- (void)testADateKeepsItsTime
{
var date = [CPDate dateWithTimeIntervalSince1970:1000.0];
[self assert:1000.0 equals:[[self archived:date] timeIntervalSince1970]];
}
- (void)testNullStaysTheSingleton
{
[self assert:[CPNull null] equals:[self archived:[CPNull null]]];
}
- (void)testADecimalNumberKeepsItsValue
{
var number = [CPDecimalNumber decimalNumberWithString:@"12.5"];
[self assert:[number stringValue] equals:[[self archived:number] stringValue]];
}
- (void)testAnAttributedStringKeepsItsText
{
var string = [[CPAttributedString alloc] initWithString:@"Text" attributes:@{ @"key": @"value" }];
[self assert:string equals:[self archived:string]];
}
- (void)testASortDescriptorKeepsItsDefinition
{
var descriptor = [CPSortDescriptor sortDescriptorWithKey:@"name" ascending:NO selector:@selector(compare:)],
decoded = [self archived:descriptor];
[self assert:@"name" equals:[decoded key]];
[self assertFalse:[decoded ascending]];
[self assert:@selector(compare:) equals:[decoded selector]];
}
- (void)testACharacterSetKeepsItsMembers
{
var decoded = [self archived:[CPCharacterSet characterSetWithCharactersInString:@"ab"]];
[self assertTrue:[decoded characterIsMember:@"a"]];
[self assertFalse:[decoded characterIsMember:@"c"]];
}
- (void)testAnErrorKeepsItsDefinition
{
var error = [CPError errorWithDomain:@"Domain" code:7 userInfo:@{ @"key": @"value" }],
decoded = [self archived:error];
[self assert:@"Domain" equals:[decoded domain]];
[self assert:7 equals:[decoded code]];
[self assert:@"value" equals:[[decoded userInfo] objectForKey:@"key"]];
}
- (void)testAnExceptionKeepsItsDefinition
{
var exception = [CPException exceptionWithName:@"Name" reason:@"Reason" userInfo:nil],
decoded = [self archived:exception];
[self assert:@"Name" equals:[decoded name]];
[self assert:@"Reason" equals:[decoded reason]];
}
- (void)testALocaleKeepsItsIdentifier
{
var locale = [[CPLocale alloc] initWithLocaleIdentifier:@"fr_FR"];
[self assert:@"fr_FR" equals:[[self archived:locale] localeIdentifier]];
}
- (void)testANumberFormatterKeepsItsStyle
{
var formatter = [[CPNumberFormatter alloc] init];
[formatter setNumberStyle:CPNumberFormatterDecimalStyle];
[self assert:CPNumberFormatterDecimalStyle equals:[[self archived:formatter] numberStyle]];
}
- (void)testADateFormatterKeepsItsFormat
{
var formatter = [[CPDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd"];
[self assert:@"yyyy-MM-dd" equals:[[self archived:formatter] dateFormat]];
}
- (void)testAComparisonPredicateStillEvaluates
{
var decoded = [self archived:[CPPredicate predicateWithFormat:@"self BEGINSWITH 'a'"]];
[self assertTrue:[decoded evaluateWithObject:@"apple"]];
[self assertFalse:[decoded evaluateWithObject:@"pear"]];
}
- (void)testACompoundPredicateStillEvaluates
{
var decoded = [self archived:[CPPredicate predicateWithFormat:@"self BEGINSWITH 'a' AND self ENDSWITH 'e'"]];
[self assertTrue:[decoded evaluateWithObject:@"apple"]];
[self assertFalse:[decoded evaluateWithObject:@"apricot"]];
}
// MARK: AppKit values
- (void)testAColorKeepsItsComponents
{
var color = [CPColor colorWithRed:0.25 green:0.5 blue:0.75 alpha:0.5];
[self assert:[color cssString] equals:[[self archived:color] cssString]];
}
- (void)testANamedFontKeepsItsDefinition
{
var font = [CPFont boldFontWithName:@"Courier" size:21.0],
decoded = [self archived:font];
[self assert:[font cssString] equals:[decoded cssString]];
[self assertTrue:[decoded isBold]];
}
- (void)testASystemFontStaysASystemFont
{
var decoded = [self archived:[CPFont systemFontOfSize:15.0]];
[self assertTrue:[decoded isSystem]];
[self assert:15.0 equals:[decoded size]];
}
- (void)testAnImageKeepsItsDefinition
{
var image = [[CPImage alloc] initWithContentsOfFile:@"image.png" size:CGSizeMake(16.0, 32.0)],
decoded = [self archived:image];
[self assert:@"image.png" equals:[decoded filename]];
[self assertTrue:CGSizeEqualToSize(CGSizeMake(16.0, 32.0), [decoded size])];
}
- (void)testAShadowKeepsItsDefinition
{
var shadow = [[CPShadow alloc] init];
[shadow setShadowOffset:CGSizeMake(2.0, 3.0)];
[shadow setShadowBlurRadius:4.0];
[shadow setShadowColor:[CPColor redColor]];
var decoded = [self archived:shadow];
[self assertTrue:CGSizeEqualToSize(CGSizeMake(2.0, 3.0), [decoded shadowOffset])];
[self assert:4.0 equals:[decoded shadowBlurRadius]];
[self assert:[[CPColor redColor] cssString] equals:[[decoded shadowColor] cssString]];
}
- (void)testANewShadowKeepsItsDefaults
{
var decoded = [self archived:[[CPShadow alloc] init]];
[self assertTrue:CGSizeEqualToSize(CGSizeMakeZero(), [decoded shadowOffset])];
[self assert:0.0 equals:[decoded shadowBlurRadius]];
[self assert:1.0 / 3.0 equals:[[decoded shadowColor] alphaComponent]];
}
- (void)testAParagraphStyleKeepsItsAlignment
{
var style = [[CPMutableParagraphStyle alloc] init];
[style setAlignment:CPCenterTextAlignment];
[self assert:CPCenterTextAlignment equals:[[self archived:style] alignment]];
}
- (void)testAnAppearanceKeepsItsName
{
var appearance = [CPAppearance appearanceNamed:CPAppearanceNameVibrantDark];
[self assert:CPAppearanceNameVibrantDark equals:[[self archived:appearance] name]];
}
// MARK: Objects that belong to views
- (void)testAMenuKeepsItsItems
{
var menu = [[CPMenu alloc] initWithTitle:@"Menu"],
item = [[CPMenuItem alloc] initWithTitle:@"Item" action:@selector(performAction:) keyEquivalent:@"i"],
submenu = [[CPMenu alloc] initWithTitle:@""];
[item setTag:7];
[item setState:CPOnState];
[item setKeyEquivalentModifierMask:CPShiftKeyMask];
[menu addItem:item];
[menu addItem:[CPMenuItem separatorItem]];
[menu setSubmenu:submenu forItem:[menu addItemWithTitle:@"Parent" action:nil keyEquivalent:@""]];
var decoded = [self archived:menu],
decodedItem = [decoded itemAtIndex:0];
[self assert:@"Menu" equals:[decoded title]];
[self assert:3 equals:[decoded numberOfItems]];
[self assert:@"Item" equals:[decodedItem title]];
[self assert:@selector(performAction:) equals:[decodedItem action]];
[self assert:@"i" equals:[decodedItem keyEquivalent]];
[self assert:CPShiftKeyMask equals:[decodedItem keyEquivalentModifierMask]];
[self assert:7 equals:[decodedItem tag]];
[self assert:CPOnState equals:[decodedItem state]];
[self assert:decoded equals:[decodedItem menu]];
[self assertTrue:[[decoded itemAtIndex:1] isSeparatorItem]];
[self assert:@"Parent" equals:[[[decoded itemAtIndex:2] submenu] title]];
[self assert:decoded equals:[[[decoded itemAtIndex:2] submenu] supermenu]];
}
- (void)testATableColumnKeepsItsDefinition
{
var column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
[column setWidth:120.0];
[column setMinWidth:50.0];
[column setMaxWidth:300.0];
[column setEditable:YES];
[column setHidden:YES];
[[column headerView] setStringValue:@"Title"];
[column setSortDescriptorPrototype:[CPSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];
var decoded = [self archived:column];
[self assert:@"Column" equals:[decoded identifier]];
[self assert:120.0 equals:[decoded width]];
[self assert:50.0 equals:[decoded minWidth]];
[self assert:300.0 equals:[decoded maxWidth]];
[self assertTrue:[decoded isEditable]];
[self assertTrue:[decoded isHidden]];
[self assert:@"Title" equals:[[decoded headerView] stringValue]];
[self assert:@"name" equals:[[decoded sortDescriptorPrototype] key]];
}
- (void)testATabViewItemKeepsItsDefinition
{
var item = [[CPTabViewItem alloc] initWithIdentifier:@"identifier"];
[item setLabel:@"Label"];
[item setView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 20.0)]];
var decoded = [self archived:item];
[self assert:@"identifier" equals:[decoded identifier]];
[self assert:@"Label" equals:[decoded label]];
[self assertNotNull:[decoded view]];
}
- (void)testAnObjectControllerKeepsItsContent
{
var controller = [[CPObjectController alloc] initWithContent:@"Content"];
[controller setEditable:NO];
var decoded = [self archived:controller];
[self assert:@"Content" equals:[decoded content]];
[self assertFalse:[decoded isEditable]];
}
- (void)testAnArrayControllerKeepsItsConfiguration
{
var controller = [[CPArrayController alloc] initWithContent:[@"One", @"Two"]];
[controller setAvoidsEmptySelection:NO];
[controller setPreservesSelection:NO];
var decoded = [self archived:controller];
[self assertFalse:[decoded avoidsEmptySelection]];
[self assertFalse:[decoded preservesSelection]];
}
@end
+348
View File
@@ -0,0 +1,348 @@
/*
* CPOutlineViewInteractionTest.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>
/*
An outline view adds a parent to every row, so it reports a drag by item and a drop
by parent item and child index. Selection and editing come from CPTableView, and
CPTableViewInteractionTest.j covers them. These tests cover what an outline view
changes.
*/
var OutlineInteractionDragType = @"OutlineInteractionDragType";
@implementation _CPOutlineInteractionView : CPOutlineView
{
CPArray dragCalls @accessors;
}
- (void)dragView:(CPView)aView at:(CGPoint)aLocation offset:(CGSize)mouseOffset event:(CPEvent)anEvent pasteboard:(CPPasteboard)aPasteboard source:(id)aSourceObject slideBack:(BOOL)slideBack
{
if (!dragCalls)
dragCalls = [];
dragCalls.push(aPasteboard);
}
@end
@implementation _CPOutlineInteractionDataSource : CPObject
{
id tree @accessors;
CPArray writtenItems @accessors;
CPArray validations @accessors;
CPArray acceptedDrops @accessors;
id retargetItem @accessors;
id retargetIndex @accessors;
BOOL shouldRetarget @accessors;
}
- (id)init
{
self = [super init];
if (self)
{
tree = {
@"root": [@"A", @"B", @"C"],
@"A": [@"A1", @"A2"],
@"B": [],
@"C": [@"C1"]
};
validations = [];
acceptedDrops = [];
}
return self;
}
- (CPArray)childrenOfItem:(id)anItem
{
return tree[anItem || @"root"] || [];
}
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
{
return [self childrenOfItem:anItem][anIndex];
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
{
return [self childrenOfItem:anItem].length > 0;
}
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
{
return [self childrenOfItem:anItem].length;
}
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
{
return anItem;
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)theItems toPasteboard:(CPPasteboard)aPasteboard
{
writtenItems = theItems;
[aPasteboard declareTypes:[OutlineInteractionDragType] owner:self];
[aPasteboard setData:@"items" forType:OutlineInteractionDragType];
return YES;
}
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id)anInfo proposedItem:(id)anItem proposedChildIndex:(int)anIndex
{
validations.push({ @"item": anItem, @"index": anIndex });
if (shouldRetarget)
[anOutlineView setDropItem:retargetItem dropChildIndex:retargetIndex];
return CPDragOperationMove;
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id)anInfo item:(id)anItem childIndex:(int)anIndex
{
acceptedDrops.push({ @"item": anItem, @"index": anIndex });
return YES;
}
@end
@implementation _CPOutlineInteractionDraggingInfo : CPObject
{
CPPasteboard draggingPasteboard @accessors;
CGPoint draggingLocation @accessors;
}
@end
@implementation CPOutlineViewInteractionTest : OJTestCase
{
CPWindow theWindow;
_CPOutlineInteractionView outlineView;
_CPOutlineInteractionDataSource dataSource;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
outlineView = [[_CPOutlineInteractionView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
var column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
[column setWidth:200.0];
[outlineView addTableColumn:column];
[outlineView setOutlineTableColumn:column];
[outlineView setAllowsMultipleSelection:YES];
[[theWindow contentView] addSubview:outlineView];
dataSource = [[_CPOutlineInteractionDataSource alloc] init];
[outlineView setDataSource:dataSource];
[outlineView registerForDraggedTypes:[OutlineInteractionDragType]];
[outlineView expandItem:@"A"];
[outlineView reloadData];
}
- (CGPoint)pointInItem:(id)anItem
{
return CGPointMake(100.0, CGRectGetMidY([outlineView rectOfRow:[outlineView rowForItem:anItem]]));
}
- (id)draggingInfoAtPoint:(CGPoint)aPoint
{
var info = [[_CPOutlineInteractionDraggingInfo alloc] init],
pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
[pasteboard declareTypes:[OutlineInteractionDragType] owner:self];
[info setDraggingPasteboard:pasteboard];
[info setDraggingLocation:[outlineView convertPoint:aPoint toView:nil]];
return info;
}
- (void)sendMouse:(CPEventType)aType atPoint:(CGPoint)aPoint
{
var event = [CPEvent mouseEventWithType:aType
location:[outlineView convertPoint:aPoint toView:nil]
modifierFlags:0
timestamp:0
windowNumber:[theWindow windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:0];
[CPApp sendEvent:event];
[outlineView trackMouse:event];
}
- (void)testDraggingARowWritesItsItem
{
var start = [self pointInItem:@"A1"];
[self sendMouse:CPLeftMouseDown atPoint:start];
[self sendMouse:CPLeftMouseDragged atPoint:CGPointMake(start.x + 20.0, start.y)];
[self assert:[@"A1"] equals:[dataSource writtenItems]];
[self assert:1 equals:[[outlineView dragCalls] count]];
}
- (void)testDraggingASelectionWritesEachItem
{
var firstRow = [outlineView rowForItem:@"A1"];
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(firstRow, 2)] byExtendingSelection:NO];
var start = [self pointInItem:@"A2"];
[self sendMouse:CPLeftMouseDown atPoint:start];
[self sendMouse:CPLeftMouseDragged atPoint:CGPointMake(start.x + 20.0, start.y)];
[self assert:[@"A1", @"A2"] equals:[dataSource writtenItems]];
}
- (void)testTheMiddleOfARowProposesADropOnThatItem
{
[outlineView draggingUpdated:[self draggingInfoAtPoint:[self pointInItem:@"B"]]];
[self assert:@"B" equals:[dataSource validations][0].item];
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource validations][0].index];
}
- (void)testTheTopOfAChildRowProposesADropInItsParent
{
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"A2"]];
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, CGRectGetMinY(rect) + 1.0)]];
[self assert:@"A" equals:[dataSource validations][0].item];
[self assert:1 equals:[dataSource validations][0].index];
}
/*
Between a child and a root row, either parent is possible, and the horizontal
position of the drag chooses between them.
*/
- (void)testAnUnindentedDropAboveARootRowProposesTheRoot
{
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"B"]];
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(5.0, CGRectGetMinY(rect) + 1.0)]];
[self assertNull:[dataSource validations][0].item];
[self assert:1 equals:[dataSource validations][0].index];
}
- (void)testAnIndentedDropAboveARootRowProposesTheEndOfThePreviousParent
{
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"B"]];
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, CGRectGetMinY(rect) + 1.0)]];
[self assert:@"A" equals:[dataSource validations][0].item];
[self assert:2 equals:[dataSource validations][0].index];
}
- (void)testBelowTheLastRowProposesADropAtTheEndOfTheRoot
{
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, 290.0)]];
[self assertNull:[dataSource validations][0].item];
[self assert:3 equals:[dataSource validations][0].index];
}
- (void)testAcceptingADropReceivesTheProposedItem
{
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
[outlineView draggingUpdated:info];
[self assertTrue:[outlineView performDragOperation:info]];
[self assert:@"B" equals:[dataSource acceptedDrops][0].item];
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource acceptedDrops][0].index];
}
- (void)testDataSourceCanRetargetADropBetweenChildren
{
[dataSource setShouldRetarget:YES];
[dataSource setRetargetItem:@"A"];
[dataSource setRetargetIndex:0];
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
[outlineView draggingUpdated:info];
[outlineView performDragOperation:info];
[self assert:@"A" equals:[dataSource acceptedDrops][0].item];
[self assert:0 equals:[dataSource acceptedDrops][0].index];
}
- (void)testDataSourceCanRetargetADropOnAnItem
{
[dataSource setShouldRetarget:YES];
[dataSource setRetargetItem:@"C"];
[dataSource setRetargetIndex:CPOutlineViewDropOnItemIndex];
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
[outlineView draggingUpdated:info];
[outlineView performDragOperation:info];
[self assert:@"C" equals:[dataSource acceptedDrops][0].item];
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource acceptedDrops][0].index];
}
- (void)testHoveringOverACollapsedItemExpandsIt
{
[self assertFalse:[outlineView isItemExpanded:@"C"]];
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
[outlineView._dragHoverTimer fire];
[self assertTrue:[outlineView isItemExpanded:@"C"]];
}
- (void)testAChildIndexCancelsTheHover
{
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
[outlineView setDropItem:@"C" dropChildIndex:0];
[self assertNull:outlineView._dragHoverTimer];
}
- (void)testEndingADragCancelsTheHover
{
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
[outlineView draggedView:nil endedAt:CGPointMakeZero() operation:CPDragOperationNone];
[self assertNull:outlineView._dragHoverTimer];
[self assertFalse:[outlineView isItemExpanded:@"C"]];
}
@end
+400
View File
@@ -0,0 +1,400 @@
/*
* CPOutlineViewRowContentTest.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>
/*
Applications draw their own content on table and outline rows. A source list
draws a count badge beside an item, for example, as SmartFoldersDemo does in its
BadgedOutlineView. That content is correct only if the table keeps redrawing it
as a person uses the table.
These tests take such a view through ordinary use: selecting, scrolling, resizing
a column, expanding an item, reloading. After each action, the table must ask for
its content again, and only for the rows now on screen.
The tests observe what a subclass observes. They do not depend on which view
performs the table's drawing.
*/
@implementation RowContentItem : CPObject
{
CPString title @accessors;
CPArray children @accessors;
}
+ (id)itemWithTitle:(CPString)aTitle children:(CPArray)someChildren
{
var item = [[self alloc] init];
[item setTitle:aTitle];
[item setChildren:someChildren];
return item;
}
@end
/*
Records what it is asked to draw, in place of drawing. The default implementations
need a canvas, which a unit test does not have.
*/
@implementation RowContentOutlineView : CPOutlineView
{
CPArray drawnSteps @accessors;
CPArray drawnRows @accessors;
CPArray drawnClipRects @accessors;
}
- (void)drawBackgroundInClipRect:(CGRect)aRect
{
[drawnSteps addObject:@"background"];
}
- (void)highlightSelectionInClipRect:(CGRect)aRect
{
[drawnSteps addObject:@"selection"];
}
- (void)drawGridInClipRect:(CGRect)aRect
{
[drawnSteps addObject:@"grid"];
}
- (void)drawRow:(CPInteger)aRow clipRect:(CGRect)aRect
{
[drawnRows addObject:aRow];
[drawnClipRects addObject:aRect];
}
@end
@implementation CPOutlineViewRowContentTest : OJTestCase
{
CPWindow theWindow;
CPScrollView scrollView;
RowContentOutlineView outlineView;
CPTableColumn firstColumn;
CPTableColumn secondColumn;
CPArray rootItems;
CPView drawingView;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
rootItems = [];
for (var index = 0; index < 40; index++)
{
var children = [[RowContentItem itemWithTitle:@"Child A" children:[]],
[RowContentItem itemWithTitle:@"Child B" children:[]]];
rootItems.push([RowContentItem itemWithTitle:@"Item" children:children]);
}
firstColumn = [[CPTableColumn alloc] initWithIdentifier:@"First"];
secondColumn = [[CPTableColumn alloc] initWithIdentifier:@"Second"];
outlineView = [[RowContentOutlineView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)];
[outlineView setDrawnSteps:[]];
[outlineView setDrawnRows:[]];
[outlineView setDrawnClipRects:[]];
[outlineView addTableColumn:firstColumn];
[outlineView addTableColumn:secondColumn];
[outlineView setOutlineTableColumn:firstColumn];
scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)];
[scrollView setDocumentView:outlineView];
[[theWindow contentView] addSubview:scrollView];
[outlineView setDataSource:self];
[outlineView reloadData];
[self settle];
}
- (id)outlineView:(CPOutlineView)anOutlineView child:(CPInteger)anIndex ofItem:(id)anItem
{
return anItem ? [anItem children][anIndex] : rootItems[anIndex];
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
{
return [[anItem children] count] > 0;
}
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
{
return anItem ? [[anItem children] count] : rootItems.length;
}
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
{
return [anItem title];
}
/*
The views that make up the table: the outline view and its subviews. Controls
are left out. Data views and disclosure buttons draw themselves, and they need
a canvas to do so.
*/
- (CPArray)tableViews
{
return [outlineView].concat([outlineView subviews].filter(function(aView)
{
return ![aView isKindOfClass:[CPControl class]];
}));
}
/*
Brings the table to rest, as the end of a display cycle does: the layout is
current, nothing is waiting to draw, and nothing has been recorded.
*/
- (void)settle
{
[outlineView layoutIfNeeded];
[[self tableViews] makeObjectsPerformSelector:@selector(displayIfNeeded)];
[[outlineView drawnSteps] removeAllObjects];
[[outlineView drawnRows] removeAllObjects];
[[outlineView drawnClipRects] removeAllObjects];
}
/*
CPView does not draw without a DOM, so the display cycle cannot reach drawRect:
in a unit test. This does its work: it lays the table out, then sends drawRect:
to each view that has been marked for display, and only to those.
*/
- (void)redraw
{
[outlineView layoutIfNeeded];
var graphicsPort = { path: NULL, gState: CGGStateCreate(), gStateStack: [] };
[CPGraphicsContext setCurrentContext:[CPGraphicsContext graphicsContextWithGraphicsPort:graphicsPort flipped:YES]];
drawingView = nil;
[self tableViews].forEach(function(aView)
{
if (![aView needsDisplay])
return;
var rowCount = [[outlineView drawnRows] count];
[aView drawRect:[aView bounds]];
if ([[outlineView drawnRows] count] > rowCount)
drawingView = aView;
});
}
/*
A badge has to remain visible, including on a selected or alternating row. So the
view that draws the row content must stay in front of anything that paints a row.
Only controls may sit in front of it. Without a DOM a subview may not yet have
its colors, so this does not look at colors. It looks at what is in front.
*/
- (void)assertNothingCoversRowContent:(CPString)anAction
{
var subviews = [outlineView subviews],
firstCovering = drawingView === outlineView ? 0 : [subviews indexOfObject:drawingView] + 1;
for (var index = firstCovering; index < [subviews count]; index++)
{
var aView = subviews[index];
if ([aView isKindOfClass:[CPControl class]] || [aView isHidden])
continue;
[self fail:anAction + ": " + [aView className] + " covers the row content"];
}
}
- (CPArray)rowsOnScreen
{
var range = [outlineView rowsInRect:[outlineView exposedRect]],
rows = [];
for (var row = range.location; row < CPMaxRange(range); row++)
rows.push(row);
return rows;
}
/*
The table has been redrawn through each of its drawing steps, for exactly the
rows on screen, and each row was asked to draw only within the visible part of
the table.
*/
- (void)assertRowContentIsCurrentAfter:(CPString)anAction
{
[self redraw];
var rows = [self rowsOnScreen],
exposedRect = [outlineView exposedRect];
[self assert:[@"background", @"selection", @"grid"] equals:[outlineView drawnSteps] message:anAction];
[self assert:rows equals:[outlineView drawnRows] message:anAction];
[outlineView drawnClipRects].forEach(function(aRect)
{
[self assertTrue:CGRectContainsRect(exposedRect, aRect) message:anAction + ": clip rect is outside the visible area"];
});
[self assertNothingCoversRowContent:anAction];
}
/*
Redrawing costs time, and a table can be large. A table with nothing to change
has nothing to draw.
*/
- (void)testNothingIsRedrawnWithoutAChange
{
[self redraw];
[self assert:[] equals:[outlineView drawnSteps]];
[self assert:[] equals:[outlineView drawnRows]];
}
- (void)testContentIsDrawnWhenTheOutlineFirstAppears
{
[outlineView setNeedsDisplay:YES];
[self assertRowContentIsCurrentAfter:@"first display"];
}
- (void)testContentIsRedrawnAfterSelectingARow
{
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
[self assertRowContentIsCurrentAfter:@"selecting a row"];
}
- (void)testContentIsRedrawnAfterChangingTheSelection
{
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
[self settle];
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:4] byExtendingSelection:NO];
[self assertRowContentIsCurrentAfter:@"changing the selection"];
}
- (void)testContentIsRedrawnAfterClearingTheSelection
{
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
[self settle];
[outlineView deselectAll:nil];
[self assertRowContentIsCurrentAfter:@"clearing the selection"];
}
- (void)testContentIsRedrawnAfterSelectingAColumn
{
[outlineView selectColumnIndexes:[CPIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
[self assertRowContentIsCurrentAfter:@"selecting a column"];
}
- (void)testContentIsRedrawnAfterChangingTheGridStyle
{
[outlineView setGridStyleMask:CPTableViewSolidVerticalGridLineMask];
[self assertRowContentIsCurrentAfter:@"changing the grid style"];
}
- (void)testContentIsRedrawnAfterChangingTheGridColor
{
[outlineView setGridColor:[CPColor redColor]];
[self assertRowContentIsCurrentAfter:@"changing the grid color"];
}
- (void)testContentIsRedrawnAfterChangingTheRowColors
{
[outlineView setAlternatingRowBackgroundColors:[[CPColor redColor], [CPColor blueColor]]];
[self assertRowContentIsCurrentAfter:@"changing the row colors"];
}
- (void)testContentIsRedrawnAfterChangingTheHighlightStyle
{
[outlineView setSelectionHighlightStyle:CPTableViewSelectionHighlightStyleNone];
[self assertRowContentIsCurrentAfter:@"changing the highlight style"];
}
- (void)testContentIsRedrawnAfterReloadingTheData
{
[outlineView reloadData];
[self assertRowContentIsCurrentAfter:@"reloading the data"];
}
- (void)testContentIsRedrawnForTheNewRowsAfterScrolling
{
var firstRows = [self rowsOnScreen];
[[scrollView contentView] scrollToPoint:CGPointMake(0.0, 300.0)];
[self assertFalse:[firstRows isEqual:[self rowsOnScreen]] message:@"scrolling must change the rows on screen"];
[self assertRowContentIsCurrentAfter:@"scrolling"];
}
- (void)testContentIsRedrawnAfterResizingAColumn
{
[secondColumn setWidth:40.0];
[self assertRowContentIsCurrentAfter:@"resizing a column"];
}
- (void)testContentIsRedrawnAfterHidingAColumn
{
[secondColumn setHidden:YES];
[self assertRowContentIsCurrentAfter:@"hiding a column"];
}
- (void)testContentIsRedrawnAfterExpandingAnItem
{
[outlineView expandItem:rootItems[1]];
[self assertRowContentIsCurrentAfter:@"expanding an item"];
}
- (void)testContentIsRedrawnAfterCollapsingAnItem
{
[outlineView expandItem:rootItems[1]];
[self settle];
[outlineView collapseItem:rootItems[1]];
[self assertRowContentIsCurrentAfter:@"collapsing an item"];
}
@end
+12
View File
@@ -5,12 +5,24 @@
@implementation CPScrollViewTest : OJTestCase
{
CPScrollerStyle globalScrollerStyle;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
globalScrollerStyle = [CPScrollView globalScrollerStyle];
}
/*
The global scroller style applies to every scroll view, including those in later
test suites, so a test that changes it must not leave it changed.
*/
- (void)tearDown
{
[CPScrollView setGlobalScrollerStyle:globalScrollerStyle];
}
/*!
+198
View File
@@ -0,0 +1,198 @@
/*
* CPTableViewDrawingTest.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>
/*
NSTableView draws itself as a sequence of named steps: the background, then
the selection highlight, then the grid, then the rows. Each step is a method.
A subclass overrides one step to change it and inherits the others, so it can
extend the table without owning the table's internals.
CPTableView follows the same design. These tests confirm that each step reaches
a subclass. How the table organizes its drawing is its own concern, so the tests
observe only what a subclass can observe.
*/
@implementation DrawingRecordingTableView : CPTableView
{
CPArray drawnSteps @accessors;
CPArray drawnRows @accessors;
}
/*
Applications override this method to draw something other than a solid color
or alternating rows behind the table's content.
*/
- (void)drawBackgroundInClipRect:(CGRect)aRect
{
[drawnSteps addObject:@"background"];
}
/*
Applications override this method to change how selected rows appear.
In Cocoa this applies to cell-based tables. A view-based NSTableView uses
NSTableRowView for this purpose. CPTableView supports the method on its
view-based tables, and existing applications depend on it.
*/
- (void)highlightSelectionInClipRect:(CGRect)aRect
{
[drawnSteps addObject:@"selection"];
}
/*
Cocoa documents this method for all tables: subclasses override it to draw
grid lines other than the standard ones.
*/
- (void)drawGridInClipRect:(CGRect)aRect
{
[drawnSteps addObject:@"grid"];
}
/*
Applications override this method to draw over a row. A source list, for
example, can draw a badge beside an item this way. Cocoa also restricts this
method to cell-based tables, and CPTableView supports it as it does the
selection highlight.
*/
- (void)drawRow:(CPInteger)aRow clipRect:(CGRect)aRect
{
[drawnRows addObject:aRow];
}
@end
@implementation CPTableViewDrawingTest : OJTestCase
{
CPWindow theWindow;
DrawingRecordingTableView tableView;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
tableView = [[DrawingRecordingTableView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
[tableView setDrawnSteps:[]];
[tableView setDrawnRows:[]];
[tableView addTableColumn:[[CPTableColumn alloc] initWithIdentifier:@"Column"]];
[tableView setDataSource:self];
[[theWindow contentView] addSubview:tableView];
[tableView reloadData];
}
- (int)numberOfRowsInTableView:(CPTableView)aTableView
{
return 3;
}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
return @"Value";
}
/*
Unit tests run without a DOM, and CPView does not draw without one, so the
display cycle never reaches drawRect: here. This method does what the display
cycle does: it sends drawRect: to the table view and to each of its subviews.
The graphics port has no backing store, because creating a bitmap context
requires a DOM. The tests therefore show that the steps run, not what they draw.
*/
- (void)display
{
[tableView load];
var graphicsPort = { path: NULL, gState: CGGStateCreate(), gStateStack: [] };
[CPGraphicsContext setCurrentContext:[CPGraphicsContext graphicsContextWithGraphicsPort:graphicsPort flipped:YES]];
[tableView drawRect:[tableView bounds]];
var subviews = [tableView subviews],
count = [subviews count];
while (count--)
[subviews[count] drawRect:[subviews[count] bounds]];
}
- (void)testDrawGridInClipRectOverrideIsCalled
{
[self display];
[self assertTrue:[[tableView drawnSteps] containsObject:@"grid"]];
}
- (void)testDrawBackgroundInClipRectOverrideIsCalled
{
[self display];
[self assertTrue:[[tableView drawnSteps] containsObject:@"background"]];
}
- (void)testHighlightSelectionInClipRectOverrideIsCalled
{
[self display];
[self assertTrue:[[tableView drawnSteps] containsObject:@"selection"]];
}
- (void)testDrawRowClipRectOverrideIsCalledForEachExposedRow
{
[self display];
[self assert:[0, 1, 2] equals:[tableView drawnRows]];
}
/*
Each step draws over the one before it. The order is part of the contract,
because a subclass that draws a background relies on the selection and the
grid appearing above it.
*/
- (void)testDrawingStepsRunInOrder
{
[self display];
[self assert:[@"background", @"selection", @"grid"] equals:[tableView drawnSteps]];
}
/*
A color that an application sets belongs to the application. Theme states
change often during normal use, for example when the table gains or loses
first responder status, and they must leave that color in place.
*/
- (void)testBackgroundColorSurvivesThemeStateChange
{
var color = [CPColor redColor];
[tableView setBackgroundColor:color];
[tableView setThemeState:CPThemeStateFirstResponder];
[self assert:color equals:[tableView backgroundColor]];
[tableView unsetThemeState:CPThemeStateFirstResponder];
[self assert:color equals:[tableView backgroundColor]];
}
@end
+785
View File
@@ -0,0 +1,785 @@
/*
* CPTableViewInteractionTest.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 person selects, edits and drags rows with the mouse and the keyboard. These tests
send a table the same events, and check what its delegate and data source receive.
The drag session itself needs a display, so the table subclass records the call
that would start it. A stub stands in for the dragging information. These tests
cannot show that a drag image follows the pointer or that a drop indicator is
visible.
*/
var InteractionDragType = @"InteractionDragType";
@implementation _CPTableInteractionTable : CPTableView
{
CPArray dragCalls @accessors;
}
- (void)dragView:(CPView)aView at:(CGPoint)aLocation offset:(CGSize)mouseOffset event:(CPEvent)anEvent pasteboard:(CPPasteboard)aPasteboard source:(id)aSourceObject slideBack:(BOOL)slideBack
{
if (!dragCalls)
dragCalls = [];
dragCalls.push({ @"pasteboard": aPasteboard, @"source": aSourceObject });
}
@end
@implementation _CPTableInteractionDataSource : CPObject
{
CPArray rows @accessors;
CPArray editedValues @accessors;
}
- (id)init
{
self = [super init];
if (self)
{
rows = [@"Zero", @"One", @"Two", @"Three", @"Four"];
editedValues = [];
}
return self;
}
- (int)numberOfRowsInTableView:(CPTableView)aTableView
{
return rows.length;
}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aColumn row:(int)aRow
{
return rows[aRow];
}
- (void)tableView:(CPTableView)aTableView setObjectValue:(id)aValue forTableColumn:(CPTableColumn)aColumn row:(int)aRow
{
editedValues.push({ @"row": aRow, @"value": aValue });
}
@end
@implementation _CPTableInteractionDragDataSource : _CPTableInteractionDataSource
{
BOOL allowsDrag @accessors;
CPIndexSet writtenRows @accessors;
CPDragOperation validation @accessors;
BOOL acceptsDrop @accessors;
CPArray validations @accessors;
CPArray acceptedDrops @accessors;
id retargetRow @accessors;
id retargetOperation @accessors;
}
- (id)init
{
self = [super init];
if (self)
{
allowsDrag = YES;
validation = CPDragOperationMove;
acceptsDrop = YES;
validations = [];
acceptedDrops = [];
}
return self;
}
- (BOOL)tableView:(CPTableView)aTableView writeRowsWithIndexes:(CPIndexSet)theIndexes toPasteboard:(CPPasteboard)aPasteboard
{
writtenRows = theIndexes;
if (allowsDrag)
{
[aPasteboard declareTypes:[InteractionDragType] owner:self];
[aPasteboard setData:@"rows" forType:InteractionDragType];
}
return allowsDrag;
}
- (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(id)anInfo proposedRow:(int)aRow proposedDropOperation:(CPTableViewDropOperation)anOperation
{
validations.push({ @"row": aRow, @"operation": anOperation });
if (retargetRow != nil)
[aTableView setDropRow:retargetRow dropOperation:retargetOperation];
return validation;
}
- (BOOL)tableView:(CPTableView)aTableView acceptDrop:(id)anInfo row:(int)aRow dropOperation:(CPTableViewDropOperation)anOperation
{
acceptedDrops.push({ @"row": aRow, @"operation": anOperation });
return acceptsDrop;
}
@end
@implementation _CPTableInteractionDelegate : CPObject
{
CPArray calls @accessors;
BOOL allowsSelectionChange @accessors;
BOOL allowsEditing @accessors;
CPArray refusedRows @accessors;
}
- (id)init
{
self = [super init];
if (self)
{
calls = [];
allowsSelectionChange = YES;
allowsEditing = YES;
refusedRows = [];
}
return self;
}
- (BOOL)selectionShouldChangeInTableView:(CPTableView)aTableView
{
calls.push(@"selectionShouldChange");
return allowsSelectionChange;
}
- (BOOL)tableView:(CPTableView)aTableView shouldSelectRow:(int)aRow
{
return refusedRows.indexOf(aRow) === -1;
}
- (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aColumn row:(int)aRow
{
calls.push(@"shouldEdit");
return allowsEditing;
}
- (void)tableViewSelectionIsChanging:(CPNotification)aNotification
{
calls.push(@"selectionIsChanging");
}
- (void)tableViewSelectionDidChange:(CPNotification)aNotification
{
calls.push(@"selectionDidChange");
}
@end
@implementation _CPTableInteractionProposalDelegate : CPObject
{
}
- (CPIndexSet)tableView:(CPTableView)aTableView selectionIndexesForProposedSelection:(CPIndexSet)theIndexes
{
return [CPIndexSet indexSetWithIndex:3];
}
@end
@implementation _CPTableInteractionDraggingInfo : CPObject
{
CPPasteboard draggingPasteboard @accessors;
CGPoint draggingLocation @accessors;
}
@end
@implementation CPTableViewInteractionTest : OJTestCase
{
CPWindow theWindow;
_CPTableInteractionTable tableView;
CPTableColumn tableColumn;
_CPTableInteractionDataSource dataSource;
_CPTableInteractionDelegate delegate;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
tableView = [[_CPTableInteractionTable alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
tableColumn = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
[tableColumn setWidth:200.0];
[tableColumn setEditable:YES];
[tableView addTableColumn:tableColumn];
[tableView setAllowsMultipleSelection:YES];
[[theWindow contentView] addSubview:tableView];
delegate = [[_CPTableInteractionDelegate alloc] init];
[self useDataSource:[[_CPTableInteractionDataSource alloc] init]];
[tableView setDelegate:delegate];
}
- (void)useDataSource:(id)aDataSource
{
dataSource = aDataSource;
[tableView setDataSource:dataSource];
[tableView reloadData];
}
- (CGPoint)pointInRow:(int)aRow
{
return CGPointMake(20.0, CGRectGetMidY([tableView rectOfRow:aRow]));
}
- (CPEvent)mouseEvent:(CPEventType)aType atPoint:(CGPoint)aPoint flags:(unsigned)theFlags clicks:(int)aClickCount
{
return [CPEvent mouseEventWithType:aType
location:[tableView convertPoint:aPoint toView:nil]
modifierFlags:theFlags
timestamp:0
windowNumber:[theWindow windowNumber]
context:nil
eventNumber:0
clickCount:aClickCount
pressure:0];
}
- (void)sendEvent:(CPEvent)anEvent
{
[CPApp sendEvent:anEvent];
[tableView trackMouse:anEvent];
}
- (void)clickPoint:(CGPoint)aPoint flags:(unsigned)theFlags clicks:(int)aClickCount
{
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:aPoint flags:theFlags clicks:aClickCount]];
[self sendEvent:[self mouseEvent:CPLeftMouseUp atPoint:aPoint flags:theFlags clicks:aClickCount]];
}
- (void)clickRow:(int)aRow
{
[self clickPoint:[self pointInRow:aRow] flags:0 clicks:1];
}
- (void)clickRow:(int)aRow flags:(unsigned)theFlags
{
[self clickPoint:[self pointInRow:aRow] flags:theFlags clicks:1];
}
- (void)pressKey:(CPString)aCharacter flags:(unsigned)theFlags
{
[tableView keyDown:[CPEvent keyEventWithType:CPKeyDown
location:CGPointMakeZero()
modifierFlags:theFlags
timestamp:0
windowNumber:[theWindow windowNumber]
context:nil
characters:aCharacter
charactersIgnoringModifiers:aCharacter
isARepeat:NO
keyCode:0]];
}
- (CPArray)selectedRows
{
var rows = [];
[[tableView selectedRowIndexes] getIndexes:rows maxCount:-1 inIndexRange:nil];
return rows;
}
- (id)draggingInfoAtPoint:(CGPoint)aPoint
{
var info = [[_CPTableInteractionDraggingInfo alloc] init],
pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
[pasteboard declareTypes:[InteractionDragType] owner:self];
[info setDraggingPasteboard:pasteboard];
[info setDraggingLocation:[tableView convertPoint:aPoint toView:nil]];
return info;
}
- (void)useDragDataSource
{
[self useDataSource:[[_CPTableInteractionDragDataSource alloc] init]];
[tableView registerForDraggedTypes:[InteractionDragType]];
}
// MARK: Selection
- (void)testClickSelectsARow
{
[self clickRow:2];
[self assert:[2] equals:[self selectedRows]];
}
- (void)testClickReplacesTheSelection
{
[self clickRow:1];
[self clickRow:3];
[self assert:[3] equals:[self selectedRows]];
}
- (void)testClickOutsideTheRowsClearsTheSelection
{
[self clickRow:1];
[self clickPoint:CGPointMake(20.0, 290.0) flags:0 clicks:1];
[self assert:[] equals:[self selectedRows]];
}
- (void)testClickOutsideTheRowsKeepsTheSelectionWhenItCannotBeEmpty
{
[tableView setAllowsEmptySelection:NO];
[self clickRow:1];
[self clickPoint:CGPointMake(20.0, 290.0) flags:0 clicks:1];
[self assert:[1] equals:[self selectedRows]];
}
- (void)testCommandClickAddsAndRemovesARow
{
[self clickRow:1];
[self clickRow:3 flags:CPCommandKeyMask];
[self assert:[1, 3] equals:[self selectedRows]];
[self clickRow:1 flags:CPCommandKeyMask];
[self assert:[3] equals:[self selectedRows]];
}
- (void)testCommandClickReplacesTheSelectionWithoutMultipleSelection
{
[tableView setAllowsMultipleSelection:NO];
[self clickRow:1];
[self clickRow:3 flags:CPCommandKeyMask];
[self assert:[3] equals:[self selectedRows]];
}
- (void)testShiftClickExtendsFromTheAnchor
{
[self clickRow:1];
[self clickRow:3 flags:CPShiftKeyMask];
[self assert:[1, 2, 3] equals:[self selectedRows]];
}
- (void)testDraggingAcrossRowsExtendsTheSelection
{
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:[self pointInRow:1] flags:0 clicks:1]];
[self sendEvent:[self mouseEvent:CPLeftMouseDragged atPoint:[self pointInRow:2] flags:0 clicks:1]];
[self sendEvent:[self mouseEvent:CPLeftMouseDragged atPoint:[self pointInRow:3] flags:0 clicks:1]];
[self sendEvent:[self mouseEvent:CPLeftMouseUp atPoint:[self pointInRow:3] flags:0 clicks:1]];
[self assert:[1, 2, 3] equals:[self selectedRows]];
}
- (void)testDelegateCanRefuseASelectionChange
{
[delegate setAllowsSelectionChange:NO];
[self clickRow:2];
[self assert:[] equals:[self selectedRows]];
[self assertTrue:[delegate calls].indexOf(@"selectionShouldChange") !== -1];
}
- (void)testDelegateCanRefuseARow
{
[delegate setRefusedRows:[2]];
[self clickRow:2];
[self assert:[] equals:[self selectedRows]];
}
- (void)testDelegateCanReplaceTheProposedSelection
{
[tableView setDelegate:[[_CPTableInteractionProposalDelegate alloc] init]];
[self clickRow:1];
[self assert:[3] equals:[self selectedRows]];
}
- (void)testAClickReportsOneSelectionChange
{
[self clickRow:2];
var changes = [delegate calls].filter(function(aCall)
{
return aCall === @"selectionDidChange";
});
[self assert:1 equals:changes.length];
}
- (void)testClickingTheSelectedRowReportsNoChange
{
[self clickRow:2];
[delegate setCalls:[]];
[self clickRow:2];
[self assertTrue:[delegate calls].indexOf(@"selectionDidChange") === -1];
}
- (void)testSelectAllSelectsEveryRow
{
[tableView selectAll:nil];
[self assert:[0, 1, 2, 3, 4] equals:[self selectedRows]];
}
- (void)testDownArrowMovesTheSelection
{
[self clickRow:1];
[self pressKey:CPDownArrowFunctionKey flags:0];
[self assert:[2] equals:[self selectedRows]];
}
- (void)testUpArrowMovesTheSelection
{
[self clickRow:2];
[self pressKey:CPUpArrowFunctionKey flags:0];
[self assert:[1] equals:[self selectedRows]];
}
- (void)testArrowStopsAtTheLastRow
{
[self clickRow:4];
[self pressKey:CPDownArrowFunctionKey flags:0];
[self assert:[4] equals:[self selectedRows]];
}
- (void)testArrowSelectsAnEndRowWhenNothingIsSelected
{
[self pressKey:CPDownArrowFunctionKey flags:0];
[self assert:[0] equals:[self selectedRows]];
[tableView deselectAll:nil];
[self pressKey:CPUpArrowFunctionKey flags:0];
[self assert:[4] equals:[self selectedRows]];
}
- (void)testShiftArrowExtendsAndShrinksTheSelection
{
[self clickRow:1];
[self pressKey:CPDownArrowFunctionKey flags:CPShiftKeyMask];
[self assert:[1, 2] equals:[self selectedRows]];
[self pressKey:CPUpArrowFunctionKey flags:CPShiftKeyMask];
[self assert:[1] equals:[self selectedRows]];
}
- (void)testArrowSkipsARefusedRow
{
[delegate setRefusedRows:[2]];
[self clickRow:1];
[self pressKey:CPDownArrowFunctionKey flags:0];
[self assert:[3] equals:[self selectedRows]];
}
- (void)testArrowRespectsARefusedSelectionChange
{
[self clickRow:1];
[delegate setAllowsSelectionChange:NO];
[self pressKey:CPDownArrowFunctionKey flags:0];
[self assert:[1] equals:[self selectedRows]];
}
// MARK: Editing
- (void)testEditingAnUnselectedRowRaises
{
[self assertThrows:function()
{
[tableView editColumn:0 row:1 withEvent:nil select:YES];
}];
}
- (void)testEditingReportsTheEditedCell
{
[self clickRow:1];
[tableView editColumn:0 row:1 withEvent:nil select:YES];
[self assert:0 equals:[tableView editedColumn]];
[self assert:1 equals:[tableView editedRow]];
}
- (void)testEditingMakesTheCellTheFirstResponder
{
[self clickRow:1];
[tableView editColumn:0 row:1 withEvent:nil select:YES];
[self assertTrue:[[theWindow firstResponder] isKindOfClass:[CPTextField class]]];
[self assert:@"One" equals:[[theWindow firstResponder] stringValue]];
}
- (void)testEndingAnEditCommitsTheValue
{
[self clickRow:1];
[tableView editColumn:0 row:1 withEvent:nil select:YES];
var field = [theWindow firstResponder];
[field setStringValue:@"Edited"];
[[CPNotificationCenter defaultCenter] postNotificationName:CPControlTextDidEndEditingNotification object:field];
[self assert:1 equals:[dataSource editedValues].length];
[self assert:1 equals:[dataSource editedValues][0].row];
[self assert:@"Edited" equals:[dataSource editedValues][0].value];
[self assert:tableView equals:[theWindow firstResponder]];
}
- (void)testDoubleClickStartsEditing
{
[self clickRow:1];
[self clickPoint:[self pointInRow:1] flags:0 clicks:2];
[self assert:1 equals:[tableView editedRow]];
[self assertTrue:[delegate calls].indexOf(@"shouldEdit") !== -1];
}
- (void)testDelegateCanRefuseAnEdit
{
[delegate setAllowsEditing:NO];
[self clickRow:1];
[self clickPoint:[self pointInRow:1] flags:0 clicks:2];
[self assert:CPNotFound equals:[tableView editedRow]];
}
- (void)testAColumnThatIsNotEditableDoesNotStartAnEdit
{
[tableColumn setEditable:NO];
[self clickRow:1];
[self clickPoint:[self pointInRow:1] flags:0 clicks:2];
[self assert:CPNotFound equals:[tableView editedRow]];
}
// MARK: Starting a drag
- (void)dragFromRow:(int)aRow toPoint:(CGPoint)aPoint
{
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:[self pointInRow:aRow] flags:0 clicks:1]];
[self sendEvent:[self mouseEvent:CPLeftMouseDragged atPoint:aPoint flags:0 clicks:1]];
}
- (void)testADraggableTableSelectsOnMouseUp
{
[self useDragDataSource];
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:[self pointInRow:2] flags:0 clicks:1]];
[self assert:[] equals:[self selectedRows]];
[self sendEvent:[self mouseEvent:CPLeftMouseUp atPoint:[self pointInRow:2] flags:0 clicks:1]];
[self assert:[2] equals:[self selectedRows]];
}
- (void)testDraggingARowStartsADrag
{
[self useDragDataSource];
var start = [self pointInRow:2];
[self dragFromRow:2 toPoint:CGPointMake(start.x + 20.0, start.y)];
[self assert:1 equals:[[tableView dragCalls] count]];
[self assert:[CPIndexSet indexSetWithIndex:2] equals:[dataSource writtenRows]];
[self assert:[InteractionDragType] equals:[[tableView dragCalls][0].pasteboard types]];
[self assert:tableView equals:[tableView dragCalls][0].source];
}
- (void)testDraggingASelectedRowDragsTheWholeSelection
{
[self useDragDataSource];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(1, 2)] byExtendingSelection:NO];
var start = [self pointInRow:2];
[self dragFromRow:2 toPoint:CGPointMake(start.x + 20.0, start.y)];
[self assert:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(1, 2)] equals:[dataSource writtenRows]];
}
- (void)testAMoveWithinTheClickSlopDoesNotStartADrag
{
[self useDragDataSource];
var start = [self pointInRow:2];
[self dragFromRow:2 toPoint:CGPointMake(start.x + 2.0, start.y)];
[self assertNull:[tableView dragCalls]];
}
- (void)testDataSourceCanRefuseADrag
{
[self useDragDataSource];
[dataSource setAllowsDrag:NO];
var start = [self pointInRow:2];
[self dragFromRow:2 toPoint:CGPointMake(start.x + 20.0, start.y)];
[self assertNull:[tableView dragCalls]];
}
// MARK: Dropping
- (void)testAnUnregisteredTypeIsRefused
{
[self useDragDataSource];
[tableView unregisterDraggedTypes];
[self assert:CPDragOperationNone equals:[tableView draggingEntered:[self draggingInfoAtPoint:[self pointInRow:2]]]];
[self assert:0 equals:[dataSource validations].length];
}
- (void)testARegisteredTypeIsValidated
{
[self useDragDataSource];
[self assert:CPDragOperationMove equals:[tableView draggingEntered:[self draggingInfoAtPoint:[self pointInRow:2]]]];
[self assert:1 equals:[dataSource validations].length];
}
- (void)testValidationCanRefuseADrop
{
[self useDragDataSource];
[dataSource setValidation:CPDragOperationNone];
[self assert:CPDragOperationNone equals:[tableView draggingUpdated:[self draggingInfoAtPoint:[self pointInRow:2]]]];
}
- (void)testTheMiddleOfARowProposesADropOnThatRow
{
[self useDragDataSource];
[tableView draggingUpdated:[self draggingInfoAtPoint:[self pointInRow:2]]];
[self assert:2 equals:[dataSource validations][0].row];
[self assert:CPTableViewDropOn equals:[dataSource validations][0].operation];
}
- (void)testTheTopOfARowProposesADropAboveThatRow
{
[self useDragDataSource];
var rect = [tableView rectOfRow:2];
[tableView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(20.0, CGRectGetMinY(rect) + 1.0)]];
[self assert:2 equals:[dataSource validations][0].row];
[self assert:CPTableViewDropAbove equals:[dataSource validations][0].operation];
}
- (void)testTheBottomOfARowProposesADropAboveTheNextRow
{
[self useDragDataSource];
var rect = [tableView rectOfRow:2];
[tableView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(20.0, CGRectGetMaxY(rect) - 1.0)]];
[self assert:3 equals:[dataSource validations][0].row];
[self assert:CPTableViewDropAbove equals:[dataSource validations][0].operation];
}
- (void)testBelowTheLastRowProposesADropAtTheEnd
{
[self useDragDataSource];
[tableView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(20.0, 290.0)]];
[self assert:5 equals:[dataSource validations][0].row];
[self assert:CPTableViewDropAbove equals:[dataSource validations][0].operation];
}
- (void)testAcceptingADropReceivesTheProposedRow
{
[self useDragDataSource];
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
[tableView draggingUpdated:info];
[self assertTrue:[tableView prepareForDragOperation:info]];
[self assertTrue:[tableView performDragOperation:info]];
[self assert:2 equals:[dataSource acceptedDrops][0].row];
[self assert:CPTableViewDropOn equals:[dataSource acceptedDrops][0].operation];
}
- (void)testDataSourceCanRefuseADrop
{
[self useDragDataSource];
[dataSource setAcceptsDrop:NO];
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
[tableView draggingUpdated:info];
[self assertFalse:[tableView performDragOperation:info]];
}
- (void)testDataSourceCanRetargetADrop
{
[self useDragDataSource];
[dataSource setRetargetRow:4];
[dataSource setRetargetOperation:CPTableViewDropAbove];
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
[tableView draggingUpdated:info];
[tableView performDragOperation:info];
[self assert:4 equals:[dataSource acceptedDrops][0].row];
[self assert:CPTableViewDropAbove equals:[dataSource acceptedDrops][0].operation];
}
- (void)testPreparingADropNeedsValidation
{
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
[self assertFalse:[tableView prepareForDragOperation:info]];
}
@end
+475
View File
@@ -0,0 +1,475 @@
/*
* CPViewDecodedBehaviorTest.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>
/*
DropInArchiveTest.j checks that a decoded view has the right content. These tests
check that it still works: that its connections were restored, and that the
objects a class creates or observes for itself were set up again.
A target or delegate is encoded conditionally, so each test archives a container
that holds the target too. Bindings are not here. They are stored in cibs as
connectors, and not in the views.
*/
@implementation _CPDecodedTarget : CPView
{
int actionCount @accessors;
id lastSender @accessors;
}
- (void)performAction:(id)aSender
{
actionCount++;
lastSender = aSender;
}
@end
@implementation _CPDecodedTableDataSource : CPObject
{
}
- (id)initWithCoder:(CPCoder)aCoder
{
return [self init];
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
}
- (int)numberOfRowsInTableView:(CPTableView)aTableView
{
return 3;
}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aColumn row:(int)aRow
{
return @"Row";
}
@end
@implementation _CPDecodedOutlineDataSource : _CPDecodedTableDataSource
{
}
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
{
return anItem ? @"Child" : @"Parent";
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
{
return anItem === @"Parent";
}
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
{
return (!anItem || anItem === @"Parent") ? 1 : 0;
}
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
{
return anItem;
}
@end
@implementation CPViewDecodedBehaviorTest : OJTestCase
{
CPView container;
_CPDecodedTarget target;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
container = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
target = [[_CPDecodedTarget alloc] initWithFrame:CGRectMakeZero()];
[container addSubview:target];
}
- (id)archived:(id)anObject
{
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]];
}
/*
Connects a control to the target, archives the container, and returns the decoded
control. The decoded target is its action's receiver.
*/
- (id)decodedConnectedControl:(CPControl)aControl
{
[aControl setTarget:target];
[aControl setAction:@selector(performAction:)];
[container addSubview:aControl];
return [[self archived:container] subviews][1];
}
// MARK: Connections
- (void)testAControlKeepsItsTargetAndAction
{
var decoded = [self decodedConnectedControl:[[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)]],
decodedTarget = [[decoded superview] subviews][0];
[self assert:decodedTarget equals:[decoded target]];
[self assert:@selector(performAction:) equals:[decoded action]];
}
- (void)testADecodedButtonSendsItsAction
{
var decoded = [self decodedConnectedControl:[[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)]];
[decoded performClick:nil];
[self assert:1 equals:[[decoded target] actionCount]];
[self assert:decoded equals:[[decoded target] lastSender]];
}
- (void)testADecodedSliderSendsItsAction
{
var decoded = [self decodedConnectedControl:[[CPSlider alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)]];
[decoded sendAction:[decoded action] to:[decoded target]];
[self assert:1 equals:[[decoded target] actionCount]];
}
- (void)testADecodedTextFieldKeepsItsDelegate
{
var field = [[CPTextField alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)];
[field setDelegate:target];
[container addSubview:field];
var decoded = [[self archived:container] subviews][1];
[self assert:[[decoded superview] subviews][0] equals:[decoded delegate]];
}
- (void)testADecodedTabViewKeepsItsDelegate
{
var tabView = [[CPTabView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
[tabView setDelegate:target];
[container addSubview:tabView];
var decoded = [[self archived:container] subviews][1];
[self assert:[[decoded superview] subviews][0] equals:[decoded delegate]];
}
- (void)testADecodedSplitViewKeepsItsDelegate
{
var splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
[splitView setDelegate:target];
[container addSubview:splitView];
var decoded = [[self archived:container] subviews][1];
[self assert:[[decoded superview] subviews][0] equals:[decoded delegate]];
}
// MARK: The view hierarchy
- (void)testADecodedSubviewKeepsItsResponderChain
{
var child = [[CPView alloc] initWithFrame:CGRectMakeZero()];
[container addSubview:child];
var decoded = [self archived:container];
[self assert:decoded equals:[[decoded subviews][1] nextResponder]];
}
- (void)testADecodedViewAutoresizesItsSubviews
{
var child = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
[child setAutoresizingMask:CPViewWidthSizable];
[container addSubview:child];
var decoded = [self archived:container];
[decoded setFrameSize:CGSizeMake(400.0, 300.0)];
[self assert:200.0 equals:CGRectGetWidth([[decoded subviews][1] frame])];
}
// MARK: Objects a class creates or observes
- (void)testADecodedButtonBarFollowsAHiddenButton
{
var buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 26.0)],
buttons = [[CPButtonBar plusButton], [CPButtonBar minusButton]];
[buttonBar setButtons:buttons];
[buttonBar layoutSubviews];
var decoded = [self archived:buttonBar],
decodedButtons = [decoded buttons];
[decoded layoutSubviews];
[_CPDisplayServer run];
var firstOrigin = CGRectGetMinX([decodedButtons[0] frame]);
[decodedButtons[0] setHidden:YES];
[_CPDisplayServer run];
[self assert:firstOrigin equals:CGRectGetMinX([decodedButtons[1] frame])];
}
- (void)testADecodedStepperStepsItsValue
{
var stepper = [[CPStepper alloc] initWithFrame:CGRectMake(0.0, 0.0, 30.0, 25.0)];
[stepper setMinValue:1.0];
[stepper setMaxValue:9.0];
[stepper setDoubleValue:4.0];
var decoded = [self decodedConnectedControl:stepper];
[decoded performClickUp:nil];
[self assert:5.0 equals:[decoded doubleValue]];
[decoded performClickDown:nil];
[self assert:4.0 equals:[decoded doubleValue]];
[self assert:2 equals:[[decoded target] actionCount]];
}
- (void)testADecodedSearchFieldCancelsItsText
{
var field = [[CPSearchField alloc] initWithFrame:CGRectMake(0.0, 0.0, 120.0, 30.0)];
[field setStringValue:@"Text"];
var decoded = [self archived:field];
[decoded cancelOperation:nil];
[self assert:@"" equals:[decoded stringValue]];
[self assertTrue:[[decoded cancelButton] isHidden]];
}
- (void)testADecodedPopUpButtonAcceptsNewItems
{
var button = [[CPPopUpButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 120.0, 24.0)];
[button addItemsWithTitles:[@"One", @"Two"]];
[button selectItemAtIndex:1];
var decoded = [self archived:button];
[self assert:1 equals:[decoded indexOfSelectedItem]];
[self assert:@"Two" equals:[decoded titleOfSelectedItem]];
[decoded addItemWithTitle:@"Three"];
[decoded selectItemAtIndex:2];
[self assert:3 equals:[decoded numberOfItems]];
[self assert:@"Three" equals:[decoded title]];
}
- (void)testADecodedSegmentedControlSelectsASegment
{
var control = [[CPSegmentedControl alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 24.0)];
[control setSegmentCount:2];
[control setLabel:@"One" forSegment:0];
[control setLabel:@"Two" forSegment:1];
[control setSelectedSegment:0];
var decoded = [self archived:control];
[self assert:0 equals:[decoded selectedSegment]];
[self assert:@"Two" equals:[decoded labelForSegment:1]];
[decoded setSelectedSegment:1];
[self assert:1 equals:[decoded selectedSegment]];
}
- (void)testADecodedTabViewSwitchesItsContent
{
var tabView = [[CPTabView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
[@"one", @"two"].forEach(function(anIdentifier)
{
var item = [[CPTabViewItem alloc] initWithIdentifier:anIdentifier];
[item setLabel:anIdentifier];
[item setView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
[tabView addTabViewItem:item];
});
[tabView selectTabViewItemAtIndex:1];
var decoded = [self archived:tabView];
[self assert:@"two" equals:[[decoded selectedTabViewItem] identifier]];
[self assertNotNull:[[[decoded selectedTabViewItem] view] superview]];
[decoded selectTabViewItemAtIndex:0];
[self assert:@"one" equals:[[decoded selectedTabViewItem] identifier]];
[self assertNotNull:[[[decoded selectedTabViewItem] view] superview]];
}
- (void)testADecodedSplitViewMovesItsDivider
{
var splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
[splitView setVertical:YES];
[splitView addSubview:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 95.0, 100.0)]];
[splitView addSubview:[[CPView alloc] initWithFrame:CGRectMake(100.0, 0.0, 95.0, 100.0)]];
var decoded = [self archived:splitView];
[decoded setPosition:60.0 ofDividerAtIndex:0];
[self assert:60.0 equals:CGRectGetWidth([[decoded arrangedSubviews][0] frame])];
}
- (void)testADecodedScrollViewScrollsItsDocument
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
[scrollView setDocumentView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)]];
var decoded = [self archived:scrollView];
[[decoded documentView] scrollPoint:CGPointMake(0.0, 50.0)];
[self assert:50.0 equals:CGRectGetMinY([decoded documentVisibleRect])];
}
- (void)testADecodedBoxAcceptsANewContentView
{
var box = [[CPBox alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
[box setContentView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
var decoded = [self archived:box],
newContentView = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 60.0, 60.0)];
[decoded setContentView:newContentView];
[self assert:newContentView equals:[decoded contentView]];
[self assertNotNull:[newContentView superview]];
}
- (void)testADecodedAccordionKeepsACollapsedItem
{
var accordion = [[CPAccordionView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)],
item = [[CPAccordionViewItem alloc] initWithIdentifier:@"item"];
[item setLabel:@"Item"];
[item setView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 50.0)]];
[accordion addItem:item];
[accordion collapseItemAtIndex:0];
var decoded = [self archived:accordion];
[self assertFalse:[[decoded expandedItemIndexes] containsIndex:0]];
[decoded toggleItemAtIndex:0];
[self assertTrue:[[decoded expandedItemIndexes] containsIndex:0]];
}
- (void)testADecodedStackViewAcceptsAnArrangedSubview
{
var stackView = [[CPStackView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
[stackView addArrangedSubview:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
var decoded = [self archived:stackView];
[decoded addArrangedSubview:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
[self assert:2 equals:[[decoded arrangedSubviews] count]];
}
- (void)testADecodedTokenFieldEndsWithItsTokens
{
var field = [[CPTokenField alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 30.0)];
[field setObjectValue:[@"One", @"Two"]];
var decoded = [self archived:field];
[self assert:[@"One", @"Two"] equals:[decoded objectValue]];
[decoded setObjectValue:[@"Two"]];
[self assert:[@"Two"] equals:[decoded objectValue]];
}
- (void)testADecodedTableReadsItsDataSource
{
var tableView = [[CPTableView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
[tableView addTableColumn:[[CPTableColumn alloc] initWithIdentifier:@"Column"]];
[tableView setDataSource:[[_CPDecodedTableDataSource alloc] init]];
var decoded = [self archived:tableView];
[decoded reloadData];
[self assert:3 equals:[decoded numberOfRows]];
}
- (void)testADecodedOutlineReadsItsDataSource
{
var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)],
column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
[outlineView addTableColumn:column];
[outlineView setOutlineTableColumn:column];
[outlineView setDataSource:[[_CPDecodedOutlineDataSource alloc] init]];
var decoded = [self archived:outlineView];
[decoded reloadData];
[self assert:1 equals:[decoded numberOfRows]];
[self assert:@"Parent" equals:[decoded itemAtRow:0]];
[self assert:@"Parent" equals:[decoded _objectValueForTableColumn:[decoded outlineTableColumn] row:0 useCache:NO]];
[decoded expandItem:@"Parent"];
[self assert:2 equals:[decoded numberOfRows]];
}
@end
+327
View File
@@ -0,0 +1,327 @@
/*
* 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];
}
- (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
+408
View File
@@ -0,0 +1,408 @@
/*
* 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
+287
View File
@@ -0,0 +1,287 @@
/*
* DropInArchiveFixtures.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 "DropInSnapshot.j"
/*
Creates the views that the archive tests write and read back. An empty view has
little to lose, so each view holds real content. The reference for a view is its
own description before it is archived, never what a build restores.
*/
/*
The only view classes without a fixture. Each entry states the reason. Any other
class that cannot be archived is a failure.
*/
var DropInArchiveExclusions = {
@"CPText": @"it is abstract and its text methods raise an exception",
@"CPWebView": @"creating one needs a DOM"
};
function DropInArchiveExclusion(aClassName)
{
return DropInArchiveExclusions[aClassName];
}
function DropInArchiveChild(aWidth, aHeight)
{
return [[CPView alloc] initWithFrame:CGRectMake(3.0, 5.0, aWidth, aHeight)];
}
function DropInArchiveTableColumn(anIdentifier)
{
var column = [[CPTableColumn alloc] initWithIdentifier:anIdentifier];
[column setWidth:60.0];
return column;
}
/*
The content of each class. A class without an entry uses the entry of its
nearest superclass.
*/
var DropInArchivePopulators = {
@"CPView": function(aView)
{
[aView addSubview:DropInArchiveChild(20.0, 10.0)];
},
@"CPControl": function(aControl)
{
[aControl setEnabled:NO];
[aControl setTag:7];
},
@"CPAccordionView": function(anAccordionView)
{
var item = [[CPAccordionViewItem alloc] initWithIdentifier:@"item"];
[item setLabel:@"Item"];
[item setView:DropInArchiveChild(40.0, 30.0)];
[anAccordionView addItem:item];
},
@"CPBox": function(aBox)
{
var content = DropInArchiveChild(60.0, 40.0);
[content addSubview:DropInArchiveChild(20.0, 10.0)];
[aBox setTitle:@"Box"];
[aBox setTitlePosition:CPAtTop];
[aBox setContentView:content];
},
@"CPButton": function(aButton)
{
[aButton setTitle:@"Button"];
[aButton setState:CPOnState];
[aButton setTag:7];
},
@"CPButtonBar": function(aButtonBar)
{
[aButtonBar setButtons:[[CPButtonBar plusButton], [CPButtonBar minusButton]]];
},
@"CPColorWell": function(aColorWell)
{
[aColorWell setColor:[CPColor redColor]];
},
@"CPComboBox": function(aComboBox)
{
[aComboBox addItemsWithObjectValues:[@"One", @"Two"]];
[aComboBox setStringValue:@"One"];
},
@"CPDatePicker": function(aDatePicker)
{
[aDatePicker setDateValue:[[CPDate alloc] initWithString:@"2026-10-31 12:00:00 +0000"]];
},
@"CPLevelIndicator": function(aLevelIndicator)
{
[aLevelIndicator setMinValue:1.0];
[aLevelIndicator setMaxValue:9.0];
[aLevelIndicator setDoubleValue:4.0];
},
@"CPOutlineView": function(anOutlineView)
{
var column = DropInArchiveTableColumn(@"one");
[anOutlineView addTableColumn:column];
[anOutlineView setOutlineTableColumn:column];
},
@"CPPopUpButton": function(aPopUpButton)
{
[aPopUpButton addItemsWithTitles:[@"One", @"Two"]];
[aPopUpButton selectItemAtIndex:1];
},
@"CPProgressIndicator": function(aProgressIndicator)
{
[aProgressIndicator setMinValue:1.0];
[aProgressIndicator setMaxValue:9.0];
[aProgressIndicator setDoubleValue:4.0];
},
@"CPScrollView": function(aScrollView)
{
[aScrollView setDocumentView:DropInArchiveChild(300.0, 400.0)];
},
@"CPSegmentedControl": function(aSegmentedControl)
{
[aSegmentedControl setSegmentCount:2];
[aSegmentedControl setLabel:@"One" forSegment:0];
[aSegmentedControl setLabel:@"Two" forSegment:1];
[aSegmentedControl setWidth:40.0 forSegment:0];
[aSegmentedControl setWidth:40.0 forSegment:1];
},
@"CPSlider": function(aSlider)
{
[aSlider setMinValue:1.0];
[aSlider setMaxValue:9.0];
[aSlider setDoubleValue:4.0];
},
@"CPSplitView": function(aSplitView)
{
[aSplitView addSubview:DropInArchiveChild(40.0, 90.0)];
[aSplitView addSubview:DropInArchiveChild(40.0, 90.0)];
},
@"CPStackView": function(aStackView)
{
[aStackView addArrangedSubview:DropInArchiveChild(40.0, 30.0)];
},
@"CPStepper": function(aStepper)
{
[aStepper setMinValue:1.0];
[aStepper setMaxValue:9.0];
[aStepper setDoubleValue:4.0];
},
@"CPTableView": function(aTableView)
{
[aTableView addTableColumn:DropInArchiveTableColumn(@"one")];
[aTableView addTableColumn:DropInArchiveTableColumn(@"two")];
},
@"CPTabView": function(aTabView)
{
["one", "two"].forEach(function(anIdentifier)
{
var item = [[CPTabViewItem alloc] initWithIdentifier:anIdentifier];
[item setLabel:anIdentifier];
[item setView:DropInArchiveChild(40.0, 30.0)];
[aTabView addTabViewItem:item];
});
[aTabView selectTabViewItemAtIndex:1];
},
@"CPTextView": function(aTextView)
{
[aTextView setString:@"Text"];
},
@"CPTextField": function(aTextField)
{
[aTextField setStringValue:@"Text"];
[aTextField setEditable:YES];
[aTextField setBezeled:YES];
},
@"CPTokenField": function(aTokenField)
{
[aTokenField setObjectValue:[@"One", @"Two"]];
}
};
function DropInArchivePopulator(aClass)
{
for (; aClass; aClass = class_getSuperclass(aClass))
if (DropInArchivePopulators[aClass.name])
return DropInArchivePopulators[aClass.name];
return nil;
}
function DropInArchiveFixture(aClass)
{
var view = [[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
DropInArchivePopulator(aClass)(view);
return view;
}
/*
Lists each place where two view descriptions differ.
*/
function DropInTreeDifferences(expected, current, aPath, differences)
{
if (expected && current && typeof expected === "object" && typeof current === "object")
{
Object.keys(expected).concat(Object.keys(current)).filter(function(aKey, anIndex, aList)
{
return aList.indexOf(aKey) === anIndex;
}).forEach(function(aKey)
{
DropInTreeDifferences(expected[aKey], current[aKey], aPath + "." + aKey, differences);
});
}
else if (expected !== current)
differences.push(aPath + ": " + JSON.stringify(expected) + " is now " + JSON.stringify(current));
return differences;
}
/*
Layout changes frames and adds subviews, and a new view has not asked for it. Every
view is laid out before it is described, so that two descriptions are comparable.
*/
function DropInArchiveLayOut(aView)
{
[aView setNeedsLayout];
[_CPDisplayServer run];
}
/*
Archives a fixture and reads it back.
*/
function DropInArchiveRoundTrip(aClass)
{
return DropInWithFixedScrollerStyle(function()
{
var view = DropInArchiveFixture(aClass);
DropInArchiveLayOut(view);
var original = DropInViewTree(view),
data = [CPKeyedArchiver archivedDataWithRootObject:view],
decoded = [CPKeyedUnarchiver unarchiveObjectWithData:data];
DropInArchiveLayOut(decoded);
return { @"archive": [data rawString], @"original": original, @"decoded": DropInViewTree(decoded) };
});
}
/*
Reads a saved archive back and describes the view.
*/
function DropInSavedArchiveTree(aRawString)
{
return DropInWithFixedScrollerStyle(function()
{
var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:aRawString]];
DropInArchiveLayOut(decoded);
return DropInViewTree(decoded);
});
}
+92
View File
@@ -0,0 +1,92 @@
/*
* DropInArchiveGenerator.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 "DropInArchiveFixtures.j"
/*
Saves the archive test data. Run this only against a build of main:
objj Tests/AppKit/DropInArchiveGenerator.j
It saves a description of what each reference cib produces. It then saves an
archive of each view fixture, with the fixture's description before archiving.
An archive is saved only if it restores that description, so a failure stops
the generator and no archive data is written.
*/
var fs = require("fs"),
path = require("path");
function main(args, namedArgs)
{
[[CPApplication alloc] init];
var resources = path.join(process.cwd(), "Tests", "AppKit", "Resources"),
cibFolder = path.join(resources, "DropInCibs"),
archives = {},
cibs = {},
failures = [];
fs.readdirSync(cibFolder).sort().forEach(function(aName)
{
cibs[aName] = DropInCibTree(aName, fs.readFileSync(path.join(cibFolder, aName), {encoding: 'utf-8'}));
print(aName + ": " + cibs[aName].length + " top level objects");
});
fs.writeFileSync(path.join(resources, "DropInCibs.json"), JSON.stringify(cibs, null, 1) + "\n");
DropInSortedClasses().forEach(function(aClass)
{
var name = aClass.name;
if (!DropInIsPublicName(name) || !DropInClassIsView(aClass))
return;
if (DropInArchiveExclusion(name))
return print("excluded " + name + ": " + DropInArchiveExclusion(name));
try
{
var result = DropInArchiveRoundTrip(aClass),
differences = DropInTreeDifferences(result.original, result.decoded, name, []);
if (differences.length)
failures = failures.concat(differences);
else
archives[name] = { @"archive": result.archive, @"state": result.original };
}
catch (anException)
{
failures.push(name + ": " + String(anException));
}
});
if (failures.length)
{
failures.forEach(function(aLine) { print("failed " + aLine); });
print(failures.length + " failures, no archive data written");
process.exit(1);
}
fs.writeFileSync(path.join(resources, "DropInArchives.json"), JSON.stringify(archives, null, 1) + "\n");
print(Object.keys(archives).length + " archived view classes");
}
+203
View File
@@ -0,0 +1,203 @@
/*
* DropInArchiveTest.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 "DropInArchiveFixtures.j"
/*
Every application with an interface built in Xcode depends on cibs, and those
cibs were written before the application's frameworks were last updated. An
application that updates its frameworks does not rebuild them first, so the
frameworks must keep reading what was already written. A new key may be added to
an archive. Its absence from an older archive must then mean the old behavior,
and not a new default.
The reference for a view is the view before it was archived, so a build cannot
pass by restoring something else. A warning during decoding is a failure. The
classes under test are those in the drop-in baseline, so a class cannot leave
these tests unnoticed.
DropInArchiveGenerator.j saves the data these tests read. Each test reports every
difference it finds.
*/
var fs = require("fs"),
path = require("path");
@implementation DropInArchiveTest : OJTestCase
{
CPString resources;
CPArray warnings;
Function logWarning;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
resources = path.join(path.dirname(__filename), "Resources");
warnings = [];
logWarning = CPLog.warn;
CPLog.warn = function(aMessage)
{
warnings.push(String(aMessage));
};
}
- (void)tearDown
{
CPLog.warn = logWarning;
}
- (id)jsonNamed:(CPString)aName
{
return JSON.parse(fs.readFileSync(path.join(resources, aName), {encoding: 'utf-8'}));
}
- (CPArray)viewClassNames
{
return Object.keys([self jsonNamed:@"DropInBaseline.json"].construction).filter(function(aClassName)
{
return !DropInArchiveExclusion(aClassName);
});
}
- (void)assertNoDifferences:(CPArray)differences
{
[self assertTrue:differences.length === 0 message:"\n" + differences.join("\n")];
}
- (void)moveWarningsNamed:(CPString)aName into:(CPArray)differences
{
warnings.forEach(function(aWarning)
{
differences.push(aName + " warned: " + aWarning);
});
warnings = [];
}
/*
A view that is archived and read back must be the same view, with the same
content.
*/
- (void)testNewArchivesRestoreTheOriginalView
{
var differences = [];
[self viewClassNames].forEach(function(aClassName)
{
try
{
var result = DropInArchiveRoundTrip(objj_getClass(aClassName));
DropInTreeDifferences(result.original, result.decoded, aClassName, differences);
}
catch (anException)
{
differences.push(aClassName + ": " + String(anException));
}
[self moveWarningsNamed:aClassName into:differences];
});
[self assertNoDifferences:differences];
}
/*
An archive written by main must restore the view it was written from.
The descriptions compared must be the same on every machine. A date, and the
string value of a control that holds one, are recorded by the date's instant in
UTC, because their text is in the local time zone (DropInSnapshot.j). GitHub's
continuous integration runs this test in UTC, so run it in UTC as well as in the
local time zone before committing a change:
TZ=UTC npx jake test
*/
- (void)testExistingArchivesRestoreTheOriginalView
{
var archivePath = path.join(resources, @"DropInArchives.json");
[self assertTrue:fs.existsSync(archivePath) message:@"DropInArchives.json has not been generated"];
var archives = [self jsonNamed:@"DropInArchives.json"],
differences = [];
[self viewClassNames].forEach(function(aClassName)
{
var saved = archives[aClassName];
if (!saved)
return differences.push(aClassName + ": no saved archive");
try
{
DropInTreeDifferences(saved.state, DropInSavedArchiveTree(saved.archive), aClassName, differences);
}
catch (anException)
{
differences.push(aClassName + ": " + String(anException));
}
[self moveWarningsNamed:aClassName into:differences];
});
[self assertNoDifferences:differences];
}
/*
These are fixed copies of real cibs, and they do not follow their sources. They are
Tests/Manual/SmartFoldersTest/Resources/PredicateEditor.cib and
Tests/Manual/TextEditor/Resources/DocumentWindow.cib, as of commit 5db5adac0.
Loading one must produce the same windows, views and frames as main. The MainMenu
cibs of those applications are not here. TextEditor's shows a menu bar, which needs
a platform window, and SmartFoldersTest's connects a data source that is defined by
the application.
*/
- (void)testExistingCibsProduceTheSameObjects
{
var expected = [self jsonNamed:@"DropInCibs.json"],
differences = [];
for (var cibName in expected)
{
var rawString = fs.readFileSync(path.join(resources, "DropInCibs", cibName), {encoding: 'utf-8'}),
current = DropInCibTree(cibName, rawString),
expectedTrees = expected[cibName];
if (current.length !== expectedTrees.length)
{
differences.push(cibName + ": " + expectedTrees.length + " top level objects are now " + current.length);
continue;
}
for (var index = 0; index < current.length; index++)
DropInTreeDifferences(JSON.parse(expectedTrees[index]), JSON.parse(current[index]), cibName, differences);
[self moveWarningsNamed:cibName into:differences];
}
[self assertNoDifferences:differences];
}
@end
+50
View File
@@ -0,0 +1,50 @@
/*
* DropInBaselineGenerator.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 "DropInSnapshot.j"
/*
Saves the drop-in baseline. Run this only against a build of main, and only
when a change to the drop-in contract is deliberate:
objj Tests/AppKit/DropInBaselineGenerator.j
*/
var fs = require("fs"),
path = require("path");
function main(args, namedArgs)
{
[[CPApplication alloc] init];
var skipped = [],
snapshot = DropInCreateSnapshot(YES, skipped),
baselinePath = path.join(process.cwd(), "Tests", "AppKit", "Resources", "DropInBaseline.json");
fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 1) + "\n");
print("Wrote " + baselinePath);
print(snapshot.publicClasses.length + " public classes, " +
Object.keys(snapshot.themeAttributes).length + " themed classes, " +
Object.keys(snapshot.construction).length + " constructed views, " +
Object.keys(snapshot.windows).length + " window styles");
skipped.forEach(function(aLine) { print("skipped " + aLine); });
}
+180
View File
@@ -0,0 +1,180 @@
/*
* DropInBaselineTest.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 "DropInSnapshot.j"
/*
Compares the running frameworks with the recorded drop-in baseline. See
DropInSnapshot.j for what the baseline holds and DropInBaselineGenerator.j for
how to update it.
Each test reports every difference it finds. A list is more useful here than
a failure at the first difference, because one change often affects several
classes.
*/
var fs = require("fs"),
path = require("path"),
DropInBaseline = nil,
DropInCurrent = nil;
@implementation DropInBaselineTest : OJTestCase
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
if (DropInBaseline)
return;
var baselinePath = path.join(path.dirname(__filename), "Resources", "DropInBaseline.json");
DropInBaseline = JSON.parse(fs.readFileSync(baselinePath, {encoding: 'utf-8'}));
DropInCurrent = DropInCreateSnapshot();
}
- (void)assertNoDifferences:(CPArray)differences
{
[self assertTrue:differences.length === 0 message:"\n" + differences.join("\n")];
}
/*
A themed application reads these values whenever its theme supplies none, so a
changed default changes the application.
*/
- (void)testThemeAttributeDefaultsAreUnchanged
{
var differences = [];
for (var className in DropInBaseline.themeAttributes)
{
var expected = DropInBaseline.themeAttributes[className],
current = DropInCurrent.themeAttributes[className];
if (!current)
{
differences.push(className + ": class no longer declares theme attributes");
continue;
}
for (var attribute in expected)
if (expected[attribute] !== current[attribute])
differences.push(className + " " + attribute + ": " + expected[attribute] + " is now " + current[attribute]);
}
[self assertNoDifferences:differences];
}
- (void)testPublicClassesExist
{
var differences = DropInBaseline.publicClasses.filter(function(aName)
{
return !objj_getClass(aName);
});
[self assertNoDifferences:differences];
}
/*
A public method may move to a superclass, since the class still responds to it.
It may not disappear. This test cannot detect a method that remains but no
longer does its work. Behavior tests cover that.
*/
- (void)testPublicMethodsExist
{
var differences = [];
DropInBaseline.publicClasses.forEach(function(aName)
{
var aClass = objj_getClass(aName);
if (!aClass)
return;
DropInBaseline.instanceMethods[aName].forEach(function(aSelector)
{
if (!class_getInstanceMethod(aClass, aSelector))
differences.push("-[" + aName + " " + aSelector + "]");
});
DropInBaseline.classMethods[aName].forEach(function(aSelector)
{
if (!class_getClassMethod(aClass, aSelector))
differences.push("+[" + aName + " " + aSelector + "]");
});
});
[self assertNoDifferences:differences];
}
/*
A view created in code has these values until the application changes them.
*/
- (void)testDefaultViewConstructionIsUnchanged
{
var differences = [];
for (var className in DropInBaseline.construction)
{
var expected = DropInBaseline.construction[className],
current = DropInCurrent.construction[className];
if (!current)
{
differences.push(className + ": cannot be created");
continue;
}
for (var key in expected)
{
var expectedValue = String(expected[key]),
currentValue = String(current[key]);
if (expectedValue !== currentValue)
differences.push(className + " " + key + ": " + expectedValue + " is now " + currentValue);
}
}
[self assertNoDifferences:differences];
}
/*
The public subviews of a window view are the controls a person sees in the
title bar, so they are part of how an existing application looks.
*/
- (void)testWindowControlsAreUnchanged
{
var differences = [];
for (var style in DropInBaseline.windows)
{
var expected = String(DropInBaseline.windows[style]),
current = String(DropInCurrent.windows[style]);
if (expected !== current)
differences.push(style + ": " + expected + " is now " + current);
}
[self assertNoDifferences:differences];
}
@end
+77
View File
@@ -0,0 +1,77 @@
/*
* DropInCompatibilityTest.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>
/*
Defaults that existing applications rely on. An application that updates its
frameworks must keep these without any change of its own.
*/
@implementation DropInCompatibilityTest : OJTestCase
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
}
- (void)testDefaultThemeNameIsAristo2WithoutInfoPlistKey
{
[self assertNull:[[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPDefaultTheme"]];
[self assert:@"Aristo2" equals:[CPApplication defaultThemeName]];
}
- (void)testResizableWindowHasOnlyACloseButton
{
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
styleMask:CPClosableWindowMask | CPResizableWindowMask],
subviews = [[aWindow _windowView] subviews],
count = [subviews count],
buttonCount = 0;
while (count--)
if ([subviews[count] isKindOfClass:[CPButton class]])
buttonCount++;
[self assert:1 equals:buttonCount];
}
- (void)testPopUpButtonKeepsTheDefaultButtonBezelStyle
{
var frame = CGRectMake(0.0, 0.0, 100.0, 24.0),
popUpButton = [[CPPopUpButton alloc] initWithFrame:frame pullsDown:NO],
button = [[CPButton alloc] initWithFrame:frame];
[self assert:[button bezelStyle] equals:[popUpButton bezelStyle]];
}
- (void)testTableViewGridColorHasAClassDefault
{
[self assertFalse:[[[CPTableView themeAttributes] objectForKey:@"grid-color"] isEqual:[CPNull null]]];
}
- (void)testTableViewAlternatingRowColorsHaveAClassDefault
{
[self assertFalse:[[[CPTableView themeAttributes] objectForKey:@"alternating-row-colors"] isEqual:[CPNull null]]];
}
@end
+521
View File
@@ -0,0 +1,521 @@
/*
* DropInSnapshot.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>
/*
An application that updates its frameworks relies on more than documented
behavior. It relies on the defaults a class supplies when no theme overrides
them, on the public methods it calls and overrides, and on the state a control
has when it is created. Together these form the drop-in contract.
This file records that contract as plain data. DropInBaselineGenerator.j saves
a snapshot as the baseline, and DropInBaselineTest.j compares the running
frameworks with it. Additions are allowed. Changes and removals are not.
*/
var DropInConstructionGetters = [
@"acceptsFirstResponder",
@"alignment",
@"autoresizingMask",
@"backgroundColor",
@"bezelStyle",
@"controlSize",
@"drawsBackground",
@"isBordered",
@"isEditable",
@"isEnabled",
@"isHidden",
@"isSelectable",
@"lineBreakMode",
@"verticalAlignment"
],
DropInWindowStyles = {
@"borderless": CPBorderlessWindowMask,
@"titled": CPTitledWindowMask,
@"closable": CPClosableWindowMask,
@"resizable": CPClosableWindowMask | CPResizableWindowMask,
@"miniaturizable": CPClosableWindowMask | CPMiniaturizableWindowMask,
@"full": CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask,
@"hud": CPHUDBackgroundWindowMask,
@"hudClosable": CPHUDBackgroundWindowMask | CPClosableWindowMask
};
/*
The getters that report a BOOL, and the getters that report a view's content.
*/
var DropInBooleanGetters = [
@"acceptsFirstResponder",
@"drawsBackground",
@"isBordered",
@"isEditable",
@"isEnabled",
@"isHidden",
@"isSelectable"
],
DropInContentGetters = [
@"color",
@"doubleValue",
@"indexOfSelectedItem",
@"maxValue",
@"minValue",
@"numberOfColumns",
@"numberOfItems",
@"numberOfTabViewItems",
@"objectValue",
@"segmentCount",
@"state",
@"string",
@"stringValue",
@"tag",
@"title"
];
function DropInIsPublicName(aName)
{
return aName.charAt(0) !== "_" && aName.charAt(0) !== "$";
}
function DropInSerializeObject(val, seen)
{
if (val === null || val === undefined)
return nil;
if (typeof val === "function")
return "[Function]";
if (typeof val !== "object")
return val;
if (seen.indexOf(val) !== -1)
return "[Circular]";
seen.push(val);
var result;
if (Array.isArray(val))
{
result = [];
for (var i = 0; i < val.length; i++)
{
result[i] = DropInSerializeObject(val[i], seen);
}
}
else
{
result = {};
var keys = Object.keys(val).sort();
for (var i = 0; i < keys.length; i++)
{
var k = keys[i];
result[k] = DropInSerializeObject(val[k], seen);
}
}
seen.pop();
return result;
}
function DropInSafeJSONStringify(aValue)
{
return JSON.stringify(DropInSerializeObject(aValue, []));
}
/*
Reduces a value to a string that does not depend on object identity, so that
two runs of the same framework produce the same snapshot.
*/
function DropInDescribeValue(aValue)
{
if (aValue === nil || aValue === undefined)
return "nil";
/*
Booleans, numbers and strings answer to isa as well, so they must be handled
first, or they are recorded by class and their values are never compared.
*/
if (typeof aValue === "boolean" || typeof aValue === "number" || typeof aValue === "string")
return String(aValue);
if (aValue === [CPNull null])
return "CPNull";
if (aValue.isa)
{
/*
A date is recorded by its instant in UTC. Its description is in the time zone
of the machine that runs the test, so it differs between machines.
*/
if ([aValue isKindOfClass:[CPDate class]])
return "CPDate:" + (isNaN(aValue.getTime()) ? "invalid" : aValue.toISOString());
if ([aValue isKindOfClass:[CPColor class]])
return "CPColor:" + [aValue cssString];
/*
The system font is a global setting that applications and tests change,
so a default that uses it records only that fact.
*/
if ([aValue isKindOfClass:[CPFont class]])
return [aValue isSystem] ? "CPFont:system" + ([aValue isBold] ? " bold" : "") : "CPFont:" + [aValue cssString];
if ([aValue isKindOfClass:[CPArray class]])
return "[" + aValue.map(DropInDescribeValue).join(", ") + "]";
return aValue.isa.name;
}
return DropInSafeJSONStringify(aValue);
}
function DropInMethodNames(aClass)
{
return class_copyMethodList(aClass).map(function(aMethod)
{
return method_getName(aMethod);
}).filter(DropInIsPublicName).sort();
}
function DropInThemeAttributes(aClass)
{
if (class_copyMethodList(aClass.isa).map(method_getName).indexOf("themeAttributes") === -1)
return nil;
var attributes = [aClass themeAttributes];
if (!attributes)
return nil;
var result = {},
keys = [[attributes allKeys] sortedArrayUsingSelector:@selector(compare:)];
keys.forEach(function(aKey)
{
result[aKey] = DropInDescribeValue([attributes objectForKey:aKey]);
});
return result;
}
function DropInSubviewClasses(aView)
{
return [aView subviews].map(function(aSubview)
{
return aSubview.isa.name;
}).filter(DropInIsPublicName).sort();
}
function DropInViewState(aView, throwIfError)
{
var result = {
@"frameSize": DropInDescribeValue([aView frameSize]),
@"subviews": DropInSubviewClasses(aView)
};
DropInConstructionGetters.forEach(function(aGetter)
{
var selector = CPSelectorFromString(aGetter);
if ([aView respondsToSelector:selector])
{
try
{
result[aGetter] = DropInDescribeValue([aView performSelector:selector]);
}
catch (anException)
{
if (throwIfError)
throw anException;
result[aGetter] = "[Exception: " + String(anException) + "]";
}
}
});
return result;
}
function DropInDefaultConstruction(aClass, throwIfError)
{
return DropInViewState([[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)], throwIfError);
}
/*
Lists the public views below a view, in order. A private view is the class's own
concern and is left out, but the public views inside it are the view's content.
An observed view has a generated isa, so its name comes from its class method.
*/
function DropInPublicSubviews(aView)
{
return [aView subviews].reduce(function(aList, aSubview)
{
return aList.concat(DropInIsPublicName([aSubview className]) ? [aSubview] : DropInPublicSubviews(aSubview));
}, []);
}
/*
Describes the state of one view. An unset BOOL is nil and means NO, so a BOOL is
recorded by its truth value.
*/
function DropInViewTreeState(aView)
{
var state = {};
DropInConstructionGetters.concat(DropInContentGetters).forEach(function(aGetter)
{
var selector = CPSelectorFromString(aGetter);
if (![aView respondsToSelector:selector])
return;
var value = [aView performSelector:selector];
/*
A control that holds a date gives its string value in the time zone of the
machine that runs the test, because the browser formats it, not Cappuccino. It
is recorded by the date's instant instead, so that every machine records the
same snapshot.
*/
if (aGetter === @"stringValue" && [aView respondsToSelector:@selector(objectValue)] && [[aView objectValue] isKindOfClass:[CPDate class]])
value = [aView objectValue];
state[aGetter] = DropInDescribeValue(DropInBooleanGetters.indexOf(aGetter) !== -1 ? !!value : value);
});
return state;
}
/*
Describes a view and everything public below it.
*/
function DropInViewTree(aView)
{
return {
@"class": [aView className],
@"frame": DropInDescribeValue([aView frame]),
@"state": DropInViewTreeState(aView),
@"subviews": DropInPublicSubviews(aView).map(function(aSubview)
{
return DropInViewTree(aSubview);
})
};
}
function DropInMenuTree(aMenu)
{
return [aMenu itemArray].map(function(anItem)
{
return {
@"title": [anItem title],
@"keyEquivalent": [anItem keyEquivalent],
@"submenu": [anItem hasSubmenu] ? DropInMenuTree([anItem submenu]) : nil
};
});
}
/*
Describes what a cib produced. Objects other than windows, views and menus are
recorded by class alone.
*/
function DropInObjectTree(anObject)
{
if ([anObject isKindOfClass:[CPWindow class]])
return { @"class": anObject.isa.name, @"title": [anObject title], @"contentView": DropInViewTree([anObject contentView]) };
if ([anObject isKindOfClass:[CPView class]])
return DropInViewTree(anObject);
if ([anObject isKindOfClass:[CPMenu class]])
return { @"class": anObject.isa.name, @"items": DropInMenuTree(anObject) };
return { @"class": anObject.isa.name };
}
/*
The owner of the reference cibs. It has the outlets and actions that those cibs
connect to it, so that loading them gives no warnings.
*/
@implementation _DropInCibOwner : CPObject
{
id folderNameField;
id predicateEditor;
id predicateSheet;
id textView;
id window;
}
- (void)closeSheet:(id)aSender
{
}
@end
/*
A scroll view takes its scroller style from a global setting, and that style changes
the frames and views inside it. Applications and tests change the setting, so a
description that is saved or compared is always made with the same style.
*/
function DropInWithFixedScrollerStyle(aFunction)
{
var scrollerStyle = [CPScrollView globalScrollerStyle];
[CPScrollView setGlobalScrollerStyle:CPScrollerStyleLegacy];
try
{
return aFunction();
}
finally
{
[CPScrollView setGlobalScrollerStyle:scrollerStyle];
}
}
/*
Loads a cib and describes its top level objects, sorted so that the order in
which the cib lists them does not matter. Layout changes frames and adds subviews,
so it is complete before anything is described.
*/
function DropInCibTree(aCibName, aRawString)
{
var topLevelObjects = [],
cib = [[CPCib alloc] _initWithData:[CPData dataWithRawString:aRawString] bundle:nil cibName:aCibName];
return DropInWithFixedScrollerStyle(function()
{
[cib instantiateCibWithExternalNameTable:@{ CPCibOwner: [[_DropInCibOwner alloc] init], CPCibTopLevelObjects: topLevelObjects }];
[_CPDisplayServer run];
return topLevelObjects.map(DropInObjectTree).map(function(aTree)
{
return JSON.stringify(aTree);
}).sort();
});
}
function DropInClassIsView(aClass)
{
var viewClass = [CPView class];
for (; aClass; aClass = class_getSuperclass(aClass))
if (aClass === viewClass)
return YES;
return NO;
}
function DropInSortedClasses()
{
var classes = [],
count = objj_getClassList(classes, 1 << 16);
return classes.slice(0, count).filter(function(aClass)
{
return aClass && aClass.name.indexOf("$") !== 0;
}).sort(function(lhs, rhs)
{
return lhs.name < rhs.name ? -1 : (lhs.name > rhs.name ? 1 : 0);
});
}
/*
Some classes cannot be created without a surrounding object, or without a DOM.
A class that cannot be created is left out of the construction section, and the
comparison does not report it. This reduces coverage, but it cannot report a
false difference.
A generator passes throwIfError, so that a getter that throws is not saved as
expected state, and passes skipped, to list each class or window style that is
left out.
*/
function DropInCreateSnapshot(throwIfError, skipped)
{
var snapshot = {
@"publicClasses": [],
@"themeAttributes": {},
@"instanceMethods": {},
@"classMethods": {},
@"construction": {},
@"windows": {}
};
DropInSortedClasses().forEach(function(aClass)
{
var name = aClass.name,
isPublic = DropInIsPublicName(name);
try
{
var attributes = DropInThemeAttributes(aClass);
if (attributes)
snapshot.themeAttributes[name] = attributes;
}
catch (anException)
{
if (skipped)
skipped.push(name + " theme attributes: " + String(anException).slice(0, 80));
}
if (!isPublic)
return;
snapshot.publicClasses.push(name);
snapshot.instanceMethods[name] = DropInMethodNames(aClass);
snapshot.classMethods[name] = DropInMethodNames(aClass.isa);
if (!DropInClassIsView(aClass))
return;
try
{
snapshot.construction[name] = DropInDefaultConstruction(aClass, throwIfError);
}
catch (anException)
{
if (skipped)
skipped.push(name + " construction: " + String(anException).slice(0, 80));
}
});
for (var style in DropInWindowStyles)
{
try
{
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
styleMask:DropInWindowStyles[style]];
snapshot.windows[style] = DropInSubviewClasses([aWindow _windowView]);
}
catch (anException)
{
if (skipped)
skipped.push(style + " window style: " + String(anException).slice(0, 80));
}
}
return snapshot;
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+10
View File
@@ -0,0 +1,10 @@
{
"SmartFoldersPredicateEditor.cib": [
"{\"class\":\"CPApplication\"}",
"{\"class\":\"CPWindow\",\"title\":\"Predicate Editor\",\"contentView\":{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":1,\\\"y\\\":1},\\\"size\\\":{\\\"height\\\":290,\\\"width\\\":551}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPTextField\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":63,\\\"y\\\":12},\\\"size\\\":{\\\"height\\\":29,\\\"width\\\":474}}\",\"state\":{\"acceptsFirstResponder\":\"true\",\"alignment\":\"4\",\"autoresizingMask\":\"34\",\"backgroundColor\":\"CPColor:rgba(255, 255, 255, 1)\",\"bezelStyle\":\"0\",\"controlSize\":\"0\",\"drawsBackground\":\"true\",\"isBordered\":\"false\",\"isEditable\":\"true\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"isSelectable\":\"true\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"\",\"stringValue\":\"\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":29,\\\"width\\\":474}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPTextField\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":19,\\\"y\\\":18},\\\"size\\\":{\\\"height\\\":17,\\\"width\\\":41}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"4\",\"autoresizingMask\":\"36\",\"backgroundColor\":\"CPColor:rgba(175, 175, 175, 1)\",\"bezelStyle\":\"0\",\"controlSize\":\"0\",\"drawsBackground\":\"false\",\"isBordered\":\"false\",\"isEditable\":\"false\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"isSelectable\":\"false\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"Name:\",\"stringValue\":\"Name:\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":17,\\\"width\\\":41}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPScrollView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":20,\\\"y\\\":55},\\\"size\\\":{\\\"height\\\":184,\\\"width\\\":512}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPClipView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":1,\\\"y\\\":1},\\\"size\\\":{\\\"height\\\":182,\\\"width\\\":510}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"CPColor:rgba(232, 232, 232, 1)\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPPredicateEditor\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":182,\\\"width\\\":510}}\",\"state\":{\"acceptsFirstResponder\":\"true\",\"alignment\":\"0\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEditable\":\"true\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"nil\",\"doubleValue\":\"0\",\"objectValue\":\"nil\",\"stringValue\":\"\",\"tag\":\"0\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":-100,\\\"y\\\":269},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":360}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"0\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEnabled\":\"false\",\"isHidden\":\"true\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"0\",\"stringValue\":\"0\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":360}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"Line truncated
],
"TextEditorDocumentWindow.cib": [
"{\"class\":\"CPApplication\"}",
"{\"class\":\"CPWindow\",\"title\":\"Window\",\"contentView\":{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":1,\\\"y\\\":92},\\\"size\\\":{\\\"height\\\":168,\\\"width\\\":474}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPScrollView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":476}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPClipView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":469}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPTextView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":270,\\\"width\\\":469}}\",\"state\":{\"acceptsFirstResponder\":\"true\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"CPColor:rgba(255, 255, 255, 1)\",\"isEditable\":\"true\",\"isHidden\":\"false\",\"isSelectable\":\"true\",\"objectValue\":\"CPTextStorage\",\"string\":\"\",\"stringValue\":\"\",\"tag\":\"0\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":-100,\\\"y\\\":355},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":240}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"0\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEnabled\":\"false\",\"isHidden\":\"true\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"0\",\"stringValue\":\"0\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":240}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":469,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":7}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"0\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"0\",\"stringValue\":\"0\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":7}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":202},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":151.1259259259259,\\\"width\\\":7}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"CPColor:rgba(255, 255, 255, 1)\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPClipView\",\"frame\":\"{\\\"origin\\\":{\\\"xLine truncated
]
}
@@ -0,0 +1 @@
280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;16E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;21E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;50E;D;K;6;CP$UIDd;2;51E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;54E;D;K;6;CP$UIDd;2;55E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;57E;D;K;6;CP$UIDd;2;58E;D;K;6;CP$UIDd;2;59E;D;K;6;CP$UIDd;2;61E;D;K;6;CP$UIDd;2;62E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;65E;D;K;6;CP$UIDd;1;0E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;54E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;65E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;66E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;67E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;22E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;68E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;28E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;69E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;35E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;70E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;24E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;71E;E;D;K;10;$classnameS;21;CPCibControlConnectorK;8;$classesA;S;21;CPCibControlConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;19E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;64E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;72E;E;D;K;6;$classD;K;6;CP$UIDd;2;19E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;65E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;72E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;73E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;23E;K;30;_CPCibWindowTemplateMinSizeKeyD;K;6;CP$UIDd;2;74E;K;30;_CPCibWindowTemplateMaxSizeKeyD;K;6;CP$UIDd;2;75E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;1;0E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;76E;K;30;_CPCiLine truncated
@@ -0,0 +1 @@
280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;16E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;38E;D;K;6;CP$UIDd;2;40E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;41E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;42E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;27E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;43E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;19E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;44E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;45E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;18E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;2;32E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;46E;K;30;_CPCibWindowTemplateWTFlagsKeyD;K;6;CP$UIDd;2;47E;K;33;_CPCibWindowTemplateWindowRectKeyD;K;6;CP$UIDd;2;48E;K;33;_CPCibWindowTemplateScreenRectKeyD;K;6;CP$UIDd;2;49E;K;30;_CPCibWindowTempatStyleMaskKeyD;K;6;CP$UIDd;2;50E;K;34;_CPCibWindowTemplateWindowTitleKeyD;K;6;CP$UIDd;2;51E;K;33;_CPCibWindowTemplateWindowViewKeyD;K;6;CP$UIDd;2;21E;E;D;K;10;$classnameS;6;CPViewK;8;$classesA;S;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;20E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;54E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;56E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;60E;E;D;K;10;$classnameS;12;CPScrollViewK;8;$classesA;S;12;CPScrollViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;22E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;21E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;61E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;21E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;62E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;2;64E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;65E;K;23;CPScrollViewContentViewD;K;6;CP$UIDd;2;25E;K;29;CPScrollViewHeaderClipViewKeyD;K;6;CP$UIDd;1;0E;K;21;CPScrollViewVScrollerD;K;6;CP$UIDd;2;30E;K;21;CPScrollViewHScrollerD;K;6Line truncated
+14
View File
@@ -1,6 +1,8 @@
@import <Foundation/CPMapTable.j>
@import <Foundation/CPDictionary.j>
@import <Foundation/CPEnumerator.j>
@import <Foundation/CPKeyedArchiver.j>
@import <Foundation/CPKeyedUnarchiver.j>
@import <Foundation/CPMutableArray.j>
@import <OJUnit/OJTestCase.j>
@@ -139,6 +141,18 @@
[self assertTrue:[foundValues containsObject:@"value3"]];
}
- (void)testArchivingPreservesEntries
{
[map_table setObject:@"first" forKey:@"one"];
[map_table setObject:@"second" forKey:@"two"];
var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:map_table]];
[self assert:2 equals:[decoded count]];
[self assert:@"first" equals:[decoded objectForKey:@"one"]];
[self assert:@"second" equals:[decoded objectForKey:@"two"]];
}
- (void)testDictionaryRepresentation
{
[map_table setObject:@"value1" forKey:@"key1"];
+35
View File
@@ -434,6 +434,41 @@
[self assertTrue:[predicate evaluateWithObject:dict] message:"Predicate " + predicate + " should evaluate to TRUE"];
}
- (void)testCustomSelectorPredicateParsing
{
var predicate = [CPPredicate predicateWithFormat:@"SELF isKindOfClass: %@", [CPArray class]];
[self assert:CPCustomSelectorPredicateOperatorType equals:[predicate predicateOperatorType]];
[self assert:@selector(isKindOfClass:) equals:[predicate customSelector]];
[self assertTrue:[predicate evaluateWithObject:[CPArray array]]];
[self assertFalse:[predicate evaluateWithObject:@{}]];
var negated = [CPPredicate predicateWithFormat:@"NOT SELF isKindOfClass: %@", [CPArray class]];
[self assertFalse:[negated evaluateWithObject:[CPArray array]]];
[self assertTrue:[negated evaluateWithObject:@{}]];
// A parse failure is reported through CPLogConsole, which is captured here.
var logConsole = CPLogConsole,
messages = [];
CPLogConsole = function(aString)
{
messages.push(aString);
};
try
{
[self assertNull:[CPPredicate predicateWithFormat:@"ANY SELF isKindOfClass: %@", [CPArray class]]];
}
finally
{
CPLogConsole = logConsole;
}
[self assertTrue:messages.length > 0];
}
- (void)testExpressionAndPredicateIsEqual
{
var cexp1 = [CPExpression expressionForConstantValue:2],
+67 -106
View File
@@ -27,6 +27,8 @@
CPLocale _locale;
CPData _data;
CPDate _date;
CPDate _januaryDate;
CPDate _julyDate;
}
- (void)setUp
@@ -34,6 +36,42 @@
_locale = [[CPLocale alloc] initWithLocaleIdentifier:@"en_US"];
_data = [CPData dataWithRawString:@"Data with string"];
_date = [[CPDate alloc] initWithString:@"2011-10-05 16:34:38 +0900"];
/*
The rules of a zone decide its offset and abbreviation on a given date. Fixed
dates in January and July give the same result on any day and on any host.
A zone with daylight saving time differs between them, and a zone without it
has the same offset on both.
*/
_januaryDate = [[CPDate alloc] initWithString:@"2026-01-15 12:00:00 +0000"];
_julyDate = [[CPDate alloc] initWithString:@"2026-07-15 12:00:00 +0000"];
}
- (void)assertPacificTimeZone:(CPTimeZone)timeZone
{
[self assert:@"America/Los_Angeles" equals:[timeZone name]];
[self assert:@"PST" equals:[timeZone abbreviationForDate:_januaryDate]];
[self assert:(-480 * 60) equals:[timeZone secondsFromGMTForDate:_januaryDate]];
[self assertFalse:[timeZone isDaylightSavingTimeForDate:_januaryDate]];
[self assert:@"PDT" equals:[timeZone abbreviationForDate:_julyDate]];
[self assert:(-420 * 60) equals:[timeZone secondsFromGMTForDate:_julyDate]];
[self assertTrue:[timeZone isDaylightSavingTimeForDate:_julyDate]];
// The abbreviation and the offset are both those of the current date.
var now = [CPDate date];
[self assert:[timeZone abbreviationForDate:now] equals:[timeZone abbreviation]];
[self assert:[timeZone secondsFromGMTForDate:now] equals:[timeZone secondsFromGMT]];
[self assert:@"America/Los_Angeles (" + [timeZone abbreviation] + ") offset " + [timeZone secondsFromGMT] equals:[timeZone description]];
[self assert:@"Pacific Standard Time" equals:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale]];
[self assert:@"PST" equals:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale]];
[self assert:@"Pacific Daylight Time" equals:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale]];
[self assert:@"PDT" equals:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale]];
[self assert:@"Pacific Time" equals:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale]];
[self assert:@"PT" equals:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale]];
}
- (void)tearDown
@@ -43,17 +81,8 @@
- (void)testTimeZoneWithAbbreviation
{
var timeZone = [CPTimeZone timeZoneWithAbbreviation:@"PDT"];
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PDT"];
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
[self assertPacificTimeZone:[CPTimeZone timeZoneWithAbbreviation:@"PDT"]];
[self assertPacificTimeZone:[CPTimeZone timeZoneWithAbbreviation:@"PST"]];
}
- (void)testTimeZoneWithWrongAbbreviation
@@ -64,26 +93,7 @@
- (void)testTimeZoneWithName
{
var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles"];
// The current implementation can return daylight saving time or standard time depending on the current date
if ([timeZone abbreviation] === @"PDT") {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PDT"];
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
} else {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PST"];
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
}
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
[self assertPacificTimeZone:[CPTimeZone timeZoneWithName:@"America/Los_Angeles"]];
}
- (void)testTimeZoneWithWrongName
@@ -109,24 +119,8 @@
{
var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles" data:_data];
if ([timeZone abbreviation] === @"PDT") {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PDT"];
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
} else {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PST"];
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
}
[self assert:[timeZone data] equals:_data];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
[self assertPacificTimeZone:timeZone];
[self assert:_data equals:[timeZone data]];
}
- (void)testTimeZoneWithWrongNameWithData
@@ -179,25 +173,7 @@
- (void)testInitTimeZoneWithName
{
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"];
if ([timeZone abbreviation] === @"PDT") {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PDT"];
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
} else {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PST"];
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
}
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
[self assertPacificTimeZone:[[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]];
}
- (void)testInitTimeZoneWithWrongName
@@ -223,24 +199,8 @@
{
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles" data:_data];
if ([timeZone abbreviation] === @"PDT") {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PDT"];
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
} else {
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
[self assert:[timeZone abbreviation] equals:@"PST"];
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
}
[self assert:[timeZone data] equals:_data];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
[self assertPacificTimeZone:timeZone];
[self assert:_data equals:[timeZone data]];
}
- (void)testInitTimeZoneWithWrongNameWithData
@@ -262,14 +222,17 @@
}
}
/*
The local time zone depends on the host, and many zones have no curated
abbreviation, so a named zone is used.
*/
- (void)testAbbreviationWithDate
{
var timeZone = [CPTimeZone localTimeZone],
abbreviation = [timeZone abbreviationForDate:_date];
var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles"];
[self assert:(abbreviation !== nil) equals:YES];
var knownAbbreviations = [CPTimeZone abbreviationDictionary];
[self assert:[knownAbbreviations containsKey:abbreviation] equals:YES];
[self assert:@"PDT" equals:[timeZone abbreviationForDate:_date]];
[self assert:@"PST" equals:[timeZone abbreviationForDate:_januaryDate]];
[self assertNotNull:[[CPTimeZone localTimeZone] abbreviationForDate:_date]];
}
- (void)testAbbreviationWithNilDate
@@ -301,22 +264,17 @@
{
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"];
if ([timeZone abbreviation] === @"PDT") {
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
} else {
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
}
[self assert:(-480 * 60) equals:[timeZone secondsFromGMTForDate:_januaryDate]];
[self assert:(-420 * 60) equals:[timeZone secondsFromGMTForDate:_julyDate]];
[self assert:[timeZone secondsFromGMTForDate:[CPDate date]] equals:[timeZone secondsFromGMT]];
}
- (void)testDescription
{
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"];
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"],
expected = [timeZone secondsFromGMT] === -480 * 60 ? @"America/Los_Angeles (PST) offset -28800" : @"America/Los_Angeles (PDT) offset -25200";
if ([timeZone abbreviation] === @"PDT") {
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
} else {
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
}
[self assert:expected equals:[timeZone description]];
}
- (void)testLocalizedName
@@ -401,11 +359,14 @@
- (void)testMSKCorrectCurrentValue
{
// MSD/MSK resolve to Europe/Moscow.
// Russia abolished DST in 2014, so there is no distinct summer offset.
// The current permanent offset is +03:00 (10800 seconds).
// MSD and MSK resolve to Europe/Moscow, which has no daylight saving time.
var timeZone = [CPTimeZone timeZoneWithAbbreviation:@"MSD"];
[self assert:[timeZone secondsFromGMT] equals:(180 * 60)];
[self assert:@"Europe/Moscow" equals:[timeZone name]];
[self assert:(180 * 60) equals:[timeZone secondsFromGMTForDate:_januaryDate]];
[self assert:(180 * 60) equals:[timeZone secondsFromGMTForDate:_julyDate]];
[self assertFalse:[timeZone isDaylightSavingTimeForDate:_julyDate]];
[self assert:[timeZone secondsFromGMTForDate:[CPDate date]] equals:[timeZone secondsFromGMT]];
}
- (void)testKnownTimeZoneNamesUsesIntlWhenAvailable