Files
cappuccino/Tests/AppKit/DropInSnapshot.j
T
David Richardson 17198938e0 Tests: Add regression test suites for table drawing and drop-in compatibility
Adds targeted test infrastructure and validation checks to protect table drawing hooks and drop-in framework contracts against unintended modifications.

* CPTableViewDrawingTest: Verifies subclass invocation order for drawing steps and ensures application-specified background colors persist across theme-state changes.

* DropInCompatibilityTest: Validates default theme names, window control configurations, pop-up button bezel styles, and table class defaults.

* DropInSnapshot: Encapsulates theme attribute defaults, public API surface, default view construction, and window controls as structured data.

* DropInBaselineGenerator: Provides tooling to capture current framework state as a baseline.

* DropInBaselineTest: Compares framework artifacts against the established baseline, permitting additions while reporting modifications and removals.

* DropInBaseline.json: Introduces the initial compatibility baseline artifact.
2026-10-31 15:58:31 -06:00

246 lines
7.1 KiB
Plaintext

/*
* 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
};
function DropInIsPublicName(aName)
{
return aName.charAt(0) !== "_" && aName.charAt(0) !== "$";
}
/*
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";
if (aValue === [CPNull null])
return "CPNull";
if (aValue.isa)
{
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;
}
if (typeof aValue === "object")
return JSON.stringify(aValue);
return String(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 DropInDefaultConstruction(aClass)
{
var view = [[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)],
result = {
@"frameSize": DropInDescribeValue([view frameSize]),
@"subviews": DropInSubviewClasses(view)
};
DropInConstructionGetters.forEach(function(aGetter)
{
if ([view respondsToSelector:CPSelectorFromString(aGetter)])
result[aGetter] = DropInDescribeValue([view performSelector:CPSelectorFromString(aGetter)]);
});
return result;
}
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.
*/
function DropInCreateSnapshot()
{
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 (!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);
}
catch (anException)
{
}
});
for (var style in DropInWindowStyles)
{
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
styleMask:DropInWindowStyles[style]];
snapshot.windows[style] = DropInSubviewClasses([aWindow _windowView]);
}
return snapshot;
}