From 88a904736036b81a03ad01ee74ae20c34517aa2a Mon Sep 17 00:00:00 2001 From: Dierk Koenig Date: Tue, 3 Oct 2023 23:30:59 +0200 Subject: [PATCH] before week 3, initial Grails artefacts --- build.gradle | 2 +- docs/week03/README-3-MVC.md | 88 +++++++++++++++++++ .../rooms/CalculatorController.groovy | 9 ++ .../rooms/InPlaceCalculatorController.groovy | 29 ++++++ .../taglib/rooms/DecorationTagLib.groovy | 19 ++++ grails-app/utils/rooms/FieldUtil.groovy | 13 +++ .../views/calculator/CalculatorOutput.gsp | 15 ++++ .../views/inPlaceCalculator/_form_row.gsp | 20 +++++ grails-app/views/inPlaceCalculator/calc.gsp | 37 ++++++++ .../groovy/rooms/CalculatorSpec.groovy | 36 ++++++++ .../groovy/rooms/HomeSecondSpec.groovy | 36 ++++++++ .../groovy/rooms/InPlaceCalculatorSpec.groovy | 69 +++++++++++++++ .../resources/public/GradeCalculator.html | 23 +++++ src/main/resources/public/Home.html | 11 +++ src/main/resources/public/Second.html | 11 +++ 15 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 docs/week03/README-3-MVC.md create mode 100644 grails-app/controllers/rooms/CalculatorController.groovy create mode 100644 grails-app/controllers/rooms/InPlaceCalculatorController.groovy create mode 100644 grails-app/taglib/rooms/DecorationTagLib.groovy create mode 100644 grails-app/utils/rooms/FieldUtil.groovy create mode 100644 grails-app/views/calculator/CalculatorOutput.gsp create mode 100644 grails-app/views/inPlaceCalculator/_form_row.gsp create mode 100644 grails-app/views/inPlaceCalculator/calc.gsp create mode 100644 src/integration-test/groovy/rooms/CalculatorSpec.groovy create mode 100644 src/integration-test/groovy/rooms/HomeSecondSpec.groovy create mode 100644 src/integration-test/groovy/rooms/InPlaceCalculatorSpec.groovy create mode 100644 src/main/resources/public/GradeCalculator.html create mode 100644 src/main/resources/public/Home.html create mode 100644 src/main/resources/public/Second.html diff --git a/build.gradle b/build.gradle index e7e7805..374f6b4 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation "org.grails.plugins:events" implementation "org.grails.plugins:gsp" profile "org.grails.profiles:web" - runtimeOnly "org.glassfish.web:el-impl:2.2.1-b05" +// runtimeOnly "org.glassfish.web:el-impl:2.2.1-b05" // disabled because of vulnerability runtimeOnly "com.h2database:h2" runtimeOnly "org.apache.tomcat:tomcat-jdbc" runtimeOnly "javax.xml.bind:jaxb-api:2.3.1" diff --git a/docs/week03/README-3-MVC.md b/docs/week03/README-3-MVC.md new file mode 100644 index 0000000..4ef02ee --- /dev/null +++ b/docs/week03/README-3-MVC.md @@ -0,0 +1,88 @@ +# WebEngineering Module, rooms + +## Goals + +### Abilities +- Writing tests for page navigation +- Writing tests for form submission +- Implementing basic workflow: form - controller - view +- Constructing html views with derived content +- Validating appropriately + +### Knowledge +- Understanding the purpose and benefit of functional tests on the web +- Understanding the purpose and benefit of unit tests +- Understanding the web rooms cycle, request-response paradigm +- Using models for request data binding and response view creation +- Where and how to validate + +## Assignment 1 + +Make sure that you have a Java JDK 17 installed and `JAVA_HOME` +set appropriately. Check by running + + java -version + +Run the tests + + grailsw test-app + +_this will take a while to download the first time_ + +Run the application + + grailsw run-app + +Browse to http://localhost:8080/static/Home.html . + +## Assignment 2 + +Have a look at rooms/src/integration-test/groovy/rooms/HomeSecondSpec.groovy . + +Write a test, that goes to http://www.fhnw.ch/de/ +and clicks on a link with text "Standorte und Kontakt". +Validate the page title. + +## Assignment 3 + +Have a look at +- http://localhost:8080/static/GradeCalculator.html +- rooms/src/integration-test/groovy/rooms/CalculatorSpec.groovy (note the commented line 26) +- rooms/src/main/resources/public/GradeCalculator.html +- rooms/grails-app/controllers/rooms/CalculatorController.groovy +- rooms/views/calculator/CalculatorOutput.gsp (note the output placeholder) +- rooms/src/test/groovy/rooms/CalculatorControllerSpec.groovy + +Uncomment line 26 in the integration test and run `grailsw test-app`. + +Open `/build/reports/tests/index.html` (or look at the internal test runner) +to see which test failed and why. + +Use `${ result }` in CalculatorOutput.gsp to put that calculated result in the right place. + +## Assignment 4 + +In the GradeCalculator: +what happens when _en_ or _exam_ do not represent numbers? + +Extend the integration test to cover the invalid input scenario. + +## Assignment 5 + +What happens when _en_ or _exam_ do not fall into 1.0 - 6.0? + +Write down how you would address this issue. +Unit test or integration test? +Which code needs change: test, controller, view? + + +## Homework + +Take the InPlaceCalculator and modify the current solution so that +- when the "en" field does not validate, + an error message is displayed right beneath the field and + the en input field itself is marked with a red border +- when the "exam" field does not validate, + an error message is displayed right beneath the field and + the exam input field itself is marked with a red border +- note that both, en and exam, may be erroneous at the same time. diff --git a/grails-app/controllers/rooms/CalculatorController.groovy b/grails-app/controllers/rooms/CalculatorController.groovy new file mode 100644 index 0000000..ad8c620 --- /dev/null +++ b/grails-app/controllers/rooms/CalculatorController.groovy @@ -0,0 +1,9 @@ +package rooms + +class CalculatorController { + + def calc(double en, double exam) { + double result = (en + exam) / 2 + render view: "CalculatorOutput", model: [result: result] + } +} diff --git a/grails-app/controllers/rooms/InPlaceCalculatorController.groovy b/grails-app/controllers/rooms/InPlaceCalculatorController.groovy new file mode 100644 index 0000000..53ccbfc --- /dev/null +++ b/grails-app/controllers/rooms/InPlaceCalculatorController.groovy @@ -0,0 +1,29 @@ +package rooms + +import grails.validation.Validateable + +class InPlaceCalculatorController { + + def calc(CalculatorModel calcModel) { + calcModel.en = Math.round(calcModel.en * 10) / 10 + calcModel.exam = Math.round(calcModel.exam * 10) / 10 + calcModel.result = Math.round((calcModel.en + calcModel.exam) / 2) + if (calcModel.hasErrors()) { + calcModel.result = "Cannot calculate. Input data was invalid." + } + render view:'calc', model: [calculatorInstance: calcModel] + } + +} + +class CalculatorModel implements Validateable { + double en = 0.0 + double exam = 0.0 + String result = "" + + static constraints = { + en min:1d, max:6d, scale:1 + exam min:1d, max:6d, scale:1 + result nullable:true + } +} diff --git a/grails-app/taglib/rooms/DecorationTagLib.groovy b/grails-app/taglib/rooms/DecorationTagLib.groovy new file mode 100644 index 0000000..1a00c69 --- /dev/null +++ b/grails-app/taglib/rooms/DecorationTagLib.groovy @@ -0,0 +1,19 @@ +package rooms + +class DecorationTagLib { + static defaultEncodeAs = 'raw' + static namespace = "rooms" + + def decorate = { attributes, body -> + String grade = attributes.grade + def decor = ""; break + case ["4","5","6"] : decor += "-qODY1kxipZ0/Tv5dwDFFntI/AAAAAAAAAjM/cLXT6KEp-bE/s400/sunglasses%2Bemoticon.png'>"; break + default: decor = "" + } + out << decor + out << body() + out << decor + } +} diff --git a/grails-app/utils/rooms/FieldUtil.groovy b/grails-app/utils/rooms/FieldUtil.groovy new file mode 100644 index 0000000..2651ef7 --- /dev/null +++ b/grails-app/utils/rooms/FieldUtil.groovy @@ -0,0 +1,13 @@ +package rooms + +import grails.validation.Validateable +import org.springframework.validation.FieldError + +class FieldUtil { + static boolean hasError(Validateable model, String propertyName) { + null != findError(model, propertyName) + } + static FieldError findError(Validateable model, String propertyName) { + model.errors.fieldErrors.find {it.field == propertyName} + } +} diff --git a/grails-app/views/calculator/CalculatorOutput.gsp b/grails-app/views/calculator/CalculatorOutput.gsp new file mode 100644 index 0000000..da91429 --- /dev/null +++ b/grails-app/views/calculator/CalculatorOutput.gsp @@ -0,0 +1,15 @@ + + + + + Average + + + + +

