Files
cappuccino/Tests/AppKit/DropInArchiveFixtures.j
T
David Richardson f468d483ff AppKit, Foundation: Fix view archiving and add regression tests
Keyed archiving preserves view hierarchies created in code. Decoded views
retain their content, subviews and state, and each view class is verified
against its own description before archiving.

* CPAccordionView: Add CPCoding to CPAccordionView and CPAccordionViewItem.

* CPBox:
  * Encode the content, title and box views unconditionally.
  * Clear the superviews of the box and title views during decoding.

* CPButton: Initialize the value to CPOffState.

* CPMapTable: Add CPCoding.

* CPPopUpButton: Clear the inherited button value.

* CPSearchField: Update the cancel button when the object value changes.

* CPSplitView: Return nil from dividerColor when the theme defines no divider
  image.

* CPStepper: Discard the decoded buttons before recreating them.

* CPTabView:
  * Encode the current tab view items.
  * Exclude the tabs from the encoded subviews.

* CPTokenField:
  * Discard the decoded scroll view before recreating it.
  * Add the decoded tokens to the new document view.

* CPVisualEffectView: Resolve the backdrop filter property only when a DOM is
  available.

* CPBoxTest: Verify preservation of the content view, hierarchy and title, and
  decoding of an untitled box without warnings.

* CPButtonTest: Verify the initial value of a button.

* CPMapTableTest: Verify preservation of entries through archiving.

* DropInArchiveFixtures: Add populated view fixtures, tree comparison, and
  round trip and saved archive helpers.

* DropInArchiveGenerator: Save cib descriptions and view archives, and write no
  archive data if any view fails its round trip.

* DropInArchiveTest: Verify new archives, saved archives and reference cibs, and
  treat decoding warnings as failures.

* DropInSnapshot:
  * Describe the view hierarchy through private views, with state and content.
  * Add DropInWithFixedScrollerStyle.

* DropInArchives, DropInCibs: Add saved view archives, cib descriptions and
  reference cibs.
2026-10-31 20:51:13 -06:00

288 lines
9.0 KiB
Plaintext

/*
* 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);
});
}