mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
chore: tests
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import * as TKUnit from '../../tk-unit';
|
import * as TKUnit from '../../tk-unit';
|
||||||
import * as helper from '../../ui-helper';
|
import * as helper from '../../ui-helper';
|
||||||
import { UITest } from '../../ui-test';
|
import { UITest } from '../../ui-test';
|
||||||
import { isAndroid, Page, View, KeyedTemplate, Utils, Observable, EventData, ObservableArray, Label, Application, ListView, ItemEventData } from '@nativescript/core';
|
import { isAndroid, Page, View, KeyedTemplate, Utils, Observable, EventData, ObservableArray, Label, Application, ListView, ItemEventData, StackLayout } from '@nativescript/core';
|
||||||
import { MyButton, MyStackLayout } from '../layouts/layout-helper';
|
import { MyButton, MyStackLayout } from '../layouts/layout-helper';
|
||||||
|
|
||||||
// >> article-item-tap
|
// >> article-item-tap
|
||||||
@@ -369,6 +369,7 @@ export class ListViewTest extends UITest<ListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public test_loadMoreItems_raised_when_showing_few_items() {
|
public test_loadMoreItems_raised_when_showing_few_items() {
|
||||||
|
this.setUp();
|
||||||
var listView = this.testView;
|
var listView = this.testView;
|
||||||
|
|
||||||
var loadMoreItemsCount = 0;
|
var loadMoreItemsCount = 0;
|
||||||
@@ -751,10 +752,136 @@ export class ListViewTest extends UITest<ListView> {
|
|||||||
listView.scrollToIndex(10000);
|
listView.scrollToIndex(10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sticky header sanity tests
|
||||||
|
public test_stickyHeader_iOS_sectioned_headers_basic() {
|
||||||
|
if (!__APPLE__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setUp();
|
||||||
|
|
||||||
|
const listView = this.testView;
|
||||||
|
listView.sectioned = true;
|
||||||
|
listView.stickyHeader = true;
|
||||||
|
listView.stickyHeaderTemplate = "<Label id='headerLabel' text='{{ title }}' />";
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ title: 'Section A', items: [1, 2, 3] },
|
||||||
|
{ title: 'Section B', items: [4, 5] },
|
||||||
|
];
|
||||||
|
listView.items = items;
|
||||||
|
|
||||||
|
// Ensure layout
|
||||||
|
this.waitUntilTestElementIsLoaded();
|
||||||
|
this.waitUntilTestElementLayoutIsValid();
|
||||||
|
|
||||||
|
const table = <UITableView>listView.ios;
|
||||||
|
TKUnit.assertEqual(table.numberOfSections, 2, 'iOS sticky headers should use sections');
|
||||||
|
|
||||||
|
// Default auto height is ~44; ensure > 0
|
||||||
|
const rect0 = table.rectForHeaderInSection(0);
|
||||||
|
TKUnit.assert(rect0.size.height > 0, 'header height > 0');
|
||||||
|
|
||||||
|
// Template binding sanity: force-create header view via delegate and read label text
|
||||||
|
const header0 = (<any>table.delegate).tableViewViewForHeaderInSection(table, 0);
|
||||||
|
const headerText0 = this.getTextFromNativeHeaderForSection(listView, header0);
|
||||||
|
TKUnit.assertEqual(headerText0, 'Section A', 'iOS header 0 text');
|
||||||
|
|
||||||
|
// Respect explicit stickyHeaderHeight
|
||||||
|
listView.stickyHeaderHeight = 60;
|
||||||
|
listView.refresh();
|
||||||
|
TKUnit.wait(0.05);
|
||||||
|
const rect0b = table.rectForHeaderInSection(0);
|
||||||
|
// iOS reports in points; allow small variance
|
||||||
|
TKUnit.assert(Math.abs(rect0b.size.height - 60) <= 1, 'explicit header height ~60');
|
||||||
|
}
|
||||||
|
|
||||||
|
public test_stickyHeader_Android_header_updates_and_padding() {
|
||||||
|
if (!isAndroid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setUp();
|
||||||
|
|
||||||
|
const listView = this.testView;
|
||||||
|
listView.sectioned = true;
|
||||||
|
listView.stickyHeader = true;
|
||||||
|
listView.stickyHeaderTemplate = "<Label id='headerLabel' text='{{ title }}' />";
|
||||||
|
listView.items = [
|
||||||
|
{ title: 'First', items: ['a', 'b', 'c'] },
|
||||||
|
{ title: 'Second', items: ['d', 'e'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.waitUntilTestElementIsLoaded();
|
||||||
|
this.waitUntilTestElementLayoutIsValid();
|
||||||
|
TKUnit.waitUntilReady(() => !!(<any>listView)._stickyHeaderView);
|
||||||
|
|
||||||
|
// Sticky header view exists and binds
|
||||||
|
const sticky = (<any>listView)._stickyHeaderView;
|
||||||
|
TKUnit.assert(!!sticky, 'sticky header view exists');
|
||||||
|
const text0 = this.getStickyHeaderTextAndroid(listView);
|
||||||
|
TKUnit.assertEqual(text0, 'First', 'Android sticky header initial text');
|
||||||
|
|
||||||
|
// ListView should have top padding to avoid content under header
|
||||||
|
const topPad = (<android.widget.ListView>listView.android).getPaddingTop();
|
||||||
|
TKUnit.assert(topPad > 0, 'ListView has top padding for sticky header');
|
||||||
|
|
||||||
|
// Update header to next section (simulate scroll)
|
||||||
|
if ((<any>listView)._updateStickyHeader) {
|
||||||
|
(<any>listView)._updateStickyHeader(1);
|
||||||
|
TKUnit.wait(0.05);
|
||||||
|
const text1 = this.getStickyHeaderTextAndroid(listView);
|
||||||
|
TKUnit.assertEqual(text1, 'Second', 'Android sticky header updated text');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private checkItemVisibleAtIndex(listView: ListView, index: number): boolean {
|
private checkItemVisibleAtIndex(listView: ListView, index: number): boolean {
|
||||||
return listView.isItemAtIndexVisible(index);
|
return listView.isItemAtIndexVisible(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTextFromNativeHeaderForSection(listView: ListView, headerView: any): string {
|
||||||
|
if (__APPLE__ && headerView && headerView.contentView && headerView.contentView.subviews) {
|
||||||
|
// subviews can be function or array-like depending on runtime bridge
|
||||||
|
try {
|
||||||
|
if (Utils.isFunction(headerView.contentView.subviews)) {
|
||||||
|
const sv = headerView.contentView.subviews();
|
||||||
|
return sv && sv.length ? sv[0].text + '' : '';
|
||||||
|
} else {
|
||||||
|
return headerView.contentView.subviews[0].text + '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStickyHeaderTextAndroid(listView: ListView): string {
|
||||||
|
if (isAndroid) {
|
||||||
|
const headerView = (<any>listView)._stickyHeaderView as StackLayout;
|
||||||
|
if (!headerView) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (headerView instanceof Label) {
|
||||||
|
return headerView.text + '';
|
||||||
|
}
|
||||||
|
if (headerView.getChildAt) {
|
||||||
|
const child = headerView.getChildAt(0) as StackLayout;
|
||||||
|
if (child instanceof Label) {
|
||||||
|
return child.text + '';
|
||||||
|
}
|
||||||
|
if (child?.getChildAt) {
|
||||||
|
const gchild = child.getChildAt(0);
|
||||||
|
if (gchild instanceof Label) {
|
||||||
|
return gchild.text + '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
private assertNoMemoryLeak(weakRef: WeakRef<ListView>) {
|
private assertNoMemoryLeak(weakRef: WeakRef<ListView>) {
|
||||||
this.tearDown();
|
this.tearDown();
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -516,7 +516,8 @@ export class ListView extends ListViewBase {
|
|||||||
|
|
||||||
for (let section = 0; section < sectionCount; section++) {
|
for (let section = 0; section < sectionCount; section++) {
|
||||||
// Check if firstVisibleItem is in this section (header or items)
|
// Check if firstVisibleItem is in this section (header or items)
|
||||||
const itemsInSection = this._getItemsInSection(section).length;
|
const sectionItems = this._getItemsInSection(section) || [];
|
||||||
|
const itemsInSection = (sectionItems as any).length || 0;
|
||||||
const sectionEndPosition = currentPosition + 1 + itemsInSection; // +1 for header
|
const sectionEndPosition = currentPosition + 1 + itemsInSection; // +1 for header
|
||||||
|
|
||||||
if (firstVisibleItem < sectionEndPosition) {
|
if (firstVisibleItem < sectionEndPosition) {
|
||||||
@@ -844,7 +845,7 @@ function ensureListViewAdapterClass() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCount() {
|
public getCount() {
|
||||||
if (!this.owner || !this.owner.items) {
|
if (!this.owner) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,18 +853,24 @@ function ensureListViewAdapterClass() {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
if (this.owner.sectioned) {
|
if (this.owner.sectioned) {
|
||||||
// Count items + section headers
|
// If items are not ready, report 0 to avoid early crashes
|
||||||
const sectionCount = this.owner._getSectionCount();
|
const sectionCount = this.owner._getSectionCount();
|
||||||
|
if (!this.owner.items || sectionCount <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count items + section headers
|
||||||
for (let i = 0; i < sectionCount; i++) {
|
for (let i = 0; i < sectionCount; i++) {
|
||||||
const itemsInSection = this.owner._getItemsInSection(i);
|
const itemsInSection = this.owner._getItemsInSection(i) || [];
|
||||||
// Only add header if section has items or we want to show empty sections
|
// Only add header if section has items
|
||||||
if (itemsInSection.length > 0) {
|
if ((itemsInSection as any).length > 0) {
|
||||||
count += 1; // Section header
|
count += 1; // Section header
|
||||||
count += itemsInSection.length; // Items in section
|
count += (itemsInSection as any).length; // Items in section
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
count = this.owner.items.length;
|
const src: any = this.owner.items as any;
|
||||||
|
count = src && typeof src.length === 'number' ? src.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the count, ensuring it's never negative
|
// Return the count, ensuring it's never negative
|
||||||
@@ -889,9 +896,10 @@ function ensureListViewAdapterClass() {
|
|||||||
return this.owner._getDataItemInSection(positionInfo.section, positionInfo.itemIndex);
|
return this.owner._getDataItemInSection(positionInfo.section, positionInfo.itemIndex);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (i < this.owner.items.length) {
|
const src: any = this.owner.items as any;
|
||||||
const getItem = (<ItemsSource>this.owner.items).getItem;
|
if (src && typeof src.length === 'number' && i < src.length) {
|
||||||
return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
|
const getItem = (<ItemsSource>src).getItem;
|
||||||
|
return getItem ? getItem.call(src, i) : src[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,10 +916,10 @@ function ensureListViewAdapterClass() {
|
|||||||
const sectionCount = this.owner._getSectionCount();
|
const sectionCount = this.owner._getSectionCount();
|
||||||
|
|
||||||
for (let section = 0; section < sectionCount; section++) {
|
for (let section = 0; section < sectionCount; section++) {
|
||||||
const itemsInSection = this.owner._getItemsInSection(section);
|
const itemsInSection = this.owner._getItemsInSection(section) || [];
|
||||||
|
|
||||||
// Skip sections with no items (they won't have headers in our count)
|
// Skip sections with no items (they won't have headers in our count)
|
||||||
if (itemsInSection.length === 0) {
|
if ((itemsInSection as any).length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,11 +930,11 @@ function ensureListViewAdapterClass() {
|
|||||||
currentPosition++; // Move past header
|
currentPosition++; // Move past header
|
||||||
|
|
||||||
// Check if position is within this section's items
|
// Check if position is within this section's items
|
||||||
if (position < currentPosition + itemsInSection.length) {
|
if (position < currentPosition + (itemsInSection as any).length) {
|
||||||
const itemIndex = position - currentPosition;
|
const itemIndex = position - currentPosition;
|
||||||
return { isHeader: false, section: section, itemIndex: itemIndex };
|
return { isHeader: false, section: section, itemIndex: itemIndex };
|
||||||
}
|
}
|
||||||
currentPosition += itemsInSection.length; // Move past items
|
currentPosition += (itemsInSection as any).length; // Move past items
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback - should not reach here with proper bounds checking
|
// Fallback - should not reach here with proper bounds checking
|
||||||
@@ -1010,6 +1018,14 @@ function ensureListViewAdapterClass() {
|
|||||||
return emptyView;
|
return emptyView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger loadMoreItems when binding the last visible row (matches prior Android behavior)
|
||||||
|
if (index === totalCount - 1) {
|
||||||
|
this.owner.notify({
|
||||||
|
eventName: LOADMOREITEMS,
|
||||||
|
object: this.owner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.owner.sectioned) {
|
if (this.owner.sectioned) {
|
||||||
const positionInfo = this._getPositionInfo(index);
|
const positionInfo = this._getPositionInfo(index);
|
||||||
|
|
||||||
@@ -1149,12 +1165,16 @@ function ensureListViewAdapterClass() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!args.view.parent) {
|
if (!args.view.parent) {
|
||||||
// Android ListView doesn't properly respect margins on direct child views.
|
// Proxy containers should not be treated as layouts. Wrap them in a real layout.
|
||||||
// Always wrap item views in a StackLayout container to ensure margins work correctly.
|
if (args.view instanceof LayoutBase && !(args.view instanceof ProxyViewContainer)) {
|
||||||
const container = new StackLayout();
|
this.owner._addView(args.view);
|
||||||
container.addChild(args.view);
|
convertView = args.view.nativeViewProtected;
|
||||||
this.owner._addView(container);
|
} else {
|
||||||
convertView = container.nativeViewProtected;
|
const sp = new StackLayout();
|
||||||
|
sp.addChild(args.view);
|
||||||
|
this.owner._addView(sp);
|
||||||
|
convertView = sp.nativeViewProtected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owner._registerViewToTemplate(template.key, convertView, args.view);
|
this.owner._registerViewToTemplate(template.key, convertView, args.view);
|
||||||
|
|||||||
Reference in New Issue
Block a user