Your average is placeholder goes here.

+ +

Back to the calculator.

+ + + diff --git a/grails-app/views/inPlaceCalculator/_form_row.gsp b/grails-app/views/inPlaceCalculator/_form_row.gsp new file mode 100644 index 0000000..e95bc00 --- /dev/null +++ b/grails-app/views/inPlaceCalculator/_form_row.gsp @@ -0,0 +1,20 @@ +<%-- + Emitting a single row of a form with label an input field. + Params: + name : name to use for id, name, labelFor + label : what the user sees beneath the input field + model : the values to show and error information + Depends on the "validate" function from outside. +--%> + +<%@ page import="static rooms.FieldUtil.*" %> + +
+ + +
diff --git a/grails-app/views/inPlaceCalculator/calc.gsp b/grails-app/views/inPlaceCalculator/calc.gsp new file mode 100644 index 0000000..e42585d --- /dev/null +++ b/grails-app/views/inPlaceCalculator/calc.gsp @@ -0,0 +1,37 @@ + + + + + + In-Place Calculator + + + + + +
+
+ + + + + +
+ + +
+
+
+ + +
+ + + ${calculatorInstance.result} + +
+ + + + + diff --git a/src/integration-test/groovy/rooms/CalculatorSpec.groovy b/src/integration-test/groovy/rooms/CalculatorSpec.groovy new file mode 100644 index 0000000..c6f831f --- /dev/null +++ b/src/integration-test/groovy/rooms/CalculatorSpec.groovy @@ -0,0 +1,36 @@ +package rooms + +import geb.spock.GebSpec +import grails.testing.mixin.integration.Integration +import spock.lang.Ignore + +/** + * See http://www.gebish.org/manual/current/ for more instructions + */ +@Integration +@Ignore +class CalculatorSpec extends GebSpec { + + + void "Basic calculation"() { + when: + go '/static/GradeCalculator.html' + then: + title == "Grade Calculator" + + when: "set valid input" + $("form").en = 5.0 + $("form").exam = 6.0 + $("input", type: "submit").click() + + then: "Result Page is displayed" + title == "Average" +// $("output").text() == "5.5" + + + when: "click on back link" + $("a", text: "calculator").click() + then: + title == "Grade Calculator" + } +} diff --git a/src/integration-test/groovy/rooms/HomeSecondSpec.groovy b/src/integration-test/groovy/rooms/HomeSecondSpec.groovy new file mode 100644 index 0000000..cf23c92 --- /dev/null +++ b/src/integration-test/groovy/rooms/HomeSecondSpec.groovy @@ -0,0 +1,36 @@ +package rooms + +import geb.spock.GebSpec +import grails.testing.mixin.integration.Integration + + +/** + * See http://www.gebish.org/manual/current/ for more instructions + */ +@Integration +class HomeSecondSpec extends GebSpec { + + void "test home is there"() { + when:"The home page is visited" + go '/static/Home.html' + then:"The title is Homepage" + title == "Homepage" + } + + void "home links to Second and Second links back"() { + when:"The home page is visited" + go '/static/Home.html' + then:"The title is Homepage" + title == "Homepage" + + when: "click on link to second" + $("a", text: "second").click() + then: "Second page is displayed" + title == "Second" + + when: "click on back link" + $("a", text: "home").click() + then: "Home page is displayed, again" + title == "Homepage" + } +} diff --git a/src/integration-test/groovy/rooms/InPlaceCalculatorSpec.groovy b/src/integration-test/groovy/rooms/InPlaceCalculatorSpec.groovy new file mode 100644 index 0000000..1cb3706 --- /dev/null +++ b/src/integration-test/groovy/rooms/InPlaceCalculatorSpec.groovy @@ -0,0 +1,69 @@ +package rooms + +import geb.spock.GebSpec +import grails.testing.mixin.integration.Integration +import spock.lang.Ignore + +/** + * See http://www.gebish.org/manual/current/ for more instructions + */ +@Integration +@Ignore +class InPlaceCalculatorSpec extends GebSpec { + + void "Calculate in place with a self-refreshing view"() { + when: "Go to start GSP page by calling it disguised as HTML" + go '/InPlaceCalculator.html?lang=en' + then: + title == "In-Place Calculator" + + when: "set valid input" + $("form").en = 5.0 + $("form").exam = 6.0 + $("input", type: "submit").click() + + then: "Result is displayed with proper rounding up" + $("output").text() == "6" + } + + void "Invalid input shows error message and sets error class"() { + when: "Go to start GSP page by calling it disguised as HTML" + go '/InPlaceCalculator.html?lang=en' + then: + title == "In-Place Calculator" + + when: "set invalid input" + $("form").en = 0.9 + $("form").exam = 3.0 + $("input", type: "submit").click() + + then: "Result contains error message" + $("output").text() == "Cannot calculate. Input data was invalid." + then: "invalid en field has error class while valid exam input has no class" + $("#en", class:'error') + $("#exam").attr('class') == "" + } + +// TODO: un-comment the commented lines below and see them failing, then make them pass + + void "Invalid input is handled in-place by JS without submission"() { + given: "a valid state" + go '/InPlaceCalculator.html?lang=en' + $("form").en = 3.0 + $("form").exam = 3.0 + when: + $("input", type: "submit").click() + then: "we should have a clean, valid state to start from" + $("#en").attr('class') == "" + when: "we enter some invalid value _without_ submitting" +// def message = withAlert { + $("form").en = 0.9 +// } + then: "the in-place JS logic should kick in" +// $("#en").attr('class') == "error" +// message == "en value needs to be at least 1.0" +// $("#en").focused + } + + +} diff --git a/src/main/resources/public/GradeCalculator.html b/src/main/resources/public/GradeCalculator.html new file mode 100644 index 0000000..242fcaf --- /dev/null +++ b/src/main/resources/public/GradeCalculator.html @@ -0,0 +1,23 @@ + + + + + Grade Calculator + + + +
+
+ + +
+
+ + +
+
+ +
+
+ + diff --git a/src/main/resources/public/Home.html b/src/main/resources/public/Home.html new file mode 100644 index 0000000..d0f7e24 --- /dev/null +++ b/src/main/resources/public/Home.html @@ -0,0 +1,11 @@ + + + + + Homepage + + + + Some text that links to a second page. + + diff --git a/src/main/resources/public/Second.html b/src/main/resources/public/Second.html new file mode 100644 index 0000000..a8d2545 --- /dev/null +++ b/src/main/resources/public/Second.html @@ -0,0 +1,11 @@ + + + + + Second + + + + Some text that links back to the home page. + +