Skip to content

Commit

Permalink
Add support for multiple selection questions (#64)
Browse files Browse the repository at this point in the history
Co-authored-by: Paul Schmiedmayer <[email protected]>
  • Loading branch information
vishnuravi and PSchmiedmayer authored Aug 25, 2023
1 parent f728c67 commit 7dc09f7
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 49 deletions.
76 changes: 63 additions & 13 deletions Example/ExampleUITests/ExampleUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// swiftlint:disable file_length
import XCTest


// We disable type body length rule because this is a test
// swiftlint:disable:next type_body_length
final class ExampleUITests: XCTestCase {
Expand Down Expand Up @@ -114,17 +115,27 @@ final class ExampleUITests: XCTestCase {
let app = XCUIApplication()
app.launch()

let containedValueSetExampleButton = app.collectionViews.buttons["Contained ValueSet Example"]

// Complete questionnaire
let containedValueSetExampleButton = app.collectionViews.buttons["Contained ValueSet Example"]
XCTAssert(containedValueSetExampleButton.waitForExistence(timeout: 2))
containedValueSetExampleButton.tap()
app.tables.staticTexts["Yes"].tap()
app.buttons["Next"].tap()

let yesButton = app.tables.staticTexts["Yes"]
XCTAssert(yesButton.waitForExistence(timeout: 2))
yesButton.tap()

let nextButton = app.buttons["Next"]
XCTAssert(nextButton.waitForExistence(timeout: 2))
nextButton.tap()

// Close the completion step
app.buttons["Done"].tap()
let doneButton = app.buttons["Done"]
XCTAssert(doneButton.waitForExistence(timeout: 2))
doneButton.tap()

// Open context menu and view results
sleep(1)
XCTAssert(containedValueSetExampleButton.waitForExistence(timeout: 2))
containedValueSetExampleButton.press(forDuration: 1.0)
app.collectionViews.buttons["View Responses"].tap()

Expand Down Expand Up @@ -223,8 +234,7 @@ final class ExampleUITests: XCTestCase {
app.launch()

let dateTimeExampleButton = app.collectionViews.buttons["Date and Time Example"]

// Open questionnaire
XCTAssert(dateTimeExampleButton.waitForExistence(timeout: 2))
dateTimeExampleButton.tap()

let dateFormatter = DateFormatter()
Expand All @@ -241,26 +251,35 @@ final class ExampleUITests: XCTestCase {
app.buttons["Next"].tap()

// Chose a date and time
sleep(1)
app.pickerWheels.element(boundBy: 0).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("MMM d"))
app.pickerWheels.element(boundBy: 1).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("h"))
app.pickerWheels.element(boundBy: 2).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("mm"))
app.pickerWheels.element(boundBy: 3).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("a"))
app.buttons["Next"].tap()

// Choose a time
sleep(1)
app.pickerWheels.element(boundBy: 0).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("h"))
app.pickerWheels.element(boundBy: 1).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("mm"))
app.pickerWheels.element(boundBy: 2).adjust(toPickerWheelValue: dateFormatOfRandomDateInProximity("a"))
app.buttons["Next"].tap()

// Close the completion step
app.buttons["Done"].tap()
let doneButton = app.buttons["Done"]
XCTAssert(doneButton.waitForExistence(timeout: 2))
doneButton.tap()

// Open context menu and view results
XCTAssert(dateTimeExampleButton.waitForExistence(timeout: 2))
dateTimeExampleButton.press(forDuration: 1.0)
app.collectionViews.buttons["View Responses"].tap()

let viewReponsesButton = app.collectionViews.buttons["View Responses"]
XCTAssert(viewReponsesButton.waitForExistence(timeout: 2))
viewReponsesButton.tap()

// Check results
sleep(1)
let buttonsInResultView = app.collectionViews.allElementsBoundByIndex[1].buttons
XCTAssertEqual(buttonsInResultView.count, 1)
buttonsInResultView.allElementsBoundByIndex[0].tap()
Expand All @@ -284,6 +303,7 @@ final class ExampleUITests: XCTestCase {
app.tables.staticTexts["Yes"].tap()
app.tables.staticTexts["Chocolate"].tap()
app.tables.staticTexts["Sprinkles"].tap()
app.tables.staticTexts["Marshmallows"].tap()
app.buttons["Next"].tap()

// Finish survey
Expand Down Expand Up @@ -332,7 +352,7 @@ final class ExampleUITests: XCTestCase {
XCTFail("Slider value is not a readable number.")
}
}

