diff --git a/404.html b/404.html index 23234e66..87ef24d5 100644 --- a/404.html +++ b/404.html @@ -5,7 +5,7 @@ Page Not Found | Geo Garden Club - + diff --git a/assets/js/31caa863.07bb8d76.js b/assets/js/31caa863.07bb8d76.js new file mode 100644 index 00000000..cf4ce795 --- /dev/null +++ b/assets/js/31caa863.07bb8d76.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4063],{2592:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>r,contentTitle:()=>s,default:()=>c,frontMatter:()=>l,metadata:()=>a,toc:()=>d});var i=t(5893),o=t(1151);const l={hide_table_of_contents:!1},s="Deployment",a={id:"develop/deployment",title:"Deployment",description:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.",source:"@site/docs/develop/deployment.md",sourceDirName:"develop",slug:"/develop/deployment",permalink:"/docs/develop/deployment",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Database management",permalink:"/docs/develop/database-management"},next:{title:"Backups",permalink:"/docs/develop/backups"}},r={},d=[{value:"Document deployment versions",id:"document-deployment-versions",level:2},{value:"Deployment management",id:"deployment-management",level:2},{value:"Prerequisites",id:"prerequisites",level:3},{value:"Update ChangeLog",id:"update-changelog",level:3},{value:"Build the deployment files",id:"build-the-deployment-files",level:3},{value:"Deploy iOS app",id:"deploy-ios-app",level:3},{value:"Deploy Android App",id:"deploy-android-app",level:3},{value:"Add new beta testers",id:"add-new-beta-testers",level:2},{value:"iOS",id:"ios",level:3},{value:"Android",id:"android",level:3}];function h(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"deployment",children:"Deployment"})}),"\n",(0,i.jsx)(n.p,{children:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet."}),"\n",(0,i.jsx)(n.h2,{id:"document-deployment-versions",children:"Document deployment versions"}),"\n",(0,i.jsxs)(n.p,{children:["Each new deployment requires a new version number (specified in the pubspec.yml file), and we document what has changed in each new version via ",(0,i.jsx)(n.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/CHANGELOG.md",children:"CHANGELOG.md"}),". To manage version numbers and the changelog file, we use ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/cider",children:"Cider"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. So, when we do a deployment, the ",(0,i.jsx)(n.code,{children:"run_deploy.sh"})," script will copy the top-level CHANGELOG.md file into the assets/ folder so that it gets included in the various apps."]}),"\n",(0,i.jsx)(n.p,{children:"We adhere to two standards:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["For the changelog format, we adhere to ",(0,i.jsx)(n.a,{href:"https://keepachangelog.com/en/1.0.0/",children:"Keep a Changelog"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["For the version number format, we adhere to ",(0,i.jsx)(n.a,{href:"https://semver.org/spec/v2.0.0.html",children:"Semantic Versioning"}),". For example, for Release 1.0 (Technology Evaluation), the ",(0,i.jsx)(n.em,{children:"major"}),' version is "1". The deploy script automatically increments the minor version and increments the build number.']}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"deployment-management",children:"Deployment management"}),"\n",(0,i.jsx)(n.admonition,{title:"Changes ahead",type:"warning",children:(0,i.jsx)(n.p,{children:"The following instructions document how to create beta (test) releases. We plan to transition to production releases (i.e. App Store and Play Store) in January 2025."})}),"\n",(0,i.jsx)(n.p,{children:'The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.'}),"\n",(0,i.jsx)(n.h3,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,i.jsx)(n.p,{children:"Prior to a deployment, it is good practice to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Do a ",(0,i.jsx)(n.a,{href:"/docs/develop/backups",children:"backup"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Run the integrity checker and resolve any violations."}),"\n",(0,i.jsx)(n.li,{children:"Run the integration tests and make sure the main branch does not generate any errors."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"update-changelog",children:"Update ChangeLog"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"cider log added "})," to document new additions (or use ",(0,i.jsx)(n.code,{children:"changed"})," or ",(0,i.jsx)(n.code,{children:"fixed"}),") since the last release. Enclose the message in quotes. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'cider log added "Terms and Conditions"\n'})}),"\n",(0,i.jsx)(n.h3,{id:"build-the-deployment-files",children:"Build the deployment files"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"./run_deploy.sh"}),". This script does the following:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"cider bump minor"})," and ",(0,i.jsx)(n.code,{children:"cider release"})," so that the unreleased changes are moved to a new release number, and that release number is recorded in the pubspec.yml."]}),"\n",(0,i.jsx)(n.li,{children:"Commits the updated CHANGELOG.md and pubspec.yml files to GitHub."}),"\n",(0,i.jsxs)(n.li,{children:['Creates a "deploy directory" at ',(0,i.jsx)(n.code,{children:"~/Desktop/ggc-deploy-"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Builds the ggc_app.ipa file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Builds the app-release.aab file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Gets the release notes for the current release and copies them to the deploy directory."}),"\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"firebase deploy"})," to build and deploy the web version of the app."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"deploy-ios-app",children:"Deploy iOS app"}),"\n",(0,i.jsx)(n.p,{children:"First, open the Transporter app and drag the ggc_app.ipa file from the Desktop folder onto the App."}),"\n",(0,i.jsxs)(n.p,{children:["Second, login to ",(0,i.jsx)(n.a,{href:"https://appstoreconnect.apple.com/login",children:"App Store Connect"}),'. Click on "Apps", then "GeoGardenClub", then "TestFlight".']}),"\n",(0,i.jsx)(n.p,{children:"Wait for a few minutes for the uploaded version to become available for distribution via TestFlight."}),"\n",(0,i.jsx)(n.p,{children:'Once available, the "internal" testers will be automatically notified.'}),"\n",(0,i.jsx)(n.p,{children:'Now submit the build for external testing. Click on "External" on the left sidebar, then click the "+" button next to the "Builds" section, and add the most recent build. It will then be submitted for review. This review appears to take 3-7 days to complete. At that point, the public URL can be distributed and anyone who already installed the app via that link should be able to update to the new build.'}),"\n",(0,i.jsx)(n.h3,{id:"deploy-android-app",children:"Deploy Android App"}),"\n",(0,i.jsxs)(n.p,{children:["Open the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/tracks/internal-testing",children:"Google Play Console Internal Testing Page"}),' and click on "Create new release".']}),"\n",(0,i.jsx)(n.p,{children:"Upload app-release.aab file from the folder containing the newly created release."}),"\n",(0,i.jsx)(n.p,{children:'Once uploaded, click "Next" to go to the Preview and Confirm page. Ensure that everything looks OK, then click "Save and Publish".'}),"\n",(0,i.jsxs)(n.p,{children:["Now go to the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/pre-launch-report/overview",children:"Prelaunch Report Overview"})," page. After about an hour, you will be able to check the results of testing on the new version and see if there are any issues that need to be addressed."]}),"\n",(0,i.jsx)(n.p,{children:'Finally, "promote" this version to the "Closed testing" track. This triggers an internal review by Google that takes a few days, but is useful as it results in additional quality assurance testing by Google.'}),"\n",(0,i.jsx)(n.h2,{id:"add-new-beta-testers",children:"Add new beta testers"}),"\n",(0,i.jsx)(n.h3,{id:"ios",children:"iOS"}),"\n",(0,i.jsx)(n.p,{children:'For iOS, we use "external" testing. This means that a new beta tester will need to download the TestFlight app, and then we can simply supply them with a URL which will enable them to install (and/or update) the GGC app from within the TestFlight app.'}),"\n",(0,i.jsx)(n.h3,{id:"android",children:"Android"}),"\n",(0,i.jsx)(n.p,{children:'For Android, we use the "internal testing" track in Google Play Store. This means that we must:'}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsx)(n.li,{children:"Obtain the gmail address of the new beta tester"}),"\n",(0,i.jsx)(n.li,{children:'Go to the "Internal Testing" page, click on "Testers", and click the right arrow on the line associated with the "GGC Beta Testers" group.'}),"\n",(0,i.jsx)(n.li,{children:'Enter their email address in the "Add email address" text field.'}),"\n",(0,i.jsx)(n.li,{children:'Back on the Tests page, click on the "Copy link" button to obtain the URL to distribute to the new beta tester. They use this URL to download and install the GGC app on their device.'}),"\n"]})]})}function c(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>s});var i=t(7294);const o={},l=i.createContext(o);function s(e){const n=i.useContext(l);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),i.createElement(l.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/31caa863.fc3b4f74.js b/assets/js/31caa863.fc3b4f74.js deleted file mode 100644 index 738e15b4..00000000 --- a/assets/js/31caa863.fc3b4f74.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4063],{2592:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>c,frontMatter:()=>s,metadata:()=>l,toc:()=>r});var i=t(5893),o=t(1151);const s={hide_table_of_contents:!1},a="Deployment",l={id:"develop/deployment",title:"Deployment",description:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.",source:"@site/docs/develop/deployment.md",sourceDirName:"develop",slug:"/develop/deployment",permalink:"/docs/develop/deployment",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Database management",permalink:"/docs/develop/database-management"},next:{title:"Backups",permalink:"/docs/develop/backups"}},d={},r=[{value:"Document deployment versions",id:"document-deployment-versions",level:2},{value:"Deployment management",id:"deployment-management",level:2},{value:"Prerequisites",id:"prerequisites",level:3},{value:"Update ChangeLog",id:"update-changelog",level:3},{value:"Build the deployment files",id:"build-the-deployment-files",level:3},{value:"Deploy iOS app",id:"deploy-ios-app",level:3},{value:"Deploy Android App",id:"deploy-android-app",level:3},{value:"Add new beta testers",id:"add-new-beta-testers",level:2},{value:"iOS",id:"ios",level:3},{value:"Android",id:"android",level:3},{value:"Test on physical device",id:"test-on-physical-device",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"deployment",children:"Deployment"})}),"\n",(0,i.jsx)(n.p,{children:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet."}),"\n",(0,i.jsx)(n.h2,{id:"document-deployment-versions",children:"Document deployment versions"}),"\n",(0,i.jsxs)(n.p,{children:["Each new deployment requires a new version number (specified in the pubspec.yml file), and we document what has changed in each new version via ",(0,i.jsx)(n.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/CHANGELOG.md",children:"CHANGELOG.md"}),". To manage version numbers and the changelog file, we use ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/cider",children:"Cider"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. So, when we do a deployment, the ",(0,i.jsx)(n.code,{children:"run_deploy.sh"})," script will copy the top-level CHANGELOG.md file into the assets/ folder so that it gets included in the various apps."]}),"\n",(0,i.jsx)(n.p,{children:"We adhere to two standards:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["For the changelog format, we adhere to ",(0,i.jsx)(n.a,{href:"https://keepachangelog.com/en/1.0.0/",children:"Keep a Changelog"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["For the version number format, we adhere to ",(0,i.jsx)(n.a,{href:"https://semver.org/spec/v2.0.0.html",children:"Semantic Versioning"}),". For example, for Release 1.0 (Technology Evaluation), the ",(0,i.jsx)(n.em,{children:"major"}),' version is "1". The deploy script automatically increments the minor version and increments the build number.']}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"deployment-management",children:"Deployment management"}),"\n",(0,i.jsx)(n.admonition,{title:"Changes ahead",type:"warning",children:(0,i.jsx)(n.p,{children:"The following instructions document how to create beta (test) releases. We plan to transition to production releases (i.e. App Store and Play Store) in January 2025."})}),"\n",(0,i.jsx)(n.p,{children:'The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.'}),"\n",(0,i.jsx)(n.h3,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,i.jsx)(n.p,{children:"Prior to a deployment, it is good practice to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Do a ",(0,i.jsx)(n.a,{href:"/docs/develop/backups",children:"backup"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Run the integrity checker and resolve any violations."}),"\n",(0,i.jsx)(n.li,{children:"Run the integration tests and make sure the main branch does not generate any errors."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"update-changelog",children:"Update ChangeLog"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"cider log added "})," to document new additions (or use ",(0,i.jsx)(n.code,{children:"changed"})," or ",(0,i.jsx)(n.code,{children:"fixed"}),") since the last release. Enclose the message in quotes. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'cider log added "Terms and Conditions"\n'})}),"\n",(0,i.jsx)(n.h3,{id:"build-the-deployment-files",children:"Build the deployment files"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"./run_deploy.sh"}),". This script does the following:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"cider bump minor"})," and ",(0,i.jsx)(n.code,{children:"cider release"})," so that the unreleased changes are moved to a new release number, and that release number is recorded in the pubspec.yml."]}),"\n",(0,i.jsx)(n.li,{children:"Commits the updated CHANGELOG.md and pubspec.yml files to GitHub."}),"\n",(0,i.jsxs)(n.li,{children:['Creates a "deploy directory" at ',(0,i.jsx)(n.code,{children:"~/Desktop/ggc-deploy-"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Builds the ggc_app.ipa file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Builds the app-release.aab file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Gets the release notes for the current release and copies them to the deploy directory."}),"\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"firebase deploy"})," to build and deploy the web version of the app."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"deploy-ios-app",children:"Deploy iOS app"}),"\n",(0,i.jsx)(n.p,{children:"First, open the Transporter app and drag the ggc_app.ipa file from the Desktop folder onto the App."}),"\n",(0,i.jsxs)(n.p,{children:["Second, login to ",(0,i.jsx)(n.a,{href:"https://appstoreconnect.apple.com/login",children:"App Store Connect"}),'. Click on "Apps", then "GeoGardenClub", then "TestFlight".']}),"\n",(0,i.jsx)(n.p,{children:"Wait for a few minutes for the uploaded version to become available for distribution via TestFlight."}),"\n",(0,i.jsx)(n.p,{children:'Once available, the "internal" testers will be automatically notified.'}),"\n",(0,i.jsx)(n.p,{children:'Now submit the build for external testing. Click on "External" on the left sidebar, then click the "+" button next to the "Builds" section, and add the most recent build. It will then be submitted for review. This review appears to take 3-7 days to complete. At that point, the public URL can be distributed and anyone who already installed the app via that link should be able to update to the new build.'}),"\n",(0,i.jsx)(n.h3,{id:"deploy-android-app",children:"Deploy Android App"}),"\n",(0,i.jsxs)(n.p,{children:["Open the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/tracks/internal-testing",children:"Google Play Console Internal Testing Page"}),' and click on "Create new release".']}),"\n",(0,i.jsx)(n.p,{children:"Upload app-release.aab file from the folder containing the newly created release."}),"\n",(0,i.jsx)(n.p,{children:'Once uploaded, click "Next" to go to the Preview and Confirm page. Ensure that everything looks OK, then click "Save and Publish".'}),"\n",(0,i.jsxs)(n.p,{children:["Now go to the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/pre-launch-report/overview",children:"Prelaunch Report Overview"})," page. After about an hour, you will be able to check the results of testing on the new version and see if there are any issues that need to be addressed."]}),"\n",(0,i.jsx)(n.p,{children:'Finally, "promote" this version to the "Closed testing" track. This triggers an internal review by Google that takes a few days, but is useful as it results in additional quality assurance testing by Google.'}),"\n",(0,i.jsx)(n.h2,{id:"add-new-beta-testers",children:"Add new beta testers"}),"\n",(0,i.jsx)(n.h3,{id:"ios",children:"iOS"}),"\n",(0,i.jsx)(n.p,{children:'For iOS, we use "external" testing. This means that a new beta tester will need to download the TestFlight app, and then we can simply supply them with a URL which will enable them to install (and/or update) the GGC app from within the TestFlight app.'}),"\n",(0,i.jsx)(n.h3,{id:"android",children:"Android"}),"\n",(0,i.jsx)(n.p,{children:'For Android, we use the "internal testing" track in Google Play Store. This means that we must:'}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsx)(n.li,{children:"Obtain the gmail address of the new beta tester"}),"\n",(0,i.jsx)(n.li,{children:'Go to the "Internal Testing" page, click on "Testers", and click the right arrow on the line associated with the "GGC Beta Testers" group.'}),"\n",(0,i.jsx)(n.li,{children:'Enter their email address in the "Add email address" text field.'}),"\n",(0,i.jsx)(n.li,{children:'Back on the Tests page, click on the "Copy link" button to obtain the URL to distribute to the new beta tester. They use this URL to download and install the GGC app on their device.'}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"test-on-physical-device",children:"Test on physical device"}),"\n",(0,i.jsx)(n.p,{children:"Sometimes it is useful to try out the app on a physical device without having to go through deployment steps. Currently only Philip can do this due to iOS signing issues."}),"\n",(0,i.jsx)(n.p,{children:"Here is what you need to do:"}),"\n",(0,i.jsx)(n.p,{children:"First, delete the app from your physical device."}),"\n",(0,i.jsx)(n.p,{children:"Second, connect the physical device to a laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Third, run ",(0,i.jsx)(n.code,{children:"flutter devices"})," to verify that the device shows up. You should see output like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:" % flutter devices \nFound 5 connected devices:\n Philip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\n iPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n (simulator)\n macOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\n Mac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\n Chrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n"})}),"\n",(0,i.jsx)(n.p,{children:"Notice that the first device is my physical device connected to my laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Now, to run the code in release mode on this physical device, you invoke ",(0,i.jsx)(n.code,{children:"flutter run --release"})," and select the physical device like so. (Make sure your device is unlocked while running this command.)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"% flutter run --release\nConnected devices:\nPhilip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\niPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n(simulator)\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\nMac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n\nChecking for wireless devices...\n\nNo wireless devices were found.\n\n[1]: Philip's iPhone (00008030-000364940A98802E)\n[2]: iPhone 11 (8E550E86-3173-4342-B197-A557B83E40A2)\n[3]: macOS (macos)\n[4]: Mac Designed for iPad (mac-designed-for-ipad)\n[5]: Chrome (chrome)\nPlease choose one (or \"q\" to quit): 1\nLaunching lib/main.dart on Philip's iPhone in release mode...\nAutomatically signing iOS for device deployment using specified development team in Xcode project: 8M69898HLM\nRunning Xcode build... \n \u2514\u2500Compiling, linking and signing... 8.8s\nXcode build done. 67.9s\nInstalling and launching... 7.1s\n\nFlutter run key commands.\nh List all available interactive commands.\nc Clear the screen\nq Quit (terminate the application on the device).\n\n"})})]})}function c(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>a});var i=t(7294);const o={},s=i.createContext(o);function a(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6b1fc3de.6d40b298.js b/assets/js/6b1fc3de.6d40b298.js new file mode 100644 index 00000000..7a20b086 --- /dev/null +++ b/assets/js/6b1fc3de.6d40b298.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8754],{8312:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>a,metadata:()=>r,toc:()=>d});var i=t(5893),o=t(1151);const a={hide_table_of_contents:!1},s="Installation",r={id:"develop/installation",title:"Installation",description:"Flutter",source:"@site/docs/develop/installation.md",sourceDirName:"develop",slug:"/develop/installation",permalink:"/docs/develop/installation",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Onboarding",permalink:"/docs/develop/onboarding"},next:{title:"Architecture",permalink:"/docs/develop/architecture"}},l={},d=[{value:"Flutter",id:"flutter",level:2},{value:"Tool versions",id:"tool-versions",level:2},{value:"Install the app",id:"install-the-app",level:2},{value:"Run the app",id:"run-the-app",level:2},{value:"...with live data",id:"with-live-data",level:3},{value:"...with test data",id:"with-test-data",level:3},{value:"... with a physical device",id:"-with-a-physical-device",level:3},{value:"Integration tests",id:"integration-tests",level:2},{value:"Editor",id:"editor",level:2},{value:"Monarch",id:"monarch",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"installation",children:"Installation"})}),"\n",(0,i.jsx)(n.h2,{id:"flutter",children:"Flutter"}),"\n",(0,i.jsxs)(n.p,{children:["Follow the ",(0,i.jsx)(n.a,{href:"https://docs.flutter.dev/get-started/install",children:"Flutter Installation"})," instructions."]}),"\n",(0,i.jsxs)(n.p,{children:["It is important that you are able to run ",(0,i.jsx)(n.code,{children:"flutter doctor"})," without error:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"% flutter doctor\nDoctor summary (to see all details, run flutter doctor -v):\n[\u2713] Flutter (Channel stable, 3.10.0, on macOS 13.3.1 22E772610a darwin-arm64, locale en-US)\n[\u2713] Android toolchain - develop for Android devices (Android SDK version 33.0.1)\n[\u2713] Xcode - develop for iOS and macOS (Xcode 14.3)\n[\u2713] Chrome - develop for the web\n[\u2713] Android Studio (version 2021.3)\n[\u2713] IntelliJ IDEA Ultimate Edition (version 2023.1)\n[\u2713] Connected device (3 available)\n[\u2713] Network resources\n\n\u2022 No issues found!\n"})}),"\n",(0,i.jsx)(n.h2,{id:"tool-versions",children:"Tool versions"}),"\n",(0,i.jsx)(n.p,{children:"It turns out that getting Flutter Doctor to report no issues is not enough. There are other tech stack components which must also be at an appropriate version in order for the app to run successfully during development. In some cases, there might be multiple possible versions, but every developer must be using the same version of the tools; otherwise the app will run for some developers but not for others."}),"\n",(0,i.jsxs)(n.p,{children:["In order to help developers ensure that they have the same tech stack environment, we have implemented a script called ",(0,i.jsx)(n.code,{children:"run_tool_versions.sh"})," that prints versions of the tech stack tools important to getting GGC to run correctly."]}),"\n",(0,i.jsxs)(n.p,{children:["Our Discord server has a channel called ",(0,i.jsx)(n.code,{children:"#tool-versions"})," where developers post the output from running this script. This helps all of us to stay on the same page, and when one person updates a component of the tech stack, they can post the new output from the script so that everyone else can update their tech stack to match the new version(s) of components."]}),"\n",(0,i.jsxs)(n.p,{children:["Here is an example of the output from ",(0,i.jsx)(n.code,{children:"run_tool_versions.sh"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"% ./run_tool_versions.sh\nComputer Name: PMJ M2 2023\nCocoapods 1.15.2\nDart SDK version: 3.5.4 \nFlutter 3.24.4 \nMacOS 14.6.1\nMonarch version 2.2.7\nruby 3.2.2 (202\nXcode 16.0\n"})}),"\n",(0,i.jsx)(n.p,{children:"Be sure to run this script locally and check it against the output from the Discord channel."}),"\n",(0,i.jsx)(n.p,{children:'Different components have different "tolerances" for version matching. In general, you should make sure that your version of Cocoapods, Dart, Flutter, Monarch, and XCode matches the Discord channel\'s latest versions exactly.'}),"\n",(0,i.jsx)(n.p,{children:'For Ruby, it appears that any 3.x version is good enough. Similarly, getting "close" with respect to MacOS version is generally close enough.'}),"\n",(0,i.jsx)(n.p,{children:"Note that all GGC development is done using macOS. We do not support Windows or Unix-based development at this time."}),"\n",(0,i.jsx)(n.h2,{id:"install-the-app",children:"Install the app"}),"\n",(0,i.jsxs)(n.p,{children:["To install the app, first clone the sources from ",(0,i.jsx)(n.a,{href:"https://github.com/geogardenclub/ggc_app",children:"https://github.com/geogardenclub/ggc_app"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Next, cd into the ggc_app directory and run ",(0,i.jsx)(n.code,{children:"flutter pub get"}),". For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'% flutter pub get\nRunning "flutter pub get" in ggc_app...\nResolving dependencies... (1.4s)\n _fe_analyzer_shared 58.0.0 (59.0.0 available)\n analyzer 5.10.0 (5.11.1 available)\n async 2.10.0 (2.11.0 available)\n build_daemon 3.1.1 (4.0.0 available)\n build_runner 2.3.3 (2.4.1 available)\n characters 1.2.1 (1.3.0 available)\n collection 1.17.0 (1.17.1 available)\n flex_color_scheme 7.0.3 (7.0.4 available)\n flutter_form_builder 7.8.0 (8.0.0 available)\n flutter_riverpod 2.3.5 (2.3.6 available)\n flutter_svg 1.1.6 (2.0.5 available)\n go_router 6.5.7 (6.5.8 available)\n intl 0.17.0 (0.18.1 available)\n js 0.6.5 (0.6.7 available)\n matcher 0.12.13 (0.12.15 available)\n material_color_utilities 0.2.0 (0.3.0 available)\n meta 1.8.0 (1.9.1 available)\n monarch 3.0.1 (3.4.0 available)\n path 1.8.2 (1.8.3 available)\n path_provider_windows 2.1.5 (2.1.6 available)\n petitparser 5.1.0 (5.4.0 available)\n riverpod 2.3.5 (2.3.6 available)\n source_span 1.9.1 (1.10.0 available)\n sqflite 2.2.6 (2.2.7 available)\n sqflite_common 2.4.3 (2.4.4 available)\n synchronized 3.0.1 (3.1.0 available)\n test_api 0.4.16 (0.5.2 available)\n vm_service 11.3.0 (11.4.0 available)\n win32 3.1.4 (4.1.3 available)\n xml 6.2.2 (6.3.0 available)\nGot dependencies!\n'})}),"\n",(0,i.jsx)(n.h2,{id:"run-the-app",children:"Run the app"}),"\n",(0,i.jsx)(n.h3,{id:"with-live-data",children:"...with live data"}),"\n",(0,i.jsxs)(n.p,{children:["To check that the ggc_app runs in your development environment, the simplest thing to do is to invoke ",(0,i.jsx)(n.code,{children:"flutter run"})," and select Chrome:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'% flutter run\nMultiple devices found:\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 13.3.1 22E261 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 112.0.5615.137\n[1]: macOS (macos)\n[2]: Chrome (chrome)\nPlease choose one (To quit, press "q/Q"): 2\nLaunching lib/main.dart on Chrome in debug mode...\nWaiting for connection from debug service on Chrome... 16.5s\nThis app is linked to the debug service: ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\nDebug service listening on ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\n\n\ud83d\udcaa Running with sound null safety \ud83d\udcaa\n\n\ud83d\udd25 To hot restart changes while running, press "r" or "R".\nFor a more detailed help message, press "h". To quit, press "q".\n\nAn Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:58007/FT3-VNs7AGk=\nWARNING: found an existing tag. Flutter Web uses its own viewport configuration for better compatibility with\nFlutter. This tag will be replaced.\nThe Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:58007/FT3-VNs7AGk=\n\n'})}),"\n",(0,i.jsx)(n.p,{children:"If all goes well, you should see a window similar to the following appear:"}),"\n",(0,i.jsx)("img",{src:"/img/develop/getting-started/installation-ggc-chrome.png"}),"\n",(0,i.jsx)(n.p,{children:"This means the app is up and is connected to the production Firebase database. At this point, you can login as one of the existing users to make sure communication with Firebase is working correctly. Contact Philip for credentials."}),"\n",(0,i.jsx)(n.h3,{id:"with-test-data",children:"...with test data"}),"\n",(0,i.jsxs)(n.p,{children:["During development, it is usually safer and more appropriate to not manipulate the production database. As an alternative, instead of invoking ",(0,i.jsx)(n.code,{children:"flutter run"}),", you invoke ",(0,i.jsx)(n.code,{children:"flutter run lib/main_test_fixture.dart"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'~/GitHub/geogardenclub/ggc_app $ flutter run lib/main_test_fixture.dart\nLaunching lib/main_test_fixture.dart on iPhone 15 Pro in debug mode...\nRunning Xcode build... \n \u2514\u2500Compiling, linking and signing... 7.2s\nXcode build done. 45.7s\nSyncing files to device iPhone 15 Pro... 149ms\n\nFlutter run key commands.\nr Hot reload. \ud83d\udd25\ud83d\udd25\ud83d\udd25\nR Hot restart.\nh List all available interactive commands.\nd Detach (terminate "flutter run" but leave application running).\nc Clear the screen\nq Quit (terminate the application on the device).\n\nA Dart VM Service on iPhone 15 Pro is available at: http://127.0.0.1:64617/Tfq60_DovMo=/\nThe Flutter DevTools debugger and profiler on iPhone 15 Pro is available at:\nhttp://127.0.0.1:9102?uri=http://127.0.0.1:64617/Tfq60_DovMo=/\n'})}),"\n",(0,i.jsxs)(n.admonition,{type:"info",children:[(0,i.jsx)(n.mdxAdmonitionTitle,{}),(0,i.jsx)(n.p,{children:'In IntelliJ, you can create a "Run Configuration" that enables you to run with the test database by clicking the green arrow at the top of the IntelliJ window.'})]}),"\n",(0,i.jsxs)(n.p,{children:["This command overrides the Riverpod providers so that they load the test fixture data in ",(0,i.jsx)(n.code,{children:"assets/test/fixture1"}),". Note that changes you make with the UI are never written to these files, so if you reload the system during development, the app state will be restored to the test fixture state. However, as long as you do not reload, changes to the data are reflected in the UI, making this a good way to test out changes to the system without fear of affecting the production database in a negative manner."]}),"\n",(0,i.jsx)(n.h3,{id:"-with-a-physical-device",children:"... with a physical device"}),"\n",(0,i.jsx)(n.p,{children:"Sometimes it is useful to run the app on a physical device rather than the simulator. Currently only Philip can do this due to iOS signing issues."}),"\n",(0,i.jsx)(n.p,{children:"Here is how to do it:"}),"\n",(0,i.jsx)(n.p,{children:"First, delete the app from your physical device."}),"\n",(0,i.jsx)(n.p,{children:"Second, connect the physical device to a laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Third, run ",(0,i.jsx)(n.code,{children:"flutter devices"})," to verify that the device shows up. You should see output like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:" % flutter devices \nFound 5 connected devices:\n Philip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\n iPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n (simulator)\n macOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\n Mac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\n Chrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n"})}),"\n",(0,i.jsx)(n.p,{children:"Notice that the first device is my phone connected to my laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Now, to run the code in release mode on this physical device, you invoke ",(0,i.jsx)(n.code,{children:"flutter run --release"})," and select the physical device as shown below. (Make sure your device is unlocked while running this command.)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"% flutter run --release\nConnected devices:\nPhilip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\niPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n(simulator)\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\nMac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n\nChecking for wireless devices...\n\nNo wireless devices were found.\n\n[1]: Philip's iPhone (00008030-000364940A98802E)\n[2]: iPhone 11 (8E550E86-3173-4342-B197-A557B83E40A2)\n[3]: macOS (macos)\n[4]: Mac Designed for iPad (mac-designed-for-ipad)\n[5]: Chrome (chrome)\nPlease choose one (or \"q\" to quit): 1\nLaunching lib/main.dart on Philip's iPhone in release mode...\nAutomatically signing iOS for device deployment using specified development team in Xcode project: 8M69898HLM\nRunning Xcode build... \n \u2514\u2500Compiling, linking and signing... 8.8s\nXcode build done. 67.9s\nInstalling and launching... 7.1s\n\nFlutter run key commands.\nh List all available interactive commands.\nc Clear the screen\nq Quit (terminate the application on the device).\n\n"})}),"\n",(0,i.jsx)(n.h2,{id:"integration-tests",children:"Integration tests"}),"\n",(0,i.jsxs)(n.p,{children:["The next step is to ensure that you can run the integration tests. Please follow the instructions on the ",(0,i.jsx)(n.a,{href:"/docs/develop/quality-assurance/testing",children:"Testing"})," page. If you cannot run the tests without encountering a test failure, please contact a developer for assistance."]}),"\n",(0,i.jsx)(n.h2,{id:"editor",children:"Editor"}),"\n",(0,i.jsx)(n.p,{children:"There are three good choices for your Editor: Visual Studio, Android Studio, or IntelliJ IDEA Ultimate (with the Dart and Flutter plugins, which makes it almost equivalent to Android Studio)."}),"\n",(0,i.jsx)(n.p,{children:"With IntelliJ IDEA Ultimate, after bringing up the project, you should see a run toolbar at the top which gives you (on a Mac) the option of opening the iOS simulator:"}),"\n",(0,i.jsx)("img",{src:"/img/develop/getting-started/installation-open-ios.png"}),"\n",(0,i.jsx)(n.p,{children:"After opening the simulator, it should appear and you should be able to emulate the system on an iOS device:"}),"\n",(0,i.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios.png"}),"\n",(0,i.jsx)(n.p,{children:"It takes a couple of minutes to do all of the XCode shenanigans the first time you run it, but eventually you should see something like the following:"}),"\n",(0,i.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios-2.png"}),"\n",(0,i.jsx)(n.p,{children:"As before, consult with Philip for login credentials."}),"\n",(0,i.jsx)(n.h2,{id:"monarch",children:"Monarch"}),"\n",(0,i.jsxs)(n.p,{children:["According to their home page, ",(0,i.jsx)(n.a,{href:"https://monarchapp.io/",children:"Monarch"}),' is a "tool for building Flutter widgets in isolation. It makes it easy to build, test and debug complex UIs." Monarch is basically a Flutter port of React Storybook, which is popular in React UI development.']}),"\n",(0,i.jsxs)(n.p,{children:["Follow the ",(0,i.jsx)(n.a,{href:"https://monarchapp.io/docs/install",children:"Monarch installation instructions"})," to install the tool."]}),"\n",(0,i.jsxs)(n.p,{children:["Then, invoke ",(0,i.jsx)(n.code,{children:"./run_monarch.sh"}),":"]}),"\n",(0,i.jsx)(n.p,{children:"You will see the Monarch UI appear:"}),"\n",(0,i.jsx)("img",{src:"/img/develop/getting-started/monarch.png"}),"\n",(0,i.jsx)(n.p,{children:'Select one of our themes (currently, "Green Theme: Light"). Then select a story, such as "showHomeScreenTasks", and Monarch will show the story:'}),"\n",(0,i.jsx)("img",{src:"/img/develop/getting-started/monarch-2.png"}),"\n",(0,i.jsxs)(n.p,{children:["Monarch loads the data in the ",(0,i.jsx)(n.code,{children:"test/fixture1"})," files to populate screens."]}),"\n",(0,i.jsx)(n.p,{children:"At present, Monarch seems most useful when experimenting with different themes. Monarch allows you to switch themes and see the results on your widget(s) of interest by simply selecting from the Theme menu. In contrast, in the simulator, you would need to navigate from the screen displaying the widget(s) of interest to the Settings page (in order to select the new theme), and then navigate back to the screen displaying the widget(s) of interest."}),"\n",(0,i.jsxs)(n.admonition,{title:"Ignore Firebase exceptions",type:"info",children:[(0,i.jsx)(n.p,{children:"Unfortunately, the console window will print out error messages similar to the following when using Monarch:"}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"\u2550\u2550\u2561 EXCEPTION CAUGHT BY MONARCH \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following message was thrown while a story was selected:\n[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()\n"})}),(0,i.jsx)(n.p,{children:"You can ignore these messages as Monarch does not actually invoke Firebase."})]})]})}function c(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>r,a:()=>s});var i=t(7294);const o={},a=i.createContext(o);function s(e){const n=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6b1fc3de.d16786a4.js b/assets/js/6b1fc3de.d16786a4.js deleted file mode 100644 index 1184bf46..00000000 --- a/assets/js/6b1fc3de.d16786a4.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8754],{8312:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>a,metadata:()=>r,toc:()=>h});var o=n(5893),i=n(1151);const a={hide_table_of_contents:!1},s="Installation",r={id:"develop/installation",title:"Installation",description:"Flutter",source:"@site/docs/develop/installation.md",sourceDirName:"develop",slug:"/develop/installation",permalink:"/docs/develop/installation",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Onboarding",permalink:"/docs/develop/onboarding"},next:{title:"Architecture",permalink:"/docs/develop/architecture"}},l={},h=[{value:"Flutter",id:"flutter",level:2},{value:"Tool versions",id:"tool-versions",level:2},{value:"Install the app",id:"install-the-app",level:2},{value:"Run the app",id:"run-the-app",level:2},{value:"...with the production database",id:"with-the-production-database",level:3},{value:"...with test data",id:"with-test-data",level:3},{value:"Integration tests",id:"integration-tests",level:2},{value:"Editor",id:"editor",level:2},{value:"Monarch",id:"monarch",level:2}];function d(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",...(0,i.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.header,{children:(0,o.jsx)(t.h1,{id:"installation",children:"Installation"})}),"\n",(0,o.jsx)(t.h2,{id:"flutter",children:"Flutter"}),"\n",(0,o.jsxs)(t.p,{children:["Follow the ",(0,o.jsx)(t.a,{href:"https://docs.flutter.dev/get-started/install",children:"Flutter Installation"})," instructions."]}),"\n",(0,o.jsxs)(t.p,{children:["It is important that you are able to run ",(0,o.jsx)(t.code,{children:"flutter doctor"})," without error:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"% flutter doctor\nDoctor summary (to see all details, run flutter doctor -v):\n[\u2713] Flutter (Channel stable, 3.10.0, on macOS 13.3.1 22E772610a darwin-arm64, locale en-US)\n[\u2713] Android toolchain - develop for Android devices (Android SDK version 33.0.1)\n[\u2713] Xcode - develop for iOS and macOS (Xcode 14.3)\n[\u2713] Chrome - develop for the web\n[\u2713] Android Studio (version 2021.3)\n[\u2713] IntelliJ IDEA Ultimate Edition (version 2023.1)\n[\u2713] Connected device (3 available)\n[\u2713] Network resources\n\n\u2022 No issues found!\n"})}),"\n",(0,o.jsx)(t.h2,{id:"tool-versions",children:"Tool versions"}),"\n",(0,o.jsx)(t.p,{children:"It turns out that getting Flutter Doctor to report no issues is not enough. There are other tech stack components which must also be at an appropriate version in order for the app to run successfully during development. In some cases, there might be multiple possible versions, but every developer must be using the same version of the tools; otherwise the app will run for some developers but not for others."}),"\n",(0,o.jsxs)(t.p,{children:["In order to help developers ensure that they have the same tech stack environment, we have implemented a script called ",(0,o.jsx)(t.code,{children:"run_tool_versions.sh"})," that prints versions of the tech stack tools important to getting GGC to run correctly."]}),"\n",(0,o.jsxs)(t.p,{children:["Our Discord server has a channel called ",(0,o.jsx)(t.code,{children:"#tool-versions"})," where developers post the output from running this script. This helps all of us to stay on the same page, and when one person updates a component of the tech stack, they can post the new output from the script so that everyone else can update their tech stack to match the new version(s) of components."]}),"\n",(0,o.jsxs)(t.p,{children:["Here is an example of the output from ",(0,o.jsx)(t.code,{children:"run_tool_versions.sh"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"% ./run_tool_versions.sh\nComputer Name: PMJ M2 2023\nCocoapods 1.15.2\nDart SDK version: 3.5.4 \nFlutter 3.24.4 \nMacOS 14.6.1\nMonarch version 2.2.7\nruby 3.2.2 (202\nXcode 16.0\n"})}),"\n",(0,o.jsx)(t.p,{children:"Be sure to run this script locally and check it against the output from the Discord channel."}),"\n",(0,o.jsx)(t.p,{children:'Different components have different "tolerances" for version matching. In general, you should make sure that your version of Cocoapods, Dart, Flutter, Monarch, and XCode matches the Discord channel\'s latest versions exactly.'}),"\n",(0,o.jsx)(t.p,{children:'For Ruby, it appears that any 3.x version is good enough. Similarly, getting "close" with respect to MacOS version is generally close enough.'}),"\n",(0,o.jsx)(t.p,{children:"Note that all GGC development is done using macOS. We do not support Windows or Unix-based development at this time."}),"\n",(0,o.jsx)(t.h2,{id:"install-the-app",children:"Install the app"}),"\n",(0,o.jsxs)(t.p,{children:["To install the app, first clone the sources from ",(0,o.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app",children:"https://github.com/geogardenclub/ggc_app"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["Next, cd into the ggc_app directory and run ",(0,o.jsx)(t.code,{children:"flutter pub get"}),". For example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:'% flutter pub get\nRunning "flutter pub get" in ggc_app...\nResolving dependencies... (1.4s)\n _fe_analyzer_shared 58.0.0 (59.0.0 available)\n analyzer 5.10.0 (5.11.1 available)\n async 2.10.0 (2.11.0 available)\n build_daemon 3.1.1 (4.0.0 available)\n build_runner 2.3.3 (2.4.1 available)\n characters 1.2.1 (1.3.0 available)\n collection 1.17.0 (1.17.1 available)\n flex_color_scheme 7.0.3 (7.0.4 available)\n flutter_form_builder 7.8.0 (8.0.0 available)\n flutter_riverpod 2.3.5 (2.3.6 available)\n flutter_svg 1.1.6 (2.0.5 available)\n go_router 6.5.7 (6.5.8 available)\n intl 0.17.0 (0.18.1 available)\n js 0.6.5 (0.6.7 available)\n matcher 0.12.13 (0.12.15 available)\n material_color_utilities 0.2.0 (0.3.0 available)\n meta 1.8.0 (1.9.1 available)\n monarch 3.0.1 (3.4.0 available)\n path 1.8.2 (1.8.3 available)\n path_provider_windows 2.1.5 (2.1.6 available)\n petitparser 5.1.0 (5.4.0 available)\n riverpod 2.3.5 (2.3.6 available)\n source_span 1.9.1 (1.10.0 available)\n sqflite 2.2.6 (2.2.7 available)\n sqflite_common 2.4.3 (2.4.4 available)\n synchronized 3.0.1 (3.1.0 available)\n test_api 0.4.16 (0.5.2 available)\n vm_service 11.3.0 (11.4.0 available)\n win32 3.1.4 (4.1.3 available)\n xml 6.2.2 (6.3.0 available)\nGot dependencies!\n'})}),"\n",(0,o.jsx)(t.h2,{id:"run-the-app",children:"Run the app"}),"\n",(0,o.jsx)(t.h3,{id:"with-the-production-database",children:"...with the production database"}),"\n",(0,o.jsxs)(t.p,{children:["To check that the ggc_app runs in your development environment, the simplest thing to do is to invoke ",(0,o.jsx)(t.code,{children:"flutter run"})," and select Chrome:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:'% flutter run\nMultiple devices found:\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 13.3.1 22E261 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 112.0.5615.137\n[1]: macOS (macos)\n[2]: Chrome (chrome)\nPlease choose one (To quit, press "q/Q"): 2\nLaunching lib/main.dart on Chrome in debug mode...\nWaiting for connection from debug service on Chrome... 16.5s\nThis app is linked to the debug service: ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\nDebug service listening on ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\n\n\ud83d\udcaa Running with sound null safety \ud83d\udcaa\n\n\ud83d\udd25 To hot restart changes while running, press "r" or "R".\nFor a more detailed help message, press "h". To quit, press "q".\n\nAn Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:58007/FT3-VNs7AGk=\nWARNING: found an existing tag. Flutter Web uses its own viewport configuration for better compatibility with\nFlutter. This tag will be replaced.\nThe Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:58007/FT3-VNs7AGk=\n\n'})}),"\n",(0,o.jsx)(t.p,{children:"If all goes well, you should see a window similar to the following appear:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-ggc-chrome.png"}),"\n",(0,o.jsx)(t.p,{children:"This means the app is up and is connected to the production Firebase database. At this point, you can login as one of the existing users to make sure communication with Firebase is working correctly. Contact Philip for credentials."}),"\n",(0,o.jsx)(t.h3,{id:"with-test-data",children:"...with test data"}),"\n",(0,o.jsxs)(t.p,{children:["During development, it is usually safer and more appropriate to not manipulate the production database. As an alternative, instead of invoking ",(0,o.jsx)(t.code,{children:"flutter run"}),", you invoke ",(0,o.jsx)(t.code,{children:"flutter run lib/main_test_fixture.dart"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:'~/GitHub/geogardenclub/ggc_app $ flutter run lib/main_test_fixture.dart\nLaunching lib/main_test_fixture.dart on iPhone 15 Pro in debug mode...\nRunning Xcode build... \n \u2514\u2500Compiling, linking and signing... 7.2s\nXcode build done. 45.7s\nSyncing files to device iPhone 15 Pro... 149ms\n\nFlutter run key commands.\nr Hot reload. \ud83d\udd25\ud83d\udd25\ud83d\udd25\nR Hot restart.\nh List all available interactive commands.\nd Detach (terminate "flutter run" but leave application running).\nc Clear the screen\nq Quit (terminate the application on the device).\n\nA Dart VM Service on iPhone 15 Pro is available at: http://127.0.0.1:64617/Tfq60_DovMo=/\nThe Flutter DevTools debugger and profiler on iPhone 15 Pro is available at:\nhttp://127.0.0.1:9102?uri=http://127.0.0.1:64617/Tfq60_DovMo=/\n'})}),"\n",(0,o.jsxs)(t.admonition,{type:"info",children:[(0,o.jsx)(t.mdxAdmonitionTitle,{}),(0,o.jsx)(t.p,{children:'In IntelliJ, you can create a "Run Configuration" that enables you to run with the test database by clicking the green arrow at the top of the IntelliJ window.'})]}),"\n",(0,o.jsxs)(t.p,{children:["This command overrides the Riverpod providers so that they load the test fixture data in ",(0,o.jsx)(t.code,{children:"assets/test/fixture1"}),". Note that changes you make with the UI are never written to these files, so if you reload the system during development, the app state will be restored to the test fixture state. However, as long as you do not reload, changes to the data are reflected in the UI, making this a good way to test out changes to the system without fear of affecting the production database in a negative manner."]}),"\n",(0,o.jsx)(t.h2,{id:"integration-tests",children:"Integration tests"}),"\n",(0,o.jsxs)(t.p,{children:["The next step is to ensure that you can run the integration tests. Please follow the instructions on the ",(0,o.jsx)(t.a,{href:"/docs/develop/quality-assurance/testing",children:"Testing"})," page. If you cannot run the tests without encountering a test failure, please contact a developer for assistance."]}),"\n",(0,o.jsx)(t.h2,{id:"editor",children:"Editor"}),"\n",(0,o.jsx)(t.p,{children:"There are three good choices for your Editor: Visual Studio, Android Studio, or IntelliJ IDEA Ultimate (with the Dart and Flutter plugins, which makes it almost equivalent to Android Studio)."}),"\n",(0,o.jsx)(t.p,{children:"With IntelliJ IDEA Ultimate, after bringing up the project, you should see a run toolbar at the top which gives you (on a Mac) the option of opening the iOS simulator:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-open-ios.png"}),"\n",(0,o.jsx)(t.p,{children:"After opening the simulator, it should appear and you should be able to emulate the system on an iOS device:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios.png"}),"\n",(0,o.jsx)(t.p,{children:"It takes a couple of minutes to do all of the XCode shenanigans the first time you run it, but eventually you should see something like the following:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios-2.png"}),"\n",(0,o.jsx)(t.p,{children:"As before, consult with Philip for login credentials."}),"\n",(0,o.jsx)(t.h2,{id:"monarch",children:"Monarch"}),"\n",(0,o.jsxs)(t.p,{children:["According to their home page, ",(0,o.jsx)(t.a,{href:"https://monarchapp.io/",children:"Monarch"}),' is a "tool for building Flutter widgets in isolation. It makes it easy to build, test and debug complex UIs." Monarch is basically a Flutter port of React Storybook, which is popular in React UI development.']}),"\n",(0,o.jsxs)(t.p,{children:["Follow the ",(0,o.jsx)(t.a,{href:"https://monarchapp.io/docs/install",children:"Monarch installation instructions"})," to install the tool."]}),"\n",(0,o.jsxs)(t.p,{children:["Then, invoke ",(0,o.jsx)(t.code,{children:"./run_monarch.sh"}),":"]}),"\n",(0,o.jsx)(t.p,{children:"You will see the Monarch UI appear:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/monarch.png"}),"\n",(0,o.jsx)(t.p,{children:'Select one of our themes (currently, "Green Theme: Light"). Then select a story, such as "showHomeScreenTasks", and Monarch will show the story:'}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/monarch-2.png"}),"\n",(0,o.jsxs)(t.p,{children:["Monarch loads the data in the ",(0,o.jsx)(t.code,{children:"test/fixture1"})," files to populate screens."]}),"\n",(0,o.jsx)(t.p,{children:"At present, Monarch seems most useful when experimenting with different themes. Monarch allows you to switch themes and see the results on your widget(s) of interest by simply selecting from the Theme menu. In contrast, in the simulator, you would need to navigate from the screen displaying the widget(s) of interest to the Settings page (in order to select the new theme), and then navigate back to the screen displaying the widget(s) of interest."}),"\n",(0,o.jsxs)(t.admonition,{title:"Ignore Firebase exceptions",type:"info",children:[(0,o.jsx)(t.p,{children:"Unfortunately, the console window will print out error messages similar to the following when using Monarch:"}),(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"\u2550\u2550\u2561 EXCEPTION CAUGHT BY MONARCH \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following message was thrown while a story was selected:\n[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()\n"})}),(0,o.jsx)(t.p,{children:"You can ignore these messages as Monarch does not actually invoke Firebase."})]})]})}function c(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>r,a:()=>s});var o=n(7294);const i={},a=o.createContext(i);function s(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/afab5b7c.3f5cd913.js b/assets/js/afab5b7c.3f5cd913.js new file mode 100644 index 00000000..b7196d81 --- /dev/null +++ b/assets/js/afab5b7c.3f5cd913.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[3029],{3430:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var i=n(5893),a=n(1151);const s={hide_table_of_contents:!1},r="Testing",o={id:"develop/quality-assurance/testing",title:"Testing",description:"Another form of quality assurance in GGC is testing.",source:"@site/docs/develop/quality-assurance/testing.md",sourceDirName:"develop/quality-assurance",slug:"/develop/quality-assurance/testing",permalink:"/docs/develop/quality-assurance/testing",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Dart analyze",permalink:"/docs/develop/quality-assurance/dart-analyze"},next:{title:"Database Integrity Checking",permalink:"/docs/develop/quality-assurance/integrity-check"}},l={},d=[{value:"Installation",id:"installation",level:2},{value:"run_tests.sh",id:"run_testssh",level:2},{value:"app_test.dart",id:"app_testdart",level:2},{value:"Testing a feature",id:"testing-a-feature",level:2},{value:"run_tests_single.sh",id:"run_tests_singlesh",level:2},{value:"Coverage",id:"coverage",level:2},{value:"Test Design Hints",id:"test-design-hints",level:2},{value:"Continuous integration",id:"continuous-integration",level:2},{value:"Test fixture design",id:"test-fixture-design",level:2},{value:"Fixture Paths",id:"fixture-paths",level:3},{value:"AssetCollectionBuilder",id:"assetcollectionbuilder",level:3},{value:"TestFixture singleton",id:"testfixture-singleton",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"testing",children:"Testing"})}),"\n",(0,i.jsx)(t.p,{children:"Another form of quality assurance in GGC is testing."}),"\n",(0,i.jsx)(t.p,{children:"In GGC, we want the main branch to always be free of any errors raised by our tests."}),"\n",(0,i.jsxs)(t.p,{children:["The current goal of testing in GeoGardenClub is to minimize the risk of ",(0,i.jsx)(t.em,{children:"catastrophic regression"})," from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'All commonly accessed screens display without error. (The tests might not check screens that are displayed "rarely", such as those resulting from anomalous conditions like network instability.)'}),"\n",(0,i.jsx)(t.li,{children:"CRUD operations on entities can be performed successfully when available."}),"\n",(0,i.jsx)(t.li,{children:"Buttons on all commonly accessed screens, when tapped, do not generate an error, and the resulting screen is checked to see that at least some of the intended results are displayed."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Currently, our approach to testing does not address many important quality issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No load testing."}),' We do not test that the system performs well under "load", where load can mean a large number of concurrent users and/or a large amount of stored data.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No external service testing."}),' We do not test "low-level" code, specifically external services such as database, photo storage, and authentication. This is because we mock external services in our test code.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No matrix (platform/device) testing."})," GGC is intended to be used on three platforms: iOS, Android, and Web. Each of these platforms supports many different devices. We only test on one platform (iOS) and one device (typically iPhone 17)."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No UX testing."})," Our tests do not ensure that user needs are met and that they have a positive experience using the app."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by running the tests. A decent test suite should enable us to incrementally improve the quality of the code over time as well as the feature set."}),"\n",(0,i.jsxs)(t.admonition,{title:"When should you run the tests?",type:"info",children:[(0,i.jsx)(t.p,{children:"Ideally, we want the main branch to always be able to run the tests without error."}),(0,i.jsx)(t.p,{children:"To achieve that, the best times to run the tests are:"}),(0,i.jsxs)(t.ol,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Whenever you update the code you're working on from the main branch."})," This ensures that the update from main has not introduced code that breaks the tests in your branch. If the tests fail, address that problem before proceeding."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Before updating the main branch with your code."})," This ensures that you are not introducing code into the main branch that breaks the tests."]}),"\n"]}),(0,i.jsx)(t.p,{children:"As you will learn below, we have not figured out a reasonable way to perform continuous integration (i.e. run the tests automatically in the cloud upon any commit to the main branch.) As a result, it is up to us to manually verify that the main branch can execute the test suite without error."})]}),"\n",(0,i.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.p,{children:"There are a few things you need to do to set up your machine to run the test suite locally."}),"\n",(0,i.jsx)(t.p,{children:"Note that at the current time, we only support testing on macOS."}),"\n",(0,i.jsx)(t.p,{children:"First, bring up the iOS simulator and verify that you can bring up GGC on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.)"}),"\n",(0,i.jsx)(t.p,{children:"Second, install lcov on your machine by invoking:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"brew install lcov\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Third, activate the ",(0,i.jsx)(t.code,{children:"remove_from_coverage"})," Dart package so that the coverage report can be customized. Do this by invoking:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-agsl",children:"dart pub global activate remove_from_coverage\n"})}),"\n",(0,i.jsx)(t.h2,{id:"run_testssh",children:"run_tests.sh"}),"\n",(0,i.jsxs)(t.p,{children:["To run the test suite, open the iOS simulator, make sure it is visible on your desktop, and then invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," in a terminal window. It should take around 5 minutes to run, and should produce output similar to the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"$ ./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s\nXcode build done. 30.1s\n00:42 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting bed feature\n... test Bed CRUD\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\nTesting chapter feature\nTesting chat feature\nTesting crop feature\n... test Crop Index Screen\n... test Crop CRUD\nTesting garden feature\n... test Garden Index Screen\n... test Garden Details Screen\n... test Garden CRUD\nTesting gardener feature\n... test Gardener Index Screen\nTesting geobot feature\nTesting home feature\nTesting observation feature\n... test Observation Feed\n... test Observation CRUD\nTesting outcome feature\n... test Outcome Garden Details View\n... test Outcome CRUD\nTesting planting feature\n... test Planting Index Screen\n... test Planting CRUD\n... test Planting Copy Planting\nTesting settings feature\nTesting task feature\n... test Task View\n... test Task CRUD\nTesting user feature\n... test User Profile Update\nTesting variety feature\n... test Variety Index Screen\n... test Variety CRUD\n... test Variety Gold Varieties\n04:52 +1: All tests passed! \n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'repositories\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'data\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'domain\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'authentication\\/.*$'\n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 322\n lines.......: 73.0% (6079 of 8329 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:'Note the line "All tests passed" after the sequence of lines documenting the feature under test.'}),"\n",(0,i.jsxs)(t.admonition,{title:"Uh oh...",type:"info",children:[(0,i.jsx)(t.p,{children:'If the tests do not run successfully, there won\'t be the line "All tests passed", and the output will instead look similar to this:'}),(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"$ ./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s\nXcode build done. 30.1s\n00:42 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting bed feature\n... test Bed CRUD\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\nTesting chapter feature\nTesting chat feature\nTesting crop feature\n... test Crop Index Screen\n... test Crop CRUD\nTesting garden feature\n... test Garden Index Screen\n... test Garden Details Screen\n... test Garden CRUD\nTesting gardener feature\n... test Gardener Index Screen\nTesting geobot feature\nTesting home feature\nTesting observation feature\n... test Observation Feed\n... test Observation CRUD\nTesting outcome feature\n... test Outcome Garden Details View\n... test Outcome CRUD\nTesting planting feature\n... test Planting Index Screen\n... test Planting CRUD\n... test Planting Copy Planting\n\u2550\u2550\u2561 EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following TestFailure was thrown running a test:\nExpected: \n Actual: \n\nWhen the exception was thrown, this was the stack:\n#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)\n\n#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)\n\n#6 main.. (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)\n\n#7 patrolWidgetTest. (package:patrol_finders/src/common.dart:50:7)\n\n#8 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15)\n\n#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)\n\n\n(elided one frame from package:stack_trace)\n\nThis was caught by the test expectation on the following line:\n file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24\nThe test description was:\n Fixture 1 Tests\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E] \n Test failed. See exception logs above.\n The test description was: Fixture 1 Tests\n \n\nTo run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'\n03:16 +0 -1: Some tests failed. \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 54.8% (7029 of 12823 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),(0,i.jsx)(t.p,{children:"You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart."}),(0,i.jsx)(t.p,{children:"If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfully using the iPhone 15 simulator under iOS 17.5."})]}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways from this test execution output:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code. Currently, the lib/ directory contains around 44K lines of code, and the integration_test/ directory contains around 1200 lines of code. So, the test code only accounts for around 2% of the total code base."}),"\n",(0,i.jsxs)(t.li,{children:['Our tests run with a specific "test fixture" (currently we\'re using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in ',(0,i.jsx)(t.code,{children:"assets/test/fixture1"}),". In the future, we might write tests that require a different fixture."]}),"\n",(0,i.jsx)(t.li,{children:"Our test architecture is organized around features."}),"\n",(0,i.jsx)(t.li,{children:'The "Rate Limiter" might be triggered. You can ignore this warning.'}),"\n",(0,i.jsx)(t.li,{children:"We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested, not to verify that the tests achieve 100% coverage (more on this below). We also remove several directories from the coverage report (i.e. data/, domain/, and repositories/) so that the coverage report does not report on code that is never executed due to mocking (i.e. code in the data/ and repositories/ directories) and also focuses more specifically on UI code."}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{title:"Keep the iOS simulator open and visible!",type:"warning",children:[(0,i.jsx)(t.p,{children:"When running the tests, be sure to keep the iOS simulator visible on your desktop for two reasons."}),(0,i.jsx)(t.p,{children:"First, we have discovered that if the iOS simulator is not visible, the tests may fail unpredictably."}),(0,i.jsx)(t.p,{children:"Second, the testing process will occasionally (and unpredictably) pause in the iOS simulator, waiting for you to click on a button to allow pasting:"}),(0,i.jsx)("img",{src:"/img/develop/testing/core-simulator-bridge.png"}),(0,i.jsx)(t.p,{children:"If you don't click the button to allow pasting, the test process will hang indefinitely."}),(0,i.jsx)(t.p,{children:"This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time."})]}),"\n",(0,i.jsx)(t.h2,{id:"app_testdart",children:"app_test.dart"}),"\n",(0,i.jsxs)(t.p,{children:["To further understand the test process, it's helpful to review the code that is run by the ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," command:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (All)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),\n badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),\n bedsProvider.overrideWith((_) => testFixture.getBedsStream()),\n bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),\n chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),\n chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),\n chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),\n chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),\n cropsProvider.overrideWith((_) => testFixture.getCropsStream()),\n cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),\n editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),\n editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),\n familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),\n familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),\n gardensProvider.overrideWith((_) => testFixture.getGardensStream()),\n gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),\n gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),\n gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),\n observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),\n observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),\n outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),\n outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),\n plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),\n plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),\n rolesProvider.overrideWith((_) => testFixture.getRolesStream()),\n roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),\n tagsProvider.overrideWith((_) => testFixture.getTagsStream()),\n tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),\n tasksProvider.overrideWith((_) => testFixture.getTasksStream()),\n taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),\n usersProvider.overrideWith((_) => testFixture.getUsersStream()),\n userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await checkIntegrity($, reason: 'startup');\n await testAdmin($);\n await checkIntegrity($, reason: 'admin feature');\n await testBadge($);\n await checkIntegrity($, reason: 'badge feature');\n await testBed($);\n await checkIntegrity($, reason: 'bed feature');\n await testChapter($);\n await checkIntegrity($, reason: 'chapter feature');\n await testChat($);\n await checkIntegrity($, reason: 'chat feature');\n await testCrop($);\n await checkIntegrity($, reason: 'crop feature');\n await testGarden($);\n await checkIntegrity($, reason: 'garden feature');\n await testGardener($);\n await checkIntegrity($, reason: 'gardener feature');\n await testGeoBot($);\n await checkIntegrity($, reason: 'geobot feature');\n await testHome($);\n await checkIntegrity($, reason: 'home feature');\n await testObservation($);\n await checkIntegrity($, reason: 'observation feature');\n await testOutcome($);\n await checkIntegrity($, reason: 'outcome feature');\n await testPlanting($);\n await checkIntegrity($, reason: 'planting feature');\n await testSettings($);\n await checkIntegrity($, reason: 'settings feature');\n await testTask($);\n await checkIntegrity($, reason: 'task feature');\n await testVariety($);\n await checkIntegrity($, reason: 'variety feature');\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are the important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["We use the ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/finders/overview",children:"Patrol Finders"})," package, which provides a very helpful syntactic sugar over the built-in Flutter testing package. We do not use the full Patrol package, just their Patrol Finder package."]}),"\n",(0,i.jsx)(t.li,{children:"We use the Riverpod overrides feature so that during testing, our code manipulates the test fixture data rather than the data in the Firebase database."}),"\n",(0,i.jsxs)(t.li,{children:["We simulate Firebase authentication (using firebase_auth_mocks) and the app starts up with the (admin) user ",(0,i.jsx)(t.a,{href:"mailto:jennacorindeane@gmail.com",children:"jennacorindeane@gmail.com"}),' already logged in. So, we don\'t currently test the registration or signin workflows. The app "starts" by displaying the Home screen for Jenna.']}),"\n",(0,i.jsx)(t.li,{children:'We test each feature by calling a "test" function (i.e. testChapter, testCrop, etc.).'}),"\n",(0,i.jsx)(t.li,{children:"After testing each feature, the test code runs the Check Integrity admin function to ensure that the test of the previous feature did not introduce a database inconsistency."}),"\n",(0,i.jsx)(t.li,{children:'Our integration testing approach is "big bang": we run the entire integration test suite in a single function. This means tests are not independent of each other, which can make individual test case design more difficult. We chose this design for pragmatic reasons: setting up the runtime environment for testing takes around 50 seconds (on my late model MacBook Pro). If we ran each of the 15 feature tests independently, that would add on an additional 12 minutes (15 features * 50 seconds) to test suite execution time. No bueno.'}),"\n",(0,i.jsxs)(t.li,{children:["You should rarely need to edit this ",(0,i.jsx)(t.code,{children:"app_test.dart"}),' file. Instead, you will usually edit one of the top-level "test" feature files (i.e. testChapter.dart, testCrop.dart, etc.) You will normally need to edit app_test.dart only when you want to introduce the testing of a new feature.']}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"testing-a-feature",children:"Testing a feature"}),"\n",(0,i.jsx)(t.p,{children:'Let\'s now look at how the "Crop" feature is currently tested. Here is the top-level feature test function for Crops:'}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop.dart\nFuture testCrop(PatrolTester $) async {\n // ignore: avoid_print\n print('Testing crop feature');\n await testCropIndexScreen($);\n await testCropCRUD($);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Each top-level feature test function starts by printing a line of output indicating that the test of this feature is starting. That makes it easier to see how far testing has gotten and helps pinpoint the location of problems when testing fails."}),"\n",(0,i.jsx)(t.li,{children:"A top-level feature test function is typically implemented by calling multiple functions, each of which tests a different aspect of the feature."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Here is testCropIndexScreen:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_index_screen.dart\nFuture testCropIndexScreen(PatrolTester $) async {\n // ignore: avoid_print\n print('... test Crop Index Screen');\n String testCrop = 'Amaranth';\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n await $(testCrop).tap();\n expect($(CropDropdown).$(testCrop).visible, equals(true));\n expect($(CropView).$(testCrop).visible, equals(true));\n // Refresh CropIndexScreen so it displays all crops.\n await gotoDrawerScreen($, ChapterIndexScreen);\n await gotoDrawerScreen($, CropIndexScreen);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We use Patrol Finder syntax to locate widgets and manipulate them through searching for widgets of a particular type and/or containing a particular text string. Please avoid creating Keys for testing. Patrol Finders make it possible to test the source code without introducing new lines of code purely for the purpose of test support."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Let's now look at the test for create, read, update, and delete of a Crop:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_crud.dart\nFuture testCropCRUD(PatrolTester $) async {\n // ignore: avoid_print\n print('... test Crop CRUD');\n String testCropName = 'AAATestCrop';\n String updatedTestCropName = 'AAAATestCrop';\n // Test Create.\n await gotoDrawerScreen($, CropIndexScreen);\n await $(GgcFAB).$('Crop').tap();\n expect($(CreateCropScreen).visible, equals(true));\n await $(CropNameField).enterText(testCropName);\n await $(FamilyDropdown).tap();\n await $('Allium').tap();\n await $(FormButtons).$('Submit').tap();\n expect($(CropIndexScreen).visible, equals(true));\n // Verify Create.\n await $(testCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Create crop');\n // Test Read and Update\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(testCropName).tap();\n await $('Update').tap();\n await $(CropNameField).enterText(updatedTestCropName);\n await $('Submit').tap();\n await $(BackButton).tap();\n await $(BackButton).tap();\n await gotoDrawerScreen($, CropIndexScreen);\n // Verify Update\n await $(updatedTestCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Update crop');\n // Test Delete\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(updatedTestCropName).tap();\n await $('Update').tap();\n await $(Icons.delete).tap();\n await $('Delete').tap();\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n // Verify delete\n expect($(updatedTestCropName).exists, equals(false));\n await checkIntegrity($, reason: 'Delete crop');\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Testing a behavior can require a relatively long sequence of UI interactions. Getting the sequence correct is way easier if you first step through the behavior manually. To make this easier, follow the instructions in the section below on "Run the simulator with test data".'}),"\n",(0,i.jsx)(t.li,{children:"It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function."}),"\n",(0,i.jsx)(t.li,{children:"The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"run_tests_singlesh",children:"run_tests_single.sh"}),"\n",(0,i.jsx)(t.p,{children:"While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code."}),"\n",(0,i.jsxs)(t.p,{children:["To speed up testing, you can use the command ",(0,i.jsx)(t.code,{children:"./run_tests_single.sh"}),". This runs the ",(0,i.jsx)(t.code,{children:"app_test_single.dart"})," file, which looks similar to this:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test_single.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (Single)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n :\n :\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await testCrop($);\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"You can freely edit this file in your branch to focus on the specific feature of interest."}),"\n",(0,i.jsxs)(t.li,{children:['Sometimes you might want to check multiple features at once, that\'s fine. You do you. The idea is that this is a kind of "sandbox" for you to develop tests so that you are not wishing to edit the global ',(0,i.jsx)(t.code,{children:"./run_tests.sh"})," and ",(0,i.jsx)(t.code,{children:"app_test.dart"})," files to speed up testing."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"coverage",children:"Coverage"}),"\n",(0,i.jsxs)(t.p,{children:["It can be useful to see the coverage of our test cases. After running the test suite, you can open the file ",(0,i.jsx)(t.code,{children:"coverage/html/index.html"}),". Here's what it looks like after clicking the button to sort the rows in order of increasing coverage."]}),"\n",(0,i.jsx)("img",{src:"/img/develop/testing/coverage.png"}),"\n",(0,i.jsx)(t.p,{children:"Note that our coverage report excludes the data/, domain/, and repositories/ directories. One reason is because the use of mocks means that the code in the data/ and repositories/ directories will never be executed by testing, so reporting (the necessarily low) coverage for that code is not useful; we can't fix that. We also exclude the domain/ directory so that the coverage information focuses more directly on UI code, which will hopefully make the report more useful in determining certain types of gaps in testing."}),"\n",(0,i.jsx)(t.p,{children:"There are clickable links that you can use to drill down to see which statements have been executed and which have not been."}),"\n",(0,i.jsx)(t.p,{children:'The primary goal of the coverage report is to simplify identification of "forgotten" areas of the UI for which we have not created any test cases.'}),"\n",(0,i.jsxs)(t.admonition,{title:"High coverage does not imply high test quality",type:"warning",children:[(0,i.jsx)(t.p,{children:"Beware that a high level of coverage does not, by itself, indicate that the test suite is high quality--i.e reliably able to indicate the absence of important errors in the code."}),(0,i.jsx)(t.p,{children:"To understand why, consider one important limitation of our test suite--the use of a test data fixture. Even if we got to 100% coverage with no errors with this (or any other) test data fixture, it would not guarantee that the code would execute correctly if the data was in some other state."}),(0,i.jsx)(t.p,{children:"That said, low coverage definitely implies low test quality: if you're not even executing code while testing, there's no way to know if it's correct or not. So, it's in our best interest to get relatively decent coverage, even if it doesn't guarantee that our tests will expose important bugs in the code."})]}),"\n",(0,i.jsx)(t.h2,{id:"test-design-hints",children:"Test Design Hints"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Set up a "Run Configuration" to simplify testing.'})," In IntelliJ, make a Run configuration that invokes ",(0,i.jsx)(t.code,{children:"lib/main_test_fixture.dart"})," so that you can push the green arrow to easily bring up the simulator with the test data loaded into it."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use the Testing Run Configuration to guide the writing of test code steps."}),' To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it\'s a one-to-one relationship.']}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Make sure that you include at least one "expect" statement to verify the results of a behavior.'})," So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["Document the test's navigation path with ",(0,i.jsx)(t.code,{children:"expect ($().visible, equals(true))"}),"."]})," It is possible to write a test with mostly ",(0,i.jsx)(t.code,{children:"await"})," statements such as the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\nawait $('Details').tap();\nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["That code is hard to follow (and potentially harder to debug and maintain) because it does not indicate which screen the test code driver is manipulating. While this test does accomplish the goal of exercising the app code, a more understandable version inserts ",(0,i.jsx)(t.code,{children:"expect"})," statements each time the test reaches a new page. This makes it easier to understand the test process:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\n\n// Now at HomeScreenGardensView\nexpect($(HomeScreenGardensView).visible, equals(true)); \nawait $('Details').tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\n\n// Now at CopyPlanting Screen\nexpect($(CopyPlantingScreen).visible, equals(true)); \nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \n"})}),"\n",(0,i.jsx)(t.p,{children:"You don't need to put those comments (or the newlines) into your test code; I add them here just to highlight the added lines. But hopefully you can see how these expect statements make the flow of the test easier to understand. It also means the test will fail with a more helpful error message if the test ends up on an unexpected screen."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Don\'t use an absolute "count" of items to do verification.'})," For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't delete or modify any entities in the test fixture."})," If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be ",(0,i.jsx)(t.em,{children:"new"})," entities added, all tests can assume that the entities in the test fixture will be there exactly as defined."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't write too much test code."}),' Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can\'t spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature\'s UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you\'ve written a ',(0,i.jsx)(t.em,{children:"very"})," helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Flutter DevTools can be helpful."})," Sometimes I get confused about what widgets are actually displayed on screen, and as a result have problems writing the correct Patrol Finder code. It can be helpful to run ",(0,i.jsx)(t.a,{href:"https://docs.flutter.dev/tools/devtools/android-studio",children:"Flutter DevTools"}),", then run the simulator manually. This enables you to navigate to a page in the simulator and use a browser window to inspect the widget hierarchy to see what type of widgets are visible."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Authentication state weirdness."})," I have discovered that if you perform the logout action during testing, it leaves the simulator in a weird, persistent state where you are navigated to the SignIn page, but the signin form (with fields for email and password) are not displayed. I am not sure why this happens, but to fix it, you can run the simulator normally (i.e. running main.dart) which will display the signin form. Login as any user, quit, and now you can run the tests and mocked authentication will work correctly."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"After fixing a bug in the app, consider writing a test to verify the correct behavior."})," Weirdly, bugs tend to congregate in certain areas, and even reappear after you thought you squashed them. It's a good idea after fixing a bug to see if you can quickly write a test that verifies the absence of that bug. It might feel like closing the barn door after the horse is gone, but it's a way of incrementally deepening the test quality."]}),"\n",(0,i.jsx)(t.h2,{id:"continuous-integration",children:"Continuous integration"}),"\n",(0,i.jsx)(t.p,{children:"It would be sweet to run the integration tests each time there is a commit to main. The simplest way to accomplish this would be via a GitHub Action that runs the integration tests. You would think this would be straight forward. Unfortunately, it is not:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'If you run the integration tests using a GitHub action that requires macOS, there is a "minutes multiplier" of 10, which means we will quickly run out of free minutes each month.'}),"\n",(0,i.jsx)(t.li,{children:"I have tried and failed to create a working GitHub action for integration testing under Linux. I have found that even running our integration tests locally under Android is unreliable: sometimes they will fail at the authentication step."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["The Patrol documentation has a section on ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/ci/platforms",children:"Continuous Integration Platforms"})," which provides interesting insights into the problems of Flutter integration testing under CI. This is a good place to start if you wish to look into it more."]}),"\n",(0,i.jsxs)(t.p,{children:["You can look at the ",(0,i.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/tree/main/.github/workflows",children:".github/workflows directory"}),' for the current situation. Notice that the integration testing workflows are set up to run when a commit is made to a branch called "never", meaning they are never actually invoked.']}),"\n",(0,i.jsx)(t.h2,{id:"test-fixture-design",children:"Test fixture design"}),"\n",(0,i.jsxs)(t.p,{children:["We use JSON files to create the test data for the tests. The test files are located in one of (potentially many) directories named ",(0,i.jsx)(t.code,{children:"assets\\test\\fixtureN"}),', when "N" is a number uniquely identifying the fixture. Currently, we only have one fixture directory.']}),"\n",(0,i.jsx)(t.p,{children:"Each fixture directory must contain the following files:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeInstanceData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"bedData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"chapterData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"cropData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"editorData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"familyData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenerData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"observationData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"outcomeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"plantingData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"roleData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"tagData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"taskData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"userData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"varietyData.json"})}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"info",children:[(0,i.jsx)(t.p,{children:"The JSON files need to have integrity, so their ids must align. Since many of the GGC IDs end with a four digit millis field we've assigned each type a unique millis field."}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"bedIDs end with 3456."}),"\n",(0,i.jsx)(t.li,{children:"cropIDs end with 5678."}),"\n",(0,i.jsx)(t.li,{children:"gardenIDs end with 7890."}),"\n",(0,i.jsx)(t.li,{children:"observationIDs end with 4567."}),"\n",(0,i.jsx)(t.li,{children:"outcomeIDs end with 2345."}),"\n",(0,i.jsx)(t.li,{children:"plantingIDs end with 1234."}),"\n",(0,i.jsx)(t.li,{children:"seedIDs end with 6789."}),"\n",(0,i.jsx)(t.li,{children:"taskIDs end with 8901."}),"\n",(0,i.jsx)(t.li,{children:"varietyIDs end with 9012."}),"\n",(0,i.jsx)(t.li,{children:"badgeInstances end with 9876."}),"\n"]})]}),"\n",(0,i.jsx)(t.h3,{id:"fixture-paths",children:"Fixture Paths"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"lib/features/fixture_paths.dart"})," file defines two constants:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"testFixturePath"})," - the path to the test fixture directory. This constant is used to load the test data in the tests."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"monarchFixturePath"})," - the path to the Monarch fixture directory used by ",(0,i.jsx)(t.code,{children:"WithMonarchData"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"assetcollectionbuilder",children:"AssetCollectionBuilder"}),"\n",(0,i.jsxs)(t.p,{children:["To facilitate the loading of the fixture files, we have created the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class. This class has three static methods to produce each of the collections from a fixture path. The three methods are as follows:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future> getTypes(String assetPath)"})," - loads the data from the fixture file and returns a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future>> getTypesStream(String assetPath)"})," - loads the data from the fixture file and returns a stream of a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future getTypeCollection(String assetPath)"})," - loads the data from the fixture file and returns a collection of the type."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["For example, to create a ",(0,i.jsx)(t.code,{children:"BedCollection"})," from the fixture path, use the following code:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"final bedCollection = await AssetCollectionBuilder.getBedCollection(testFixturePath);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In addition, the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class has three build methods that build the collections with all the data like the ",(0,i.jsx)(t.code,{children:"WithAllData"})," classes. The methods are as follows"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildChapterCollection(String assetPath, String chapterId)"})," - builds a ",(0,i.jsx)(t.code,{children:"ChapterCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildGardenCollection(String assetPath, String gardenId)"})," - builds a ",(0,i.jsx)(t.code,{children:"GardenCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildUserCollection(String assetPath, String currentUserID, String currentUserUID)"})," - builds a ",(0,i.jsx)(t.code,{children:"UserCollection"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"testfixture-singleton",children:"TestFixture singleton"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"TestFixture"})," singleton is used to load the test fixture data. The singleton has the following methods:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getInstance(String assetPath)"})," - returns a Future with the singleton instance. The first time it is called, it will load the test fixture data."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"setup()"})," - initializes the singleton by loading the test fixture data."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"There are two methods for each entity in the test fixture:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getStream()"})," - returns a Stream of the List of the entities from the test fixture."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getDatabase()"})," - returns The ",(0,i.jsx)(t.code,{children:"FixtureDatabase"})," from the test fixture."]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>r});var i=n(7294);const a={},s=i.createContext(a);function r(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/afab5b7c.65ccad16.js b/assets/js/afab5b7c.65ccad16.js deleted file mode 100644 index 0d5d6964..00000000 --- a/assets/js/afab5b7c.65ccad16.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[3029],{3430:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var i=n(5893),a=n(1151);const s={hide_table_of_contents:!1},r="Testing",o={id:"develop/quality-assurance/testing",title:"Testing",description:"Another form of quality assurance in GGC is testing.",source:"@site/docs/develop/quality-assurance/testing.md",sourceDirName:"develop/quality-assurance",slug:"/develop/quality-assurance/testing",permalink:"/docs/develop/quality-assurance/testing",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Dart analyze",permalink:"/docs/develop/quality-assurance/dart-analyze"},next:{title:"Database Integrity Checking",permalink:"/docs/develop/quality-assurance/integrity-check"}},l={},d=[{value:"Installation",id:"installation",level:2},{value:"Run the tests",id:"run-the-tests",level:2},{value:"About app_test.dart",id:"about-app_testdart",level:2},{value:"Testing a feature",id:"testing-a-feature",level:2},{value:"About run_tests_single.sh and app_test_single.dart",id:"about-run_tests_singlesh-and-app_test_singledart",level:2},{value:"Coverage",id:"coverage",level:2},{value:"Test Design Hints",id:"test-design-hints",level:2},{value:"Continuous integration",id:"continuous-integration",level:2},{value:"Test fixture design",id:"test-fixture-design",level:2},{value:"Fixture Paths",id:"fixture-paths",level:3},{value:"AssetCollectionBuilder",id:"assetcollectionbuilder",level:3},{value:"TestFixture singleton",id:"testfixture-singleton",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"testing",children:"Testing"})}),"\n",(0,i.jsx)(t.p,{children:"Another form of quality assurance in GGC is testing."}),"\n",(0,i.jsx)(t.p,{children:"In GGC, we want the main branch to always be free of any errors raised by our tests."}),"\n",(0,i.jsxs)(t.p,{children:["The current goal of testing in GeoGardenClub is to minimize the risk of ",(0,i.jsx)(t.em,{children:"catastrophic regression"})," from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'All commonly accessed screens display without error. (The tests might not check screens that are displayed "rarely", such as those resulting from anomalous conditions like network instability.)'}),"\n",(0,i.jsx)(t.li,{children:"CRUD operations on entities can be performed successfully when available."}),"\n",(0,i.jsx)(t.li,{children:"Buttons on all commonly accessed screens, when tapped, do not generate an error, and the resulting screen is checked to see that at least some of the intended results are displayed."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Currently, our approach to testing does not address many important quality issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No load testing."}),' We do not test that the system performs well under "load", where load can mean a large number of concurrent users and/or a large amount of stored data.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No external service testing."}),' We do not test "low-level" code, specifically external services such as database, photo storage, and authentication. This is because we mock external services in our test code.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No matrix (platform/device) testing."})," GGC is intended to be used on three platforms: iOS, Android, and Web. Each of these platforms supports many different devices. We only test on one platform (iOS) and one device (typically iPhone 17)."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"No UX testing."})," Our tests do not ensure that user needs are met and that they have a positive experience using the app."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by running the tests. A decent test suite should enable us to incrementally improve the quality of the code over time as well as the feature set."}),"\n",(0,i.jsxs)(t.admonition,{title:"When should you run the tests?",type:"info",children:[(0,i.jsx)(t.p,{children:"Ideally, we want the main branch to always be able to run the tests without error."}),(0,i.jsx)(t.p,{children:"To achieve that, the best times to run the tests are:"}),(0,i.jsxs)(t.ol,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Whenever you update the code you're working on from the main branch."})," This ensures that the update from main has not introduced code that breaks the tests in your branch. If the tests fail, address that problem before proceeding."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Before updating the main branch with your code."})," This ensures that you are not introducing code into the main branch that breaks the tests."]}),"\n"]}),(0,i.jsx)(t.p,{children:"As you will learn below, we have not figured out a reasonable way to perform continuous integration (i.e. run the tests automatically in the cloud upon any commit to the main branch.) As a result, it is up to us to manually verify that the main branch can execute the test suite without error."})]}),"\n",(0,i.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.p,{children:"There are a few things you need to do to set up your machine to run the test suite locally."}),"\n",(0,i.jsx)(t.p,{children:"Note that at the current time, we only support testing on macOS."}),"\n",(0,i.jsx)(t.p,{children:"First, bring up the iOS simulator and verify that you can bring up GGC on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.)"}),"\n",(0,i.jsx)(t.p,{children:"Second, install lcov on your machine by invoking:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"brew install lcov\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Third, activate the ",(0,i.jsx)(t.code,{children:"remove_from_coverage"})," Dart package so that the coverage report can be customized. Do this by invoking:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-agsl",children:"dart pub global activate remove_from_coverage\n"})}),"\n",(0,i.jsx)(t.h2,{id:"run-the-tests",children:"Run the tests"}),"\n",(0,i.jsxs)(t.p,{children:["To run the test suite, open the iOS simulator, make sure it is visible on your desktop, and then invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," in a terminal window. It should take around 5 minutes to run, and should produce output similar to the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"$ ./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s\nXcode build done. 30.1s\n00:42 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting bed feature\n... test Bed CRUD\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\nTesting chapter feature\nTesting chat feature\nTesting crop feature\n... test Crop Index Screen\n... test Crop CRUD\nTesting garden feature\n... test Garden Index Screen\n... test Garden Details Screen\n... test Garden CRUD\nTesting gardener feature\n... test Gardener Index Screen\nTesting geobot feature\nTesting home feature\nTesting observation feature\n... test Observation Feed\n... test Observation CRUD\nTesting outcome feature\n... test Outcome Garden Details View\n... test Outcome CRUD\nTesting planting feature\n... test Planting Index Screen\n... test Planting CRUD\n... test Planting Copy Planting\nTesting settings feature\nTesting task feature\n... test Task View\n... test Task CRUD\nTesting user feature\n... test User Profile Update\nTesting variety feature\n... test Variety Index Screen\n... test Variety CRUD\n... test Variety Gold Varieties\n04:52 +1: All tests passed! \n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'repositories\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'data\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'domain\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'authentication\\/.*$'\n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 322\n lines.......: 73.0% (6079 of 8329 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:'Note the line "All tests passed" after the sequence of lines documenting the feature under test.'}),"\n",(0,i.jsxs)(t.admonition,{title:"Uh oh...",type:"info",children:[(0,i.jsx)(t.p,{children:'If the tests do not run successfully, there won\'t be the line "All tests passed", and the output will instead look similar to this:'}),(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"$ ./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s\nXcode build done. 30.1s\n00:42 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting bed feature\n... test Bed CRUD\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\nTesting chapter feature\nTesting chat feature\nTesting crop feature\n... test Crop Index Screen\n... test Crop CRUD\nTesting garden feature\n... test Garden Index Screen\n... test Garden Details Screen\n... test Garden CRUD\nTesting gardener feature\n... test Gardener Index Screen\nTesting geobot feature\nTesting home feature\nTesting observation feature\n... test Observation Feed\n... test Observation CRUD\nTesting outcome feature\n... test Outcome Garden Details View\n... test Outcome CRUD\nTesting planting feature\n... test Planting Index Screen\n... test Planting CRUD\n... test Planting Copy Planting\n\u2550\u2550\u2561 EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following TestFailure was thrown running a test:\nExpected: \n Actual: \n\nWhen the exception was thrown, this was the stack:\n#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)\n\n#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)\n\n#6 main.. (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)\n\n#7 patrolWidgetTest. (package:patrol_finders/src/common.dart:50:7)\n\n#8 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15)\n\n#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)\n\n\n(elided one frame from package:stack_trace)\n\nThis was caught by the test expectation on the following line:\n file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24\nThe test description was:\n Fixture 1 Tests\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E] \n Test failed. See exception logs above.\n The test description was: Fixture 1 Tests\n \n\nTo run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'\n03:16 +0 -1: Some tests failed. \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 54.8% (7029 of 12823 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),(0,i.jsx)(t.p,{children:"You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart."}),(0,i.jsx)(t.p,{children:"If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfully using the iPhone 15 simulator under iOS 17.5."})]}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways from this test execution output:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code. Currently, the lib/ directory contains around 44K lines of code, and the integration_test/ directory contains around 1200 lines of code. So, the test code only accounts for around 2% of the total code base."}),"\n",(0,i.jsxs)(t.li,{children:['Our tests run with a specific "test fixture" (currently we\'re using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in ',(0,i.jsx)(t.code,{children:"assets/test/fixture1"}),". In the future, we might write tests that require a different fixture."]}),"\n",(0,i.jsx)(t.li,{children:"Our test architecture is organized around features."}),"\n",(0,i.jsx)(t.li,{children:'The "Rate Limiter" might be triggered. You can ignore this warning.'}),"\n",(0,i.jsx)(t.li,{children:"We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested, not to verify that the tests achieve 100% coverage (more on this below). We also remove several directories from the coverage report (i.e. data/, domain/, and repositories/) so that the coverage report does not report on code that is never executed due to mocking (i.e. code in the data/ and repositories/ directories) and also focuses more specifically on UI code."}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{title:"Keep the iOS simulator open and visible!",type:"warning",children:[(0,i.jsx)(t.p,{children:"When running the tests, be sure to keep the iOS simulator visible on your desktop for two reasons."}),(0,i.jsx)(t.p,{children:"First, we have discovered that if the iOS simulator is not visible, the tests may fail unpredictably."}),(0,i.jsx)(t.p,{children:"Second, the testing process will occasionally (and unpredictably) pause in the iOS simulator, waiting for you to click on a button to allow pasting:"}),(0,i.jsx)("img",{src:"/img/develop/testing/core-simulator-bridge.png"}),(0,i.jsx)(t.p,{children:"If you don't click the button to allow pasting, the test process will hang indefinitely."}),(0,i.jsx)(t.p,{children:"This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time."})]}),"\n",(0,i.jsx)(t.h2,{id:"about-app_testdart",children:"About app_test.dart"}),"\n",(0,i.jsxs)(t.p,{children:["To further understand the test process, it's helpful to review the code that is run by the ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," command:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (All)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),\n badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),\n bedsProvider.overrideWith((_) => testFixture.getBedsStream()),\n bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),\n chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),\n chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),\n chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),\n chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),\n cropsProvider.overrideWith((_) => testFixture.getCropsStream()),\n cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),\n editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),\n editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),\n familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),\n familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),\n gardensProvider.overrideWith((_) => testFixture.getGardensStream()),\n gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),\n gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),\n gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),\n observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),\n observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),\n outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),\n outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),\n plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),\n plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),\n rolesProvider.overrideWith((_) => testFixture.getRolesStream()),\n roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),\n tagsProvider.overrideWith((_) => testFixture.getTagsStream()),\n tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),\n tasksProvider.overrideWith((_) => testFixture.getTasksStream()),\n taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),\n usersProvider.overrideWith((_) => testFixture.getUsersStream()),\n userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await checkIntegrity($, reason: 'startup');\n await testAdmin($);\n await checkIntegrity($, reason: 'admin feature');\n await testBadge($);\n await checkIntegrity($, reason: 'badge feature');\n await testBed($);\n await checkIntegrity($, reason: 'bed feature');\n await testChapter($);\n await checkIntegrity($, reason: 'chapter feature');\n await testChat($);\n await checkIntegrity($, reason: 'chat feature');\n await testCrop($);\n await checkIntegrity($, reason: 'crop feature');\n await testGarden($);\n await checkIntegrity($, reason: 'garden feature');\n await testGardener($);\n await checkIntegrity($, reason: 'gardener feature');\n await testGeoBot($);\n await checkIntegrity($, reason: 'geobot feature');\n await testHome($);\n await checkIntegrity($, reason: 'home feature');\n await testObservation($);\n await checkIntegrity($, reason: 'observation feature');\n await testOutcome($);\n await checkIntegrity($, reason: 'outcome feature');\n await testPlanting($);\n await checkIntegrity($, reason: 'planting feature');\n await testSettings($);\n await checkIntegrity($, reason: 'settings feature');\n await testTask($);\n await checkIntegrity($, reason: 'task feature');\n await testVariety($);\n await checkIntegrity($, reason: 'variety feature');\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are the important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["We use the ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/finders/overview",children:"Patrol Finders"})," package, which provides a very helpful syntactic sugar over the built-in Flutter testing package. We do not use the full Patrol package, just their Patrol Finder package."]}),"\n",(0,i.jsx)(t.li,{children:"We use the Riverpod overrides feature so that during testing, our code manipulates the test fixture data rather than the data in the Firebase database."}),"\n",(0,i.jsxs)(t.li,{children:["We simulate Firebase authentication (using firebase_auth_mocks) and the app starts up with the (admin) user ",(0,i.jsx)(t.a,{href:"mailto:jennacorindeane@gmail.com",children:"jennacorindeane@gmail.com"}),' already logged in. So, we don\'t currently test the registration or signin workflows. The app "starts" by displaying the Home screen for Jenna.']}),"\n",(0,i.jsx)(t.li,{children:'We test each feature by calling a "test" function (i.e. testChapter, testCrop, etc.).'}),"\n",(0,i.jsx)(t.li,{children:"After testing each feature, the test code runs the Check Integrity admin function to ensure that the test of the previous feature did not introduce a database inconsistency."}),"\n",(0,i.jsx)(t.li,{children:'Our integration testing approach is "big bang": we run the entire integration test suite in a single function. This means tests are not independent of each other, which can make individual test case design more difficult. We chose this design for pragmatic reasons: setting up the runtime environment for testing takes around 50 seconds (on my late model MacBook Pro). If we ran each of the 15 feature tests independently, that would add on an additional 12 minutes (15 features * 50 seconds) to test suite execution time. No bueno.'}),"\n",(0,i.jsxs)(t.li,{children:["You should rarely need to edit this ",(0,i.jsx)(t.code,{children:"app_test.dart"}),' file. Instead, you will usually edit one of the top-level "test" feature files (i.e. testChapter.dart, testCrop.dart, etc.) You will normally need to edit app_test.dart only when you want to introduce the testing of a new feature.']}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"testing-a-feature",children:"Testing a feature"}),"\n",(0,i.jsx)(t.p,{children:'Let\'s now look at how the "Crop" feature is currently tested. Here is the top-level feature test function for Crops:'}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop.dart\nFuture testCrop(PatrolTester $) async {\n // ignore: avoid_print\n print('Testing crop feature');\n await testCropIndexScreen($);\n await testCropCRUD($);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Each top-level feature test function starts by printing a line of output indicating that the test of this feature is starting. That makes it easier to see how far testing has gotten and helps pinpoint the location of problems when testing fails."}),"\n",(0,i.jsx)(t.li,{children:"A top-level feature test function is typically implemented by calling multiple functions, each of which tests a different aspect of the feature."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Here is testCropIndexScreen:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_index_screen.dart\nFuture testCropIndexScreen(PatrolTester $) async {\n // ignore: avoid_print\n print('... test Crop Index Screen');\n String testCrop = 'Amaranth';\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n await $(testCrop).tap();\n expect($(CropDropdown).$(testCrop).visible, equals(true));\n expect($(CropView).$(testCrop).visible, equals(true));\n // Refresh CropIndexScreen so it displays all crops.\n await gotoDrawerScreen($, ChapterIndexScreen);\n await gotoDrawerScreen($, CropIndexScreen);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We use Patrol Finder syntax to locate widgets and manipulate them through searching for widgets of a particular type and/or containing a particular text string. Please avoid creating Keys for testing. Patrol Finders make it possible to test the source code without introducing new lines of code purely for the purpose of test support."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Let's now look at the test for create, read, update, and delete of a Crop:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_crud.dart\nFuture testCropCRUD(PatrolTester $) async {\n // ignore: avoid_print\n print('... test Crop CRUD');\n String testCropName = 'AAATestCrop';\n String updatedTestCropName = 'AAAATestCrop';\n // Test Create.\n await gotoDrawerScreen($, CropIndexScreen);\n await $(GgcFAB).$('Crop').tap();\n expect($(CreateCropScreen).visible, equals(true));\n await $(CropNameField).enterText(testCropName);\n await $(FamilyDropdown).tap();\n await $('Allium').tap();\n await $(FormButtons).$('Submit').tap();\n expect($(CropIndexScreen).visible, equals(true));\n // Verify Create.\n await $(testCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Create crop');\n // Test Read and Update\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(testCropName).tap();\n await $('Update').tap();\n await $(CropNameField).enterText(updatedTestCropName);\n await $('Submit').tap();\n await $(BackButton).tap();\n await $(BackButton).tap();\n await gotoDrawerScreen($, CropIndexScreen);\n // Verify Update\n await $(updatedTestCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Update crop');\n // Test Delete\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(updatedTestCropName).tap();\n await $('Update').tap();\n await $(Icons.delete).tap();\n await $('Delete').tap();\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n // Verify delete\n expect($(updatedTestCropName).exists, equals(false));\n await checkIntegrity($, reason: 'Delete crop');\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Testing a behavior can require a relatively long sequence of UI interactions. Getting the sequence correct is way easier if you first step through the behavior manually. To make this easier, follow the instructions in the section below on "Run the simulator with test data".'}),"\n",(0,i.jsx)(t.li,{children:"It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function."}),"\n",(0,i.jsx)(t.li,{children:"The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"about-run_tests_singlesh-and-app_test_singledart",children:"About run_tests_single.sh and app_test_single.dart"}),"\n",(0,i.jsx)(t.p,{children:"While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code."}),"\n",(0,i.jsxs)(t.p,{children:["To speed up testing, you can use the command ",(0,i.jsx)(t.code,{children:"./run_tests_single.sh"}),". This runs the ",(0,i.jsx)(t.code,{children:"app_test_single.dart"})," file, which looks similar to this:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test_single.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (Single)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n :\n :\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await testCrop($);\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"You can freely edit this file in your branch to focus on the specific feature of interest."}),"\n",(0,i.jsxs)(t.li,{children:['Sometimes you might want to check multiple features at once, that\'s fine. You do you. The idea is that this is a kind of "sandbox" for you to develop tests so that you are not wishing to edit the global ',(0,i.jsx)(t.code,{children:"./run_tests.sh"})," and ",(0,i.jsx)(t.code,{children:"app_test.dart"})," files to speed up testing."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"coverage",children:"Coverage"}),"\n",(0,i.jsxs)(t.p,{children:["It can be useful to see the coverage of our test cases. After running the test suite, you can open the file ",(0,i.jsx)(t.code,{children:"coverage/html/index.html"}),". Here's what it looks like after clicking the button to sort the rows in order of increasing coverage."]}),"\n",(0,i.jsx)("img",{src:"/img/develop/testing/coverage.png"}),"\n",(0,i.jsx)(t.p,{children:"Note that our coverage report excludes the data/, domain/, and repositories/ directories. One reason is because the use of mocks means that the code in the data/ and repositories/ directories will never be executed by testing, so reporting (the necessarily low) coverage for that code is not useful; we can't fix that. We also exclude the domain/ directory so that the coverage information focuses more directly on UI code, which will hopefully make the report more useful in determining certain types of gaps in testing."}),"\n",(0,i.jsx)(t.p,{children:"There are clickable links that you can use to drill down to see which statements have been executed and which have not been."}),"\n",(0,i.jsx)(t.p,{children:'The primary goal of the coverage report is to simplify identification of "forgotten" areas of the UI for which we have not created any test cases.'}),"\n",(0,i.jsxs)(t.admonition,{title:"High coverage does not imply high test quality",type:"warning",children:[(0,i.jsx)(t.p,{children:"Beware that a high level of coverage does not, by itself, indicate that the test suite is high quality--i.e reliably able to indicate the absence of important errors in the code."}),(0,i.jsx)(t.p,{children:"To understand why, consider one important limitation of our test suite--the use of a test data fixture. Even if we got to 100% coverage with no errors with this (or any other) test data fixture, it would not guarantee that the code would execute correctly if the data was in some other state."}),(0,i.jsx)(t.p,{children:"That said, low coverage definitely implies low test quality: if you're not even executing code while testing, there's no way to know if it's correct or not. So, it's in our best interest to get relatively decent coverage, even if it doesn't guarantee that our tests will expose important bugs in the code."})]}),"\n",(0,i.jsx)(t.h2,{id:"test-design-hints",children:"Test Design Hints"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Set up a "Run Configuration" to simplify testing.'})," In IntelliJ, make a Run configuration that invokes ",(0,i.jsx)(t.code,{children:"lib/main_test_fixture.dart"})," so that you can push the green arrow to easily bring up the simulator with the test data loaded into it."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use the Testing Run Configuration to guide the writing of test code steps."}),' To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it\'s a one-to-one relationship.']}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Make sure that you include at least one "expect" statement to verify the results of a behavior.'})," So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["Document the test's navigation path with ",(0,i.jsx)(t.code,{children:"expect ($().visible, equals(true))"}),"."]})," It is possible to write a test with mostly ",(0,i.jsx)(t.code,{children:"await"})," statements such as the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\nawait $('Details').tap();\nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["That code is hard to follow (and potentially harder to debug and maintain) because it does not indicate which screen the test code driver is manipulating. While this test does accomplish the goal of exercising the app code, a more understandable version inserts ",(0,i.jsx)(t.code,{children:"expect"})," statements each time the test reaches a new page. This makes it easier to understand the test process:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\n\n// Now at HomeScreenGardensView\nexpect($(HomeScreenGardensView).visible, equals(true)); \nawait $('Details').tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\n\n// Now at CopyPlanting Screen\nexpect($(CopyPlantingScreen).visible, equals(true)); \nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \n"})}),"\n",(0,i.jsx)(t.p,{children:"You don't need to put those comments (or the newlines) into your test code; I add them here just to highlight the added lines. But hopefully you can see how these expect statements make the flow of the test easier to understand. It also means the test will fail with a more helpful error message if the test ends up on an unexpected screen."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Don\'t use an absolute "count" of items to do verification.'})," For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't delete or modify any entities in the test fixture."})," If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be ",(0,i.jsx)(t.em,{children:"new"})," entities added, all tests can assume that the entities in the test fixture will be there exactly as defined."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't write too much test code."}),' Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can\'t spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature\'s UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you\'ve written a ',(0,i.jsx)(t.em,{children:"very"})," helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Flutter DevTools can be helpful."})," Sometimes I get confused about what widgets are actually displayed on screen, and as a result have problems writing the correct Patrol Finder code. It can be helpful to run ",(0,i.jsx)(t.a,{href:"https://docs.flutter.dev/tools/devtools/android-studio",children:"Flutter DevTools"}),", then run the simulator manually. This enables you to navigate to a page in the simulator and use a browser window to inspect the widget hierarchy to see what type of widgets are visible."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Authentication state weirdness."})," I have discovered that if you perform the logout action during testing, it leaves the simulator in a weird, persistent state where you are navigated to the SignIn page, but the signin form (with fields for email and password) are not displayed. I am not sure why this happens, but to fix it, you can run the simulator normally (i.e. running main.dart) which will display the signin form. Login as any user, quit, and now you can run the tests and mocked authentication will work correctly."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"After fixing a bug in the app, consider writing a test to verify the correct behavior."})," Weirdly, bugs tend to congregate in certain areas, and even reappear after you thought you squashed them. It's a good idea after fixing a bug to see if you can quickly write a test that verifies the absence of that bug. It might feel like closing the barn door after the horse is gone, but it's a way of incrementally deepening the test quality."]}),"\n",(0,i.jsx)(t.h2,{id:"continuous-integration",children:"Continuous integration"}),"\n",(0,i.jsx)(t.p,{children:"It would be sweet to run the integration tests each time there is a commit to main. The simplest way to accomplish this would be via a GitHub Action that runs the integration tests. You would think this would be straight forward. Unfortunately, it is not:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'If you run the integration tests using a GitHub action that requires macOS, there is a "minutes multiplier" of 10, which means we will quickly run out of free minutes each month.'}),"\n",(0,i.jsx)(t.li,{children:"I have tried and failed to create a working GitHub action for integration testing under Linux. I have found that even running our integration tests locally under Android is unreliable: sometimes they will fail at the authentication step."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["The Patrol documentation has a section on ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/ci/platforms",children:"Continuous Integration Platforms"})," which provides interesting insights into the problems of Flutter integration testing under CI. This is a good place to start if you wish to look into it more."]}),"\n",(0,i.jsxs)(t.p,{children:["You can look at the ",(0,i.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/tree/main/.github/workflows",children:".github/workflows directory"}),' for the current situation. Notice that the integration testing workflows are set up to run when a commit is made to a branch called "never", meaning they are never actually invoked.']}),"\n",(0,i.jsx)(t.h2,{id:"test-fixture-design",children:"Test fixture design"}),"\n",(0,i.jsxs)(t.p,{children:["We use JSON files to create the test data for the tests. The test files are located in one of (potentially many) directories named ",(0,i.jsx)(t.code,{children:"assets\\test\\fixtureN"}),', when "N" is a number uniquely identifying the fixture. Currently, we only have one fixture directory.']}),"\n",(0,i.jsx)(t.p,{children:"Each fixture directory must contain the following files:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeInstanceData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"bedData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"chapterData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"cropData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"editorData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"familyData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenerData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"observationData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"outcomeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"plantingData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"roleData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"tagData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"taskData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"userData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"varietyData.json"})}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"info",children:[(0,i.jsx)(t.p,{children:"The JSON files need to have integrity, so their ids must align. Since many of the GGC IDs end with a four digit millis field we've assigned each type a unique millis field."}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"bedIDs end with 3456."}),"\n",(0,i.jsx)(t.li,{children:"cropIDs end with 5678."}),"\n",(0,i.jsx)(t.li,{children:"gardenIDs end with 7890."}),"\n",(0,i.jsx)(t.li,{children:"observationIDs end with 4567."}),"\n",(0,i.jsx)(t.li,{children:"outcomeIDs end with 2345."}),"\n",(0,i.jsx)(t.li,{children:"plantingIDs end with 1234."}),"\n",(0,i.jsx)(t.li,{children:"seedIDs end with 6789."}),"\n",(0,i.jsx)(t.li,{children:"taskIDs end with 8901."}),"\n",(0,i.jsx)(t.li,{children:"varietyIDs end with 9012."}),"\n",(0,i.jsx)(t.li,{children:"badgeInstances end with 9876."}),"\n"]})]}),"\n",(0,i.jsx)(t.h3,{id:"fixture-paths",children:"Fixture Paths"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"lib/features/fixture_paths.dart"})," file defines two constants:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"testFixturePath"})," - the path to the test fixture directory. This constant is used to load the test data in the tests."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"monarchFixturePath"})," - the path to the Monarch fixture directory used by ",(0,i.jsx)(t.code,{children:"WithMonarchData"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"assetcollectionbuilder",children:"AssetCollectionBuilder"}),"\n",(0,i.jsxs)(t.p,{children:["To facilitate the loading of the fixture files, we have created the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class. This class has three static methods to produce each of the collections from a fixture path. The three methods are as follows:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future> getTypes(String assetPath)"})," - loads the data from the fixture file and returns a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future>> getTypesStream(String assetPath)"})," - loads the data from the fixture file and returns a stream of a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future getTypeCollection(String assetPath)"})," - loads the data from the fixture file and returns a collection of the type."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["For example, to create a ",(0,i.jsx)(t.code,{children:"BedCollection"})," from the fixture path, use the following code:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"final bedCollection = await AssetCollectionBuilder.getBedCollection(testFixturePath);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In addition, the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class has three build methods that build the collections with all the data like the ",(0,i.jsx)(t.code,{children:"WithAllData"})," classes. The methods are as follows"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildChapterCollection(String assetPath, String chapterId)"})," - builds a ",(0,i.jsx)(t.code,{children:"ChapterCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildGardenCollection(String assetPath, String gardenId)"})," - builds a ",(0,i.jsx)(t.code,{children:"GardenCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildUserCollection(String assetPath, String currentUserID, String currentUserUID)"})," - builds a ",(0,i.jsx)(t.code,{children:"UserCollection"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"testfixture-singleton",children:"TestFixture singleton"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"TestFixture"})," singleton is used to load the test fixture data. The singleton has the following methods:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getInstance(String assetPath)"})," - returns a Future with the singleton instance. The first time it is called, it will load the test fixture data."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"setup()"})," - initializes the singleton by loading the test fixture data."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"There are two methods for each entity in the test fixture:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getStream()"})," - returns a Stream of the List of the entities from the test fixture."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getDatabase()"})," - returns The ",(0,i.jsx)(t.code,{children:"FixtureDatabase"})," from the test fixture."]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>r});var i=n(7294);const a={},s=i.createContext(a);function r(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.5f652d04.js b/assets/js/runtime~main.7a0a4abb.js similarity index 97% rename from assets/js/runtime~main.5f652d04.js rename to assets/js/runtime~main.7a0a4abb.js index b6dc91a7..ce280e1d 100644 --- a/assets/js/runtime~main.5f652d04.js +++ b/assets/js/runtime~main.7a0a4abb.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,c,f,b,d={},r={};function t(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return d[e].call(c.exports,c,c.exports,t),c.exports}t.m=d,e=[],t.O=(a,c,f,b)=>{if(!c){var d=1/0;for(i=0;i=b)&&Object.keys(t.O).every((e=>t.O[e](c[o])))?c.splice(o--,1):(r=!1,b0&&e[i-1][2]>b;i--)e[i]=e[i-1];e[i]=[c,f,b]},t.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return t.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,t.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var b=Object.create(null);t.r(b);var d={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,t.d(b,d),b},t.d=(e,a)=>{for(var c in a)t.o(a,c)&&!t.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},t.f={},t.e=e=>Promise.all(Object.keys(t.f).reduce(((a,c)=>(t.f[c](e,a),a)),[])),t.u=e=>"assets/js/"+({1:"ff0b8175",183:"96083bb9",186:"a8521eb9",196:"505d7517",200:"a6327429",234:"39838d4e",403:"3ad77611",814:"95f4d37c",825:"7be5f79d",860:"2cd8ab24",1130:"0f1bb644",1340:"e2299c6d",1420:"6741c1a9",1549:"1506d638",1585:"2450005c",1666:"c401bc0d",1744:"10921c5b",1868:"3fa09aed",1937:"49882d99",1966:"e37f90c6",2535:"814f3328",2743:"968b4846",2967:"e4eb6786",3029:"afab5b7c",3085:"1f391b9e",3089:"a6aa9e1f",3560:"17d8eee1",3608:"9e4087bc",3629:"aba21aa0",3844:"772c3429",4e3:"afc29949",4031:"f81c1134",4057:"c03baef0",4063:"31caa863",4076:"7d1225b6",4088:"0058b4c6",4195:"c4f5d8e4",4368:"a94703ab",4524:"e27695c2",4713:"bc03b1b7",5014:"38346c4b",5622:"70c70343",5857:"3e240fbf",5980:"a7456010",6033:"6f165e31",6041:"a9a08fef",6103:"ccc49370",6142:"bebdd554",6265:"906ac375",6414:"3d832522",6427:"59628a4d",6642:"c15d9823",6800:"2f9db241",6801:"7bda6e26",6906:"9ebba4ea",6957:"a5b8d3e9",6974:"af21c641",7222:"0bd3a280",7346:"f3759001",7393:"acecf23e",7414:"393be207",7540:"0f1af657",7664:"reactPlayerPreview",7918:"17896441",7937:"c48bbb24",8294:"3463d78f",8518:"a7bd4aaa",8653:"ec0f34d7",8754:"6b1fc3de",8911:"682a0ea6",9183:"04ee15db",9208:"36994c47",9256:"3bea7cd1",9268:"ba771284",9572:"7d56ced7",9586:"3b4579e8",9601:"18fc9463",9661:"5e95c892",9866:"11f6a8a1",9929:"1863cff0"}[e]||e)+"."+{1:"a06d7a3a",183:"d2117bbc",186:"4c804139",196:"40c652f2",200:"f238d8bf",234:"6da8b591",403:"9fc2a256",814:"4e08262b",825:"d42ee47b",860:"d7028e73",1130:"c05c53ae",1340:"11ae0623",1420:"aeb48ccb",1549:"7d1c392d",1585:"db463b44",1666:"b33ef993",1744:"5152fab0",1772:"3d06e0e2",1868:"bd4243ae",1937:"96ed6c84",1966:"22c93dd9",2535:"0ba125c5",2700:"83aa9a75",2743:"67c47945",2967:"a7c4ab03",3029:"65ccad16",3085:"3dba5538",3089:"911b8dd5",3560:"cffc9f0b",3608:"ee0c677a",3629:"eb980bea",3844:"e18b7954",4e3:"b7c2e4b1",4031:"6f8546d9",4057:"efd80a40",4063:"fc3b4f74",4076:"e5f344cb",4088:"f48ef793",4195:"b92bf9f8",4368:"4aef8496",4524:"f13327a6",4713:"4d897916",5014:"bd156c61",5622:"d841cc33",5655:"ab3e12ff",5857:"241b5abc",5980:"f93cbc61",6033:"beb535d0",6041:"3cafd262",6103:"4990621e",6142:"9adb781d",6265:"ecc041e6",6414:"19d9c76b",6427:"eaac142f",6642:"b756708b",6800:"8b822b23",6801:"fd330214",6906:"df274206",6957:"cd212d63",6974:"242c3e8d",7222:"7b0ad8f2",7346:"6fa99e97",7393:"9d35c647",7414:"37f8208f",7540:"45d5369b",7664:"e5a1011e",7918:"b96e81ff",7937:"881a7b56",8041:"e22d43c6",8294:"b6b9d61d",8518:"eaa77d27",8653:"1c9ade88",8754:"d16786a4",8911:"169cbed9",9183:"f83f3ae0",9208:"203bad01",9256:"0698ba6b",9268:"3bf7361c",9572:"8973ef7f",9586:"0ae33a44",9601:"3a3217ec",9661:"2dcb0623",9866:"6c00f0dc",9929:"e7250df4"}[e]+".js",t.miniCssF=e=>{},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},b="geogardenclub-github-io:",t.l=(e,a,c,d)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var b=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),b&&b.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.p="/",t.gca=function(e){return e={17896441:"7918",ff0b8175:"1","96083bb9":"183",a8521eb9:"186","505d7517":"196",a6327429:"200","39838d4e":"234","3ad77611":"403","95f4d37c":"814","7be5f79d":"825","2cd8ab24":"860","0f1bb644":"1130",e2299c6d:"1340","6741c1a9":"1420","1506d638":"1549","2450005c":"1585",c401bc0d:"1666","10921c5b":"1744","3fa09aed":"1868","49882d99":"1937",e37f90c6:"1966","814f3328":"2535","968b4846":"2743",e4eb6786:"2967",afab5b7c:"3029","1f391b9e":"3085",a6aa9e1f:"3089","17d8eee1":"3560","9e4087bc":"3608",aba21aa0:"3629","772c3429":"3844",afc29949:"4000",f81c1134:"4031",c03baef0:"4057","31caa863":"4063","7d1225b6":"4076","0058b4c6":"4088",c4f5d8e4:"4195",a94703ab:"4368",e27695c2:"4524",bc03b1b7:"4713","38346c4b":"5014","70c70343":"5622","3e240fbf":"5857",a7456010:"5980","6f165e31":"6033",a9a08fef:"6041",ccc49370:"6103",bebdd554:"6142","906ac375":"6265","3d832522":"6414","59628a4d":"6427",c15d9823:"6642","2f9db241":"6800","7bda6e26":"6801","9ebba4ea":"6906",a5b8d3e9:"6957",af21c641:"6974","0bd3a280":"7222",f3759001:"7346",acecf23e:"7393","393be207":"7414","0f1af657":"7540",reactPlayerPreview:"7664",c48bbb24:"7937","3463d78f":"8294",a7bd4aaa:"8518",ec0f34d7:"8653","6b1fc3de":"8754","682a0ea6":"8911","04ee15db":"9183","36994c47":"9208","3bea7cd1":"9256",ba771284:"9268","7d56ced7":"9572","3b4579e8":"9586","18fc9463":"9601","5e95c892":"9661","11f6a8a1":"9866","1863cff0":"9929"}[e]||e,t.p+t.u(e)},(()=>{var e={1303:0,532:0};t.f.j=(a,c)=>{var f=t.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var b=new Promise(((c,b)=>f=e[a]=[c,b]));c.push(f[2]=b);var d=t.p+t.u(a),r=new Error;t.l(d,(c=>{if(t.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var b=c&&("load"===c.type?"missing":c.type),d=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+b+": "+d+")",r.name="ChunkLoadError",r.type=b,r.request=d,f[1](r)}}),"chunk-"+a,a)}},t.O.j=a=>0===e[a];var a=(a,c)=>{var f,b,d=c[0],r=c[1],o=c[2],n=0;if(d.some((a=>0!==e[a]))){for(f in r)t.o(r,f)&&(t.m[f]=r[f]);if(o)var i=o(t)}for(a&&a(c);n{"use strict";var e,a,c,f,b,d={},r={};function t(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return d[e].call(c.exports,c,c.exports,t),c.exports}t.m=d,e=[],t.O=(a,c,f,b)=>{if(!c){var d=1/0;for(i=0;i=b)&&Object.keys(t.O).every((e=>t.O[e](c[o])))?c.splice(o--,1):(r=!1,b0&&e[i-1][2]>b;i--)e[i]=e[i-1];e[i]=[c,f,b]},t.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return t.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,t.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var b=Object.create(null);t.r(b);var d={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,t.d(b,d),b},t.d=(e,a)=>{for(var c in a)t.o(a,c)&&!t.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},t.f={},t.e=e=>Promise.all(Object.keys(t.f).reduce(((a,c)=>(t.f[c](e,a),a)),[])),t.u=e=>"assets/js/"+({1:"ff0b8175",183:"96083bb9",186:"a8521eb9",196:"505d7517",200:"a6327429",234:"39838d4e",403:"3ad77611",814:"95f4d37c",825:"7be5f79d",860:"2cd8ab24",1130:"0f1bb644",1340:"e2299c6d",1420:"6741c1a9",1549:"1506d638",1585:"2450005c",1666:"c401bc0d",1744:"10921c5b",1868:"3fa09aed",1937:"49882d99",1966:"e37f90c6",2535:"814f3328",2743:"968b4846",2967:"e4eb6786",3029:"afab5b7c",3085:"1f391b9e",3089:"a6aa9e1f",3560:"17d8eee1",3608:"9e4087bc",3629:"aba21aa0",3844:"772c3429",4e3:"afc29949",4031:"f81c1134",4057:"c03baef0",4063:"31caa863",4076:"7d1225b6",4088:"0058b4c6",4195:"c4f5d8e4",4368:"a94703ab",4524:"e27695c2",4713:"bc03b1b7",5014:"38346c4b",5622:"70c70343",5857:"3e240fbf",5980:"a7456010",6033:"6f165e31",6041:"a9a08fef",6103:"ccc49370",6142:"bebdd554",6265:"906ac375",6414:"3d832522",6427:"59628a4d",6642:"c15d9823",6800:"2f9db241",6801:"7bda6e26",6906:"9ebba4ea",6957:"a5b8d3e9",6974:"af21c641",7222:"0bd3a280",7346:"f3759001",7393:"acecf23e",7414:"393be207",7540:"0f1af657",7664:"reactPlayerPreview",7918:"17896441",7937:"c48bbb24",8294:"3463d78f",8518:"a7bd4aaa",8653:"ec0f34d7",8754:"6b1fc3de",8911:"682a0ea6",9183:"04ee15db",9208:"36994c47",9256:"3bea7cd1",9268:"ba771284",9572:"7d56ced7",9586:"3b4579e8",9601:"18fc9463",9661:"5e95c892",9866:"11f6a8a1",9929:"1863cff0"}[e]||e)+"."+{1:"a06d7a3a",183:"d2117bbc",186:"4c804139",196:"40c652f2",200:"f238d8bf",234:"6da8b591",403:"9fc2a256",814:"4e08262b",825:"d42ee47b",860:"d7028e73",1130:"c05c53ae",1340:"11ae0623",1420:"aeb48ccb",1549:"7d1c392d",1585:"db463b44",1666:"b33ef993",1744:"5152fab0",1772:"3d06e0e2",1868:"bd4243ae",1937:"96ed6c84",1966:"22c93dd9",2535:"0ba125c5",2700:"83aa9a75",2743:"67c47945",2967:"a7c4ab03",3029:"3f5cd913",3085:"3dba5538",3089:"911b8dd5",3560:"cffc9f0b",3608:"ee0c677a",3629:"eb980bea",3844:"e18b7954",4e3:"b7c2e4b1",4031:"6f8546d9",4057:"efd80a40",4063:"07bb8d76",4076:"e5f344cb",4088:"f48ef793",4195:"b92bf9f8",4368:"4aef8496",4524:"f13327a6",4713:"4d897916",5014:"bd156c61",5622:"d841cc33",5655:"ab3e12ff",5857:"241b5abc",5980:"f93cbc61",6033:"beb535d0",6041:"3cafd262",6103:"4990621e",6142:"9adb781d",6265:"ecc041e6",6414:"19d9c76b",6427:"eaac142f",6642:"b756708b",6800:"8b822b23",6801:"fd330214",6906:"df274206",6957:"cd212d63",6974:"242c3e8d",7222:"7b0ad8f2",7346:"6fa99e97",7393:"9d35c647",7414:"37f8208f",7540:"45d5369b",7664:"e5a1011e",7918:"b96e81ff",7937:"881a7b56",8041:"e22d43c6",8294:"b6b9d61d",8518:"eaa77d27",8653:"1c9ade88",8754:"6d40b298",8911:"169cbed9",9183:"f83f3ae0",9208:"203bad01",9256:"0698ba6b",9268:"3bf7361c",9572:"8973ef7f",9586:"0ae33a44",9601:"3a3217ec",9661:"2dcb0623",9866:"6c00f0dc",9929:"e7250df4"}[e]+".js",t.miniCssF=e=>{},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},b="geogardenclub-github-io:",t.l=(e,a,c,d)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var b=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),b&&b.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.p="/",t.gca=function(e){return e={17896441:"7918",ff0b8175:"1","96083bb9":"183",a8521eb9:"186","505d7517":"196",a6327429:"200","39838d4e":"234","3ad77611":"403","95f4d37c":"814","7be5f79d":"825","2cd8ab24":"860","0f1bb644":"1130",e2299c6d:"1340","6741c1a9":"1420","1506d638":"1549","2450005c":"1585",c401bc0d:"1666","10921c5b":"1744","3fa09aed":"1868","49882d99":"1937",e37f90c6:"1966","814f3328":"2535","968b4846":"2743",e4eb6786:"2967",afab5b7c:"3029","1f391b9e":"3085",a6aa9e1f:"3089","17d8eee1":"3560","9e4087bc":"3608",aba21aa0:"3629","772c3429":"3844",afc29949:"4000",f81c1134:"4031",c03baef0:"4057","31caa863":"4063","7d1225b6":"4076","0058b4c6":"4088",c4f5d8e4:"4195",a94703ab:"4368",e27695c2:"4524",bc03b1b7:"4713","38346c4b":"5014","70c70343":"5622","3e240fbf":"5857",a7456010:"5980","6f165e31":"6033",a9a08fef:"6041",ccc49370:"6103",bebdd554:"6142","906ac375":"6265","3d832522":"6414","59628a4d":"6427",c15d9823:"6642","2f9db241":"6800","7bda6e26":"6801","9ebba4ea":"6906",a5b8d3e9:"6957",af21c641:"6974","0bd3a280":"7222",f3759001:"7346",acecf23e:"7393","393be207":"7414","0f1af657":"7540",reactPlayerPreview:"7664",c48bbb24:"7937","3463d78f":"8294",a7bd4aaa:"8518",ec0f34d7:"8653","6b1fc3de":"8754","682a0ea6":"8911","04ee15db":"9183","36994c47":"9208","3bea7cd1":"9256",ba771284:"9268","7d56ced7":"9572","3b4579e8":"9586","18fc9463":"9601","5e95c892":"9661","11f6a8a1":"9866","1863cff0":"9929"}[e]||e,t.p+t.u(e)},(()=>{var e={1303:0,532:0};t.f.j=(a,c)=>{var f=t.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var b=new Promise(((c,b)=>f=e[a]=[c,b]));c.push(f[2]=b);var d=t.p+t.u(a),r=new Error;t.l(d,(c=>{if(t.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var b=c&&("load"===c.type?"missing":c.type),d=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+b+": "+d+")",r.name="ChunkLoadError",r.type=b,r.request=d,f[1](r)}}),"chunk-"+a,a)}},t.O.j=a=>0===e[a];var a=(a,c)=>{var f,b,d=c[0],r=c[1],o=c[2],n=0;if(d.some((a=>0!==e[a]))){for(f in r)t.o(r,f)&&(t.m[f]=r[f]);if(o)var i=o(t)}for(a&&a(c);n Blog | Geo Garden Club - + diff --git a/blog/2023/02/10/welcome.html b/blog/2023/02/10/welcome.html index c2b5b528..5c175771 100644 --- a/blog/2023/02/10/welcome.html +++ b/blog/2023/02/10/welcome.html @@ -5,7 +5,7 @@ Welcome, Geo Garden Club! Aloha, Agile Garden Club! | Geo Garden Club - + diff --git a/blog/archive.html b/blog/archive.html index 703ed2b7..42ecc816 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -5,7 +5,7 @@ Archive | Geo Garden Club - + diff --git a/docs/business.html b/docs/business.html index ab467348..00e5b7d0 100644 --- a/docs/business.html +++ b/docs/business.html @@ -5,7 +5,7 @@ Welcome to the GGC Business Development Guide | Geo Garden Club - + diff --git a/docs/business/market-size.html b/docs/business/market-size.html index 318170e8..aa3e8888 100644 --- a/docs/business/market-size.html +++ b/docs/business/market-size.html @@ -5,7 +5,7 @@ Market Size Estimation (USA) | Geo Garden Club - + diff --git a/docs/business/milestones.html b/docs/business/milestones.html index 654f8493..c6ca0f3d 100644 --- a/docs/business/milestones.html +++ b/docs/business/milestones.html @@ -5,7 +5,7 @@ Milestones | Geo Garden Club - + diff --git a/docs/business/roadmap.html b/docs/business/roadmap.html index bc453646..b0c642e5 100644 --- a/docs/business/roadmap.html +++ b/docs/business/roadmap.html @@ -5,7 +5,7 @@ Roadmap | Geo Garden Club - + diff --git a/docs/develop.html b/docs/develop.html index 077293e0..61c5e36e 100644 --- a/docs/develop.html +++ b/docs/develop.html @@ -5,7 +5,7 @@ Welcome to the GGC Developers Guide | Geo Garden Club - + diff --git a/docs/develop/adaptive-design.html b/docs/develop/adaptive-design.html index df629cee..007ee32e 100644 --- a/docs/develop/adaptive-design.html +++ b/docs/develop/adaptive-design.html @@ -5,7 +5,7 @@ Adaptive design | Geo Garden Club - + diff --git a/docs/develop/architecture.html b/docs/develop/architecture.html index 9284cbcd..5eb24901 100644 --- a/docs/develop/architecture.html +++ b/docs/develop/architecture.html @@ -5,7 +5,7 @@ Architecture | Geo Garden Club - + diff --git a/docs/develop/backups.html b/docs/develop/backups.html index ba0d6378..ac6f2cfc 100644 --- a/docs/develop/backups.html +++ b/docs/develop/backups.html @@ -5,7 +5,7 @@ Backups | Geo Garden Club - + diff --git a/docs/develop/data-model.html b/docs/develop/data-model.html index a914f737..eb4549d4 100644 --- a/docs/develop/data-model.html +++ b/docs/develop/data-model.html @@ -5,7 +5,7 @@ Data Model | Geo Garden Club - + diff --git a/docs/develop/database-management.html b/docs/develop/database-management.html index 2f97ff7c..1e469f35 100644 --- a/docs/develop/database-management.html +++ b/docs/develop/database-management.html @@ -5,7 +5,7 @@ Database management | Geo Garden Club - + diff --git a/docs/develop/deployment.html b/docs/develop/deployment.html index b3aabbdb..89b53982 100644 --- a/docs/develop/deployment.html +++ b/docs/develop/deployment.html @@ -5,7 +5,7 @@ Deployment | Geo Garden Club - + @@ -65,16 +65,6 @@

