Files
SwiftFormat/Snapshots/Layout/LayoutTests/LayoutExpressionTests.swift
2026-01-25 09:02:04 -08:00

623 lines
21 KiB
Swift
Executable File

// Copyright © 2017 Schibsted. All rights reserved.
import XCTest
@testable import Layout
final class TestClass: NSObject {
@objc static var foo: Double {
return 5
}
}
func makeLayout(
expressions: [String: String] = [:],
parameters: [String: RuntimeType] = [:],
macros: [String: String] = [:],
children: [Layout] = []
) -> Layout {
return Layout(
className: "UIView",
id: nil,
expressions: expressions,
parameters: parameters,
macros: macros,
children: children,
body: nil,
xmlPath: nil,
templatePath: nil,
childrenTagIndex: nil,
relativePath: nil,
rootURL: nil
)
}
final class LayoutExpressionTests: XCTestCase {
// MARK: Expression parsing
func testParseExpressionWithoutBraces() {
let expression = try? parseExpression("4 + 5")
XCTAssertNotNil(expression)
XCTAssertEqual(expression?.symbols, [.infix("+")])
}
func testParseExpressionWithBraces() {
let expression = try? parseExpression("{4 + 5}")
XCTAssertNotNil(expression)
XCTAssertEqual(expression?.symbols, [.infix("+")])
}
func testParseExpressionWithBracesAndWhitespace() {
let expression = try? parseExpression(" {4 + 5} ")
XCTAssertNotNil(expression)
XCTAssertEqual(expression?.symbols, [.infix("+")])
}
func testParseExpressionWithLeadingGarbage() {
XCTAssertThrowsError(try parseExpression("foo {4 + 5}"))
}
func testParseExpressionWithTrailingGarbage() {
XCTAssertThrowsError(try parseExpression("{4 + 5} foo"))
}
func testParseExpressionWithBracesWithLeadingWhitespace() {
XCTAssertNoThrow(try parseExpression("{ 4 + 5}"))
}
func testParseExpressionWithLeadingWhitespace() {
XCTAssertNoThrow(try parseExpression(" 4 + 5"))
}
func testParseEmptyExpression() {
XCTAssertNoThrow(try parseExpression(""))
}
func testParseExpressionWithEmptyBraces() {
XCTAssertNoThrow(try parseExpression("{}"))
}
func testParseExpressionOpeningBrace() {
XCTAssertThrowsError(try parseExpression("{"))
}
func testParseExpressionWithClosingBrace() {
XCTAssertThrowsError(try parseExpression("}"))
}
func testParseExpressionWithMissingClosingBrace() {
XCTAssertThrowsError(try parseExpression("{4 + 5"))
}
func testParseExpressionWithMissingOpeningBrace() {
XCTAssertThrowsError(try parseExpression("4 + 5}"))
}
func testParseExpressionWithExtraOpeningBrace() {
XCTAssertThrowsError(try parseExpression("{{4 + 5}"))
}
func testParseExpressionWithExtraClosingBrace() {
XCTAssertThrowsError(try parseExpression("{4 + 5}}"))
}
func testParseExpressionWithClosingBraceInQuotes() {
let expression = try? parseExpression("{'}'}")
XCTAssertNotNil(expression)
XCTAssertNil(expression?.error)
}
func testParseExpressionWithOpeningBraceInQuotes() {
let expression = try? parseExpression("{'{'}")
XCTAssertNotNil(expression)
XCTAssertNil(expression?.error)
}
func testParseExpressionWithBracesInQuotes() {
let expression = try? parseExpression("{'{foo}'}")
XCTAssertNotNil(expression)
XCTAssertNil(expression?.error)
}
// MARK: String expression parsing
func testParseStringExpressionWithoutBraces() {
let parts = (try? parseStringExpression("4 + 5")) ?? []
XCTAssertEqual(parts.count, 1)
guard let part = parts.first, case let .string(string) = part else {
XCTFail()
return
}
XCTAssertEqual(string, "4 + 5")
}
func testParseStringExpressionWithBraces() {
let parts = (try? parseStringExpression("{4 + 5}")) ?? []
XCTAssertEqual(parts.count, 1)
guard let part = parts.first, case let .expression(expression) = part else {
XCTFail()
return
}
XCTAssertEqual(expression.symbols, [.infix("+")])
}
func testParseStringExpressionWithBracesAndWhitespace() {
let parts = (try? parseStringExpression(" {4 + 5} ")) ?? []
guard parts.count == 3 else {
XCTFail()
return
}
guard case let .string(a) = parts[0], a == " " else {
XCTFail()
return
}
guard case let .expression(b) = parts[1], b.symbols == [.infix("+")] else {
XCTFail()
return
}
guard case let .string(c) = parts[2], c == " " else {
XCTFail()
return
}
}
func testParseStringExpressionWithMultipleBraces() {
let parts = (try? parseStringExpression("{4} + {5}")) ?? []
guard parts.count == 3 else {
XCTFail()
return
}
guard case let .expression(a) = parts[0], a.symbols == [] else {
XCTFail()
return
}
guard case let .string(b) = parts[1], b == " + " else {
XCTFail()
return
}
guard case let .expression(c) = parts[2], c.symbols == [] else {
XCTFail()
return
}
}
func testParseEmptyStringExpression() {
do {
let parts = try parseStringExpression("")
XCTAssertTrue(parts.isEmpty)
} catch {
XCTFail()
}
}
func testParseStringExpressionWithEmptyBraces() {
XCTAssertNoThrow(try parseStringExpression("{}"))
}
func testParseStringExpressionOpeningBrace() {
XCTAssertThrowsError(try parseStringExpression("{"))
}
func testParseStringExpressionClosingBrace() {
XCTAssertThrowsError(try parseStringExpression("}"))
}
func testParseStringExpressionWithMissingClosingBrace() {
XCTAssertThrowsError(try parseStringExpression("{4 + 5"))
}
func testParseStringExpressionWithMissingOpeningBrace() {
XCTAssertThrowsError(try parseStringExpression("4 + 5}"))
}
func testParseStringExpressionWithExtraOpeningBrace() {
XCTAssertThrowsError(try parseStringExpression("{{4 + 5}"))
}
func testParseStringExpressionWithExtraClosingBrace() {
XCTAssertThrowsError(try parseStringExpression("{4 + 5}}"))
}
func testParseStringExpressionWithClosingBraceInQuotes() {
let parts = (try? parseStringExpression("{'}'}")) ?? []
XCTAssertEqual(parts.count, 1)
guard let part = parts.first, case let .expression(expression) = part else {
XCTFail()
return
}
XCTAssertNil(expression.error)
}
func testParseStringExpressionWithOpeningBraceInQuotes() {
let parts = (try? parseStringExpression("{'{'}")) ?? []
XCTAssertEqual(parts.count, 1)
guard let part = parts.first, case let .expression(expression) = part else {
XCTFail()
return
}
XCTAssertNil(expression.error)
}
func testParseStringExpressionWithBracesInQuotes() {
let parts = (try? parseStringExpression("{'{foo}'}")) ?? []
XCTAssertEqual(parts.count, 1)
guard let part = parts.first, case let .expression(expression) = part else {
XCTFail()
return
}
XCTAssertNil(expression.error)
}
// MARK: Expression comments
func testParseExpressionWithCommentWithoutBraces() throws {
let expression = try parseExpression("4 + 5 // hello")
XCTAssertEqual(expression.symbols, [.infix("+")])
XCTAssertEqual(expression.description, "4 + 5 // hello")
}
func testCommentedOutExpressionWithoutBraces() throws {
let expression = try parseExpression(" //4 + 5")
XCTAssertEqual(expression.isEmpty, true)
XCTAssertEqual(expression.description, "// 4 + 5")
}
func testParseExpressionWithCommentInBraces() throws {
let expression = try parseExpression("{4 + 5 // hello}")
XCTAssertEqual(expression.symbols, [.infix("+")])
XCTAssertEqual(expression.description, "4 + 5 // hello")
}
func testParseExpressionWithCommentAfterBraces() throws {
let expression = try parseExpression("{4 + 5} // hello")
XCTAssertEqual(expression.symbols, [.infix("+")])
XCTAssertEqual(expression.description, "4 + 5 // hello")
}
func testParseExpressionWithCommentInAndAfterBraces() throws {
let expression = try parseExpression("{4 + 5 // hello} // world")
XCTAssertEqual(expression.symbols, [.infix("+")])
XCTAssertEqual(expression.description, "4 + 5 // hello // world")
}
func testCommentedOutExpressionWithBraces() throws {
let expression = try parseExpression("{ //4 + 5}")
XCTAssertEqual(expression.isEmpty, true)
XCTAssertEqual(expression.description, "// 4 + 5")
}
func testParseExpressionWithCommentBeforeBraces() throws {
let expression = try parseExpression(" //{4 + 5}")
XCTAssertEqual(expression.isEmpty, true)
XCTAssertEqual(expression.description, "// {4 + 5}")
}
// MARK: String expression comments
func testParseStringExpressionWithComment() throws {
let expression = "foo {4 + 5 // hello } bar"
let parts = try parseStringExpression(expression)
guard parts.count == 3 else {
XCTFail()
return
}
guard case .string("foo ") = parts[0] else {
XCTFail()
return
}
guard case let .expression(exp) = parts[1] else {
XCTFail()
return
}
XCTAssertEqual(exp.symbols, [.infix("+")])
XCTAssertEqual(exp.description, "4 + 5 // hello")
XCTAssertNil(exp.error)
guard case .string(" bar") = parts[2] else {
XCTFail()
return
}
guard let layoutExpression = LayoutExpression(stringExpression: expression, for: LayoutNode()) else {
XCTFail()
return
}
XCTAssertEqual(try layoutExpression.evaluate() as? String, "foo 9 bar")
}
func testParseStringExpressionWithCommentedOutClause() throws {
let expression = "foo {// 4 + 5} bar"
let parts = try parseStringExpression(expression)
guard parts.count == 3 else {
XCTFail()
return
}
guard case .string("foo ") = parts[0] else {
XCTFail()
return
}
guard case let .expression(exp) = parts[1] else {
XCTFail()
return
}
XCTAssertEqual(exp.symbols, [])
XCTAssertEqual(exp.description, "// 4 + 5")
XCTAssertTrue(exp.isEmpty)
guard let layoutExpression = LayoutExpression(stringExpression: expression, for: LayoutNode()) else {
XCTFail()
return
}
XCTAssertEqual(try layoutExpression.evaluate() as? String, "foo bar")
}
func testCommentedOutStringExpression() throws {
let expression = " //hello {'world'}"
let parts = try parseStringExpression(expression)
guard parts.count == 1 else {
XCTFail()
return
}
guard case let .comment(comment) = parts[0] else {
XCTFail()
return
}
XCTAssertEqual(comment, "hello {'world'}")
XCTAssertEqual(parts.description, "// hello {'world'}")
XCTAssertNil(LayoutExpression(stringExpression: expression, for: LayoutNode()))
}
// MARK: Image and color expression comments
func testCommentedOutColorExpression() {
let node = LayoutNode()
let expression = LayoutExpression(colorExpression: "// red", for: node)
XCTAssertNil(expression)
}
func testCommentedOutImageExpression() {
let node = LayoutNode()
let expression = LayoutExpression(imageExpression: "// MyImage.png", for: node)
XCTAssertNil(expression)
}
func testColorExpressionWithComment() {
let node = LayoutNode()
let expression = LayoutExpression(colorExpression: "red // comment", for: node)
XCTAssertNotNil(expression)
XCTAssertEqual(try expression?.evaluate() as? UIColor, .red)
}
func testImageExpressionWithComment() { // Not supported
let node = LayoutNode()
let expression = LayoutExpression(imageExpression: "MyImage.png // comment", for: node)
XCTAssertNotNil(expression)
XCTAssertThrowsError(try expression?.evaluate())
}
// MARK: Class properties
func testClassPropertyInDoubleExpression() {
let node = LayoutNode()
let className = NSStringFromClass(TestClass.self)
let expression = LayoutExpression(doubleExpression: "\(className).foo", for: node)
XCTAssertEqual(try expression?.evaluate() as? Double, 5)
}
func testUIColorPropertyInColorExpression() {
let node = LayoutNode()
let expression = LayoutExpression(colorExpression: "UIColor.red", for: node)
XCTAssertEqual(try expression?.evaluate() as? UIColor, .red)
}
func testEnumPropertyInEnumExpression() {
let node = LayoutNode()
let expression = LayoutExpression(expression: "UIViewContentMode.center", type: .uiViewContentMode, for: node)
XCTAssertEqual(try expression?.evaluate() as? UIView.ContentMode, .center)
}
func testOptionSetPropertyInEnumExpression() {
let node = LayoutNode()
let expression = LayoutExpression(expression: "UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight", type: .uiViewAutoresizing, for: node)
XCTAssertEqual(try expression?.evaluate() as? UIView.AutoresizingMask, [.flexibleWidth, .flexibleHeight])
}
// MARK: Integration tests
func testOptionalBracesInNumberExpression() {
let node = LayoutNode()
let expression = LayoutExpression(doubleExpression: "{4 + 5}", for: node)
XCTAssertEqual(try expression?.evaluate() as? Double, 9)
}
func testOptionalBracesInColorExpression() {
let node = LayoutNode()
let expression = LayoutExpression(colorExpression: "{white}", for: node)
XCTAssertEqual(try expression?.evaluate() as? UIColor, .white)
}
func testOptionalMultipleExpressionBodiesDisallowedInNumberExpression() {
let node = LayoutNode()
let expression = LayoutExpression(doubleExpression: "{5}{6}", for: node)
XCTAssertThrowsError(try expression?.evaluate())
}
func testFalseTreatedAsConstant() {
let node = LayoutNode()
let expression = LayoutExpression(boolExpression: "false", for: node)
XCTAssertEqual(expression?.symbols.isEmpty, true)
}
func testSetLayerContentsWithCGImageConstant() throws {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
let image: AnyObject = try XCTUnwrap(UIGraphicsGetImageFromCurrentImageContext()?.cgImage)
UIGraphicsEndImageContext()
let node = LayoutNode(
constants: ["image": image],
expressions: ["layer.contents": "{image}"]
)
node.update()
XCTAssertTrue(node.view.layer.contents as AnyObject === image)
}
func testSetLayerContentsWithUIImageConstant() throws {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
let image = try XCTUnwrap(UIGraphicsGetImageFromCurrentImageContext())
UIGraphicsEndImageContext()
let node = LayoutNode(
constants: ["image": image],
expressions: ["layer.contents": "{image}"]
)
node.update()
XCTAssertTrue(node.view.layer.contents as AnyObject === image.cgImage as AnyObject)
}
func testSetLayerShadowPathWithConstant() {
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let path = CGPath(rect: rect, transform: nil)
let node = LayoutNode(
constants: ["path": path],
expressions: ["layer.shadowPath": "path"]
)
node.update()
XCTAssertEqual(node.view.layer.shadowPath, path)
}
func testThrowErrorForConstantExpression() {
let node = LayoutNode(
constants: ["foo": "Not a color"],
expressions: ["backgroundColor": "{foo}"]
)
node.update()
XCTAssertThrowsError(try node.throwUnhandledError())
}
// MARK: Constant optimization
func testLiteral() {
let node = LayoutNode()
let expression = LayoutExpression(doubleExpression: "5 + 6", for: node)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testConstant() {
let node = LayoutNode(constants: ["foo": 5])
let expression = LayoutExpression(doubleExpression: "foo + 6", for: node)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testState() {
let node = LayoutNode(state: ["foo": 5])
let expression = LayoutExpression(doubleExpression: "foo + 6", for: node)
XCTAssertEqual(expression?.isConstant, false)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testInheritedConstant() {
let child = LayoutNode()
let parent = LayoutNode(constants: ["foo": 5], children: [child])
parent.update()
let expression = LayoutExpression(doubleExpression: "foo + 6", for: child)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testParentLiteralExpression() {
let child = LayoutNode()
let parent = LayoutNode(expressions: ["height": "5"], children: [child])
parent.update()
let expression = LayoutExpression(doubleExpression: "parent.height + 6", for: child)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testParentConstantExpression() {
let child = LayoutNode()
let parent = LayoutNode(constants: ["foo": 5], expressions: ["height": "foo + 6"], children: [child])
parent.update()
let expression = LayoutExpression(doubleExpression: "parent.height + 6", for: child)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 17)
}
func testLiteralParameter() throws {
let layout = makeLayout(
expressions: ["foo": "5"],
parameters: ["foo": RuntimeType(Int.self)],
children: [
makeLayout(),
]
)
let parent = try LayoutNode(layout: layout)
let child = parent.children[0]
parent.update()
let expression = LayoutExpression(doubleExpression: "foo + 6", for: child)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testConstantParameter() throws {
let layout = makeLayout(
expressions: ["foo": "bar + 3"],
parameters: ["foo": RuntimeType(Int.self)],
children: [
makeLayout(),
]
)
let parent = try LayoutNode(layout: layout, constants: ["bar": 5])
let child = parent.children[0]
parent.update()
let expression = LayoutExpression(doubleExpression: "foo + 6", for: child)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 14)
}
func testLiteralMacro() throws {
let layout = makeLayout(
macros: ["BAR": "5"],
children: [
makeLayout(),
]
)
let parent = try LayoutNode(layout: layout)
let child = parent.children[0]
parent.update()
let expression = LayoutExpression(doubleExpression: "BAR + 6", for: child)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 11)
}
func testArrayMacro() throws {
let layout = makeLayout(macros: ["ITEMS": "1,2,3"])
let node = try LayoutNode(layout: layout)
node.update()
let expression = LayoutExpression(doubleExpression: "ITEMS[1]", for: node)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 2)
}
func testArrayMacro2() throws {
let layout = makeLayout(macros: ["ITEMS": "[1,2,3]"])
let node = try LayoutNode(layout: layout)
node.update()
let expression = LayoutExpression(doubleExpression: "ITEMS[1]", for: node)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? Double, 2)
}
func testArrayConstant() {
let node = LayoutNode(constants: ["items": ["foo", "bar", "baz"]])
node.update()
let expression = LayoutExpression(stringExpression: "{items[0]}", for: node)
XCTAssertEqual(expression?.isConstant, true)
XCTAssertEqual(try expression?.evaluate() as? String, "foo")
}
// MARK: Edge cases
func testPercentOperatorSpacingAmbiguity() {
let child = LayoutNode(expressions: ["width": "100%-5"])
let parent = LayoutNode(expressions: ["width": "50"], children: [child])
parent.update()
XCTAssertEqual(child.frame.width, 45)
}
}