// MARK: UI Tests for Clinical Questionnaires

func testPHQ9Example() throws {
Expand Down Expand Up @@ -450,47 +470,72 @@ final class ExampleUITests: XCTestCase {
app.launch()

let multipleEnableWhenButton = app.collectionViews.buttons["Multiple EnableWhen Expressions"]
XCTAssert(multipleEnableWhenButton.waitForExistence(timeout: 2))
multipleEnableWhenButton.tap()

// We will first answer all questions correctly
XCTAssert(app.tables.staticTexts["Yes"].waitForExistence(timeout: 2))
app.tables.staticTexts["Yes"].tap()
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

XCTAssert(app.tables.staticTexts["green"].waitForExistence(timeout: 2))
app.tables.staticTexts["green"].tap()
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()


let integerField = app.textFields.element(boundBy: 0)
XCTAssert(integerField.waitForExistence(timeout: 2))
integerField.tap()
integerField.typeText("12\n")
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

// First result screen appears if at least one answer is correct.
sleep(1)
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

// Second result screen appears if all answers are correct.
sleep(1)
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

// Now the completion screen will appear with a "Done" button that we can tap
sleep(1)
XCTAssert(app.buttons["Done"].waitForExistence(timeout: 2))
app.buttons["Done"].tap()

// Now we relaunch the survey
XCTAssert(multipleEnableWhenButton.waitForExistence(timeout: 2))
multipleEnableWhenButton.tap()

// This time we answer only one question correctly
XCTAssert(app.tables.staticTexts["Yes"].waitForExistence(timeout: 2))
app.tables.staticTexts["Yes"].tap()
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

XCTAssert(app.tables.staticTexts["orange"].waitForExistence(timeout: 2))
app.tables.staticTexts["orange"].tap()
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

XCTAssert(integerField.waitForExistence(timeout: 2))
integerField.tap()
integerField.typeText("2\n")
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

// Only one result screen should appear.
sleep(1)
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2))
app.buttons["Next"].tap()

// Now the completion screen should appear.
sleep(1)
XCTAssert(app.buttons["Done"].waitForExistence(timeout: 2))
app.buttons["Done"].tap()
}