AndroidTest on physical device

-

Sometimes it is useful to try out the app on a physical device without having to go through deployment steps. Currently only Philip can do this due to iOS signing issues.

-

Here is what you need to do:

-

First, delete the app from your physical device.

-

Second, connect the physical device to a laptop.

-

Third, run flutter devices to verify that the device shows up. You should see output like this:

-
 % flutter devices    
Found 5 connected devices:
Philip's iPhone (mobile) • 00008030-000364940A98802E • ios • iOS 17.5.1 21F90
iPhone 11 (mobile) • 8E550E86-3173-4342-B197-A557B83E40A2 • ios • com.apple.CoreSimulator.SimRuntime.iOS-17-5
(simulator)
macOS (desktop) • macos • darwin-arm64 • macOS 14.4.1 23E224 darwin-arm64
Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 14.4.1 23E224 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 125.0.6422.113
-

Notice that the first device is my physical device connected to my laptop.

-

Now, to run the code in release mode on this physical device, you invoke flutter run --release and select the physical device like so. (Make sure your device is unlocked while running this command.)

-
% flutter run --release
Connected devices:
Philip's iPhone (mobile) • 00008030-000364940A98802E • ios • iOS 17.5.1 21F90
iPhone 11 (mobile) • 8E550E86-3173-4342-B197-A557B83E40A2 • ios • com.apple.CoreSimulator.SimRuntime.iOS-17-5
(simulator)
macOS (desktop) • macos • darwin-arm64 • macOS 14.4.1 23E224 darwin-arm64
Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 14.4.1 23E224 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 125.0.6422.113

