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/
76 lines
3.5 KiB
Swift
76 lines
3.5 KiB
Swift
import SwiftUI
|
||
import Testing
|
||
import UIKit
|
||
@testable import WebTerm
|
||
|
||
/// T-iPad-4 · Projects 大屏化的纯布局决策单测。判据是**设备 idiom + 可用宽度**
|
||
/// (单点,仿 `LayoutPolicy` 先例)——见 `ProjectsLayout` 注释解释为何用 idiom
|
||
/// 而非 size class(iPad 的 Projects 表单 sheet 内部恒 compact,与 iPhone 横屏
|
||
/// 无异,size class 无法在不回归 iPhone 的前提下区分二者)。
|
||
///
|
||
/// iPhone 零回归的可测护栏:`.phone` 分支必须与「现状」逐字节等价——列数恒
|
||
/// 为 1(现有单列 List)、不走网格、sheet 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])
|
||
}
|
||
}
|