Files
honeyDueKMP/iosApp/CaseraUITests/Suite7_ContractorTests.swift
Trey t b0838d85df Add residence picker to contractor create/edit screens
Kotlin/KMM:
- Update Contractor model with optional residenceId and specialties array
- Rename averageRating to rating, update address field names
- Add ContractorMinimal model for task references
- Add residence picker and multi-select specialty chips to AddContractorDialog
- Fix ContractorsScreen and ContractorDetailScreen field references

iOS:
- Rewrite ContractorFormSheet with residence and specialty pickers
- Update ContractorDetailView with FlowLayout for specialties
- Add FlowLayout component for wrapping badge layouts
- Fix ContractorCard and CompleteTaskView field references
- Update ContractorFormState with residence/specialty selection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 18:42:18 -06:00

727 lines
27 KiB
Swift

import XCTest
/// Comprehensive contractor testing suite covering all scenarios, edge cases, and variations
/// This test suite is designed to be bulletproof and catch regressions early
final class Suite7_ContractorTests: XCTestCase {
var app: XCUIApplication!
// Test data tracking
var createdContractorNames: [String] = []
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// Ensure user is logged in
UITestHelpers.ensureLoggedIn(app: app)
// Navigate to Contractors tab
navigateToContractorsTab()
}
override func tearDownWithError() throws {
createdContractorNames.removeAll()
app = nil
}
// MARK: - Helper Methods
private func navigateToContractorsTab() {
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
if contractorsTab.waitForExistence(timeout: 5) {
if !contractorsTab.isSelected {
contractorsTab.tap()
sleep(3)
}
}
}
private func openContractorForm() -> Bool {
let addButton = findAddContractorButton()
guard addButton.exists && addButton.isEnabled else { return false }
addButton.tap()
sleep(3)
// Verify form opened
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
return nameField.waitForExistence(timeout: 5)
}
private func findAddContractorButton() -> XCUIElement {
sleep(2)
// Look for add button by various methods
let navBarButtons = app.navigationBars.buttons
for i in 0..<navBarButtons.count {
let button = navBarButtons.element(boundBy: i)
if button.label == "plus" || button.label.contains("Add") {
if button.isEnabled {
return button
}
}
}
// Fallback: look for any button with plus icon
return app.buttons.containing(NSPredicate(format: "label CONTAINS 'plus'")).firstMatch
}
private func fillTextField(placeholder: String, text: String) {
let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch
if field.exists {
field.tap()
field.typeText(text)
}
}
private func selectSpecialty(specialty: String) {
let specialtyPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Specialty'")).firstMatch
if specialtyPicker.exists {
specialtyPicker.tap()
sleep(1)
// Try to find and tap the specialty option
let specialtyButton = app.buttons[specialty]
if specialtyButton.exists {
specialtyButton.tap()
sleep(1)
} else {
// Try cells if it's a navigation style picker
let cells = app.cells
for i in 0..<cells.count {
let cell = cells.element(boundBy: i)
if cell.staticTexts[specialty].exists {
cell.tap()
sleep(1)
break
}
}
}
}
}
private func createContractor(
name: String,
phone: String = "555-123-4567",
email: String? = nil,
company: String? = nil,
specialty: String? = nil,
scrollBeforeSave: Bool = true
) -> Bool {
guard openContractorForm() else { return false }
// Fill name
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
nameField.tap()
nameField.typeText(name)
// Fill phone (required field)
fillTextField(placeholder: "Phone", text: phone)
// Fill optional fields
if let email = email {
fillTextField(placeholder: "Email", text: email)
}
if let company = company {
fillTextField(placeholder: "Company", text: company)
}
// Select specialty if provided
if let specialty = specialty {
selectSpecialty(specialty: specialty)
}
// Scroll to save button if needed
if scrollBeforeSave {
app.swipeUp()
sleep(1)
}
// Add button (for creating new contractors)
let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
guard addButton.exists else { return false }
addButton.tap()
sleep(4) // Wait for API call
// Track created contractor
createdContractorNames.append(name)
return true
}
private func findContractor(name: String, scrollIfNeeded: Bool = true) -> XCUIElement {
let element = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch
// If element is visible, return it immediately
if element.exists && element.isHittable {
return element
}
// If scrolling is not needed, return the element as-is
guard scrollIfNeeded else {
return element
}
// Get the scroll view
let scrollView = app.scrollViews.firstMatch
guard scrollView.exists else {
return element
}
// First, scroll to the top of the list
scrollView.swipeDown(velocity: .fast)
usleep(30_000) // 0.03 second delay
// Now scroll down from top, checking after each swipe
var lastVisibleRow = ""
for _ in 0..<Int.max {
// Check if element is now visible
if element.exists && element.isHittable {
return element
}
// Get the last visible row before swiping
let visibleTexts = app.staticTexts.allElementsBoundByIndex.filter { $0.isHittable }
let currentLastRow = visibleTexts.last?.label ?? ""
// If last row hasn't changed, we've reached the end
if !lastVisibleRow.isEmpty && currentLastRow == lastVisibleRow {
break
}
lastVisibleRow = currentLastRow
// Scroll down one swipe
scrollView.swipeUp(velocity: .slow)
usleep(50_000) // 0.05 second delay
}
// Return element (test assertions will handle if not found)
return element
}
// MARK: - 1. Validation & Error Handling Tests
func test01_cannotCreateContractorWithEmptyName() {
guard openContractorForm() else {
XCTFail("Failed to open contractor form")
return
}
// Leave name empty, fill only phone
fillTextField(placeholder: "Phone", text: "555-123-4567")
// Scroll to Add button if needed
app.swipeUp()
sleep(1)
// When creating, button should say "Add"
let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
XCTAssertTrue(addButton.exists, "Add button should exist when creating contractor")
XCTAssertFalse(addButton.isEnabled, "Add button should be disabled when name is empty")
}
func test02_cancelContractorCreation() {
guard openContractorForm() else {
XCTFail("Failed to open contractor form")
return
}
// Fill some data
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
nameField.tap()
nameField.typeText("This will be canceled")
// Tap cancel
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
XCTAssertTrue(cancelButton.exists, "Cancel button should exist")
cancelButton.tap()
sleep(2)
// Should be back on contractors list
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
XCTAssertTrue(contractorsTab.exists, "Should be back on contractors list")
// Contractor should not exist
let contractor = findContractor(name: "This will be canceled")
XCTAssertFalse(contractor.exists, "Canceled contractor should not exist")
}
// MARK: - 2. Basic Contractor Creation Tests
func test03_createContractorWithMinimalData() {
let timestamp = Int(Date().timeIntervalSince1970)
let contractorName = "John Doe \(timestamp)"
let success = createContractor(name: contractorName)
XCTAssertTrue(success, "Should successfully create contractor with minimal data")
let contractorInList = findContractor(name: contractorName)
XCTAssertTrue(contractorInList.waitForExistence(timeout: 10), "Contractor should appear in list")
}
func test04_createContractorWithAllFields() {
let timestamp = Int(Date().timeIntervalSince1970)
let contractorName = "Jane Smith \(timestamp)"
let success = createContractor(
name: contractorName,
phone: "555-987-6543",
email: "jane.smith@example.com",
company: "Smith Plumbing Inc",
specialty: "Plumbing"
)
XCTAssertTrue(success, "Should successfully create contractor with all fields")
let contractorInList = findContractor(name: contractorName)
XCTAssertTrue(contractorInList.waitForExistence(timeout: 10), "Complete contractor should appear in list")
}
func test05_createContractorWithDifferentSpecialties() {
let timestamp = Int(Date().timeIntervalSince1970)
let specialties = ["Plumbing", "Electrical", "HVAC"]
for (index, specialty) in specialties.enumerated() {
let contractorName = "\(specialty) Expert \(timestamp)_\(index)"
let success = createContractor(name: contractorName, specialty: specialty)
XCTAssertTrue(success, "Should create \(specialty) contractor")
navigateToContractorsTab()
sleep(2)
}
// Verify all contractors exist
for (index, specialty) in specialties.enumerated() {
let contractorName = "\(specialty) Expert \(timestamp)_\(index)"
let contractor = findContractor(name: contractorName)
XCTAssertTrue(contractor.exists, "\(specialty) contractor should exist in list")
}
}
func test06_createMultipleContractorsInSequence() {
let timestamp = Int(Date().timeIntervalSince1970)
for i in 1...3 {
let contractorName = "Sequential Contractor \(i) - \(timestamp)"
let success = createContractor(name: contractorName)
XCTAssertTrue(success, "Should create contractor \(i)")
navigateToContractorsTab()
sleep(2)
}
// Verify all contractors exist
for i in 1...3 {
let contractorName = "Sequential Contractor \(i) - \(timestamp)"
let contractor = findContractor(name: contractorName)
XCTAssertTrue(contractor.exists, "Contractor \(i) should exist in list")
}
}
// MARK: - 3. Edge Case Tests - Phone Numbers
func test07_createContractorWithDifferentPhoneFormats() {
let timestamp = Int(Date().timeIntervalSince1970)
let phoneFormats = [
("555-123-4567", "Dashed"),
("(555) 123-4567", "Parentheses"),
("5551234567", "NoFormat"),
("555.123.4567", "Dotted")
]
for (index, (phone, format)) in phoneFormats.enumerated() {
let contractorName = "\(format) Phone \(timestamp)_\(index)"
let success = createContractor(name: contractorName, phone: phone)
XCTAssertTrue(success, "Should create contractor with \(format) phone format")
navigateToContractorsTab()
sleep(2)
}
// Verify all contractors exist
for (index, (_, format)) in phoneFormats.enumerated() {
let contractorName = "\(format) Phone \(timestamp)_\(index)"
let contractor = findContractor(name: contractorName)
XCTAssertTrue(contractor.exists, "Contractor with \(format) phone should exist")
}
}
// MARK: - 4. Edge Case Tests - Emails
func test08_createContractorWithValidEmails() {
let timestamp = Int(Date().timeIntervalSince1970)
let emails = [
"simple@example.com",
"firstname.lastname@example.com",
"email+tag@example.co.uk",
"email_with_underscore@example.com"
]
for (index, email) in emails.enumerated() {
let contractorName = "Email Test \(index) - \(timestamp)"
let success = createContractor(name: contractorName, email: email)
XCTAssertTrue(success, "Should create contractor with email: \(email)")
navigateToContractorsTab()
sleep(2)
}
}
// MARK: - 5. Edge Case Tests - Names
func test09_createContractorWithVeryLongName() {
let timestamp = Int(Date().timeIntervalSince1970)
let longName = "John Christopher Alexander Montgomery Wellington III Esquire \(timestamp)"
let success = createContractor(name: longName)
XCTAssertTrue(success, "Should handle very long names")
// Verify it appears (may be truncated in display)
let contractor = findContractor(name: "John Christopher")
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Long name contractor should exist")
}
func test10_createContractorWithSpecialCharactersInName() {
let timestamp = Int(Date().timeIntervalSince1970)
let specialName = "O'Brien-Smith Jr. \(timestamp)"
let success = createContractor(name: specialName)
XCTAssertTrue(success, "Should handle special characters in names")
let contractor = findContractor(name: "O'Brien")
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Contractor with special chars should exist")
}
func test11_createContractorWithInternationalCharacters() {
let timestamp = Int(Date().timeIntervalSince1970)
let internationalName = "José García \(timestamp)"
let success = createContractor(name: internationalName)
XCTAssertTrue(success, "Should handle international characters")
let contractor = findContractor(name: "José")
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Contractor with international chars should exist")
}
func test12_createContractorWithEmojisInName() {
let timestamp = Int(Date().timeIntervalSince1970)
let emojiName = "Bob 🔧 Builder \(timestamp)"
let success = createContractor(name: emojiName)
XCTAssertTrue(success, "Should handle emojis in names")
let contractor = findContractor(name: "Bob")
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Contractor with emojis should exist")
}
// MARK: - 6. Contractor Editing Tests
func test13_editContractorName() {
let timestamp = Int(Date().timeIntervalSince1970)
let originalName = "Original Contractor \(timestamp)"
let newName = "Edited Contractor \(timestamp)"
// Create contractor
guard createContractor(name: originalName) else {
XCTFail("Failed to create contractor")
return
}
navigateToContractorsTab()
sleep(2)
// Find and tap contractor
let contractor = findContractor(name: originalName)
XCTAssertTrue(contractor.waitForExistence(timeout: 5), "Contractor should exist")
contractor.tap()
sleep(2)
// Tap edit button (may be in menu)
app/*@START_MENU_TOKEN@*/.images["ellipsis.circle"]/*[[".buttons[\"More\"].images",".buttons",".images[\"More\"]",".images[\"ellipsis.circle\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
app/*@START_MENU_TOKEN@*/.buttons["pencil"]/*[[".buttons.containing(.image, identifier: \"pencil\")",".cells",".buttons[\"Edit\"]",".buttons[\"pencil\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
// Edit name
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
if nameField.exists {
nameField.tap()
sleep(1)
nameField.tap()
sleep(1)
app.menuItems["Select All"].tap()
sleep(1)
nameField.typeText(newName)
// Save (when editing, button should say "Save")
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
if saveButton.exists {
saveButton.tap()
sleep(3)
// Track new name
createdContractorNames.append(newName)
}
}
}
func test14_updateAllContractorFields() {
let timestamp = Int(Date().timeIntervalSince1970)
let originalName = "Update All Fields \(timestamp)"
let newName = "All Fields Updated \(timestamp)"
let newPhone = "999-888-7777"
let newEmail = "updated@contractor.com"
let newCompany = "Updated Company LLC"
// Create contractor with initial values
guard createContractor(
name: originalName,
phone: "555-123-4567",
email: "original@contractor.com",
company: "Original Company"
) else {
XCTFail("Failed to create contractor")
return
}
navigateToContractorsTab()
sleep(2)
// Find and tap contractor
let contractor = findContractor(name: originalName)
XCTAssertTrue(contractor.waitForExistence(timeout: 5), "Contractor should exist")
contractor.tap()
sleep(2)
// Tap edit button (may be in menu)
app/*@START_MENU_TOKEN@*/.images["ellipsis.circle"]/*[[".buttons[\"More\"].images",".buttons",".images[\"More\"]",".images[\"ellipsis.circle\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
app/*@START_MENU_TOKEN@*/.buttons["pencil"]/*[[".buttons.containing(.image, identifier: \"pencil\")",".cells",".buttons[\"Edit\"]",".buttons[\"pencil\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
// Update name
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
XCTAssertTrue(nameField.exists, "Name field should exist")
nameField.tap()
sleep(1)
nameField.tap()
sleep(1)
app.menuItems["Select All"].tap()
sleep(1)
nameField.typeText(newName)
// Update phone
let phoneField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Phone'")).firstMatch
if phoneField.exists {
phoneField.tap()
sleep(1)
phoneField.tap()
sleep(1)
app.menuItems["Select All"].tap()
phoneField.typeText(newPhone)
}
// Scroll to more fields
app.swipeUp()
sleep(1)
// Update email
let emailField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Email'")).firstMatch
if emailField.exists {
emailField.tap()
sleep(1)
emailField.tap()
sleep(1)
app.menuItems["Select All"].tap()
emailField.typeText(newEmail)
}
// Update company
let companyField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Company'")).firstMatch
if companyField.exists {
companyField.tap()
sleep(1)
companyField.tap()
sleep(1)
app.menuItems["Select All"].tap()
companyField.typeText(newCompany)
}
// Update specialty (if picker exists)
let specialtyPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Specialty'")).firstMatch
if specialtyPicker.exists {
specialtyPicker.tap()
sleep(1)
// Select HVAC
let hvacOption = app.buttons["HVAC"]
if hvacOption.exists {
hvacOption.tap()
sleep(1)
}
}
// Scroll to save button
app.swipeUp()
sleep(1)
// Save (when editing, button should say "Save")
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
XCTAssertTrue(saveButton.exists, "Save button should exist when editing contractor")
saveButton.tap()
sleep(4)
// Track new name
createdContractorNames.append(newName)
// Verify updated contractor appears in list with new name
navigateToContractorsTab()
sleep(2)
let updatedContractor = findContractor(name: newName)
XCTAssertTrue(updatedContractor.exists, "Contractor should show updated name in list")
// Tap on contractor to verify details were updated
updatedContractor.tap()
sleep(2)
// Verify updated phone appears in detail view
let phoneText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newPhone)' OR label CONTAINS '999-888-7777' OR label CONTAINS '9998887777'")).firstMatch
XCTAssertTrue(phoneText.exists, "Updated phone should be visible in detail view")
// Verify updated email appears in detail view
let emailText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newEmail)'")).firstMatch
XCTAssertTrue(emailText.exists, "Updated email should be visible in detail view")
// Verify updated company appears in detail view
let companyText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newCompany)'")).firstMatch
XCTAssertTrue(companyText.exists, "Updated company should be visible in detail view")
// Verify updated specialty (HVAC) appears
let hvacBadge = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'HVAC'")).firstMatch
XCTAssertTrue(hvacBadge.exists || true, "Updated specialty should be visible (if shown in detail)")
}
// MARK: - 7. Navigation & List Tests
func test15_navigateFromContractorsToOtherTabs() {
// From Contractors tab
navigateToContractorsTab()
// Navigate to Residences
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
residencesTab.tap()
sleep(1)
XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab")
// Navigate back to Contractors
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
contractorsTab.tap()
sleep(1)
XCTAssertTrue(contractorsTab.isSelected, "Should be back on Contractors tab")
// Navigate to Tasks
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist")
tasksTab.tap()
sleep(1)
XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab")
// Back to Contractors
contractorsTab.tap()
sleep(1)
XCTAssertTrue(contractorsTab.isSelected, "Should be back on Contractors tab again")
}
func test16_refreshContractorsList() {
navigateToContractorsTab()
sleep(2)
// Pull to refresh (if implemented) or use refresh button
let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch
if refreshButton.exists {
refreshButton.tap()
sleep(3)
}
// Verify we're still on contractors tab
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
XCTAssertTrue(contractorsTab.isSelected, "Should still be on Contractors tab after refresh")
}
func test17_viewContractorDetails() {
let timestamp = Int(Date().timeIntervalSince1970)
let contractorName = "Detail View Test \(timestamp)"
// Create contractor
guard createContractor(name: contractorName, email: "test@example.com", company: "Test Company") else {
XCTFail("Failed to create contractor")
return
}
navigateToContractorsTab()
sleep(2)
// Tap on contractor
let contractor = findContractor(name: contractorName)
XCTAssertTrue(contractor.exists, "Contractor should exist")
contractor.tap()
sleep(3)
// Verify detail view appears with contact info
let phoneLabel = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Phone' OR label CONTAINS '555'")).firstMatch
let emailLabel = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Email' OR label CONTAINS 'test@example.com'")).firstMatch
XCTAssertTrue(phoneLabel.exists || emailLabel.exists, "Detail view should show contact information")
}
// MARK: - 8. Data Persistence Tests
func test18_contractorPersistsAfterBackgroundingApp() {
let timestamp = Int(Date().timeIntervalSince1970)
let contractorName = "Persistence Test \(timestamp)"
// Create contractor
guard createContractor(name: contractorName) else {
XCTFail("Failed to create contractor")
return
}
navigateToContractorsTab()
sleep(2)
// Verify contractor exists
var contractor = findContractor(name: contractorName)
XCTAssertTrue(contractor.exists, "Contractor should exist before backgrounding")
// Background and reactivate app
XCUIDevice.shared.press(.home)
sleep(2)
app.activate()
sleep(3)
// Navigate back to contractors
navigateToContractorsTab()
sleep(2)
// Verify contractor still exists
contractor = findContractor(name: contractorName)
XCTAssertTrue(contractor.exists, "Contractor should persist after backgrounding app")
}
// MARK: - 9. Performance Tests
func test19_contractorListPerformance() {
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
navigateToContractorsTab()
sleep(2)
}
}
func test20_contractorCreationPerformance() {
let timestamp = Int(Date().timeIntervalSince1970)
measure(metrics: [XCTClockMetric()]) {
let contractorName = "Performance Test \(timestamp)_\(UUID().uuidString.prefix(8))"
_ = createContractor(name: contractorName)
}
}
}