Checking for wireless devices...

No wireless devices were found.

[1]: Philip's iPhone (00008030-000364940A98802E)
[2]: iPhone 11 (8E550E86-3173-4342-B197-A557B83E40A2)
[3]: macOS (macos)
[4]: Mac Designed for iPad (mac-designed-for-ipad)
[5]: Chrome (chrome)
Please choose one (or "q" to quit): 1
Launching lib/main.dart on Philip's iPhone in release mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: 8M69898HLM
Running Xcode build...
└─Compiling, linking and signing... 8.8s
Xcode build done. 67.9s
Installing and launching... 7.1s

Flutter run key commands.
h List all available interactive commands.
c Clear the screen
q Quit (terminate the application on the device).

+ \ No newline at end of file diff --git a/docs/develop/design/badges.html b/docs/develop/design/badges.html index 9dd562ab..942d9c2e 100644 --- a/docs/develop/design/badges.html +++ b/docs/develop/design/badges.html @@ -5,7 +5,7 @@ Badges | Geo Garden Club - + diff --git a/docs/develop/design/data-mutation.html b/docs/develop/design/data-mutation.html index 663cb802..9687cf1f 100644 --- a/docs/develop/design/data-mutation.html +++ b/docs/develop/design/data-mutation.html @@ -5,7 +5,7 @@ Data Mutation | Geo Garden Club - + diff --git a/docs/develop/design/features.html b/docs/develop/design/features.html index 64574fb7..14c3c0c4 100644 --- a/docs/develop/design/features.html +++ b/docs/develop/design/features.html @@ -5,7 +5,7 @@ Features | Geo Garden Club - + diff --git a/docs/develop/design/ids.html b/docs/develop/design/ids.html index a18c5940..9e7a9ebb 100644 --- a/docs/develop/design/ids.html +++ b/docs/develop/design/ids.html @@ -5,7 +5,7 @@ ID management | Geo Garden Club - + diff --git a/docs/develop/design/input-management.html b/docs/develop/design/input-management.html index d994fcdd..b781d9e6 100644 --- a/docs/develop/design/input-management.html +++ b/docs/develop/design/input-management.html @@ -5,7 +5,7 @@ Input Management | Geo Garden Club - + diff --git a/docs/develop/design/with-widgets.html b/docs/develop/design/with-widgets.html index c1759467..482a7393 100644 --- a/docs/develop/design/with-widgets.html +++ b/docs/develop/design/with-widgets.html @@ -5,7 +5,7 @@ "With" widgets | Geo Garden Club - + diff --git a/docs/develop/installation.html b/docs/develop/installation.html index 04ea045c..15d8cdf0 100644 --- a/docs/develop/installation.html +++ b/docs/develop/installation.html @@ -5,7 +5,7 @@ Installation | Geo Garden Club - + @@ -29,7 +29,7 @@

