mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-11 18:05:06 +00:00
Tests/ 根层原有 138 个平铺测试文件,无法从目录读出测试覆盖的功能领域。 按功能领域重组为 17 个目录(Catalog/Workflow 与 Rules/Custom 两处嵌套,分别 对应 App/Application/CatalogWorkflow 与 Domain/Rules/Custom):Architecture / Support / Agent / Activation / Catalog / Subscription / Rules / ProxyRuntime / ProxyControl / Runtime / Settings / SystemProxy / Plugin / Diagnostics / Credentials / Workspace / Servers;Smoke/ 保持独立 target 不动。138 个移动中 132 个是 100% 相似度的纯重命名,其余 6 个在重命名之上带路径反推改写(见下)。 本次不是纯搬移:测试代码比生产代码多一层位置耦合,有 7 处用固定层数的 deletingLastPathComponent() 从 #filePath 反推仓库路径(都假定文件就在 Tests/ 根层)。这 7 处改走新增的 Tests/Support/TestSourceTree.swift——从调用点沿父 目录上溯找到含 project.yml 的目录,因此不再依赖父目录层数,以后移动测试文件 也不会静默指错。 配套改动: - 新增 Tests 布局守卫 TestsLayoutTests,断言 Tests/ 根层无平铺 .swift。比 Domain 那条弱一档——根层的 Info.plist(info.path 生成物)与 Fixtures/RuleSnapshots 下的 JSON(folder resource)都必须保留,不能用 「树下只有 .swift」覆盖。 - project.yml:Smoke target 按文件引用的 Tests/AppArtifact.swift 改指 Tests/Support/;两处注释里写死的 Tests/SkeletonTests.swift 随移动更新。 excludes 的裸名 "Smoke" / "Fixtures" 仍匹配 Tests/ 直属子目录,无需改动。 - README 的 Tests 条目展开为目录说明,并修掉 Domain 条目里写死的 Tests/SkeletonTests.swift。 - 顺带删去 DomainLayoutTests 文档注释里「整理前 62 个文件」这类易漂移的数字。 验证:xcodegen generate 后单测 target 为 139 个源文件(全部非 Smoke 文件,0 个 Smoke 文件混入)且仍以 Fixtures/RuleSnapshots 为资源,Smoke target 为 8 个 Smoke 文件加 AppArtifact;依赖路径反推的 6 个测试类全绿(路径指错即失败); task format 未改写任何被移动文件;task lint 与整理前同为 21 条内容型 warning; build 0 error;task test 1025 用例全通过(整理前 1024,+1 为新守卫); TestsLayoutTests 经变异验证(根层投放探针即失败);packaging-gate ALL CHECKS PASSED。
113 lines
4.7 KiB
Swift
113 lines
4.7 KiB
Swift
import AppKit
|
||
import SwiftUI
|
||
import Testing
|
||
|
||
@testable import ShadowsocksX_NG2
|
||
|
||
/// 系统对安全输入框默认提供「密码自动填充」,`textContentType(nil)` 只清字段
|
||
/// 语义、关不掉它;生产代码改字段级私有开关 `_setPasswordAutofillEnabled:`
|
||
/// (`PasswordAutofillOptOut` 探针认领同帧文本框后经 KVC 写入)。
|
||
///
|
||
/// 这些用例固定认领范围:只关密码列自己的文本框,同一表单的其他输入框保持
|
||
/// 系统默认。私有开关哪天消失,用例失败即提示重做这个缝,而不是静默回退。
|
||
@MainActor
|
||
struct PasswordAutofillOptOutTests {
|
||
@Test
|
||
func securePasswordFieldOptsOutWithoutTouchingNeighbours() async throws {
|
||
let fields = ServerFormFields.newForm()
|
||
let hosted = await Self.hostGrid(fields)
|
||
|
||
let textFields = Self.textFields(in: hosted.host)
|
||
let secure = textFields.filter { $0 is NSSecureTextField }
|
||
#expect(secure.count == 1, "密码列是全表单唯一的安全输入框")
|
||
#expect(Self.disabledAutofillFields(in: hosted.host).count == 1, "只允许关掉密码列")
|
||
#expect(Self.passwordAutofillEnabled(try #require(secure.first)) == false)
|
||
#expect(
|
||
textFields.filter { !($0 is NSSecureTextField) }
|
||
.allSatisfy { Self.passwordAutofillEnabled($0) == true },
|
||
"名称/服务器地址/端口等字段保持系统默认")
|
||
}
|
||
|
||
@Test
|
||
func visiblePasswordBranchOptsOutToo() async throws {
|
||
let fields = ServerFormFields.newForm()
|
||
fields.showPassword = true
|
||
let hosted = await Self.hostGrid(fields)
|
||
|
||
let textFields = Self.textFields(in: hosted.host)
|
||
#expect(textFields.allSatisfy { !($0 is NSSecureTextField) }, "明文分支不渲染安全输入框")
|
||
#expect(Self.disabledAutofillFields(in: hosted.host).count == 1, "只允许关掉密码列")
|
||
}
|
||
|
||
@Test
|
||
func togglingPasswordVisibilityKeepsAutofillOff() async throws {
|
||
let fields = ServerFormFields.newForm()
|
||
let hosted = await Self.hostGrid(fields)
|
||
#expect(Self.disabledAutofillFields(in: hosted.host).count == 1)
|
||
|
||
// 明文/密文切换会换掉承载的文本框,认领必须跟着新实例重做。
|
||
fields.showPassword = true
|
||
await Self.settle(hosted.host)
|
||
#expect(Self.textFields(in: hosted.host).allSatisfy { !($0 is NSSecureTextField) })
|
||
#expect(Self.disabledAutofillFields(in: hosted.host).count == 1)
|
||
|
||
fields.showPassword = false
|
||
await Self.settle(hosted.host)
|
||
#expect(Self.textFields(in: hosted.host).filter { $0 is NSSecureTextField }.count == 1)
|
||
#expect(Self.disabledAutofillFields(in: hosted.host).count == 1)
|
||
}
|
||
|
||
// MARK: 夹具
|
||
|
||
/// 复用生产表单:探针、栅格与字段实现都在真实布局里跑一遍。
|
||
private struct GridHarness: View {
|
||
@ObservedObject var fields: ServerFormFields
|
||
@FocusState private var fieldFocus: ServerFormField?
|
||
|
||
var body: some View {
|
||
ServerFormFieldsGrid(
|
||
fields: fields, plugin: nil, isEditable: true, fieldFocus: $fieldFocus)
|
||
}
|
||
}
|
||
|
||
/// 探针在窗口坐标里认领文本框:无窗口的 hosting view 不构成呈现,认领会落空,
|
||
/// 因此夹具必须给一个离屏窗口(不呈现、不抢 key)。
|
||
private static func hostGrid(
|
||
_ fields: ServerFormFields
|
||
) async -> (window: NSWindow, host: NSHostingView<GridHarness>) {
|
||
let host = NSHostingView(rootView: GridHarness(fields: fields))
|
||
host.frame = NSRect(x: 0, y: 0, width: 560, height: 720)
|
||
let window = NSWindow(
|
||
contentRect: NSRect(x: 0, y: 0, width: 560, height: 720),
|
||
styleMask: [.titled], backing: .buffered, defer: false)
|
||
window.contentView = host
|
||
await settle(host)
|
||
return (window, host)
|
||
}
|
||
|
||
/// 让 SwiftUI 的渲染事务与探针的布局回调都跑完再断言。
|
||
private static func settle(_ host: NSHostingView<GridHarness>) async {
|
||
for _ in 0..<2 {
|
||
await Task { @MainActor in }.value
|
||
host.layoutSubtreeIfNeeded()
|
||
}
|
||
}
|
||
|
||
private static func textFields(in view: NSView) -> [NSTextField] {
|
||
if let field = view as? NSTextField { return [field] + view.subviews.flatMap(textFields) }
|
||
return view.subviews.flatMap(textFields)
|
||
}
|
||
|
||
private static func disabledAutofillFields(in view: NSView) -> [NSTextField] {
|
||
textFields(in: view).filter { passwordAutofillEnabled($0) == false }
|
||
}
|
||
|
||
/// BOOL 返回值不能走 `perform`(会被当成对象返回值),只能取 IMP 直调。
|
||
private static func passwordAutofillEnabled(_ field: NSTextField) -> Bool? {
|
||
let selector = NSSelectorFromString("_isPasswordAutofillEnabled")
|
||
guard let method = class_getInstanceMethod(type(of: field), selector) else { return nil }
|
||
typealias Getter = @convention(c) (AnyObject, Selector) -> Bool
|
||
return unsafeBitCast(method_getImplementation(method), to: Getter.self)(field, selector)
|
||
}
|
||
}
|