Expand All @@ -499,14 +544,19 @@ final class ExampleUITests: XCTestCase {
app.launch()

let imageCaptureButton = app.collectionViews.buttons["Image Capture Example"]
XCTAssert(imageCaptureButton.waitForExistence(timeout: 2))
imageCaptureButton.tap()

// This example requires access to a device camera, which can't be simulated,
// so we will get an error message.
XCTAssert(app.staticTexts["No camera found. This step cannot be completed."].exists)
XCTAssert(app.staticTexts["No camera found. This step cannot be completed."].waitForExistence(timeout: 2))

app.buttons["Skip"].tap()
let skipButton = app.buttons["Skip"]
XCTAssert(skipButton.waitForExistence(timeout: 2))
skipButton.tap()

app.buttons["Done"].tap()
let doneButton = app.buttons["Done"]
XCTAssert(doneButton.waitForExistence(timeout: 2))
doneButton.tap()
}
}
16 changes: 15 additions & 1 deletion Sources/FHIRQuestionnaires/Resources/FormExample.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
},
{
"linkId": "5ad9456c-1451-4190-fff5-3fd71545a591",
"type": "choice",
"type": "open-choice",
"text": "What is your favorite flavor of ice cream?",
"required": true,
"answerOption": [
Expand Down Expand Up @@ -105,6 +105,20 @@
"type": "choice",
"text": "What is your favorite topping on ice cream?",
"required": false,
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-item-control",
"code": "check-box",
"display": "Checkbox"
}
]
}
},
],
"answerOption": [
{
"valueCoding": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ extension QuestionnaireItem {
guard !answerOptions.isEmpty else {
throw FHIRToResearchKitConversionError.noOptions
}
return ORKTextChoiceAnswerFormat(style: ORKChoiceAnswerStyle.singleChoice, textChoices: answerOptions)
var choiceAnswerStyle = ORKChoiceAnswerStyle.singleChoice
if itemControl == "check-box" {
choiceAnswerStyle = .multipleChoice
}
return ORKTextChoiceAnswerFormat(style: choiceAnswerStyle, textChoices: answerOptions)
case .date:
return ORKDateAnswerFormat(style: ORKDateAnswerStyle.date)
case .dateTime:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,33 +43,42 @@ extension ORKTaskResult {

// MARK: Functions for creating FHIR responses from ResearchKit results

private func appendResponseAnswer(_ value: QuestionnaireResponseItemAnswer.ValueX?, to responseAnswers: inout [QuestionnaireResponseItemAnswer]) {
let responseAnswer = QuestionnaireResponseItemAnswer()
responseAnswer.value = value
responseAnswers.append(responseAnswer)
}

private func createResponse(_ result: ORKResult) -> QuestionnaireResponseItem {
let response = QuestionnaireResponseItem(linkId: FHIRPrimitive(FHIRString(result.identifier)))
let responseAnswer = QuestionnaireResponseItemAnswer()
var responseAnswers: [QuestionnaireResponseItemAnswer] = []

switch result {
case let result as ORKBooleanQuestionResult:
responseAnswer.value = createBooleanResponse(result)
appendResponseAnswer(createBooleanResponse(result), to: &responseAnswers)
case let result as ORKChoiceQuestionResult:
responseAnswer.value = createChoiceResponse(result)
let values = createChoiceResponse(result)
for value in values {
appendResponseAnswer(value, to: &responseAnswers)
}
case let result as ORKFileResult:
responseAnswer.value = createAttachmentResponse(result)
appendResponseAnswer(createAttachmentResponse(result), to: &responseAnswers)
case let result as ORKNumericQuestionResult:
responseAnswer.value = createNumericResponse(result)
appendResponseAnswer(createNumericResponse(result), to: &responseAnswers)
case let result as ORKDateQuestionResult:
responseAnswer.value = createDateResponse(result)
appendResponseAnswer(createDateResponse(result), to: &responseAnswers)
case let result as ORKScaleQuestionResult:
responseAnswer.value = createScaleResponse(result)
appendResponseAnswer(createScaleResponse(result), to: &responseAnswers)
case let result as ORKTextQuestionResult:
responseAnswer.value = createTextResponse(result)
appendResponseAnswer(createTextResponse(result), to: &responseAnswers)
case let result as ORKTimeOfDayQuestionResult:
responseAnswer.value = createTimeResponse(result)
appendResponseAnswer(createTimeResponse(result), to: &responseAnswers)
default:
// Unsupported result type
responseAnswer.value = nil
appendResponseAnswer(nil, to: &responseAnswers)
}

response.answer = [responseAnswer]
response.answer = responseAnswers
return response
}

Expand Down Expand Up @@ -110,27 +119,28 @@ extension ORKTaskResult {
return .string(FHIRPrimitive(FHIRString(text)))
}

private func createChoiceResponse(_ result: ORKChoiceQuestionResult) -> QuestionnaireResponseItemAnswer.ValueX? {
private func createChoiceResponse(_ result: ORKChoiceQuestionResult) -> [QuestionnaireResponseItemAnswer.ValueX] {
guard let answerArray = result.answer as? NSArray, answerArray.count > 0 else { // swiftlint:disable:this empty_count
return nil
return []
}

// If the result is a string (i.e. the user selected the "other" option and entered a free-text answer), return a String
if let answerString = answerArray[0] as? String {
return .string(FHIRPrimitive(FHIRString(answerString)))
}
var responses: [QuestionnaireResponseItemAnswer.ValueX] = []

// If the result is a dictionary containing a code and system, return a Coding
guard let valueCodingString = answerArray[0] as? String,
let valueCoding = ValueCoding(rawValue: valueCodingString) else {
return nil
for answer in answerArray {
// Check if answer can be treated as a ValueCoding first
if let valueCodingString = answer as? String, let valueCoding = ValueCoding(rawValue: valueCodingString) {
let coding = Coding(
code: FHIRPrimitive(FHIRString(valueCoding.code)),
system: FHIRPrimitive(FHIRURI(stringLiteral: valueCoding.system))
)
responses += [.coding(coding)]
} else if let answerString = answer as? String {
// If not, fall back to treating it as a regular string
responses += [.string(FHIRPrimitive(FHIRString(answerString)))]
}
}

let coding = Coding(
code: FHIRPrimitive(FHIRString(valueCoding.code)),
system: FHIRPrimitive(FHIRURI(stringLiteral: valueCoding.system))
)
return .coding(coding)
return responses
}

private func createBooleanResponse(_ result: ORKBooleanQuestionResult) -> QuestionnaireResponseItemAnswer.ValueX? {
Expand Down
Loading

0 comments on commit 7dc09f7

Please sign in to comment.