Install the

Next, cd into the ggc_app directory and run flutter pub get. For example:

% flutter pub get
Running "flutter pub get" in ggc_app...
Resolving dependencies... (1.4s)
_fe_analyzer_shared 58.0.0 (59.0.0 available)
analyzer 5.10.0 (5.11.1 available)
async 2.10.0 (2.11.0 available)
build_daemon 3.1.1 (4.0.0 available)
build_runner 2.3.3 (2.4.1 available)
characters 1.2.1 (1.3.0 available)
collection 1.17.0 (1.17.1 available)
flex_color_scheme 7.0.3 (7.0.4 available)
flutter_form_builder 7.8.0 (8.0.0 available)
flutter_riverpod 2.3.5 (2.3.6 available)
flutter_svg 1.1.6 (2.0.5 available)
go_router 6.5.7 (6.5.8 available)
intl 0.17.0 (0.18.1 available)
js 0.6.5 (0.6.7 available)
matcher 0.12.13 (0.12.15 available)
material_color_utilities 0.2.0 (0.3.0 available)
meta 1.8.0 (1.9.1 available)
monarch 3.0.1 (3.4.0 available)
path 1.8.2 (1.8.3 available)
path_provider_windows 2.1.5 (2.1.6 available)
petitparser 5.1.0 (5.4.0 available)
riverpod 2.3.5 (2.3.6 available)
source_span 1.9.1 (1.10.0 available)
sqflite 2.2.6 (2.2.7 available)
sqflite_common 2.4.3 (2.4.4 available)
synchronized 3.0.1 (3.1.0 available)
test_api 0.4.16 (0.5.2 available)
vm_service 11.3.0 (11.4.0 available)
win32 3.1.4 (4.1.3 available)
xml 6.2.2 (6.3.0 available)
Got dependencies!

