From 0313aa0a00ae3bf2940c3131b7a7bce9c8c7234d Mon Sep 17 00:00:00 2001 From: Philip Johnson Date: Fri, 22 Nov 2024 11:11:39 -1000 Subject: [PATCH] Deploy website - based on 13fec80bf4f0bc7db7898128e9ee22e0a1a6367f --- 404.html | 2 +- assets/js/afab5b7c.7716c6ad.js | 1 + assets/js/afab5b7c.f223c5d2.js | 1 - ...ime~main.8e00896c.js => runtime~main.dd70b35a.js} | 2 +- blog.html | 2 +- blog/2023/02/10/welcome.html | 2 +- blog/archive.html | 2 +- docs/business.html | 2 +- docs/business/market-size.html | 2 +- docs/business/milestones.html | 2 +- docs/business/roadmap.html | 2 +- docs/develop.html | 2 +- docs/develop/architecture.html | 2 +- docs/develop/backups.html | 2 +- docs/develop/data-model.html | 2 +- docs/develop/database-management.html | 2 +- docs/develop/deployment.html | 2 +- docs/develop/design/badges.html | 2 +- docs/develop/design/data-mutation.html | 2 +- docs/develop/design/features.html | 2 +- docs/develop/design/ids.html | 2 +- docs/develop/design/input-fields.html | 2 +- docs/develop/design/with-widgets.html | 2 +- docs/develop/installation.html | 2 +- docs/develop/onboarding.html | 2 +- docs/develop/privacy.html | 2 +- docs/develop/quality-assurance/coding-standards.html | 2 +- docs/develop/quality-assurance/dart-analyze.html | 2 +- docs/develop/quality-assurance/integrity-check.html | 2 +- docs/develop/quality-assurance/testing.html | 12 ++++++------ .../releases/release-0.0/chatgpt-feedback.html | 2 +- .../releases/release-0.0/customer-feedback.html | 2 +- docs/develop/releases/release-0.0/design.html | 2 +- .../releases/release-0.0/entrepreneur-feedback.html | 2 +- docs/develop/releases/release-1.0/cvp.html | 2 +- .../releases/release-1.0/end-of-season-feedback.html | 2 +- docs/develop/releases/release-1.0/goals.html | 2 +- .../releases/release-1.0/onboarding-feedback.html | 2 +- docs/develop/scripts.html | 2 +- docs/home/food-security.html | 2 +- docs/home/innovations.html | 2 +- docs/home/related-work.html | 2 +- docs/home/serious-gardeners.html | 2 +- docs/home/sneak-peek.html | 2 +- docs/home/team.html | 2 +- docs/home/welcome.html | 2 +- docs/user-guide/adding-plantings.html | 2 +- docs/user-guide/adding-vendors-crops-varieties.html | 2 +- docs/user-guide/badges.html | 2 +- docs/user-guide/chat-rooms.html | 2 +- docs/user-guide/define-a-garden.html | 2 +- docs/user-guide/downloading.html | 2 +- docs/user-guide/explore-a-chapter.html | 2 +- docs/user-guide/explore-a-garden.html | 2 +- docs/user-guide/geobot.html | 2 +- docs/user-guide/guided-tour.html | 2 +- docs/user-guide/observations.html | 2 +- docs/user-guide/outcomes.html | 2 +- docs/user-guide/overview.html | 2 +- docs/user-guide/privacy.html | 2 +- docs/user-guide/registration.html | 2 +- docs/user-guide/scenarios.html | 2 +- docs/user-guide/seeds.html | 2 +- docs/user-guide/tasks.html | 2 +- docs/user-guide/terms-and-conditions.html | 2 +- index.html | 2 +- markdown-page.html | 2 +- 67 files changed, 71 insertions(+), 71 deletions(-) create mode 100644 assets/js/afab5b7c.7716c6ad.js delete mode 100644 assets/js/afab5b7c.f223c5d2.js rename assets/js/{runtime~main.8e00896c.js => runtime~main.dd70b35a.js} (99%) diff --git a/404.html b/404.html index 9fb96711..ea47146c 100644 --- a/404.html +++ b/404.html @@ -5,7 +5,7 @@ Page Not Found | Geo Garden Club - + diff --git a/assets/js/afab5b7c.7716c6ad.js b/assets/js/afab5b7c.7716c6ad.js new file mode 100644 index 00000000..199f3683 --- /dev/null +++ b/assets/js/afab5b7c.7716c6ad.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 the tests",id:"run-the-tests",level:2},{value:"Always monitor the iOS simulator!",id:"always-monitor-the-ios-simulator",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",mdxAdmonitionTitle:"mdxAdmonitionTitle",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, invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"}),". 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.jsx)(t.h2,{id:"always-monitor-the-ios-simulator",children:"Always monitor the iOS simulator!"}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsx)(t.p,{children:"While testing with the iOS simulator, the testing process will occasionally (and unpredictably) pause 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:"For this reason, it's important to always monitor the simulator at least until the tests start, because you might need to click a button to allow pasting in order to let the tests proceed. Otherwise, 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/afab5b7c.f223c5d2.js b/assets/js/afab5b7c.f223c5d2.js deleted file mode 100644 index 434a6eb4..00000000 --- a/assets/js/afab5b7c.f223c5d2.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:"Always monitor the iOS simulator!",id:"always-monitor-the-ios-simulator",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",mdxAdmonitionTitle:"mdxAdmonitionTitle",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 excludes many important issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"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:"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:"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:"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, invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"}),". 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.jsx)(t.h2,{id:"always-monitor-the-ios-simulator",children:"Always monitor the iOS simulator!"}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsx)(t.p,{children:"While testing with the iOS simulator, the testing process will occasionally (and unpredictably) pause 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:"For this reason, it's important to always monitor the simulator at least until the tests start, because you might need to click a button to allow pasting in order to let the tests proceed. Otherwise, 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.8e00896c.js b/assets/js/runtime~main.dd70b35a.js similarity index 99% rename from assets/js/runtime~main.8e00896c.js rename to assets/js/runtime~main.dd70b35a.js index a0b54151..3a14c8d0 100644 --- a/assets/js/runtime~main.8e00896c.js +++ b/assets/js/runtime~main.dd70b35a.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",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",8392:"ed0568ab",8518:"a7bd4aaa",8653:"ec0f34d7",8754:"6b1fc3de",8911:"682a0ea6",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:"3adcaf0a",234:"6da8b591",403:"9fc2a256",814:"4e08262b",825:"d42ee47b",860:"d7028e73",1130:"c05c53ae",1340:"11ae0623",1420:"aeb48ccb",1549:"7d1c392d",1585:"db463b44",1666:"ffe69db6",1744:"5ff58a23",1772:"3d06e0e2",1868:"bd4243ae",1937:"96ed6c84",2535:"0ba125c5",2700:"83aa9a75",2743:"67c47945",2967:"a7c4ab03",3029:"f223c5d2",3085:"3dba5538",3089:"911b8dd5",3560:"cffc9f0b",3608:"ee0c677a",3629:"eb980bea",3844:"84e52c31",4e3:"1038aca8",4031:"6f8546d9",4057:"efd80a40",4063:"097a8498",4076:"739b5077",4088:"4b9cca5f",4195:"b92bf9f8",4368:"4aef8496",4524:"f13327a6",4713:"802bf1c8",5014:"bd156c61",5622:"e865676c",5655:"ab3e12ff",5857:"241b5abc",5980:"f93cbc61",6033:"fb906847",6041:"3cafd262",6103:"4990621e",6142:"9adb781d",6265:"ecc041e6",6414:"19d9c76b",6427:"2704a200",6642:"b756708b",6800:"2dc6905e",6801:"fd330214",6906:"df274206",6957:"ba87d584",6974:"242c3e8d",7222:"7b0ad8f2",7346:"277b9e7e",7393:"9d35c647",7414:"37f8208f",7540:"5d4247b4",7664:"e5a1011e",7918:"b96e81ff",7937:"881a7b56",8041:"e22d43c6",8294:"97dc70b8",8392:"40070c7f",8518:"eaa77d27",8653:"1c9ade88",8754:"a1357efd",8911:"169cbed9",9208:"203bad01",9256:"0698ba6b",9268:"f1b885cf",9572:"aac6a3b6",9586:"82224d7d",9601:"e6c986e7",9661:"2dcb0623",9866:"6c00f0dc",9929:"e36f6a83"}[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","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",ed0568ab:"8392",a7bd4aaa:"8518",ec0f34d7:"8653","6b1fc3de":"8754","682a0ea6":"8911","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",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",8392:"ed0568ab",8518:"a7bd4aaa",8653:"ec0f34d7",8754:"6b1fc3de",8911:"682a0ea6",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:"3adcaf0a",234:"6da8b591",403:"9fc2a256",814:"4e08262b",825:"d42ee47b",860:"d7028e73",1130:"c05c53ae",1340:"11ae0623",1420:"aeb48ccb",1549:"7d1c392d",1585:"db463b44",1666:"ffe69db6",1744:"5ff58a23",1772:"3d06e0e2",1868:"bd4243ae",1937:"96ed6c84",2535:"0ba125c5",2700:"83aa9a75",2743:"67c47945",2967:"a7c4ab03",3029:"7716c6ad",3085:"3dba5538",3089:"911b8dd5",3560:"cffc9f0b",3608:"ee0c677a",3629:"eb980bea",3844:"84e52c31",4e3:"1038aca8",4031:"6f8546d9",4057:"efd80a40",4063:"097a8498",4076:"739b5077",4088:"4b9cca5f",4195:"b92bf9f8",4368:"4aef8496",4524:"f13327a6",4713:"802bf1c8",5014:"bd156c61",5622:"e865676c",5655:"ab3e12ff",5857:"241b5abc",5980:"f93cbc61",6033:"fb906847",6041:"3cafd262",6103:"4990621e",6142:"9adb781d",6265:"ecc041e6",6414:"19d9c76b",6427:"2704a200",6642:"b756708b",6800:"2dc6905e",6801:"fd330214",6906:"df274206",6957:"ba87d584",6974:"242c3e8d",7222:"7b0ad8f2",7346:"277b9e7e",7393:"9d35c647",7414:"37f8208f",7540:"5d4247b4",7664:"e5a1011e",7918:"b96e81ff",7937:"881a7b56",8041:"e22d43c6",8294:"97dc70b8",8392:"40070c7f",8518:"eaa77d27",8653:"1c9ade88",8754:"a1357efd",8911:"169cbed9",9208:"203bad01",9256:"0698ba6b",9268:"f1b885cf",9572:"aac6a3b6",9586:"82224d7d",9601:"e6c986e7",9661:"2dcb0623",9866:"6c00f0dc",9929:"e36f6a83"}[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","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",ed0568ab:"8392",a7bd4aaa:"8518",ec0f34d7:"8653","6b1fc3de":"8754","682a0ea6":"8911","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 2b1947be..f8ee239a 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 5bfeb31d..2c700add 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 4cac1ca5..aa17c6f6 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 61a3ee82..eb5b20a0 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 e808ee2d..69d015ee 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 78c20023..67e87ef6 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 445950bc..968eb38e 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/architecture.html b/docs/develop/architecture.html index 1e39a999..22bf5c39 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 9e7c20d5..a43d9faf 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 49985669..11098df9 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 1b9d22d4..d0e02b3c 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 aa109748..56297203 100644 --- a/docs/develop/deployment.html +++ b/docs/develop/deployment.html @@ -5,7 +5,7 @@ Deployment | Geo Garden Club - + diff --git a/docs/develop/design/badges.html b/docs/develop/design/badges.html index 52fff465..36b2baa3 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 f5a39cfd..b823092f 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 0d3f017c..876cd1dd 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 3bd7ca46..1bc006b1 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-fields.html b/docs/develop/design/input-fields.html index 96408466..266376fe 100644 --- a/docs/develop/design/input-fields.html +++ b/docs/develop/design/input-fields.html @@ -5,7 +5,7 @@ Input Fields | Geo Garden Club - + diff --git a/docs/develop/design/with-widgets.html b/docs/develop/design/with-widgets.html index 98a7da3b..8553648c 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 da3e108a..7ae7c540 100644 --- a/docs/develop/installation.html +++ b/docs/develop/installation.html @@ -5,7 +5,7 @@ Installation | Geo Garden Club - + diff --git a/docs/develop/onboarding.html b/docs/develop/onboarding.html index a3cc1566..05c55e2c 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 bfe78a05..4bd83005 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 ded47fad..d2099525 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 46dce4ba..b2c8d5ca 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 7df06231..d97343cf 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 c5087352..9d074c97 100644 --- a/docs/develop/quality-assurance/testing.html +++ b/docs/develop/quality-assurance/testing.html @@ -5,7 +5,7 @@ Testing | Geo Garden Club - + @@ -18,12 +18,12 @@
  • CRUD operations on entities can be performed successfully when available.
  • 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.
  • -

    Currently, our approach to testing excludes many important issues:

    +

    Currently, our approach to testing does not address many important quality issues:

      -
    • 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.
    • -
    • 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.
    • -
    • 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).
    • -
    • UX testing. Our tests do not ensure that user needs are met and that they have a positive experience using the app.
    • +
    • 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.
    • +
    • 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.
    • +
    • 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).
    • +
    • No UX testing. Our tests do not ensure that user needs are met and that they have a positive experience using the app.

    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.

    When should you run the tests?

    Ideally, we want the main branch to always be able to run the tests without error.

    To achieve that, the best times to run the tests are:

      diff --git a/docs/develop/releases/release-0.0/chatgpt-feedback.html b/docs/develop/releases/release-0.0/chatgpt-feedback.html index d6ee880b..03884bb4 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 0bbf6324..292c11d7 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 a7fd34ba..aa91bec8 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 c4955a83..4192e7b0 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 ec435b99..575d04ab 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 7d72397e..e5a0a526 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 88e2dacd..be2b90e1 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 26df1059..ee2be13a 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 c905d99e..2bcd6bac 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 ad4d143e..05d335e0 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 8cd02b7a..5d63fab1 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 41f78ccc..7f55197c 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 416ced7d..44e990f7 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 7aec7c95..403221b8 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 335c01d0..d19d642b 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 7db29a85..d51b8620 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 5b2ace25..d6398ba8 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 53e045f3..75cf7b27 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, Varieties, Vendors to the Chapter Database | Geo Garden Club - + diff --git a/docs/user-guide/badges.html b/docs/user-guide/badges.html index e7a7b414..3a6b4546 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 8709a517..26988b6f 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 c5f95faa..c6708bbf 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 6e1ef123..b66c45f6 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 fc977af5..67f5b986 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 fdfcea22..a67a9321 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 962cbe6b..1824bd25 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 8ed6d79c..090c27a6 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 cd80568c..ab7bf5f8 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 a84bcd3e..023ffc9b 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 bf60cf6f..d9f06629 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 dfa6c6ab..3a45d314 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 4a6241b2..bc9064ef 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 f792b96f..ace57dcf 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 3080c577..565cf8ad 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 7eca2126..a7adc84e 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 54c1f318..6b4234cd 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 6b2b4db2..c827c16a 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 101e0430..12f5cfa5 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -5,7 +5,7 @@ Markdown page example | Geo Garden Club - +