diff --git a/INSTALL.md b/INSTALL.md index 250d2cd0..f0f9530f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -36,7 +36,7 @@ Full installation details for these tools are provided by the projects themselve - You'll be using a *WSL Terminal* for many of the steps below. - Then follow the instructions for installing [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/). Be sure to follow the directions for the "WSL 2 backend" (not the "Hyper-V backend"). - For MacOS: - - Follow the instructions for installing [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/). Be sure to choose chose "Mac with Intel Chip" or "Mac with Apple Silicon" as appropriate for your Mac. If you are unsure, choose "About this Mac" from the Apple menu and check which "Chip" is in your Mac (Intel or Apple). + - Follow the instructions for installing [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/). Be sure to choose "Mac with Intel Chip" or "Mac with Apple Silicon" as appropriate for your Mac. If you are unsure, choose "About this Mac" from the Apple menu and check which "Chip" is in your Mac (Intel or Apple). - For Linux: - Follow the instructions for installing [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/). - Test your installation of Docker Desktop. diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 1514a6d1..366e7d29 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -3,9 +3,11 @@ FROM node:alpine3.17 WORKDIR /app -# Get the package.json and install dependencies -COPY package.json . -RUN npm install +# Get the package.json, package-lock.json and install the dependencies +# NOTE: Use npm ci here so that we use package-lock.json and +# so that we are alerted if there are any changes. +COPY package.json package-lock.json ./ +RUN npm ci # Start the express api hander. CMD ["npm", "start"] \ No newline at end of file diff --git a/docker/api/after.bash b/docker/api/after.bash index 43fa448e..53a77105 100644 --- a/docker/api/after.bash +++ b/docker/api/after.bash @@ -1,2 +1,3 @@ # Delete the package.json file that was copied into the build context. -rm package.json \ No newline at end of file +rm package.json +rm package-lock.json \ No newline at end of file diff --git a/docker/api/before.bash b/docker/api/before.bash index 7e8ce85a..8e68c8b9 100644 --- a/docker/api/before.bash +++ b/docker/api/before.bash @@ -1,2 +1,3 @@ # Copy the package.json file to the build context. -cp ../../farmdata2/farmdata2_api/package.json . +cp ../../farmdata2/farmdata2_api/package.json ./ +cp ../../farmdata2/farmdata2_api/package-lock.json ./ \ No newline at end of file diff --git a/docker/api/repo.txt b/docker/api/repo.txt index 43ef8435..43ba32af 100644 --- a/docker/api/repo.txt +++ b/docker/api/repo.txt @@ -1 +1 @@ -api:fd2.1 +api:fd2.2 diff --git a/docker/build-images.bash b/docker/build-images.bash index 3345cb42..ed74ce99 100755 --- a/docker/build-images.bash +++ b/docker/build-images.bash @@ -1,15 +1,26 @@ #!/bin/bash -# Build and push to DockerHub multi architecture images -# for all of the containers used by FarmData2. +# Build and optionally push to DockerHub multi architecture images +# or single architecture local images for FarmData2. -LOGGED_IN=$(cat ~/.docker/config.json 2> /dev/null | grep "index.docker.io" | wc -l | cut -f 8 -d ' ') -if [ "$LOGGED_IN" == "0" ]; -then - echo "Please log into Docker Hub before building images." - echo " Use: docker login" - echo "This allows multi architecture images to be pushed." - exit -1 +LOCAL_BUILD=0 + +# Check for --local flag +if [ "$1" == "--local" ]; then + LOCAL_BUILD=1 + shift # Remove the --local argument +fi + +# Check for Docker Hub login only if not a local build +if [ "$LOCAL_BUILD" -eq 0 ]; then + LOGGED_IN=$(cat ~/.docker/config.json 2> /dev/null | grep "index.docker.io" | wc -l | cut -f 8 -d ' ') + if [ "$LOGGED_IN" == "0" ]; + then + echo "Please log into Docker Hub before building images." + echo " Use: docker login" + echo "This allows multi architecture images to be pushed." + exit -1 + fi fi if [ $# -lt 1 ]; @@ -21,19 +32,21 @@ then exit -1 fi -# Create the builder if it doesn't exist. -FD2_BUILDER=$(docker buildx ls | grep "fd2builder" | wc -l | cut -f 8 -d ' ') -if [ "$FD2_BUILDER" == "0" ]; -then - echo "Making new builder for FarmData2 images." - docker buildx create --name fd2builder -fi +# Create the builder if it doesn't exist and if not a local build +if [ "$LOCAL_BUILD" -eq 0 ]; then + FD2_BUILDER=$(docker buildx ls | grep "fd2builder" | wc -l | cut -f 8 -d ' ') + if [ "$FD2_BUILDER" == "0" ]; + then + echo "Making new builder for FarmData2 images." + docker buildx create --name fd2builder + fi -# Switch to use the fd2builder. -echo "Using the fd2bilder." -docker buildx use fd2builder + # Switch to use the fd2builder. + echo "Using the fd2builder." + docker buildx use fd2builder +fi -# Build and push each of the images to Docker Hub. +# Build (and push) each of the images for IMAGE in "$@" do if [ ! -f $IMAGE/Dockerfile ] | [ ! -f $IMAGE/repo.txt ]; @@ -54,7 +67,14 @@ do TAG=$(cat repo.txt) REPO="farmdata2/$TAG" echo " Performing docker build using tag $REPO ..." - docker buildx build --platform linux/amd64,linux/arm64 -t $REPO --push . + + if [ "$LOCAL_BUILD" -eq 1 ]; then + # Local build for the architecture of the local machine + docker build -t $REPO . + else + # Multi architecture build and push + docker buildx build --platform linux/amd64,linux/arm64 -t $REPO --push . + fi if [ -f after.bash ]; then diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index cd163f68..9fd44d32 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -49,6 +49,7 @@ RUN apt install -y --no-install-recommends \ && npm install -g \ @vue/cli@5.0.8 \ jsdoc@3.6.7 \ + jsdoc-escape-at \ jsdoc-vuejs@3.0.9 \ vue-template-compiler@2.6.14 @@ -142,7 +143,9 @@ RUN npm install -d \ @cypress/webpack-dev-server@2.5.0 \ @cypress/vue2@1.1.2 \ path@0.12.7 \ - axios@0.24.0 + axios@0.24.0 \ + bootstrap@3.3.7 + WORKDIR /home/$USERNAME # Configure the VNC server so that it allows fd2dev to connect without diff --git a/docker/dev/bash_aliases b/docker/dev/bash_aliases index 099c6f1c..62616baf 100644 --- a/docker/dev/bash_aliases +++ b/docker/dev/bash_aliases @@ -8,7 +8,7 @@ setDB () } # Clear the drupal cache in the fd2_farmdata2 container. -alias clearDupalCache="docker exec -it fd2_farmdata2 drush cc all" +alias clearDrupalCache="docker exec -it fd2_farmdata2 drush cc all" # Alias codium for running VSCodium with no-sandbox. codium () diff --git a/docker/dev/repo.txt b/docker/dev/repo.txt index 768cc8e6..4b73c190 100644 --- a/docker/dev/repo.txt +++ b/docker/dev/repo.txt @@ -1 +1 @@ -dev:fd2.3 +dev:fd2.4 diff --git a/docker/dev/startup.bash b/docker/dev/startup.bash index 12a0807f..fb668436 100755 --- a/docker/dev/startup.bash +++ b/docker/dev/startup.bash @@ -1,36 +1,36 @@ #!/bin/bash -# Cleanup from past vnc session that may hav been running in the container. +# Cleanup from past VNC session that may have been running in the container. # This clean-up is not done when the container is stopped (not deleted). -rm /tmp/.X11-unix/X1 -rm /tmp/.X1-lock +# Using `rm -f` to ignore nonexistent files and suppress error messages. +rm -f /tmp/.X11-unix/X1 +rm -f /tmp/.X1-lock -# Ensure that the fd2dev user is in a group that has RW permission to -# the docker.sock file. This will allow fd2dev to use the docker on docker -# to interact with containers within the development environment. -# Note: This must be here instead of in Dockerfile so the GID for the -# docker.sock in the container can match the one on the host. -# Note: The docker.gid file is mounted into the container by docker-compose. -HOST_DOCKER_GID=$(cat ~/.fd2/gids/docker.gid) -HOST_DOCKER_GID_IN_CONTAINER=$(echo /etc/group | grep ":$HOST_DOCKER_GID:") -if [ -z "$HOST_DOCKER_GID_IN_CONTAINER" ]; -then - echo "fd2dev" | sudo -S groupadd --gid $HOST_DOCKER_GID fd2docker +# The sudo password is assumed to be 'fd2dev' for all operations. +# Using a single invocation of sudo and a here-document to execute multiple commands. +# This reduces the number of times the password needs to be echoed and sudo is called. +# This also reduces the script complexity and potential points of failure. +sudo -S sh < + + + diff --git a/farmdata2/farmdata2_modules/fd2_school/api2/api2.html b/farmdata2/farmdata2_modules/fd2_school/api2/api2.html new file mode 100644 index 00000000..73679eb4 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/api2/api2.html @@ -0,0 +1,142 @@ + + + + Harvest Report + + + +
+