Run the app

-

...with the production database

+

...with live data

To check that the ggc_app runs in your development environment, the simplest thing to do is to invoke flutter run and select Chrome:

% flutter run
Multiple devices found:
macOS (desktop) • macos • darwin-arm64 • macOS 13.3.1 22E261 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 112.0.5615.137
[1]: macOS (macos)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 2
Launching lib/main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 16.5s
This app is linked to the debug service: ws://127.0.0.1:58007/FT3-VNs7AGk=/ws
Debug service listening on ws://127.0.0.1:58007/FT3-VNs7AGk=/ws

💪 Running with sound null safety 💪

🔥 To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

An Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:58007/FT3-VNs7AGk=
WARNING: found an existing <meta name="viewport"> tag. Flutter Web uses its own viewport configuration for better compatibility with
Flutter. This tag will be replaced.
The Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:58007/FT3-VNs7AGk=

If all goes well, you should see a window similar to the following appear:

@@ -40,6 +40,16 @@

...with test
~/GitHub/geogardenclub/ggc_app $ flutter run lib/main_test_fixture.dart
Launching lib/main_test_fixture.dart on iPhone 15 Pro in debug mode...
Running Xcode build...
└─Compiling, linking and signing... 7.2s
Xcode build done. 45.7s
Syncing files to device iPhone 15 Pro... 149ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

