228 lines
11 KiB
Swift
228 lines
11 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AppKit
|
|
import GhosttyTerminal
|
|
import SwiftUI
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindPresentation
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite(.serialized) @MainActor
|
|
struct TerminalCustomConfigurationEditorLayoutTests {
|
|
@Test(arguments: [CGFloat(650), CGFloat(1000)])
|
|
func nameFieldLeavesRoomForItsNativeFocusRing(width: CGFloat) async throws {
|
|
let fixture = Fixture()
|
|
defer { fixture.close() }
|
|
fixture.window.setContentSize(CGSize(width: width, height: 800))
|
|
await fixture.layout()
|
|
let field = try #require(fixture.descendant(NSTextField.self))
|
|
let scroll = try #require(field.enclosingScrollView)
|
|
let focusFrame = field.convert(field.bounds.insetBy(dx: -3, dy: -3), to: scroll.contentView)
|
|
#expect(scroll.contentView.bounds.contains(focusFrame))
|
|
}
|
|
|
|
@Test func discardedDraftCannotBeSavedByThePendingAutosaveTask() async throws {
|
|
let fixture = Fixture()
|
|
defer { fixture.close() }
|
|
await fixture.layout()
|
|
fixture.draft.value.configuration = "background = 123456"
|
|
fixture.model.deleteCustomTheme(id: fixture.draft.id)
|
|
fixture.navigation.clearTerminalThemeEditor()
|
|
await fixture.layout()
|
|
#expect(fixture.model.themeLibrary.themes.isEmpty)
|
|
}
|
|
|
|
@Test(arguments: ["en", "zh-Hans", "zh-Hant"], [false, true])
|
|
func editorAndPreviewFillThePageRegardlessOfTextLength(language: String, isDark: Bool) async throws {
|
|
let fixture = Fixture(language: language, isDark: isDark)
|
|
defer { fixture.close() }
|
|
await fixture.layout()
|
|
let editor = try #require(fixture.descendant(NSTextView.self))
|
|
let scroll = try #require(editor.enclosingScrollView)
|
|
let preview = try #require(fixture.descendant(AppTerminalView.self))
|
|
scroll.scrollerStyle = .legacy
|
|
let editorHeight = scroll.bounds.height
|
|
let previewHeight = preview.bounds.height
|
|
#expect(editorHeight > 400)
|
|
|
|
for text in ["", "background = 152033", Self.longConfiguration, ""] {
|
|
var updated = fixture.draft.value
|
|
updated.configuration = text
|
|
fixture.model.saveCustomTheme(updated, isNew: false)
|
|
await fixture.layout()
|
|
#expect(editor.string == text)
|
|
#expect(abs(scroll.bounds.height - editorHeight) <= 1)
|
|
#expect(abs(preview.bounds.height - previewHeight) <= 1)
|
|
fixture.expectFillsPage(editor: scroll, preview: preview)
|
|
#expect(scroll.autohidesScrollers)
|
|
if text == Self.longConfiguration {
|
|
#expect(scroll.verticalScroller?.isHidden == false)
|
|
#expect(scroll.verticalScroller?.isEnabled == true)
|
|
#expect(editor.bounds.height > scroll.contentSize.height)
|
|
editor.scrollRangeToVisible(NSRange(location: (text as NSString).length, length: 0))
|
|
#expect(scroll.contentView.bounds.minY > 0)
|
|
#expect(abs(scroll.bounds.height - editorHeight) <= 1)
|
|
} else {
|
|
#expect(scroll.verticalScroller?.isHidden == true)
|
|
}
|
|
}
|
|
#expect(!fixture.window.isVisible)
|
|
}
|
|
|
|
@Test
|
|
func resizingPreservesTheEditorSelectionAndPreviewWhileChangingLayout() async throws {
|
|
let fixture = Fixture()
|
|
defer { fixture.close() }
|
|
await fixture.layout()
|
|
let editor = try #require(fixture.descendant(NSTextView.self))
|
|
let scroll = try #require(editor.enclosingScrollView)
|
|
let preview = try #require(fixture.descendant(AppTerminalView.self))
|
|
let previewController = preview.controller
|
|
editor.insertText(Self.longConfiguration, replacementRange: NSRange(location: 0, length: 0))
|
|
await fixture.layout()
|
|
let selection = NSRange(location: 4, length: 8)
|
|
editor.setSelectedRange(selection)
|
|
editor.scrollRangeToVisible(selection)
|
|
|
|
for size in [
|
|
CGSize(width: 1000, height: 1000),
|
|
CGSize(width: 1000, height: 620),
|
|
CGSize(width: 650, height: 620),
|
|
CGSize(width: 1000, height: 800),
|
|
] {
|
|
fixture.window.setContentSize(size)
|
|
await fixture.layout()
|
|
#expect(fixture.descendant(NSTextView.self) === editor)
|
|
#expect(fixture.descendant(AppTerminalView.self) === preview)
|
|
#expect(preview.controller === previewController)
|
|
#expect(editor.string == Self.longConfiguration)
|
|
#expect(editor.selectedRange() == selection)
|
|
if size.width >= 700 {
|
|
fixture.expectFillsPage(editor: scroll, preview: preview)
|
|
} else {
|
|
#expect(abs(scroll.bounds.height - 360) <= 1)
|
|
let pageScroll = try #require(scroll.superview?.enclosingScrollView)
|
|
let document = try #require(pageScroll.documentView)
|
|
#expect(document.bounds.height > pageScroll.contentSize.height)
|
|
let editorFrame = scroll.convert(scroll.bounds, to: fixture.view)
|
|
let previewFrame = preview.convert(preview.bounds, to: fixture.view)
|
|
#expect(abs(editorFrame.minX - previewFrame.minX) <= 1)
|
|
}
|
|
}
|
|
#expect(fixture.draft.value.configuration == Self.longConfiguration)
|
|
#expect(fixture.model.themeLibrary.themes.first?.configuration == Self.longConfiguration)
|
|
#expect(!fixture.window.isVisible)
|
|
}
|
|
|
|
@Test
|
|
func accessibilityTextKeepsTheStackedEditorScrollableAtWideWidths() async throws {
|
|
let fixture = Fixture(typeSize: .accessibility1)
|
|
defer { fixture.close() }
|
|
fixture.window.setContentSize(CGSize(width: 1000, height: 620))
|
|
await fixture.layout()
|
|
let editor = try #require(fixture.descendant(NSTextView.self))
|
|
let scroll = try #require(editor.enclosingScrollView)
|
|
let preview = try #require(fixture.descendant(AppTerminalView.self))
|
|
#expect(abs(scroll.bounds.height - 360) <= 1)
|
|
let editorFrame = scroll.convert(scroll.bounds, to: fixture.view)
|
|
let previewFrame = preview.convert(preview.bounds, to: fixture.view)
|
|
#expect(abs(editorFrame.minX - previewFrame.minX) <= 1)
|
|
let pageScroll = try #require(scroll.superview?.enclosingScrollView)
|
|
let document = try #require(pageScroll.documentView)
|
|
#expect(document.bounds.height > pageScroll.contentSize.height)
|
|
#expect(!fixture.window.isVisible)
|
|
}
|
|
|
|
private static var longConfiguration: String {
|
|
(["background = 152033"] + (0..<160).map { "# Configuration line \($0)" }).joined(separator: "\n")
|
|
}
|
|
|
|
private final class Fixture {
|
|
let model = TerminalSettingsModel()
|
|
let app: AppModel
|
|
let navigation = WindowModel(workspaces: [])
|
|
let draft: TerminalCustomThemeDraft
|
|
let controller: NSHostingController<AnyView>
|
|
let window: NSWindow
|
|
let language: String
|
|
var view: NSView { controller.view }
|
|
|
|
init(language: String = "en", isDark: Bool = true, typeSize: DynamicTypeSize = .large) {
|
|
self.language = language
|
|
let theme = TerminalCustomTheme(
|
|
name: "Layout", appearance: isDark ? .dark : .light,
|
|
baseThemeName: isDark ? "Termind Dark" : "Termind Light")
|
|
model.saveCustomTheme(theme, isNew: true)
|
|
draft = .init(theme, isNew: false)
|
|
app = AppModel(hosts: HostLibraryModel(), terminalSettings: model)
|
|
navigation.openSettings(route: .terminalCustomThemes)
|
|
navigation.terminalThemeDraft = draft
|
|
navigation.settingsRoute = .terminalCustomTheme(theme.id)
|
|
controller = NSHostingController(
|
|
rootView: AnyView(
|
|
TerminalCustomConfigurationEditorPage(draft: draft, model: model)
|
|
.environment(app)
|
|
.environment(navigation)
|
|
.environment(\.locale, Locale(identifier: language))
|
|
.environment(\.colorScheme, isDark ? .dark : .light)
|
|
.environment(\.dynamicTypeSize, typeSize)))
|
|
controller.sizingOptions = []
|
|
window = NSWindow(
|
|
contentRect: CGRect(x: 0, y: 0, width: 1000, height: 800),
|
|
styleMask: [.borderless], backing: .buffered, defer: false)
|
|
window.isReleasedWhenClosed = false
|
|
window.appearance = NSAppearance(named: isDark ? .darkAqua : .aqua)
|
|
window.contentViewController = controller
|
|
window.setContentSize(CGSize(width: 1000, height: 800))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 1000, height: 800)
|
|
}
|
|
|
|
func layout() async {
|
|
view.layoutSubtreeIfNeeded()
|
|
try? await Task.sleep(for: .milliseconds(350))
|
|
view.layoutSubtreeIfNeeded()
|
|
}
|
|
|
|
func descendant<T: NSView>(_ type: T.Type) -> T? {
|
|
func find(in view: NSView) -> T? {
|
|
if let result = view as? T { return result }
|
|
return view.subviews.lazy.compactMap { find(in: $0) }.first
|
|
}
|
|
return find(in: view)
|
|
}
|
|
|
|
func expectFillsPage(editor: NSScrollView, preview: AppTerminalView) {
|
|
let editorFrame = editor.convert(editor.bounds, to: view)
|
|
let previewFrame = preview.convert(preview.bounds, to: view)
|
|
let footer = NSHostingView(
|
|
rootView: Text(AppStrings.Settings.terminalConfigurationExplanation)
|
|
.font(.caption)
|
|
.frame(width: view.bounds.width - 32, alignment: .leading)
|
|
.environment(\.locale, Locale(identifier: language)))
|
|
let footerHeight = footer.fittingSize.height
|
|
let editorBottom = view.isFlipped ? view.bounds.maxY - editorFrame.maxY : editorFrame.minY
|
|
let previewBottom = view.isFlipped ? view.bounds.maxY - previewFrame.maxY : previewFrame.minY
|
|
// Leave the localized footer, its spacing, and the page inset below both panels.
|
|
#expect(footerHeight > 0)
|
|
#expect(abs(editorBottom - footerHeight - 32) <= 1)
|
|
// The terminal and color specimens share one panel; specimens sit below the native surface.
|
|
#expect(previewBottom > editorBottom)
|
|
#expect(previewFrame.height >= 220)
|
|
let editorTop = view.isFlipped ? editorFrame.minY : editorFrame.maxY
|
|
let previewTop = view.isFlipped ? previewFrame.minY : previewFrame.maxY
|
|
#expect(abs(editorTop - previewTop) <= 1)
|
|
#expect(previewFrame.minX > editorFrame.maxX)
|
|
}
|
|
|
|
func close() {
|
|
window.contentViewController = nil
|
|
window.close()
|
|
app.shutdownForTermination()
|
|
}
|
|
}
|
|
}
|