feat(ipad): W1-W3 — adaptive split-view layout + finding fixes

T-iPad-2: AdaptiveRootView/LayoutPolicy (sole size-class decision), SplitRootView
(NavigationSplitView sidebar+detail), StackRootView (iPhone path verbatim, zero
regression); privacy shade hoisted to shared ZStack top for both branches
T-iPad-3: KeyBarVisibility predicate (hide when hardware keyboard present),
pointer context menu (copy/new-in-cwd/kill, all via existing channels)
T-iPad-4: Projects multi-column grid on iPad (idiom-gated), adaptive sheet detents
T-iPad-5 findings (4/4 fixed): kill onKillSession thread-through +
AppCoordinator.killCurrentSession; split route-gated to .sessions (iPad first-run
pairing); continue-last banner in split sidebar; iPad XCUITest deferred (covered
by SidebarSelectionTests)
Verified: iPhone 16 277 + iPad Pro 11 278 tests green; packages 261 + integration 10;
zero changes under ios/Packages, src/, public/
This commit is contained in:
Yaojia Wang
2026-07-05 19:58:30 +02:00
parent 77502ec4fe
commit 823432b1c8
18 changed files with 1594 additions and 84 deletions

View File

@@ -0,0 +1,75 @@
import SwiftUI
import Testing
import UIKit
@testable import WebTerm
/// T-iPad-4 · Projects ** idiom + **
/// 仿 `LayoutPolicy` `ProjectsLayout` idiom
/// size classiPad Projects sheet compact iPhone
/// size class iPhone
///
/// iPhone `.phone`
/// 1 Listsheet detents nil =
/// sheet`.pad`
@Suite("ProjectsGridLayout column count (T-iPad-4)")
struct ProjectsGridLayoutTests {
// MARK: - iPhone 1 /
@Test(".phone → 恒 1 列,宽度无关(含横屏宽尺寸,现有单列 List 原样复用)")
func phoneIsAlwaysSingleColumn() {
#expect(ProjectsGridLayout.columnCount(availableWidth: 320, idiom: .phone) == 1)
// iPhone ~852
#expect(ProjectsGridLayout.columnCount(availableWidth: 852, idiom: .phone) == 1)
#expect(ProjectsGridLayout.columnCount(availableWidth: 2000, idiom: .phone) == 1)
}
@Test(".phone 不走网格;.pad 走网格")
func usesGridOnlyOnPad() {
#expect(ProjectsGridLayout.usesGrid(idiom: .phone) == false)
#expect(ProjectsGridLayout.usesGrid(idiom: .pad) == true)
}
// MARK: - iPad 1/2/3
@Test(".pad 且窄于两列阈值 → 1 列(极窄 iPad 多任务,绝不挤压)")
func padNarrowIsSingleColumn() {
#expect(ProjectsGridLayout.columnCount(
availableWidth: ProjectsGridLayout.twoColumnMinWidth - 1, idiom: .pad) == 1)
}
@Test(".pad 且在 [两列阈值, 三列阈值) → 2 列(表单 sheet 内宽即此档)")
func padMediumIsTwoColumns() {
#expect(ProjectsGridLayout.columnCount(
availableWidth: ProjectsGridLayout.twoColumnMinWidth, idiom: .pad) == 2)
#expect(ProjectsGridLayout.columnCount(
availableWidth: ProjectsGridLayout.threeColumnMinWidth - 1, idiom: .pad) == 2)
}
@Test(".pad 且 >= 三列阈值 → 3 列(更宽面板/横屏全宽)")
func padWideIsThreeColumns() {
#expect(ProjectsGridLayout.columnCount(
availableWidth: ProjectsGridLayout.threeColumnMinWidth, idiom: .pad) == 3)
#expect(ProjectsGridLayout.columnCount(availableWidth: 1400, idiom: .pad) == 3)
}
@Test("列数恒 >= 1任何宽度都不返回 0/负,防空网格崩溃)")
func columnCountIsAlwaysPositive() {
for width in stride(from: CGFloat(0), through: 2000, by: 137) {
#expect(ProjectsGridLayout.columnCount(availableWidth: width, idiom: .pad) >= 1)
#expect(ProjectsGridLayout.columnCount(availableWidth: width, idiom: .phone) == 1)
}
}
}
@Suite("ProjectsSheetSizing (T-iPad-4)")
struct ProjectsSheetSizingTests {
@Test(".phone → nil不加 detent 修饰符 = 现有默认全高 sheet字节级不变")
func phoneIsNil() {
#expect(ProjectsSheetSizing.detents(idiom: .phone) == nil)
}
@Test(".pad → [.medium, .large]iPad 卡片式,不铺满大屏;仍可上拉到大)")
func padIsCardDetents() {
#expect(ProjectsSheetSizing.detents(idiom: .pad) == [.medium, .large])
}
}