A Dart VM Service on iPhone 15 Pro is available at: http://127.0.0.1:64617/Tfq60_DovMo=/
The Flutter DevTools debugger and profiler on iPhone 15 Pro is available at:
http://127.0.0.1:9102?uri=http://127.0.0.1:64617/Tfq60_DovMo=/
info

In IntelliJ, you can create a "Run Configuration" that enables you to run with the test database by clicking the green arrow at the top of the IntelliJ window.

This command overrides the Riverpod providers so that they load the test fixture data in assets/test/fixture1. Note that changes you make with the UI are never written to these files, so if you reload the system during development, the app state will be restored to the test fixture state. However, as long as you do not reload, changes to the data are reflected in the UI, making this a good way to test out changes to the system without fear of affecting the production database in a negative manner.

+

... with a physical device

+

Sometimes it is useful to run the app on a physical device rather than the simulator. Currently only Philip can do this due to iOS signing issues.

+

Here is how to do it:

+

First, delete the app from your physical device.

+

Second, connect the physical device to a laptop.

+

Third, run flutter devices to verify that the device shows up. You should see output like this:

+
 % flutter devices    
Found 5 connected devices:
Philip's iPhone (mobile) • 00008030-000364940A98802E • ios • iOS 17.5.1 21F90
iPhone 11 (mobile) • 8E550E86-3173-4342-B197-A557B83E40A2 • ios • com.apple.CoreSimulator.SimRuntime.iOS-17-5
(simulator)
macOS (desktop) • macos • darwin-arm64 • macOS 14.4.1 23E224 darwin-arm64
Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 14.4.1 23E224 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 125.0.6422.113
+