{{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

+

This page is a mockup of a simplified harvest report.

+ + +
+ + + + +
+ + + + +
+ +
+
+

{{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

+ +

There are no matching records.

+ + + + + + + + + + + + + + + + + +
#DateAreaCropYieldUnits
{{ index + 1 }}{{ row.date }}{{ row.area }}{{ row.crop }}{{ row.yield }}{{ row.units }}
+ +

Farm: {{ farm }}

+

User: {{ user }}

+

Language: {{ language }}

+
+ + + + + diff --git a/farmdata2/farmdata2_modules/fd2_school/e2e/e2e.defaults.spec.js b/farmdata2/farmdata2_modules/fd2_school/e2e/e2e.defaults.spec.js new file mode 100644 index 00000000..00133054 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/e2e/e2e.defaults.spec.js @@ -0,0 +1,16 @@ +describe("Test the harvest report default values", () => { + beforeEach(() => { + cy.login("manager1", "farmdata2"); + cy.visit("/farm/fd2-school/e2e"); + }); + + it("Check report details", () => { + cy.get("[data-cy=farm-name]").should("not.exist") + cy.get("[data-cy=language]").should("not.exist") + cy.get("[data-cy=user]").should("not.exist") + cy.get("[data-cy=generate-report]").click() + cy.get("[data-cy=farm-name]").should("have.text","Farm: Sample Farm") + cy.get("[data-cy=language]").should("have.text","en") + cy.get("[data-cy=user]").should("have.text","manager1") + }) +}); diff --git a/farmdata2/farmdata2_modules/fd2_school/e2e/e2e.html b/farmdata2/farmdata2_modules/fd2_school/e2e/e2e.html new file mode 100644 index 00000000..62c00177 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/e2e/e2e.html @@ -0,0 +1,143 @@ + + + +

Harvest Report

+ + + +
+

{{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

+

This page is a mockup of a simplified harvest report.

+ + +
+ + + + +
+ + + + +
+ +
+
+

{{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

+ +

There are no matching records.

+ + + + + + + + + + + + + + + + + +
#DateAreaCropYieldUnits
{{ index + 1 }}{{ row.date }}{{ row.area }}{{ row.crop }}{{ row.yield }}{{ row.units }}
+ +
  • Farm: {{ farm }}
  • +
  • User: {{ user }}
  • +
  • Language: {{ language }}
  • + +
    + + + + + diff --git a/farmdata2/farmdata2_modules/fd2_school/fd2/fd2.defaults.spec.js b/farmdata2/farmdata2_modules/fd2_school/fd2/fd2.defaults.spec.js new file mode 100644 index 00000000..e49cc083 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/fd2/fd2.defaults.spec.js @@ -0,0 +1,13 @@ +describe("Test the harvest report default values", () => { + beforeEach(() => { + cy.login("manager1", "farmdata2"); + cy.visit("/farm/fd2-school/e2e"); + }); + + it("Check crop list", () => { + cy.get("[data-cy=crop-list] > [data-cy=dropdown-input] > [data-cy=option0]").should("have.text", "All") + cy.get("[data-cy=crop-list] > [data-cy=dropdown-input] > [data-cy=option1]").should("have.text", "ARUGULA") + cy.get("[data-cy=crop-list] > [data-cy=dropdown-input] > [data-cy=option5]").should("have.text", "BEAN-FAVA") + cy.get("[data-cy=crop-list] > [data-cy=dropdown-input]").children().should("have.length", "112") + }) +}); diff --git a/farmdata2/farmdata2_modules/fd2_school/fd2/fd2.html b/farmdata2/farmdata2_modules/fd2_school/fd2/fd2.html new file mode 100644 index 00000000..ad608c95 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/fd2/fd2.html @@ -0,0 +1,154 @@ + + + +

    Harvest Report

    + + + +
    +

    {{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

    +

    This page is a mockup of a simplified harvest report.

    + + +
    + + + + +
    + + Crop: + + + +
    + +
    +
    +

    {{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

    + +

    There are no matching records.

    + + + + + + + + + + + + + + + + + +
    #DateAreaCropYieldUnits
    {{ index + 1 }}{{ row.date }}{{ row.area }}{{ row.crop }}{{ row.yield }}{{ row.units }}
    + +
  • Farm: {{ farm }}
  • +
  • User: {{ user }}
  • +
  • Language: {{ language }}
  • + +
    + + + + + diff --git a/farmdata2/farmdata2_modules/fd2_school/fd2_school.module b/farmdata2/farmdata2_modules/fd2_school/fd2_school.module index 167ca8ef..c10b0822 100644 --- a/farmdata2/farmdata2_modules/fd2_school/fd2_school.module +++ b/farmdata2/farmdata2_modules/fd2_school/fd2_school.module @@ -61,6 +61,75 @@ function fd2_school_menu() { ); // Add items blocks for new sub-tabs here. + // Add a sub-tab named HTML with content in ./html/html.html. + $items['farm/fd2-school/html'] = array( + 'title' => 'HTML', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('html'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); + + // Add a sub-tab named Vue1 with content in ./vue1/vue1.html. + $items['farm/fd2-school/vue1'] = array( + 'title' => 'Vue1', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('vue1'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); + + // Add a sub-tab named Vue2 with content in ./vue2/vue2.html. + $items['farm/fd2-school/vue2'] = array( + 'title' => 'Vue2', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('vue2'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); + + // Add a sub-tab named API with content in ./api/api.html. + $items['farm/fd2-school/api'] = array( + 'title' => 'API', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('api'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); + + // Add a sub-tab named API2 with content in ./api/api2.html. + $items['farm/fd2-school/api2'] = array( + 'title' => 'API2', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('api2'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); + + // Add a sub-tab named e2e with content in ./e2e/e2e.html. + $items['farm/fd2-school/e2e'] = array( + 'title' => 'e2e', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('e2e'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); + + // Add a sub-tab named fd2 with content in ./fd2/fd2.html. + $items['farm/fd2-school/fd2'] = array( + 'title' => 'fd2', + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'fd2_school_view', + 'page arguments' => array('fd2'), + 'access arguments' => array('view fd2 school'), + 'weight' => 110, + ); return $items; }; diff --git a/farmdata2/farmdata2_modules/fd2_school/first.spec.js b/farmdata2/farmdata2_modules/fd2_school/first.spec.js new file mode 100644 index 00000000..8f9b21e0 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/first.spec.js @@ -0,0 +1,5 @@ +describe('empty spec', () => { + it('passes', () => { + cy.visit('https://example.cypress.io') + }) +}) \ No newline at end of file diff --git a/farmdata2/farmdata2_modules/fd2_school/html/html.html b/farmdata2/farmdata2_modules/fd2_school/html/html.html new file mode 100644 index 00000000..6a9eb51c --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/html/html.html @@ -0,0 +1,81 @@ + + + + Harvest Report + + + +

    Harvest Report

    +

    This page is a mockup of a simplified harvest report.

    + + +
    + + + + +
    + + + + +
    + +
    +
    +

    My Sample Harvest Report

    + +

    Details:

    + + + + + + + + + + + + + + + + + + + + + + + +
    DateAreaCropYieldUnits
    05/02/2018Chuau-1Kale10Bunches
    05/05/2018SQ7Kale7Bunches
    + + + \ No newline at end of file diff --git a/farmdata2/farmdata2_modules/fd2_school/vue1/vue1.html b/farmdata2/farmdata2_modules/fd2_school/vue1/vue1.html new file mode 100644 index 00000000..ff5ebad2 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/vue1/vue1.html @@ -0,0 +1,84 @@ + + + + Harvest Report + + + +
    +

    {{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

    +

    This page is a mockup of a simplified harvest report.

    + + +
    + + + + +
    + + + + +
    + +
    +
    +

    {{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

    + +

    Details:

    + + + + + + + + + + + + + + + +
    DateAreaCropYieldUnits
    {{ log.date }}{{ log.area }}{{ log.crop }}{{ log.yield }}{{ log.units }}
    +
    + + + + + diff --git a/farmdata2/farmdata2_modules/fd2_school/vue2/vue2.html b/farmdata2/farmdata2_modules/fd2_school/vue2/vue2.html new file mode 100644 index 00000000..23720ec2 --- /dev/null +++ b/farmdata2/farmdata2_modules/fd2_school/vue2/vue2.html @@ -0,0 +1,92 @@ + + + + Harvest Report + + + +
    +

    {{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

    +

    This page is a mockup of a simplified harvest report.

    + + +
    + + + + +
    + + + + +
    + +
    +
    +

    {{ reportTitle ? reportTitle : 'Mock Harvest Report' }}

    + +

    There are no matching records.

    + + + + + + + + + + + + + + + + + +
    #DateAreaCropYieldUnits
    {{ index + 1 }}{{ log.date }}{{ log.area }}{{ log.crop }}{{ log.yield }}{{ log.units }}
    +
    + + + + + diff --git a/farmdata2/farmdata2_modules/resources/BannerComponent.js b/farmdata2/farmdata2_modules/resources/BannerComponent.js new file mode 100644 index 00000000..74f4f6b5 --- /dev/null +++ b/farmdata2/farmdata2_modules/resources/BannerComponent.js @@ -0,0 +1,133 @@ +/** + * A Vue component that augments the Bootstrap CSS class for the alert banner to generate dynamic success/error/message banner for pages. + * + *

    data-cy attributes

    + * + * + * + * + * + * + * + *
    Value Description
    banner-handler div containing bootstrap alert banner
    banner-close The button element that contains the 'X' symbol that must be clicked to close the alert banner
    banner-message The paragraph element that contains the message displayed inside the alert banner
    + * + *
    + * @vue-prop {Object} message - Contains the message and class for the alert banner + * @vue-prop {Boolean} visible - Set to true to show the alert banner. False makes the banner invisible. + * @vue-prop {Boolean} timeout - Sets whether or not the banner dismisses itself which disables the 'x' to close. Set to false by default. Currently set to self-dismiss after 5 seconds. + *
    + * + *
    + * @vue-event banner-hidden - Indicates to the parent page that the visibility in the child component has + * been set to 'false' and the parent page needs to update its own variable for banner visibility. This means + * each page needs to implement a function that catches this emit and matches their visibility with the child's internal visibility. + *
    + * + * + * @example Handling Banner Visibility in Parent Page + * // A Banner component element tag may look like this: + * + * + * + * // where 'hideBanner' is the name of your method in the parent page. + * // the method in the parent page should look like the following: + * function hideBanner(){ + * // 'bannerVisibility' would be the Vue data in charge of determining the banner's visibility in the parent page + * this.bannerVisibility = false + * } + * + * @module + */ +let BannerComponent = { + template: + `
    + +

    {{ bannerMsg }}

    +
    `, + props: { + message: { + type: Object, + default: null + }, + visible: { + type: Boolean, + required: true, + }, + timeout: { + type: Boolean, + default: false + } + }, + data() { + return { + bannerClass: this.message.class, + bannerMsg: this.message.msg, + isVisible: this.visible, + timeoutBool: this.timeout, + topNav: '0px', + } + }, + watch: { + message(newObj) { + this.bannerClass = newObj.class + this.bannerMsg = newObj.msg + + }, + visible(newbool) { + this.isVisible = newbool + if(this.timeoutBool == true && this.isVisible){ + setTimeout(() => { + this.isVisible = false + this.$emit('banner-hidden') + }, 5000) + } + }, + + timeout(newBool){ + this.timeoutBool = newBool + } + }, + methods: { + hideBanner() { + this.isVisible = false + this.$emit('banner-hidden') + }, + }, + mounted: + // try/catch block exists for Cypress component testing since those are + // isolated. Naturally there will be no navbar in those isolated tests. + function () { + this.$nextTick(function () { + try{ + var pageHeader = document.getElementById('navbar') + var headerBounds = pageHeader.getBoundingClientRect() + this.topNav = String(headerBounds.height) + 'px' + } + catch(err){ + this.topNav = '0px' + } + }) + }, +} +/* + * Export the ErrorBannerComponent object as a CommonJS component + * so that it can be required by the component test. + */ +try { + module.exports = { + BannerComponent + } +} +catch {} diff --git a/farmdata2/farmdata2_modules/resources/BannerComponent.spec.comp.js b/farmdata2/farmdata2_modules/resources/BannerComponent.spec.comp.js new file mode 100644 index 00000000..ea8d7c4e --- /dev/null +++ b/farmdata2/farmdata2_modules/resources/BannerComponent.spec.comp.js @@ -0,0 +1,219 @@ +import { mount } from '@cypress/vue2' +import { shallowMount } from '@vue/test-utils' + +var BannerComponent = require("./BannerComponent.js") +var BannerComponent = BannerComponent.BannerComponent + +describe('BannerComponent tests', () => { + context('test if props set the initial values', () => { + beforeEach(() => { + mount(BannerComponent, { + propsData: { + message: {"msg": "Test: Hello, I render!", "class": "alert alert-info"}, + visible: true, + }, + }) + }) + + it('renders the component in DOM', () => { + cy.get('[data-cy=banner-handler]').should('exist') + }) + + it('component should be visible', () => { + cy.get('[data-cy=banner-handler]').should('be.visible') + }) + + it('has default banner message', () => { + cy.get('[data-cy=banner-handler] > [data-cy=banner-message]') + .should('have.text', 'Test: Hello, I render!') + }) + + it('has default banner class', () => { + cy.get('[data-cy=banner-handler]') + .should('have.class', 'alert alert-info') + }) + }) + + context('test prop with an updated banner object', () => { + beforeEach(() => { + mount(BannerComponent, { + propsData: { + message: {"msg": "Test: I am a test message", "class": "alert alert-info"}, + visible: true, + }, + }) + }) + + it('renders the component', () => { + cy.get('[data-cy=banner-handler]').should('exist') + }) + + it('has updated banner message', () => { + cy.get('[data-cy=banner-handler] > [data-cy=banner-message]') + .should('have.text', 'Test: I am a test message') + }) + + it('has updated banner class', () => { + cy.get('[data-cy=banner-handler]') + .should('have.class', 'alert alert-info') + }) + }) + + context('test banner with timeout', () => { + let wrapper + let div + let spy + beforeEach(() => { + div = document.createElement('div') + document.body.appendChild(div) + spy = cy.spy() + wrapper = shallowMount(BannerComponent, { + attachTo: div, + propsData: { + message: {"msg": "Test: I am a test message", "class": "alert alert-info"}, + visible: false, + timeout: true, + }, + }) + }) + + afterEach(() => { + wrapper.destroy() + }) + + it('test hide banner', () => { + expect(wrapper.vm.visible).to.equal(false) + expect(wrapper.vm.isVisible).to.equal(false) + cy.wrap(wrapper.setProps({ visible: true })) + cy.wait(5000) + .then(() => { + expect(wrapper.vm.isVisible).to.equal(false) + }) + }) + + it('test no emit if parent page set prop to false', () => { + expect(wrapper.vm.visible).to.equal(false) + expect(wrapper.vm.isVisible).to.equal(false) + cy.wrap(wrapper.setProps({ visible: true })) + .then(() => { + expect(wrapper.vm.isVisible).to.equal(false) + }) + cy.wrap(wrapper.setProps({ visible: false })) + .then(() => { + expect(wrapper.vm.isVisible).to.equal(false) + expect(spy).to.not.be.called + }) + }) + }) + + context('test banner without timeout', () => { + beforeEach(() => { + mount(BannerComponent, { + propsData: { + message: {"msg": "Test: I am a test message", "class": "alert alert-info"}, + visible: true, + }, + }) + }) + + it('test hide banner', () => { + cy.get('[data-cy=banner-handler]') + .should('be.visible') + + cy.get('[data-cy=banner-handler] > [data-cy=banner-close]') + .click() + + cy.get('[data-cy=banner-handler]') + .should('not.be.visible') + }) + }) + + context('test prop changes', () => { + let comp; + beforeEach(() => { + comp = shallowMount(BannerComponent, { + propsData: { + message: {"msg": "Test: I am a test message", "class": "alert alert-info"}, + visible: true, + timeout: 5000, + }, + }) + }) + + it('change update banner object', () => { + expect(comp.vm.message.msg).to.equal('Test: I am a test message') + expect(comp.vm.message.class).to.equal('alert alert-info') + cy.wrap(comp.setProps({ message: {"msg": "Cypress Test: Hello!", "class": "alert alert-warning"}})) + .then(() => { + expect(comp.vm.message.msg).to.equal('Cypress Test: Hello!') + expect(comp.vm.message.class).to.equal('alert alert-warning') + }) + }) + + it('change visibility', () => { + expect(comp.vm.visible).to.equal(true) + cy.wrap(comp.setProps({ visible: false })) + .then(() => { + expect(comp.vm.visible).to.equal(false) + }) + }) + + it('change timeout', () => { + expect(comp.vm.timeout).to.equal(5000) + cy.wrap(comp.setProps({ timeout: null })) + .then(() => { + expect(comp.vm.timeout).to.equal(null) + }) + }) + }) + + context('test banner hidden emit', () => { + + it('banner emit occurs when x is clicked', () => { + const spy = cy.spy() + mount(BannerComponent, { + propsData: { + message: {"msg": "Test: I am a test message", "class": "alert alert-info"}, + visible: true, + }, + listeners: { + 'banner-hidden' : spy + }, + }) + cy.get('[data-cy=banner-handler]').should('exist') + cy.get('[data-cy=banner-handler] > [data-cy=banner-close]') + .click() + .then(() => { + expect(spy).to.be.called + }) + }) + + it('banner emit occurs when timeout is enabled', () => { + let wrapper + let div + const spy = cy.spy() + + div = document.createElement('div') + document.body.appendChild(div) + + wrapper = shallowMount(BannerComponent, { + attachTo: div, + propsData: { + message: {"msg": "Test: I am a test message", "class": "alert alert-info"}, + visible: false, + timeout: true, + }, + listeners: { + 'banner-hidden' : spy + }, + }) + + cy.get('[data-cy=banner-handler]').should('exist') + cy.wrap(wrapper.setProps({visible : true})) + cy.wait(5000) + .then(() => { + expect(spy).to.be.called + }) + }) + }) +}) \ No newline at end of file diff --git a/farmdata2/farmdata2_modules/resources/BannerMessageConstants.js b/farmdata2/farmdata2_modules/resources/BannerMessageConstants.js new file mode 100644 index 00000000..6788eb1c --- /dev/null +++ b/farmdata2/farmdata2_modules/resources/BannerMessageConstants.js @@ -0,0 +1,18 @@ +/** + * Map of common banner messages used in the BannerComponent. + * // ui { contains banner messages for the ui page. } + */ +var bannerMessageMap = { + ui: { + error: {"msg": "Error! This is what an error banner looks like.", "class": "alert alert-danger alert-dismissible"}, + success: {"msg": "Success! This is what a success banner looks like.", "class": "alert alert-success alert-dismissible"}, + msg: {"msg": "Message! This is what a message banner looks like.", "class": "alert alert-info alert-dismissible"}, + } +} + +try { + module.exports = { + bannerMessageMap + } +} +catch {} \ No newline at end of file diff --git a/farmdata2/farmdata2_modules/resources/CustomTableComponent.js b/farmdata2/farmdata2_modules/resources/CustomTableComponent.js index 4b9b0cdf..cbb1169f 100644 --- a/farmdata2/farmdata2_modules/resources/CustomTableComponent.js +++ b/farmdata2/farmdata2_modules/resources/CustomTableComponent.js @@ -30,8 +30,8 @@ catch(err) { * ri-cbutton The td element containing the checkbox for the ith table row if it appears. i=0,1,2... * ri-cbuttonCheckbox The checkbox element for the ith table row if custom buttons or deleting is enabled, i=0,1,2,... * ri The tr element for the ith table row, i=0,1,2,... - * td-ricj The td element in the ith row and jth column, i,j=0,1,2... - * ri-* The div for plain text in the ith row and the indicated column, * is replaced by the column header. i=0,1,2.... + * td-ricj The td element in the ith row and jth column, i,j=0,1,2... Note: using td-ricj in cypress tests will cause five additional whitespaces to appear in the td element retrieved. To fix this issue, make sure to use contains.text instead of have.text + * ri-* The div for plain text in the ith row and the indicated column, * is replaced by the column header. i=0,1,2.... Note: using this cy attribute circumvents the issue mentioned in the line above since it does not add five extra whitespaces to the td element. Therefore, it is fine to use have.text * ri-*-input The input element ith row and the indicated column in edit mode, * is replaced by the column header. i=0,1,2.... * ri-edit-button The edit button in the ith row, i=0,1,2.... * ri-save-button The save button in the ith row, i=0,1,2.... diff --git a/farmdata2/farmdata2_modules/resources/FarmOSAPI.js b/farmdata2/farmdata2_modules/resources/FarmOSAPI.js index 59fc6d4b..002b22ad 100644 --- a/farmdata2/farmdata2_modules/resources/FarmOSAPI.js +++ b/farmdata2/farmdata2_modules/resources/FarmOSAPI.js @@ -97,14 +97,12 @@ function getAllPages(endpoint, arr=[]) { * // Note that the Map will not be available until the then() executes. */ function getIDToUserMap(){ - return new Promise((resolve, reject) => { - getMap('/user', 'uid', 'name') - .then((map) => { - map.delete(null); - resolve(map) - }, - (error) => { - reject(error); + return new Promise ((resolve, reject) => { + axios.get("http://fd2_api/users/mapById") + .then((response) => { + resolve(new Map(Object.entries(response.data))) + }).catch(function (err) { + reject(err) }) }) } @@ -127,14 +125,12 @@ function getIDToUserMap(){ * // Note that the Map will not be available until the then() executes. */ function getUserToIDMap(){ - return new Promise((resolve, reject) => { - getMap('/user', 'name', 'uid') - .then((map) => { - map.delete('Anonymous'); - resolve(map) - }, - (error) => { - reject(error); + return new Promise ((resolve, reject) => { + axios.get("http://fd2_api/users/mapByName") + .then((response) => { + resolve(new Map(Object.entries(response.data))) + }).catch(function (err) { + reject(err) }) }) } @@ -157,7 +153,14 @@ function getUserToIDMap(){ * // Note that the Map will not be available until the then() executes. */ function getIDToCropMap(){ - return getMap('/taxonomy_term.json?bundle=farm_crops', 'tid', 'name') + return new Promise ((resolve, reject) => { + axios.get("http://fd2_api/crops/mapById") + .then((response) => { + resolve(new Map(Object.entries(response.data))) + }).catch(function (err) { + reject(err) + }) + }) } /** @@ -178,7 +181,14 @@ function getIDToCropMap(){ * // Note that the Map will not be available until the then() executes. */ function getCropToIDMap(){ - return getMap('/taxonomy_term.json?bundle=farm_crops', 'name', 'tid') + return new Promise ((resolve, reject) => { + axios.get("http://fd2_api/crops/mapByName") + .then((response) => { + resolve(new Map(Object.entries(response.data))) + }).catch(function (err) { + reject(err) + }) + }) } /** @@ -199,7 +209,14 @@ function getCropToIDMap(){ * // Note that the Map will not be available until the then() executes. */ function getIDToAreaMap(){ - return getMap('/taxonomy_term.json?bundle=farm_areas', 'tid', 'name') + return new Promise ((resolve, reject) => { + axios.get("http://fd2_api/areas/mapById") + .then((response) => { + resolve(new Map(Object.entries(response.data))) + }).catch(function (err) { + reject(err) + }) + }) } /** @@ -220,7 +237,14 @@ function getIDToAreaMap(){ * // Note that the Map will not be available until the then() executes. */ function getAreaToIDMap(){ - return getMap('/taxonomy_term.json?bundle=farm_areas', 'name', 'tid') + return new Promise ((resolve, reject) => { + axios.get("http://fd2_api/areas/mapByName") + .then((response) => { + resolve(new Map(Object.entries(response.data))) + }).catch(function (err) { + reject(err) + }) + }) } /** diff --git a/farmdata2/farmdata2_modules/resources/FarmOSAPI.spec.js b/farmdata2/farmdata2_modules/resources/FarmOSAPI.spec.js index e17f439f..d3237e86 100644 --- a/farmdata2/farmdata2_modules/resources/FarmOSAPI.spec.js +++ b/farmdata2/farmdata2_modules/resources/FarmOSAPI.spec.js @@ -185,7 +185,7 @@ describe('API Request Functions', () => { it('map failure', () => { // All of the get functions for maps use the same // helper function so only need to test the failure once. - cy.intercept('GET', '/user', + cy.intercept('GET', 'http://fd2_api/users/mapByName', { statusCode: 500, body: '500 Interal Server Error!', diff --git a/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/Configuration.spec.js b/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/Configuration.spec.js new file mode 100644 index 00000000..043d4add --- /dev/null +++ b/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/Configuration.spec.js @@ -0,0 +1,280 @@ +var FarmOSAPI = require("../FarmOSAPI.js") + +var getAllPages = FarmOSAPI.getAllPages +var getSessionToken = FarmOSAPI.getSessionToken +var getConfiguration = FarmOSAPI.getConfiguration +var setConfiguration = FarmOSAPI.setConfiguration +var getCropToIDMap = FarmOSAPI.getCropToIDMap +var quantityLocation = FarmOSAPI.quantityLocation + +describe('API Request Functions', () => { + beforeEach(() => { + // Login as restws1, which is a user that can make api requests. + cy.login('restws1', 'farmdata2') + }) + + context('getAllPages API request function', () => { + it('Test on a request with a one page response.', () => { + + let requests = 0 + let testArray = [] + + cy.intercept('GET', /log\?type=farm_seeding/, (req) => { + requests++ // count requests made on this route. + }) + .then(() => { + // wrap and alias the getAllPages here. + // It returns a promise that resolves when all pages have been + // fetched into the array. + cy.wrap(getAllPages('/log?type=farm_seeding&id[le]=50', testArray)) + .as('done') + }) + + // Wait here for all pages to be fetched. + cy.get('@done') + .then(() => { + expect(requests).to.equal(1) + expect(testArray).to.have.length(50) + }) + }) + + it('Test on a request with multiple pages', () => { + let firstCalls = 0 + let secondCalls = 0 + let lastCalls = 0 + let testArray = [] + + cy.intercept("GET", "/log?type=farm_seeding&page=5", (req) => { + firstCalls++ + }) + cy.intercept("GET", "/log?type=farm_seeding&page=6", (req) => { + secondCalls++ + }) + cy.intercept("GET", "/log?type=farm_seeding&page=9", (req) => { + lastCalls++ + }) + .then(() => { + cy.wrap(getAllPages("/log?type=farm_seeding&page=5", testArray)) + .as('done') + }) + + cy.get('@done').should(() => { + expect(firstCalls).to.equal(1) + expect(secondCalls).to.equal(1) + expect(lastCalls).to.equal(1) + expect(testArray).to.have.length.gt(400) + }) + }) + + it('check that data property is parsed', () => { + let cropToIDMap + cy.wrap(getCropToIDMap()).as('cropMap') + cy.get('@cropMap').then((theMap) => { + cropToIDMap = theMap + }) + + //let testArray + cy.wrap(getAllPages('/log?type=farm_seeding&id[le]=150')) + .as('done') + + // Wait here for all pages to be fetched. + cy.get('@done') + .then((array) => { + // check log from first page of response. + expect(array[0].data.crop_tid).to.equal(cropToIDMap.get("ASPARAGUS")) + // check log from second page of response. + expect(array[149].data.crop_tid).to.equal(cropToIDMap.get("RADISH-DAIKON")) + }) + }) + + it('check that data is not parsed if not present', () => { + // Assets do not have data properties so this fails + // if that isn't handled properly + cy.wrap(getAllPages('/farm_asset?type=planting&id[le]=75')) + .as('done') + + // Wait here for all pages to be fetched. + cy.get('@done') + .then((array) => { + expect(array[0].data).to.be.null + expect(array[74].data).to.be.null + }) + }) + + it('failed request', () => { + cy.intercept('GET', '/fail', + // stub an error response so it looks like the request failed. + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + getAllPages('/fail') + .then(() => { + // The request should fail and be rejected + // so we should not get here. + // If we do, force the test to fail, + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + // Wait for everything to finish. + cy.get('@fail') + }) + }) + + context('getSessionToken API request function', () => { + it('returns a token when it resolves', () => { + getSessionToken().then(token => { + expect(token).to.not.be.null + expect(token.length).to.equal(43) + }) + }) + + it('fail to get token', () => { + cy.intercept('GET', '/restws/session/token', + // stub an error response so it looks like the request failed. + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + getSessionToken() + .then(() => { + // The request should fail and be rejected + // so we should not get here. + // If we do, force the test to fail, + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + // Wait for everything to finish. + cy.get('@fail') + }) + }) + context('test quantity location function', () => { + let quantity = [{ + "measure": "length", + "value": 5, + "unit": { + "id": "1987", + "resource": "taxonomy_term" + }, + "label": "Amount planted" + }, + { + "measure": "ratio", + "value": 19, + "unit": { + "id": "98", + "resource": "taxonomy_term" + }, + "label": "Rows/Bed" + }, + { + "measure": "time", + "value": 178, + "unit": { + "id": "80", + "resource": "taxonomy_term" + }, + "label": "Labor" + }, + { + "measure": "count", + "value": 1, + "unit": { + "id": "90", + "resource": "taxonomy_term" + }, + "label": "Workers" + }] + + it('test if returns 2 for Labor', () => { + expect(quantityLocation(quantity, 'Labor')).to.equal(2) + }) + it('test if returns 0 for "Amount planted"', () => { + expect(quantityLocation(quantity, 'Amount planted')).to.equal(0) + }) + it('returns -1 when no label equal the label input', () => { + expect(quantityLocation(quantity, 'Yeehaw')).to.equal(-1) + }) + }) + + context('test configuration functions', () => { + + it('gets an existing configuration log', () => { + cy.wrap(getConfiguration()).as('done') + + cy.get('@done').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.id).to.equal('1') + expect(response.data.labor).to.equal('Required') + }) + }) + + it('sets labor to optional, then back to Required', () => { + let token = null + cy.wrap(getSessionToken()) + // First request for the session token. + .then((sessionToken) => { + token = sessionToken + }) + // Then update the log using the token + .then(() => { + let updateData = + { + "id": "1", + "labor": "Optional" + } + cy.wrap(setConfiguration(updateData, token)).as('update') + }) + cy.get('@update').should((response) => { + expect(response.status).to.equal(200) + }) + // If the update was successful, change the labor data to optional. + .then(() => { + cy.wrap(getConfiguration()).as('changed') + }) + cy.get('@changed').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.id).to.equal('1') + expect(response.data.labor).to.equal('Optional') + }) + // If the change was successful, change it back to required + .then(() => { + let resetData = + { + "id": "1", + "labor": "Required" + } + + cy.wrap(setConfiguration(resetData, token)).as('default') + }) + cy.get('@default').should((response) => { + expect(response.status).to.equal(200) + }) + .then(() => { + cy.wrap(getConfiguration()).as('reset') + }) + cy.get('@reset').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.id).to.equal('1') + expect(response.data.labor).to.equal('Required') + }) + }) + + }) +}) diff --git a/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/MapID.spec.js b/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/MapID.spec.js new file mode 100644 index 00000000..195f25de --- /dev/null +++ b/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/MapID.spec.js @@ -0,0 +1,225 @@ +var FarmOSAPI = require("../FarmOSAPI.js") +var getIDToUserMap = FarmOSAPI.getIDToUserMap +var getIDToCropMap = FarmOSAPI.getIDToCropMap +var getIDToAreaMap = FarmOSAPI.getIDToAreaMap +var getIDToUnitMap = FarmOSAPI.getIDToUnitMap +var getIDToLogTypeMap = FarmOSAPI.getIDToLogTypeMap + +var getUserToIDMap = FarmOSAPI.getUserToIDMap +var getCropToIDMap = FarmOSAPI.getCropToIDMap +var getAreaToIDMap = FarmOSAPI.getAreaToIDMap +var getUnitToIDMap = FarmOSAPI.getUnitToIDMap +var getLogTypeToIDMap = FarmOSAPI.getLogTypeToIDMap +describe("API request for ID mapping funtion", () => { + beforeEach(() => { + // Login as restws1, which is a user that can make api requests. + cy.login('restws1', 'farmdata2') + }) + context('test maping functions', () => { + it('User map functions get the proper name/id for the users', () => { + let manager1ID = -1 + let adminID = -1 + let worker2ID = -1 + let guestID = -1 + let restws1ID = -1 + + cy.wrap(getUserToIDMap()).as('nameMap') + cy.get('@nameMap').should(function (nameToIdMap) { + expect(nameToIdMap).to.not.be.null + expect(nameToIdMap).to.be.a('Map') + expect(nameToIdMap.size).to.equal(10) + + manager1ID = nameToIdMap.get('manager1') + adminID = nameToIdMap.get('admin') + worker2ID = nameToIdMap.get('worker2') + guestID = nameToIdMap.get('guest') + restws1ID = nameToIdMap.get('restws1') + }) + .then(() => { + cy.wrap(getIDToUserMap()).as('idMap') + cy.get('@idMap').should(function (idToNameMap) { + expect(idToNameMap).to.not.be.null + expect(idToNameMap).to.be.a('Map') + expect(idToNameMap.size).to.equal(10) + + expect(idToNameMap.get(manager1ID)).to.equal('manager1') + expect(idToNameMap.get(adminID)).to.equal('admin') + expect(idToNameMap.get(worker2ID)).to.equal('worker2') + expect(idToNameMap.get(guestID)).to.equal('guest') + expect(idToNameMap.get(restws1ID)).to.equal('restws1') + }) + }) + }) + + it('map failure', () => { + // All of the get functions for maps use the same + // helper function so only need to test the failure once. + cy.intercept('GET', 'http://fd2_api/users/mapByName', + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + getUserToIDMap() + .then(() => { + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + cy.get('@fail') + }) + + it('Crop map functions get the proper name/id for the crops', () => { + //first and last of the first page of the response + let arugulaID = -1 + let strawberryID = -1 + //first and last of the second page of the response + let sunflowerSeedsID = -1 + let zuchiniID = -1 + // test some compound names too + let onionSpringID = -1 + let cornSweetID = -1 + + cy.wrap(getCropToIDMap()).as('cropMap') + cy.get('@cropMap').should((cropToIdMap) => { + expect(cropToIdMap).to.not.be.null + expect(cropToIdMap).to.be.a('Map') + expect(cropToIdMap.size).to.equal(111) + + arugulaID = cropToIdMap.get('ARUGULA') + strawberryID = cropToIdMap.get('STRAWBERRY') + sunflowerSeedsID = cropToIdMap.get('SUNFLOWER SEEDS') + zuchiniID = cropToIdMap.get('ZUCCHINI') + onionSpringID = cropToIdMap.get('ONION-SPRING') + cornSweetID = cropToIdMap.get('CORN-SWEET') + }) + .then(() => { + cy.wrap(getIDToCropMap()).as('idMap') + cy.get('@idMap').should((idToCropMap) => { + expect(idToCropMap).to.not.be.null + expect(idToCropMap).to.be.a('Map') + expect(idToCropMap.size).to.equal(111) + + expect(idToCropMap.get(arugulaID)).to.equal('ARUGULA') + expect(idToCropMap.get(strawberryID)).to.equal('STRAWBERRY') + expect(idToCropMap.get(sunflowerSeedsID)).to.equal('SUNFLOWER SEEDS') + expect(idToCropMap.get(zuchiniID)).to.equal('ZUCCHINI') + expect(idToCropMap.get(onionSpringID)).to.equal('ONION-SPRING') + expect(idToCropMap.get(cornSweetID)).to.equal('CORN-SWEET') + }) + }) + }) + + it('Area map functions get the proper name/id for the areas', () => { + let aID = -1 + let zID = -1 + let chuauID = -1 + let chuau1ID = -1 + let chuau5ID = -1 + + cy.wrap(getAreaToIDMap()).as('areaMap') + cy.get('@areaMap').should(function (areaToIDMap) { + expect(areaToIDMap).to.not.be.null + expect(areaToIDMap).to.be.a('Map') + expect(areaToIDMap.size).to.equal(70) + + aID = areaToIDMap.get('A') + zID = areaToIDMap.get('Z') + chuauID = areaToIDMap.get('CHUAU') + chuau1ID = areaToIDMap.get('CHUAU-1') + chuau5ID = areaToIDMap.get('CHUAU-5') + }) + .then(() => { + cy.wrap(getIDToAreaMap()).as('idMap') + cy.get('@idMap').should(function (idToAreaMap) { + expect(idToAreaMap).to.not.be.null + expect(idToAreaMap).to.be.a('Map') + expect(idToAreaMap.size).to.equal(70) + + expect(idToAreaMap.get(aID)).to.equal('A') + expect(idToAreaMap.get(zID)).to.equal('Z') + expect(idToAreaMap.get(chuauID)).to.equal('CHUAU') + expect(idToAreaMap.get(chuau1ID)).to.equal('CHUAU-1') + expect(idToAreaMap.get(chuau5ID)).to.equal('CHUAU-5') + }) + }) + }) + + it('Unit map functions get the proper name/id for the units', () => { + let seedsID = -1 + let rowFeetID = -1 + let flatsID = -1 + let hoursID = -1 + let peopleID = -1 + + cy.wrap(getUnitToIDMap()).as('unitMap') + cy.get('@unitMap').should(function (unitToIDMap) { + expect(unitToIDMap).to.not.be.null + expect(unitToIDMap).to.be.a('Map') + expect(unitToIDMap.size).to.equal(33) + + seedsID = unitToIDMap.get('SEEDS') + rowFeetID = unitToIDMap.get('ROW FEET') + flatsID = unitToIDMap.get('FLATS') + hoursID = unitToIDMap.get('HOURS') + peopleID = unitToIDMap.get('PEOPLE') + }) + .then(() => { + cy.wrap(getIDToUnitMap()).as('idMap') + cy.get('@idMap').should(function (idToUnitMap) { + expect(idToUnitMap).to.not.be.null + expect(idToUnitMap).to.be.a('Map') + expect(idToUnitMap.size).to.equal(33) + + expect(idToUnitMap.get(seedsID)).to.equal('SEEDS') + expect(idToUnitMap.get(rowFeetID)).to.equal('ROW FEET') + expect(idToUnitMap.get(flatsID)).to.equal('FLATS') + expect(idToUnitMap.get(hoursID)).to.equal('HOURS') + expect(idToUnitMap.get(peopleID)).to.equal('PEOPLE') + }) + }) + }) + + it('Log Type map functions get the proper name/id for the log types', () => { + let directSeedingsID = -1 + let traySeedingsID = -1 + let waterID = -1 + let transplantingsID = -1 + let animalsID = -1 + + cy.wrap(getLogTypeToIDMap()).as('logMap') + cy.get('@logMap').should(function (logTypeToIDMap) { + expect(logTypeToIDMap).to.not.be.null + expect(logTypeToIDMap).to.be.a('Map') + expect(logTypeToIDMap.size).to.equal(9) + + directSeedingsID = logTypeToIDMap.get('Direct Seedings') + traySeedingsID = logTypeToIDMap.get('Tray Seedings') + waterID = logTypeToIDMap.get('Water') + transplantingsID = logTypeToIDMap.get('Transplantings') + animalsID = logTypeToIDMap.get('Animals') + }) + .then(() => { + cy.wrap(getIDToLogTypeMap()).as('idMap') + cy.get('@idMap').should(function (idToLogTypeMap) { + expect(idToLogTypeMap).to.not.be.null + expect(idToLogTypeMap).to.be.a('Map') + expect(idToLogTypeMap.size).to.equal(9) + + expect(idToLogTypeMap.get(directSeedingsID)).to.equal('Direct Seedings') + expect(idToLogTypeMap.get(traySeedingsID)).to.equal('Tray Seedings') + expect(idToLogTypeMap.get(waterID)).to.equal('Water') + expect(idToLogTypeMap.get(transplantingsID)).to.equal('Transplantings') + expect(idToLogTypeMap.get(animalsID)).to.equal('Animals') + }) + }) + }) + }) + +}) diff --git a/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/Record.spec.js b/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/Record.spec.js new file mode 100644 index 00000000..b524e4ac --- /dev/null +++ b/farmdata2/farmdata2_modules/resources/TestFarmOSAPI/Record.spec.js @@ -0,0 +1,439 @@ +var FarmOSAPI = require("../FarmOSAPI.js") + +var getSessionToken = FarmOSAPI.getSessionToken +var getCropToIDMap = FarmOSAPI.getCropToIDMap +var updateRecord = FarmOSAPI.updateRecord +var createRecord = FarmOSAPI.createRecord +var deleteRecord = FarmOSAPI.deleteRecord +var getRecord = FarmOSAPI.getRecord + + +describe('API Request Functions', () => { + beforeEach(() => { + // Login as restws1, which is a user that can make api requests. + cy.login('restws1', 'farmdata2') + }) + + context('getRecord API request function', () => { + it('gets an existing log', () => { + + cy.wrap(getRecord('/log/100')).as('done') + + cy.get('@done').should(function (response) { + expect(response.status).to.equal(200) + expect(response.data.id).to.equal('100') + }) + }) + + it('gets an existing asset', () => { + cy.wrap(getRecord('/farm_asset/1')).as('done') + + cy.get('@done').should(function (response) { + expect(response.status).to.equal(200) + expect(response.data.id).to.equal('1') + }) + }) + + it('attempt to get a non-existent record', () => { + cy.wrap( + getRecord('/log/9999999') + .then(() => { + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(404) + }) + ).as('fail') + + cy.get('@fail') + }) + + it('test that JSON in data property is parsed', () => { + let cropToIDMap + cy.wrap(getCropToIDMap()).as('cropMap') + cy.get('@cropMap').then((theMap) => { + cropToIDMap = theMap + }) + + // log #1 is a seeding so will have a data field. + cy.wrap(getRecord('/log/1')).as('done') + cy.get('@done').should((response) => { + // Should not need to parse JSON here... so don't. + expect(response.data.data.crop_tid).to.equal(cropToIDMap.get('ASPARAGUS')) + }) + }) + + it('test record without a data property', () => { + // Assets do not have a data property. This would fail + // due to an error if the getRecord function did not handle + // that condition properly. + cy.wrap(getRecord('/farm_asset/1')).as('done') + cy.get('@done').should((response) => { + expect(response.data.data).to.be.null + }) + }) + + it('fail to get a log', () => { + cy.intercept('GET', '/log/12345', + // stub an error response so it looks like the request failed. + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + getRecord('/log/12345') + .then(() => { + // The request should fail and be rejected + // so we should not get here. + // If we do, force the test to fail, + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + // Wait for everything to finish. + cy.get('@fail') + }) + }) + + context('deleteRecord API request function', () => { + it('deletes a log', () => { + let logID = -1 + let token = null + + // Creates a new log entry & ensures it was successful. + // Deletes the log entry using the deleteRecord function. + // Requests the log to ensure that it has been deleted. + + cy.wrap(getSessionToken()) + .then(sessionToken => { + token = sessionToken + + let req = { + url: '/log', + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-TOKEN': token, + }, + body: { + "name": "Delete Test", + "type": "farm_observation", + "timestamp": "123", + } + } + + cy.request(req).as('create') + }) + + cy.get('@create').should(function (response) { + expect(response.status).to.equal(201) + logID = response.body.id + }) + .then(() => { + cy.wrap(deleteRecord('/log/' + logID, token)).as('delete') + }) + + cy.get('@delete').should((response) => { + expect(response.status).to.equal(200) + }) + .then(() => { + cy.wrap( + getRecord('/log/' + logID) + .then(() => { + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(404) // 404 - not found + }) + ).as('check') + }) + + cy.get('@check') + }) + + it('failed delete', () => { + cy.intercept('DELETE', '/log/12345', + // stub an error response so it looks like the request failed. + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + deleteRecord('/log/12345', null) + .then(() => { + // The request should fail and be rejected + // so we should not get here. + // If we do, force the test to fail, + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + // Wait for everything to finish. + cy.get('@fail') + }) + }) + + context('create API request function', () => { + it('creates a new log', () => { + + let logID = -1 + let token = null + + // Creates a new log using the createRecord function + // Checks that it exists + // Deletes it using the deleteRecord function (tested above) + + cy.wrap(getSessionToken()) + .then((sessionToken) => { + token = sessionToken + + let newLog = { + "name": "Create Test", + "type": "farm_observation", + "timestamp": "123", + } + + cy.wrap(createRecord('/log', newLog, token)).as('create') + }) + + cy.get('@create').should((response) => { + logID = response.data.id + expect(response.status).to.equal(201) + }) + .then(() => { + cy.wrap(getRecord('/log/' + logID)).as('exists') + }) + + cy.get('@exists').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.name).to.equal('Create Test') + expect(response.data.data).to.be.null + }) + .then(() => { + cy.wrap(deleteRecord('/log/' + logID, token)).as('delete') + }) + + cy.get('@delete').should(function (response) { + expect(response.status).to.equal(200) + }) + }) + + it('test create log with a data property', () => { + let logID = -1 + let token = null + + cy.wrap(getSessionToken()) + .then((sessionToken) => { + token = sessionToken + + let newLog = { + "name": "Create Test", + "type": "farm_observation", + "timestamp": "123", + "data": { crop_tid: 123 } + } + + cy.wrap(createRecord('/log', newLog, token)).as('create') + cy.get('@create').should((response) => { + logID = response.data.id + expect(response.status).to.equal(201) + }) + .then(() => { + cy.wrap(getRecord('/log/' + logID)).as('exists') + }) + + cy.get('@exists').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.name).to.equal('Create Test') + expect(response.data.data).to.not.be.null + expect(response.data.data.crop_tid).to.equal(123) + }) + .then(() => { + cy.wrap(deleteRecord('/log/' + logID, token)).as('delete') + }) + + cy.get('@delete').should(function (response) { + expect(response.status).to.equal(200) + }) + }) + }) + + it('failed create', () => { + cy.intercept('POST', '/log', + // stub an error response so it looks like the request failed. + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + createRecord('/log', { "data": "null" }, null) + .then(() => { + // The request should fail and be rejected + // so we should not get here. + // If we do, force the test to fail, + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + // Wait for everything to finish. + cy.get('@fail') + }) + }) + + context('update function testing', () => { + it('change the name of an observation log', () => { + let logID = -1 + let token = null + + // Creates a new log using the createRecord function (tested above) + // Updates the log using the updateRecord function. + // Requests the log using the getLog function (tested above) + // Deletes the log using the deleteRecord function (tested above) + + cy.wrap(getSessionToken()) + .then((sessionToken) => { + token = sessionToken + + let newLog = { + "name": "Update Test", + "type": "farm_observation", + "timestamp": "123", + } + + cy.wrap(createRecord('/log', newLog, token)).as('create') + }) + + cy.get('@create').should((response) => { + logID = response.data.id + expect(response.status).to.equal(201) + }) + .then(() => { + let update = { + "name": "Update Test Updated" + } + + cy.wrap(updateRecord('/log/' + logID, update, token)).as('update') + }) + + cy.get('@update').should((response) => { + expect(response.status).to.equal(200) + }) + .then(() => { + cy.wrap(getRecord('/log/' + logID)).as('check') + }) + + cy.get('@check').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.name).to.equal('Update Test Updated') + expect(response.data.data).to.be.null + }) + .then(() => { + cy.wrap(deleteRecord('/log/' + logID, token)).as('delete') + }) + + cy.get('@delete').should(function (response) { + expect(response.status).to.equal(200) + }) + }) + + it('updte a record that has a data property', () => { + let logID = -1 + let token = null + + cy.wrap(getSessionToken()) + .then((sessionToken) => { + token = sessionToken + + let newLog = { + "name": "Update Test", + "type": "farm_observation", + "timestamp": "123", + "data": { crop_tid: 123 } + } + + cy.wrap(createRecord('/log', newLog, token)).as('create') + }) + + cy.get('@create').should((response) => { + logID = response.data.id + expect(response.status).to.equal(201) + }) + .then(() => { + let update = { + "name": "Update Test Updated", + "data": { crop_tid: 234 } + } + + cy.wrap(updateRecord('/log/' + logID, update, token)).as('update') + }) + + cy.get('@update').should((response) => { + expect(response.status).to.equal(200) + }) + .then(() => { + cy.wrap(getRecord('/log/' + logID)).as('check') + }) + + cy.get('@check').should((response) => { + expect(response.status).to.equal(200) + expect(response.data.name).to.equal('Update Test Updated') + expect(response.data.data).to.not.be.null + expect(response.data.data.crop_tid).to.equal(234) + }) + .then(() => { + cy.wrap(deleteRecord('/log/' + logID, token)).as('delete') + }) + + cy.get('@delete').should(function (response) { + expect(response.status).to.equal(200) + }) + }) + + it('failed update', () => { + cy.intercept('PUT', '/log', + // stub an error response so it looks like the request failed. + { + statusCode: 500, + body: '500 Interal Server Error!', + } + ) + .then(() => { + cy.wrap( + updateRecord('/log', { "data": "null" }, null) + .then(() => { + // The request should fail and be rejected + // so we should not get here. + // If we do, force the test to fail, + expect(true).to.equal(false) + }) + .catch((err) => { + expect(err.response.status).to.equal(500) + }) + ).as('fail') + }) + + // Wait for everything to finish. + cy.get('@fail') + }) + }) + + +}) diff --git a/farmdata2/jsdoc/JSDoc.json b/farmdata2/jsdoc/JSDoc.json index 37a301a0..a7a5a3ce 100644 --- a/farmdata2/jsdoc/JSDoc.json +++ b/farmdata2/jsdoc/JSDoc.json @@ -1,6 +1,7 @@ { "plugins": [ - "jsdoc-vuejs" + "jsdoc-vuejs", + "jsdoc-escape-at" ], "source": { "includePattern": "\\.(vue|js)$"