Notice that the first device is my phone connected to my laptop.

+

Now, to run the code in release mode on this physical device, you invoke flutter run --release and select the physical device as shown below. (Make sure your device is unlocked while running this command.)

+
% flutter run --release
Connected devices:
Philip's iPhone (mobile) • 00008030-000364940A98802E • ios • iOS 17.5.1 21F90
iPhone 11 (mobile) • 8E550E86-3173-4342-B197-A557B83E40A2 • ios • com.apple.CoreSimulator.SimRuntime.iOS-17-5
(simulator)
macOS (desktop) • macos • darwin-arm64 • macOS 14.4.1 23E224 darwin-arm64
Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 14.4.1 23E224 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 125.0.6422.113

Checking for wireless devices...

No wireless devices were found.

[1]: Philip's iPhone (00008030-000364940A98802E)
[2]: iPhone 11 (8E550E86-3173-4342-B197-A557B83E40A2)
[3]: macOS (macos)
[4]: Mac Designed for iPad (mac-designed-for-ipad)
[5]: Chrome (chrome)
Please choose one (or "q" to quit): 1
Launching lib/main.dart on Philip's iPhone in release mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: 8M69898HLM
Running Xcode build...
└─Compiling, linking and signing... 8.8s
Xcode build done. 67.9s
Installing and launching... 7.1s

Flutter run key commands.
h List all available interactive commands.
c Clear the screen
q Quit (terminate the application on the device).

Integration tests

The next step is to ensure that you can run the integration tests. Please follow the instructions on the Testing page. If you cannot run the tests without encountering a test failure, please contact a developer for assistance.

Editor

@@ -61,6 +71,6 @@

Monarch

Monarch loads the data in the test/fixture1 files to populate screens.

At present, Monarch seems most useful when experimenting with different themes. Monarch allows you to switch themes and see the results on your widget(s) of interest by simply selecting from the Theme menu. In contrast, in the simulator, you would need to navigate from the screen displaying the widget(s) of interest to the Settings page (in order to select the new theme), and then navigate back to the screen displaying the widget(s) of interest.

-
Ignore Firebase exceptions

Unfortunately, the console window will print out error messages similar to the following when using Monarch:

══╡ EXCEPTION CAUGHT BY MONARCH ╞═══════════════════════════════════════════════════════════════════
The following message was thrown while a story was selected:
[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()

You can ignore these messages as Monarch does not actually invoke Firebase.

+
Ignore Firebase exceptions

Unfortunately, the console window will print out error messages similar to the following when using Monarch:

══╡ EXCEPTION CAUGHT BY MONARCH ╞═══════════════════════════════════════════════════════════════════
The following message was thrown while a story was selected:
[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()

You can ignore these messages as Monarch does not actually invoke Firebase.

\ No newline at end of file diff --git a/docs/develop/onboarding.html b/docs/develop/onboarding.html index dd292d05..e5038c45 100644 --- a/docs/develop/onboarding.html +++ b/docs/develop/onboarding.html @@ -5,7 +5,7 @@ Onboarding | Geo Garden Club - + diff --git a/docs/develop/privacy.html b/docs/develop/privacy.html index cd26e15c..365c6ce3 100644 --- a/docs/develop/privacy.html +++ b/docs/develop/privacy.html @@ -5,7 +5,7 @@ Privacy and security | Geo Garden Club - + diff --git a/docs/develop/quality-assurance/coding-standards.html b/docs/develop/quality-assurance/coding-standards.html index 12c28337..fc8ce422 100644 --- a/docs/develop/quality-assurance/coding-standards.html +++ b/docs/develop/quality-assurance/coding-standards.html @@ -5,7 +5,7 @@ Coding Standards | Geo Garden Club - + diff --git a/docs/develop/quality-assurance/dart-analyze.html b/docs/develop/quality-assurance/dart-analyze.html index 84ae973e..91d54e96 100644 --- a/docs/develop/quality-assurance/dart-analyze.html +++ b/docs/develop/quality-assurance/dart-analyze.html @@ -5,7 +5,7 @@ Dart analyze | Geo Garden Club - + diff --git a/docs/develop/quality-assurance/integrity-check.html b/docs/develop/quality-assurance/integrity-check.html index afa87cb8..a2e2759e 100644 --- a/docs/develop/quality-assurance/integrity-check.html +++ b/docs/develop/quality-assurance/integrity-check.html @@ -5,7 +5,7 @@ Database Integrity Checking | Geo Garden Club - + diff --git a/docs/develop/quality-assurance/testing.html b/docs/develop/quality-assurance/testing.html index 262aa69a..ed518209 100644 --- a/docs/develop/quality-assurance/testing.html +++ b/docs/develop/quality-assurance/testing.html @@ -5,7 +5,7 @@ Testing | Geo Garden Club - + @@ -38,11 +38,11 @@

Installation
brew install lcov

Third, activate the remove_from_coverage Dart package so that the coverage report can be customized. Do this by invoking:

dart pub global activate remove_from_coverage
-

Run the tests

+

run_tests.sh

To run the test suite, open the iOS simulator, make sure it is visible on your desktop, and then invoke ./run_tests.sh in a terminal window. It should take around 5 minutes to run, and should produce output similar to the following:

$ ./run_tests.sh
+ flutter test integration_test/app_test.dart --coverage
00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart
00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s
Xcode build done. 30.1s
00:42 +0: GGC Integration Test (All) Fixture 1 Tests
Testing admin feature
Testing badge feature
Testing bed feature
... test Bed CRUD
#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)
Rate limiting enabled.
#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)
Rate limiting enabled.
Testing chapter feature
Testing chat feature
Testing crop feature
... test Crop Index Screen
... test Crop CRUD
Testing garden feature
... test Garden Index Screen
... test Garden Details Screen
... test Garden CRUD
Testing gardener feature
... test Gardener Index Screen
Testing geobot feature
Testing home feature
Testing observation feature
... test Observation Feed
... test Observation CRUD
Testing outcome feature
... test Outcome Garden Details View
... test Outcome CRUD
Testing planting feature
... test Planting Index Screen
... test Planting CRUD
... test Planting Copy Planting
Testing settings feature
Testing task feature
... test Task View
... test Task CRUD
Testing user feature
... test User Profile Update
Testing variety feature
... test Variety Index Screen
... test Variety CRUD
... test Variety Gold Varieties
04:52 +1: All tests passed!
+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'repositories\/.*$'
+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'data\/.*$'
+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'domain\/.*$'
+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'authentication\/.*$'
+ genhtml -q coverage/lcov.info -o coverage/html
Overall coverage rate:
source files: 322
lines.......: 73.0% (6079 of 8329 lines)
functions...: no data found
Message summary:
no messages were reported

Note the line "All tests passed" after the sequence of lines documenting the feature under test.

-
Uh oh...

If the tests do not run successfully, there won't be the line "All tests passed", and the output will instead look similar to this:

$ ./run_tests.sh
+ flutter test integration_test/app_test.dart --coverage
00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart
00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s
Xcode build done. 30.1s
00:42 +0: GGC Integration Test (All) Fixture 1 Tests
Testing admin feature
Testing badge feature
Testing bed feature
... test Bed CRUD
#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)
Rate limiting enabled.
#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)
Rate limiting enabled.
Testing chapter feature
Testing chat feature
Testing crop feature
... test Crop Index Screen
... test Crop CRUD
Testing garden feature
... test Garden Index Screen
... test Garden Details Screen
... test Garden CRUD
Testing gardener feature
... test Gardener Index Screen
Testing geobot feature
Testing home feature
Testing observation feature
... test Observation Feed
... test Observation CRUD
Testing outcome feature
... test Outcome Garden Details View
... test Outcome CRUD
Testing planting feature
... test Planting Index Screen
... test Planting CRUD
... test Planting Copy Planting
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: <true>
Actual: <false>

When the exception was thrown, this was the stack:
#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)
<asynchronous suspension>
#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)
<asynchronous suspension>
#6 main.<anonymous closure>.<anonymous closure> (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)
<asynchronous suspension>
#7 patrolWidgetTest.<anonymous closure> (package:patrol_finders/src/common.dart:50:7)
<asynchronous suspension>
#8 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15)
<asynchronous suspension>
#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

This was caught by the test expectation on the following line:
file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24
The test description was:
Fixture 1 Tests
════════════════════════════════════════════════════════════════════════════════════════════════════
03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E]
Test failed. See exception logs above.
The test description was: Fixture 1 Tests


To run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'
03:16 +0 -1: Some tests failed.
+ genhtml -q coverage/lcov.info -o coverage/html
Overall coverage rate:
source files: 472
lines.......: 54.8% (7029 of 12823 lines)
functions...: no data found
Message summary:
no messages were reported

You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart.

If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfully using the iPhone 15 simulator under iOS 17.5.

+
Uh oh...

If the tests do not run successfully, there won't be the line "All tests passed", and the output will instead look similar to this:

$ ./run_tests.sh
+ flutter test integration_test/app_test.dart --coverage
00:05 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:28 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart
00:35 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 7.0s
Xcode build done. 30.1s
00:42 +0: GGC Integration Test (All) Fixture 1 Tests
Testing admin feature
Testing badge feature
Testing bed feature
... test Bed CRUD
#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)
Rate limiting enabled.
#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)
Rate limiting enabled.
Testing chapter feature
Testing chat feature
Testing crop feature
... test Crop Index Screen
... test Crop CRUD
Testing garden feature
... test Garden Index Screen
... test Garden Details Screen
... test Garden CRUD
Testing gardener feature
... test Gardener Index Screen
Testing geobot feature
Testing home feature
Testing observation feature
... test Observation Feed
... test Observation CRUD
Testing outcome feature
... test Outcome Garden Details View
... test Outcome CRUD
Testing planting feature
... test Planting Index Screen
... test Planting CRUD
... test Planting Copy Planting
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: <true>
Actual: <false>

When the exception was thrown, this was the stack:
#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)
<asynchronous suspension>
#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)
<asynchronous suspension>
#6 main.<anonymous closure>.<anonymous closure> (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)
<asynchronous suspension>
#7 patrolWidgetTest.<anonymous closure> (package:patrol_finders/src/common.dart:50:7)
<asynchronous suspension>
#8 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15)
<asynchronous suspension>
#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

This was caught by the test expectation on the following line:
file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24
The test description was:
Fixture 1 Tests
════════════════════════════════════════════════════════════════════════════════════════════════════
03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E]
Test failed. See exception logs above.
The test description was: Fixture 1 Tests


To run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'
03:16 +0 -1: Some tests failed.
+ genhtml -q coverage/lcov.info -o coverage/html
Overall coverage rate:
source files: 472
lines.......: 54.8% (7029 of 12823 lines)
functions...: no data found
Message summary:
no messages were reported

You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart.

If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfully using the iPhone 15 simulator under iOS 17.5.

Here are some important takeaways from this test execution output:

  • We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code. Currently, the lib/ directory contains around 44K lines of code, and the integration_test/ directory contains around 1200 lines of code. So, the test code only accounts for around 2% of the total code base.
  • @@ -52,7 +52,7 @@

    Run the tests<
  • We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested, not to verify that the tests achieve 100% coverage (more on this below). We also remove several directories from the coverage report (i.e. data/, domain/, and repositories/) so that the coverage report does not report on code that is never executed due to mocking (i.e. code in the data/ and repositories/ directories) and also focuses more specifically on UI code.
Keep the iOS simulator open and visible!

When running the tests, be sure to keep the iOS simulator visible on your desktop for two reasons.

First, we have discovered that if the iOS simulator is not visible, the tests may fail unpredictably.

Second, the testing process will occasionally (and unpredictably) pause in the iOS simulator, waiting for you to click on a button to allow pasting:

If you don't click the button to allow pasting, the test process will hang indefinitely.

This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time.

-

About app_test.dart

+

app_test.dart

To further understand the test process, it's helpful to review the code that is run by the ./run_tests.sh command:

// integration_test/app_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('GGC Integration Test (All)', () {
patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {
await Firebase.initializeApp();
setFirebaseUiIsTestMode(true);
FirebaseAuth mockAuth = MockFirebaseAuth();
String email = 'jennacorindeane@gmail.com';
mockAuth.createUserWithEmailAndPassword(email: email, password: '');
TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);
await $.pumpWidgetAndSettle(ProviderScope(
overrides: [
firebaseAuthProvider.overrideWithValue(mockAuth),
badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),
badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),
badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),
badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),
bedsProvider.overrideWith((_) => testFixture.getBedsStream()),
bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),
chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),
chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),
chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),
chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),
cropsProvider.overrideWith((_) => testFixture.getCropsStream()),
cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),
editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),
editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),
familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),
familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),
gardensProvider.overrideWith((_) => testFixture.getGardensStream()),
gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),
gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),
gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),
observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),
observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),
outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),
outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),
plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),
plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),
rolesProvider.overrideWith((_) => testFixture.getRolesStream()),
roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),
tagsProvider.overrideWith((_) => testFixture.getTagsStream()),
tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),
tasksProvider.overrideWith((_) => testFixture.getTasksStream()),
taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),
usersProvider.overrideWith((_) => testFixture.getUsersStream()),
userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),
varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),
varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),
],
child: const MyApp(),
));
expect($(HomeScreen).visible, equals(true), reason: 'Login fails');
await checkIntegrity($, reason: 'startup');
await testAdmin($);
await checkIntegrity($, reason: 'admin feature');
await testBadge($);
await checkIntegrity($, reason: 'badge feature');
await testBed($);
await checkIntegrity($, reason: 'bed feature');
await testChapter($);
await checkIntegrity($, reason: 'chapter feature');
await testChat($);
await checkIntegrity($, reason: 'chat feature');
await testCrop($);
await checkIntegrity($, reason: 'crop feature');
await testGarden($);
await checkIntegrity($, reason: 'garden feature');
await testGardener($);
await checkIntegrity($, reason: 'gardener feature');
await testGeoBot($);
await checkIntegrity($, reason: 'geobot feature');
await testHome($);
await checkIntegrity($, reason: 'home feature');
await testObservation($);
await checkIntegrity($, reason: 'observation feature');
await testOutcome($);
await checkIntegrity($, reason: 'outcome feature');
await testPlanting($);
await checkIntegrity($, reason: 'planting feature');
await testSettings($);
await checkIntegrity($, reason: 'settings feature');
await testTask($);
await checkIntegrity($, reason: 'task feature');
await testVariety($);
await checkIntegrity($, reason: 'variety feature');
});
});
}

Here are the important takeaways:

@@ -87,7 +87,7 @@

Testing a
  • It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function.
  • The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation.
  • -

    About run_tests_single.sh and app_test_single.dart

    +

    run_tests_single.sh

    While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code.

    To speed up testing, you can use the command ./run_tests_single.sh. This runs the app_test_single.dart file, which looks similar to this:

    // integration_test/app_test_single.dart
    void main() {
    IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    group('GGC Integration Test (Single)', () {
    patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {
    await Firebase.initializeApp();
    setFirebaseUiIsTestMode(true);
    FirebaseAuth mockAuth = MockFirebaseAuth();
    String email = 'jennacorindeane@gmail.com';
    mockAuth.createUserWithEmailAndPassword(email: email, password: '');
    TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);
    await $.pumpWidgetAndSettle(ProviderScope(
    overrides: [
    firebaseAuthProvider.overrideWithValue(mockAuth),
    badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),
    badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),
    :
    :
    varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),
    varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),
    ],
    child: const MyApp(),
    ));
    expect($(HomeScreen).visible, equals(true), reason: 'Login fails');
    await testCrop($);
    });
    });
    }
    @@ -191,6 +191,6 @@

    TestFi
    • get<Entity>Stream() - returns a Stream of the List of the entities from the test fixture.
    • get<Entity>Database() - returns The Fixture<Entity>Database from the test fixture.
    • -
    + \ No newline at end of file diff --git a/docs/develop/releases/release-0.0/chatgpt-feedback.html b/docs/develop/releases/release-0.0/chatgpt-feedback.html index 2378861f..d1d604d9 100644 --- a/docs/develop/releases/release-0.0/chatgpt-feedback.html +++ b/docs/develop/releases/release-0.0/chatgpt-feedback.html @@ -5,7 +5,7 @@ ChatGPT feedback | Geo Garden Club - + diff --git a/docs/develop/releases/release-0.0/customer-feedback.html b/docs/develop/releases/release-0.0/customer-feedback.html index 5962dd88..522a5d2e 100644 --- a/docs/develop/releases/release-0.0/customer-feedback.html +++ b/docs/develop/releases/release-0.0/customer-feedback.html @@ -5,7 +5,7 @@ Customer feedback | Geo Garden Club - + diff --git a/docs/develop/releases/release-0.0/design.html b/docs/develop/releases/release-0.0/design.html index 6040f4fb..ae8be8c4 100644 --- a/docs/develop/releases/release-0.0/design.html +++ b/docs/develop/releases/release-0.0/design.html @@ -5,7 +5,7 @@ Design and implementation | Geo Garden Club - + diff --git a/docs/develop/releases/release-0.0/entrepreneur-feedback.html b/docs/develop/releases/release-0.0/entrepreneur-feedback.html index 327aa772..69b59777 100644 --- a/docs/develop/releases/release-0.0/entrepreneur-feedback.html +++ b/docs/develop/releases/release-0.0/entrepreneur-feedback.html @@ -5,7 +5,7 @@ Entrepreneur feedback | Geo Garden Club - + diff --git a/docs/develop/releases/release-1.0/cvp.html b/docs/develop/releases/release-1.0/cvp.html index 239d0fee..ae6b0bf6 100644 --- a/docs/develop/releases/release-1.0/cvp.html +++ b/docs/develop/releases/release-1.0/cvp.html @@ -5,7 +5,7 @@ Core Value Propositions | Geo Garden Club - + diff --git a/docs/develop/releases/release-1.0/end-of-season-feedback.html b/docs/develop/releases/release-1.0/end-of-season-feedback.html index 2ea2f45a..507327dd 100644 --- a/docs/develop/releases/release-1.0/end-of-season-feedback.html +++ b/docs/develop/releases/release-1.0/end-of-season-feedback.html @@ -5,7 +5,7 @@ End of Season Feedback | Geo Garden Club - + diff --git a/docs/develop/releases/release-1.0/goals.html b/docs/develop/releases/release-1.0/goals.html index 848efde6..1752c3c8 100644 --- a/docs/develop/releases/release-1.0/goals.html +++ b/docs/develop/releases/release-1.0/goals.html @@ -5,7 +5,7 @@ Technology Goals | Geo Garden Club - + diff --git a/docs/develop/releases/release-1.0/onboarding-feedback.html b/docs/develop/releases/release-1.0/onboarding-feedback.html index 962b9d55..d39feb03 100644 --- a/docs/develop/releases/release-1.0/onboarding-feedback.html +++ b/docs/develop/releases/release-1.0/onboarding-feedback.html @@ -5,7 +5,7 @@ Onboarding Feedback | Geo Garden Club - + diff --git a/docs/develop/scripts.html b/docs/develop/scripts.html index 412ea18a..2227ce67 100644 --- a/docs/develop/scripts.html +++ b/docs/develop/scripts.html @@ -5,7 +5,7 @@ Scripts | Geo Garden Club - + diff --git a/docs/home/food-security.html b/docs/home/food-security.html index 528d3e7c..241b90de 100644 --- a/docs/home/food-security.html +++ b/docs/home/food-security.html @@ -5,7 +5,7 @@ Food Security | Geo Garden Club - + diff --git a/docs/home/innovations.html b/docs/home/innovations.html index be62cd79..f8f4d72e 100644 --- a/docs/home/innovations.html +++ b/docs/home/innovations.html @@ -5,7 +5,7 @@ Design Innovations | Geo Garden Club - + diff --git a/docs/home/related-work.html b/docs/home/related-work.html index 7bee8d70..6099070c 100644 --- a/docs/home/related-work.html +++ b/docs/home/related-work.html @@ -5,7 +5,7 @@ Garden Planning Tools | Geo Garden Club - + diff --git a/docs/home/serious-gardeners.html b/docs/home/serious-gardeners.html index a8ca5643..4b068dfe 100644 --- a/docs/home/serious-gardeners.html +++ b/docs/home/serious-gardeners.html @@ -5,7 +5,7 @@ "Serious" Gardeners | Geo Garden Club - + diff --git a/docs/home/sneak-peek.html b/docs/home/sneak-peek.html index ed6e91e8..145406e7 100644 --- a/docs/home/sneak-peek.html +++ b/docs/home/sneak-peek.html @@ -5,7 +5,7 @@ Mobile App Sneak Peek | Geo Garden Club - + diff --git a/docs/home/team.html b/docs/home/team.html index 654e6a74..c46bc854 100644 --- a/docs/home/team.html +++ b/docs/home/team.html @@ -5,7 +5,7 @@ The Team | Geo Garden Club - + diff --git a/docs/home/welcome.html b/docs/home/welcome.html index 16887481..b7732528 100644 --- a/docs/home/welcome.html +++ b/docs/home/welcome.html @@ -5,7 +5,7 @@ Welcome | Geo Garden Club - + diff --git a/docs/user-guide/adding-plantings.html b/docs/user-guide/adding-plantings.html index 3f4ad79d..da1b49e7 100644 --- a/docs/user-guide/adding-plantings.html +++ b/docs/user-guide/adding-plantings.html @@ -5,7 +5,7 @@ Add Plantings to Beds | Geo Garden Club - + diff --git a/docs/user-guide/adding-vendors-crops-varieties.html b/docs/user-guide/adding-vendors-crops-varieties.html index 3cc253a2..13805079 100644 --- a/docs/user-guide/adding-vendors-crops-varieties.html +++ b/docs/user-guide/adding-vendors-crops-varieties.html @@ -5,7 +5,7 @@ Add Crops and Varieties to the Chapter Database | Geo Garden Club - + diff --git a/docs/user-guide/badges.html b/docs/user-guide/badges.html index 7e757b20..9a191532 100644 --- a/docs/user-guide/badges.html +++ b/docs/user-guide/badges.html @@ -5,7 +5,7 @@ Badges | Geo Garden Club - + diff --git a/docs/user-guide/chat-rooms.html b/docs/user-guide/chat-rooms.html index ada6265b..a98fb82a 100644 --- a/docs/user-guide/chat-rooms.html +++ b/docs/user-guide/chat-rooms.html @@ -5,7 +5,7 @@ Chat Rooms | Geo Garden Club - + diff --git a/docs/user-guide/define-a-garden.html b/docs/user-guide/define-a-garden.html index 4af6efcb..21dd6a0a 100644 --- a/docs/user-guide/define-a-garden.html +++ b/docs/user-guide/define-a-garden.html @@ -5,7 +5,7 @@ Define a Garden | Geo Garden Club - + diff --git a/docs/user-guide/downloading.html b/docs/user-guide/downloading.html index 78191386..212d25bd 100644 --- a/docs/user-guide/downloading.html +++ b/docs/user-guide/downloading.html @@ -5,7 +5,7 @@ Downloading | Geo Garden Club - + diff --git a/docs/user-guide/explore-a-chapter.html b/docs/user-guide/explore-a-chapter.html index c8df6fce..847f3af4 100644 --- a/docs/user-guide/explore-a-chapter.html +++ b/docs/user-guide/explore-a-chapter.html @@ -5,7 +5,7 @@ Explore a Chapter | Geo Garden Club - + diff --git a/docs/user-guide/explore-a-garden.html b/docs/user-guide/explore-a-garden.html index 28c25693..cc458093 100644 --- a/docs/user-guide/explore-a-garden.html +++ b/docs/user-guide/explore-a-garden.html @@ -5,7 +5,7 @@ Explore a Garden | Geo Garden Club - + diff --git a/docs/user-guide/geobot.html b/docs/user-guide/geobot.html index 357f82f4..10eda260 100644 --- a/docs/user-guide/geobot.html +++ b/docs/user-guide/geobot.html @@ -5,7 +5,7 @@ GeoBot | Geo Garden Club - + diff --git a/docs/user-guide/guided-tour.html b/docs/user-guide/guided-tour.html index ed5cafa1..8f997bb0 100644 --- a/docs/user-guide/guided-tour.html +++ b/docs/user-guide/guided-tour.html @@ -5,7 +5,7 @@ Frequently Asked (Gardening) Questions | Geo Garden Club - + diff --git a/docs/user-guide/observations.html b/docs/user-guide/observations.html index b5554f16..034feb0a 100644 --- a/docs/user-guide/observations.html +++ b/docs/user-guide/observations.html @@ -5,7 +5,7 @@ Observations | Geo Garden Club - + diff --git a/docs/user-guide/outcomes.html b/docs/user-guide/outcomes.html index 4ccfe15e..6c696ccf 100644 --- a/docs/user-guide/outcomes.html +++ b/docs/user-guide/outcomes.html @@ -5,7 +5,7 @@ Outcomes | Geo Garden Club - + diff --git a/docs/user-guide/overview.html b/docs/user-guide/overview.html index 31f79adc..b9487f9d 100644 --- a/docs/user-guide/overview.html +++ b/docs/user-guide/overview.html @@ -5,7 +5,7 @@ Overview | Geo Garden Club - + diff --git a/docs/user-guide/privacy.html b/docs/user-guide/privacy.html index b4e3f257..f49a73f2 100644 --- a/docs/user-guide/privacy.html +++ b/docs/user-guide/privacy.html @@ -5,7 +5,7 @@ Privacy Policy | Geo Garden Club - + diff --git a/docs/user-guide/registration.html b/docs/user-guide/registration.html index b488e931..c0219f8f 100644 --- a/docs/user-guide/registration.html +++ b/docs/user-guide/registration.html @@ -5,7 +5,7 @@ Registration | Geo Garden Club - + diff --git a/docs/user-guide/scenarios.html b/docs/user-guide/scenarios.html index 26285350..949170a2 100644 --- a/docs/user-guide/scenarios.html +++ b/docs/user-guide/scenarios.html @@ -5,7 +5,7 @@ Planting Scenarios | Geo Garden Club - + diff --git a/docs/user-guide/seeds.html b/docs/user-guide/seeds.html index 1cb09879..f5d95dfc 100644 --- a/docs/user-guide/seeds.html +++ b/docs/user-guide/seeds.html @@ -5,7 +5,7 @@ Seeds | Geo Garden Club - + diff --git a/docs/user-guide/tasks.html b/docs/user-guide/tasks.html index 418d47d5..bd50e158 100644 --- a/docs/user-guide/tasks.html +++ b/docs/user-guide/tasks.html @@ -5,7 +5,7 @@ Tasks | Geo Garden Club - + diff --git a/docs/user-guide/terms-and-conditions.html b/docs/user-guide/terms-and-conditions.html index db820052..d280d330 100644 --- a/docs/user-guide/terms-and-conditions.html +++ b/docs/user-guide/terms-and-conditions.html @@ -5,7 +5,7 @@ Terms and Conditions | Geo Garden Club - + diff --git a/index.html b/index.html index 475ce600..345fc3e9 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ Geo Garden Club | Geo Garden Club - + diff --git a/markdown-page.html b/markdown-page.html index 014394ad..e030f650 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -5,7 +5,7 @@ Markdown page example | Geo Garden Club - +