Merge branch 'main' into 397-qualitygradestatus-component

This commit is contained in:
Angie Ta
2023-02-01 15:15:45 -08:00
76 changed files with 923 additions and 622 deletions

View File

@@ -10,7 +10,8 @@ module.exports = {
extends: [
"airbnb",
"plugin:i18next/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
"plugin:@tanstack/eslint-plugin-query/recommended",
"plugin:react-native-a11y/ios"
],
plugins: [
"module-resolver",
@@ -28,12 +29,15 @@ module.exports = {
"consistent-return": [2, { treatUndefinedAsUnspecified: true }],
"func-names": 0,
"global-require": 0,
"i18next/no-literal-string": [2, {
words: {
// Minor change to the default to disallow all-caps string literals as well
exclude: ["[0-9!-/:-@[-`{-~]+"]
"i18next/no-literal-string": [
2,
{
words: {
// Minor change to the default to disallow all-caps string literals as well
exclude: ["[0-9!-/:-@[-`{-~]+"]
}
}
}],
],
// The AirBNB approach at
// https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/imports.js#L71
// is quite particular and forbids imports of devDependencies anywhere
@@ -45,22 +49,30 @@ module.exports = {
// raise alarms when you try to import things not declared in
// package.json.
"import/no-extraneous-dependencies": ["error", {}],
"max-len": ["error", 100, 2, {
ignoreUrls: true,
ignoreComments: false,
ignoreRegExpLiterals: true,
ignoreStrings: false,
ignoreTemplateLiterals: false
}],
"max-len": [
"error",
100,
2,
{
ignoreUrls: true,
ignoreComments: false,
ignoreRegExpLiterals: true,
ignoreStrings: false,
ignoreTemplateLiterals: false
}
],
"no-alert": 0,
"no-underscore-dangle": 0,
"no-unused-vars": ["error", {
vars: "all",
args: "after-used",
// Overriding airbnb to allow leading underscore to indicate unused var
argsIgnorePattern: "^_",
ignoreRestSiblings: true
}],
"no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
// Overriding airbnb to allow leading underscore to indicate unused var
argsIgnorePattern: "^_",
ignoreRestSiblings: true
}
],
"no-void": 0,
"prefer-destructuring": [2, { object: true, array: false }],
quotes: [2, "double"],
@@ -76,7 +88,10 @@ module.exports = {
"react/prop-types": 0,
"react/destructuring-assignment": 0,
"react/jsx-filename-extension": 0,
"react/function-component-definition": [2, { namedComponents: "arrow-function" }],
"react/function-component-definition": [
2,
{ namedComponents: "arrow-function" }
],
"react/require-default-props": 0,
// React-Hooks Plugin
@@ -87,7 +102,21 @@ module.exports = {
"react-native/no-inline-styles": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
"simple-import-sort/exports": "error",
"react-native-a11y/has-accessibility-hint": 1,
"react-native-a11y/has-accessibility-props": 1,
"react-native-a11y/has-valid-accessibility-actions": 1,
"react-native-a11y/has-valid-accessibility-role": 1,
"react-native-a11y/has-valid-accessibility-state": 1,
"react-native-a11y/has-valid-accessibility-states": 1,
"react-native-a11y/has-valid-accessibility-component-type": 1,
"react-native-a11y/has-valid-accessibility-traits": 1,
"react-native-a11y/has-valid-accessibility-value": 1,
"react-native-a11y/no-nested-touchables": 1,
"react-native-a11y/has-valid-accessibility-descriptors": 1,
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": 1,
"react-native-a11y/has-valid-accessibility-live-region": 1,
"react-native-a11y/has-valid-important-for-accessibility": 1
},
// need this so jest doesn't show as undefined in jest.setup.js
env: {

View File

@@ -75,10 +75,10 @@ jobs:
- run: brew install applesimutils
- name: Build test app
run: npm run e2e:build
run: npm run e2e:build:ios
- name: Run e2e test
run: npm run e2e:test -- --cleanup --debug-synchronization 200 --take-screenshots failing --record-videos failing -l trace
run: npm run e2e:test:ios -- --cleanup --debug-synchronization 200 --take-screenshots failing --record-videos failing -l trace
# The artifacts for the failing tests are available for download on github.com on the page of the individual actions run
- name: Store Detox artifacts on test failure

View File

@@ -127,7 +127,7 @@ We're using Nativewind, a styling system for React Native based on Tailwind CSS.
1. Download custom icon from Figma as an SVG file.
2. Add new icon to the iNaturalist icon set in Fontastic. Select all relevant iNaturalist icons, tap the Publish tab, and download the zip of icons.
3. Create a glpyh file from the CSS file you just downloaded, using the following command (be sure to replace /path/to/styles with your path):
3. Create a glyph file from the CSS file you just downloaded, using the following command (be sure to replace /path/to/styles with your path):
```
./node_modules/.bin/generate-icon '/path/to/styles.css' --componentName=INatIcon --fontFamily=inaturalisticons > 'src/components/INatIcon.js'

View File

Binary file not shown.

View File

@@ -1,14 +1,6 @@
{
"migIndex": 1,
"data": [
{
"path": "assets/fonts/Whitney-Book-Pro.otf",
"sha1": "639b8a8bf3e1cc3de30d0f49e666aca3999ca65a"
},
{
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
"sha1": "15854f60175a0e82b794c259431ec45ea4b40103"
},
{
"path": "assets/fonts/Whitney-Light-Pro.otf",
"sha1": "d15560faea2b18aef9867a1d2b9e2efb54b17b5b"
@@ -17,14 +9,6 @@
"path": "assets/fonts/Whitney-Medium-Pro.otf",
"sha1": "33ca073c11f46dc266a7dc1adeaa102891bd76d1"
},
{
"path": "assets/fonts/Whitney-Semibold-Pro.otf",
"sha1": "7a107095a453c8cd8046d9ccded5d88e6809e89f"
},
{
"path": "assets/fonts/Whitney-SemiboldItalic-Pro.otf",
"sha1": "53e8a56ceb630b63316db41017125c52fbf0e845"
},
{
"path": "assets/fonts/inaturalisticons.ttf",
"sha1": "76435d11edb09f7914ebf9cb388db461c50d7fda"

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -34,15 +34,15 @@ describe( "Add observation without evidence", () => {
} );
it( "should navigate to observation add screen on add evidence button pressed", async () => {
await waitFor( element( by.id( "camera-options-button" ) ) )
await waitFor( element( by.id( "add-obs-button" ) ) )
.toBeVisible()
.withTimeout( 10000 );
await element( by.id( "camera-options-button" ) ).tap();
await element( by.id( "add-obs-button" ) ).tap();
await expect( element( by.id( "evidence-text" ) ) ).toBeVisible();
await expect(
element( by.id( "camera-options-button-square-edit-outline" ) )
element( by.id( "observe-without-evidence-button" ) )
).toBeVisible();
await element( by.id( "camera-options-button-square-edit-outline" ) ).tap();
await element( by.id( "observe-without-evidence-button" ) ).tap();
await waitFor( element( by.id( "new-observation-text" ) ) ).toBeVisible().withTimeout( 10000 );
} );
} );

View File

@@ -7,19 +7,15 @@
objects = {
/* Begin PBXBuildFile section */
0085E50460D749DB8D818C1F /* inaturalisticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */; };
00E356F31AD99517003FC87E /* iNaturalistReactNativeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* iNaturalistReactNativeTests.m */; };
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
7EBBE0515B6D88FD724A5C47 /* libPods-iNaturalistReactNative.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DD42FAECED8C66268DDB37F /* libPods-iNaturalistReactNative.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
829672AB5DD3412FB120DE40 /* Whitney-BookItalic-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = B5359D98B3384738A8ED6C74 /* Whitney-BookItalic-Pro.otf */; };
A252B2AEA64E47C9AC1D20E8 /* Whitney-Light-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */; };
B5241D089ADB4290B5566898 /* Whitney-Semibold-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = C6C5CABEB9CB44B5ABF940A8 /* Whitney-Semibold-Pro.otf */; };
BA2479FA3D7B40A7BEF7B3CD /* Whitney-Medium-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */; };
C8EEAD27B69C424BAB2EB23F /* Whitney-SemiboldItalic-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = A788948E590442F385C5FD6C /* Whitney-SemiboldItalic-Pro.otf */; };
CF25D941972D42BDBBD309E0 /* Whitney-Book-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8860EC2616494880B36297C2 /* Whitney-Book-Pro.otf */; };
0085E50460D749DB8D818C1F /* inaturalisticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -45,16 +41,12 @@
19A5877328F8E3310016D128 /* iNaturalistReactNative-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iNaturalistReactNative-Bridging-Header.h"; sourceTree = "<group>"; };
27F9BBBBAB49ABBAF88433C9 /* Pods-iNaturalistReactNative.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.debug.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.debug.xcconfig"; sourceTree = "<group>"; };
7DD42FAECED8C66268DDB37F /* libPods-iNaturalistReactNative.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = inaturalisticons.ttf; path = ../assets/fonts/inaturalisticons.ttf; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = iNaturalistReactNative/LaunchScreen.storyboard; sourceTree = "<group>"; };
8860EC2616494880B36297C2 /* Whitney-Book-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Book-Pro.otf"; path = "../assets/fonts/Whitney-Book-Pro.otf"; sourceTree = "<group>"; };
A788948E590442F385C5FD6C /* Whitney-SemiboldItalic-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-SemiboldItalic-Pro.otf"; path = "../assets/fonts/Whitney-SemiboldItalic-Pro.otf"; sourceTree = "<group>"; };
B5359D98B3384738A8ED6C74 /* Whitney-BookItalic-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-BookItalic-Pro.otf"; path = "../assets/fonts/Whitney-BookItalic-Pro.otf"; sourceTree = "<group>"; };
BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Light-Pro.otf"; path = "../assets/fonts/Whitney-Light-Pro.otf"; sourceTree = "<group>"; };
C544256CF572EB52B9E2B9CB /* Pods-iNaturalistReactNative.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.release.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.release.xcconfig"; sourceTree = "<group>"; };
C6C5CABEB9CB44B5ABF940A8 /* Whitney-Semibold-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Semibold-Pro.otf"; path = "../assets/fonts/Whitney-Semibold-Pro.otf"; sourceTree = "<group>"; };
D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Medium-Pro.otf"; path = "../assets/fonts/Whitney-Medium-Pro.otf"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */ = {isa = PBXFileReference; name = "inaturalisticons.ttf"; path = "../assets/fonts/inaturalisticons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -160,12 +152,8 @@
E7A0E0AA690545D78AA7CC73 /* Resources */ = {
isa = PBXGroup;
children = (
8860EC2616494880B36297C2 /* Whitney-Book-Pro.otf */,
B5359D98B3384738A8ED6C74 /* Whitney-BookItalic-Pro.otf */,
BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */,
D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */,
C6C5CABEB9CB44B5ABF940A8 /* Whitney-Semibold-Pro.otf */,
A788948E590442F385C5FD6C /* Whitney-SemiboldItalic-Pro.otf */,
81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */,
);
name = Resources;
@@ -264,12 +252,8 @@
files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
CF25D941972D42BDBBD309E0 /* Whitney-Book-Pro.otf in Resources */,
829672AB5DD3412FB120DE40 /* Whitney-BookItalic-Pro.otf in Resources */,
A252B2AEA64E47C9AC1D20E8 /* Whitney-Light-Pro.otf in Resources */,
BA2479FA3D7B40A7BEF7B3CD /* Whitney-Medium-Pro.otf in Resources */,
B5241D089ADB4290B5566898 /* Whitney-Semibold-Pro.otf in Resources */,
C8EEAD27B69C424BAB2EB23F /* Whitney-SemiboldItalic-Pro.otf in Resources */,
0085E50460D749DB8D818C1F /* inaturalisticons.ttf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -55,12 +55,8 @@
<array>
<string>MaterialCommunityIcons.ttf</string>
<string>MaterialIcons.ttf</string>
<string>Whitney-Book-Pro.otf</string>
<string>Whitney-BookItalic-Pro.otf</string>
<string>Whitney-Light-Pro.otf</string>
<string>Whitney-Medium-Pro.otf</string>
<string>Whitney-Semibold-Pro.otf</string>
<string>Whitney-SemiboldItalic-Pro.otf</string>
<string>inaturalisticons.ttf</string>
</array>
<key>UILaunchStoryboardName</key>

View File

@@ -1,14 +1,6 @@
{
"migIndex": 1,
"data": [
{
"path": "assets/fonts/Whitney-Book-Pro.otf",
"sha1": "639b8a8bf3e1cc3de30d0f49e666aca3999ca65a"
},
{
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
"sha1": "15854f60175a0e82b794c259431ec45ea4b40103"
},
{
"path": "assets/fonts/Whitney-Light-Pro.otf",
"sha1": "d15560faea2b18aef9867a1d2b9e2efb54b17b5b"
@@ -17,14 +9,6 @@
"path": "assets/fonts/Whitney-Medium-Pro.otf",
"sha1": "33ca073c11f46dc266a7dc1adeaa102891bd76d1"
},
{
"path": "assets/fonts/Whitney-Semibold-Pro.otf",
"sha1": "7a107095a453c8cd8046d9ccded5d88e6809e89f"
},
{
"path": "assets/fonts/Whitney-SemiboldItalic-Pro.otf",
"sha1": "53e8a56ceb630b63316db41017125c52fbf0e845"
},
{
"path": "assets/fonts/inaturalisticons.ttf",
"sha1": "76435d11edb09f7914ebf9cb388db461c50d7fda"

55
package-lock.json generated
View File

@@ -113,7 +113,9 @@
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-native": "^4.0.0",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-testing-library": "^5.10.0",
"factoria": "^3.2.2",
"faker": "^5.5.3",
"flow-bin": "^0.182.0",
@@ -9709,6 +9711,23 @@
"eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8"
}
},
"node_modules/eslint-plugin-react-native-a11y": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native-a11y/-/eslint-plugin-react-native-a11y-3.3.0.tgz",
"integrity": "sha512-21bIs/0yROcMq7KtAG+OVNDWAh8M+6scII0iXcO3i9NYHe2xZ443yPs5KSUMSvQJeRLLjuKB7V5saqNjoMWDHA==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"ast-types-flow": "^0.0.7",
"jsx-ast-utils": "^3.2.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
}
},
"node_modules/eslint-plugin-react-native-globals": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz",
@@ -9753,6 +9772,22 @@
"eslint": ">=5.0.0"
}
},
"node_modules/eslint-plugin-testing-library": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.0.tgz",
"integrity": "sha512-aTOsCAEI9trrX3TLOnsskfhe57DmsjP/yMKLPqg4ftdRvfR4qut2PGWUa8TwP7whZbwMzJjh98tgAPcE8vdHow==",
"dev": true,
"dependencies": {
"@typescript-eslint/utils": "^5.43.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0",
"npm": ">=6"
},
"peerDependencies": {
"eslint": "^7.5.0 || ^8.0.0"
}
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -30549,6 +30584,17 @@
"eslint-plugin-react-native-globals": "^0.1.1"
}
},
"eslint-plugin-react-native-a11y": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native-a11y/-/eslint-plugin-react-native-a11y-3.3.0.tgz",
"integrity": "sha512-21bIs/0yROcMq7KtAG+OVNDWAh8M+6scII0iXcO3i9NYHe2xZ443yPs5KSUMSvQJeRLLjuKB7V5saqNjoMWDHA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.15.4",
"ast-types-flow": "^0.0.7",
"jsx-ast-utils": "^3.2.1"
}
},
"eslint-plugin-react-native-globals": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz",
@@ -30562,6 +30608,15 @@
"dev": true,
"requires": {}
},
"eslint-plugin-testing-library": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.0.tgz",
"integrity": "sha512-aTOsCAEI9trrX3TLOnsskfhe57DmsjP/yMKLPqg4ftdRvfR4qut2PGWUa8TwP7whZbwMzJjh98tgAPcE8vdHow==",
"dev": true,
"requires": {
"@typescript-eslint/utils": "^5.43.0"
}
},
"eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",

View File

@@ -9,15 +9,17 @@
"clean-start": "npx react-native clean-project-auto && npx pod-install && npm start",
"test": "jest",
"lint": "npm run lint:eslint && npm run lint:flow",
"lint:eslint": "eslint . --fix",
"lint:eslint": "eslint . --fix --quiet",
"lint:flow": "flow check",
"postinstall": "husky install",
"translate": "node src/i18n/i18ncli.js build",
"e2e:build:android": "npx detox build --configuration android.release",
"e2e:build": "npx detox build --configuration ios.release",
"e2e:build:ios": "npx detox build --configuration ios.release",
"e2e:build": "npm run e2e:build:ios && npm run e2e:build:android",
"e2e:test:android": "npx detox test --configuration android.release",
"e2e:test": "npx detox test --configuration ios.release",
"e2e": "npm run e2e:build && npm run e2e:test"
"e2e:test:ios": "npx detox test --configuration ios.release",
"e2e:test": "npm run e2e:test:ios && npm run e2e:test:android",
"e2e": "npm run e2e:build && npm run e2e:test "
},
"dependencies": {
"@babel/eslint-parser": "^7.18.2",
@@ -124,7 +126,9 @@
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-native": "^4.0.0",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-testing-library": "^5.10.0",
"factoria": "^3.2.2",
"faker": "^5.5.3",
"flow-bin": "^0.182.0",

View File

@@ -46,10 +46,10 @@ const AddObsModal = ( { closeModal }: Props ): React.Node => {
t( "Record-a-sound" )
];
const renderIconButton = ( icon, className, onPress, accessibilityLabel, size = 30 ) => (
const renderIconButton = ( icon, className, onPress, accessibilityLabel, testID ) => (
<IconButton
testID={`camera-options-button-${icon}`}
size={size}
testID={testID}
size={30}
icon={icon}
containerColor={theme.colors.secondary}
iconColor={theme.colors.onSecondary}
@@ -74,12 +74,19 @@ const AddObsModal = ( { closeModal }: Props ): React.Node => {
</View>
</View>
<View className="flex-row items-center justify-center">
{renderIconButton( "camera", "mx-5", navToStandardCamera, t( "Navigate-to-camera" ) )}
{renderIconButton(
"camera",
"mx-5",
navToStandardCamera,
t( "Navigate-to-camera" ),
"camera-button"
)}
{renderIconButton(
"icon-gallery",
"mx-5",
navToPhotoGallery,
t( "Navigate-to-photo-importer" )
t( "Navigate-to-photo-importer" ),
"import-media-button"
)}
</View>
<View className="flex-row justify-center">
@@ -87,19 +94,22 @@ const AddObsModal = ( { closeModal }: Props ): React.Node => {
"pen-and-paper",
"mx-2",
navToObsEdit,
t( "Navigate-to-observation-edit-screen" )
t( "Navigate-to-observation-edit-screen" ),
"observe-without-evidence-button"
)}
{renderIconButton(
"close-button-x",
"self-center h-24 w-24 rounded-[99px]",
( ) => closeModal( ),
t( "Close-camera-options-modal" )
t( "Close-camera-options-modal" ),
"close-camera-options-button"
)}
{renderIconButton(
"microphone",
"mx-2",
navToSoundRecorder,
t( "Navigate-to-sound-recorder" )
t( "Navigate-to-sound-recorder" ),
"record-sound-button"
)}
</View>
</View>

View File

@@ -17,15 +17,15 @@ const LoggedOutCard = ( ): Node => {
onPress={( ) => navigation.navigate( "login" )}
accessibilityRole="link"
accessibilityLabel={t( "Navigate-to-login-screen" )}
className="rounded-bl-3xl rounded-br-3xl bg-primary h-24 justify-center"
className="rounded-bl-3xl rounded-br-3xl h-24 justify-center"
>
<Text
testID="log-in-to-iNaturalist-text"
className="self-center color-white text-2xl"
className="self-center text-2xl"
>
{t( "Log-in-to-iNaturalist" )}
</Text>
<Text className="self-center color-white text-base">
<Text className="self-center text-base">
{t( "X-unuploaded-observations", { observationCount: numUnuploadedObs } )}
</Text>
</Pressable>

View File

@@ -4,13 +4,8 @@ import BottomSheet from "components/SharedComponents/BottomSheet";
import type { Node } from "react";
import React from "react";
import useCurrentUser from "sharedHooks/useCurrentUser";
import useLocalObservations from "sharedHooks/useLocalObservations";
import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations";
import useUploadObservations from "sharedHooks/useUploadObservations";
import LoginPrompt from "./LoginPrompt";
import UploadProgressBar from "./UploadProgressBar";
import UploadPrompt from "./UploadPrompt";
type Props = {
hasScrolled: boolean
@@ -18,14 +13,6 @@ type Props = {
const ObsListBottomSheet = ( { hasScrolled }: Props ): Node => {
const currentUser = useCurrentUser( );
const { allObsToUpload } = useLocalObservations( );
const numUnuploadedObs = useNumUnuploadedObservations( );
const {
stopUpload,
uploadInProgress,
startUpload
} = useUploadObservations( allObsToUpload );
if ( !currentUser ) {
return (
@@ -35,21 +22,6 @@ const ObsListBottomSheet = ( { hasScrolled }: Props ): Node => {
);
}
if ( uploadInProgress ) {
return (
<UploadProgressBar
stopUpload={stopUpload}
allObsToUpload={allObsToUpload}
/>
);
}
if ( numUnuploadedObs > 0 && currentUser ) {
return (
<BottomSheet hide={hasScrolled}>
<UploadPrompt startUpload={startUpload} />
</BottomSheet>
);
}
return null;
};

View File

@@ -1,25 +1,22 @@
// @flow
import { View } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
import useCurrentUser from "sharedHooks/useCurrentUser";
import LoggedOutCard from "./LoggedOutCard";
import Toolbar from "./Toolbar";
import ObsListToolbar from "./ObsListToolbar";
import UserCard from "./UserCard";
type Props = {
setView: Function
setLayout: Function;
layout: string
}
const ObsListHeader = ( {
setView
setLayout,
layout
}: Props ): Node => {
const currentUser = useCurrentUser( );
if ( currentUser === null ) {
return <View className="rounded-bl-3xl rounded-br-3xl bg-primary h-24" />;
}
return (
// $FlowIgnore
@@ -27,7 +24,7 @@ const ObsListHeader = ( {
{currentUser
? <UserCard />
: <LoggedOutCard />}
<Toolbar setView={setView} />
<ObsListToolbar setLayout={setLayout} layout={layout} />
</>
);
};

View File

@@ -0,0 +1,159 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import { Pressable, Text, View } from "components/styledComponents";
import { t } from "i18next";
import { ObsEditContext } from "providers/contexts";
import type { Node } from "react";
import React, { useContext } from "react";
import { Animated, Easing } from "react-native";
import { ProgressBar } from "react-native-paper";
import IconMaterial from "react-native-vector-icons/MaterialIcons";
import useCurrentUser from "sharedHooks/useCurrentUser";
import useLocalObservations from "sharedHooks/useLocalObservations";
import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations";
import useUploadObservations from "sharedHooks/useUploadObservations";
import colors from "styles/tailwindColors";
type Props = {
setLayout: Function,
layout: string,
};
const Toolbar = ( { setLayout, layout }: Props ): Node => {
const currentUser = useCurrentUser( );
const obsEditContext = useContext( ObsEditContext );
const { allObsToUpload } = useLocalObservations( );
const numUnuploadedObs = useNumUnuploadedObservations( );
const navigation = useNavigation( );
const {
stopUpload,
uploadInProgress,
startUpload,
progress,
error: uploadError
} = useUploadObservations( allObsToUpload );
const spinValue = new Animated.Value( 1 );
Animated.timing( spinValue, {
toValue: 0,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
} ).start( );
const spin = spinValue.interpolate( {
inputRange: [0, 1],
outputRange: ["0deg", "360deg"]
} );
const loading = obsEditContext?.loading;
const syncObservations = obsEditContext?.syncObservations;
const getSyncClick = ( ) => {
if ( numUnuploadedObs > 0 ) {
return startUpload;
}
return syncObservations;
};
const getStatusText = ( ) => {
if ( !uploadInProgress && numUnuploadedObs > 0 ) {
return t( "Upload-x-observations", { count: numUnuploadedObs } );
}
if ( numUnuploadedObs > 0 ) {
return t( "Uploading-X-Observations", { count: numUnuploadedObs } );
}
return null;
};
const getSyncIconColor = ( ) => {
if ( uploadInProgress || numUnuploadedObs > 0 ) {
return colors.inatGreen;
}
return colors.darkGray;
};
const statusText = getStatusText( );
/* eslint-disable react-native/no-inline-styles */
return (
<View className="bg-white border-b border-[#e8e8e8]">
<View className="py-5 flex flex-row items-center px-[15px]">
{currentUser && (
<Pressable
className="mr-3"
accessibilityRole="button"
onPress={( ) => navigation.navigate( "MainStack", { screen: "Explore" } )}
>
<IconMaterial name="language" size={30} />
</Pressable>
)}
<Pressable
onPress={getSyncClick( )}
accessibilityRole="button"
disabled={loading || uploadInProgress}
accessibilityState={{ disabled: loading || uploadInProgress }}
>
<Animated.View
style={uploadInProgress ? { transform: [{ rotate: spin }] } : {}}
>
<IconMaterial name="sync" size={26} color={getSyncIconColor( )} />
</Animated.View>
</Pressable>
{statusText && (
<View>
<Text className="ml-1">{statusText}</Text>
{uploadError && (
<Text
className="ml-1 mt-[3px]"
style={{ color: colors.warningRed }}
>
{uploadError}
</Text>
)}
</View>
)}
<View className="ml-auto flex flex-row items-center">
{uploadInProgress && (
<Pressable onPress={stopUpload} accessibilityRole="button">
<IconMaterial name="close" size={20} color={colors.darkGray} />
</Pressable>
)}
<Pressable
className="ml-2"
testID={
layout === "list"
? "ObsList.toggleGridView"
: "ObsList.toggleListView"
}
onPress={( ) => setLayout( currentView => {
if ( currentView === "list" ) {
return "grid";
}
return "list";
} )}
accessibilityRole="button"
>
<IconMaterial
name={layout === "grid" ? "format-list-bulleted" : "grid-view"}
size={30}
/>
</Pressable>
</View>
</View>
<ProgressBar
progress={progress}
color={colors.primary}
style={{ backgroundColor: "transparent" }}
visible={uploadInProgress && progress !== 0}
/>
</View>
);
};
export default Toolbar;

View File

@@ -26,7 +26,7 @@ const HEADER_HEIGHT = 101;
const ObservationViews = ( ): Node => {
const localObservations = useLocalObservations( );
const [view, setView] = useState( "list" );
const [layout, setLayout] = useState( "list" );
const navigation = useNavigation( );
const currentUser = useCurrentUser( );
const { observationList } = localObservations;
@@ -90,7 +90,7 @@ const ObservationViews = ( ): Node => {
if ( currentUser === false ) { return <View />; }
return (
<InfiniteScrollFooter
view={view}
view={layout}
isLoading={isLoading}
/>
);
@@ -118,15 +118,15 @@ const ObservationViews = ( ): Node => {
<Animated.View style={[{ transform: [{ translateY }] }]}>
<Animated.FlatList
data={observationList}
key={view === "grid" ? 1 : 0}
key={layout === "grid" ? 1 : 0}
style={{ height }}
testID="ObservationViews.myObservations"
numColumns={view === "grid" ? 2 : 1}
renderItem={view === "grid" ? renderGridItem : renderItem}
numColumns={layout === "grid" ? 2 : 1}
renderItem={layout === "grid" ? renderGridItem : renderItem}
ListEmptyComponent={renderEmptyState}
ListHeaderComponent={<ObsListHeader setView={setView} />}
ListHeaderComponent={<ObsListHeader setLayout={setLayout} layout={layout} />}
ListFooterComponent={renderFooter}
ItemSeparatorComponent={view !== "grid" && renderItemSeparator}
ItemSeparatorComponent={layout !== "grid" && renderItemSeparator}
stickyHeaderIndices={[0]}
bounces={false}
initialNumToRender={10}

View File

@@ -1,55 +0,0 @@
// @flow
import { Pressable, View } from "components/styledComponents";
import { ObsEditContext } from "providers/contexts";
import type { Node } from "react";
import React, { useContext } from "react";
import { ActivityIndicator } from "react-native";
import IconMaterial from "react-native-vector-icons/MaterialIcons";
import useCurrentUser from "sharedHooks/useCurrentUser";
type Props = {
setView: Function
}
const Toolbar = ( { setView }: Props ): Node => {
const currentUser = useCurrentUser( );
const obsEditContext = useContext( ObsEditContext );
const loading = obsEditContext?.loading;
const syncObservations = obsEditContext?.syncObservations;
return (
<View className="py-5 flex-row justify-between bg-white">
{currentUser ? (
<Pressable
onPress={syncObservations}
className="mx-3"
accessibilityRole="button"
disabled={loading}
>
<IconMaterial name="sync" size={30} />
</Pressable>
) : (
<View className="mx-3" />
)}
{loading && <ActivityIndicator />}
<View className="flex-row mx-3">
<Pressable
onPress={( ) => setView( "list" )}
accessibilityRole="button"
>
<IconMaterial name="format-list-bulleted" size={30} />
</Pressable>
<Pressable
onPress={( ) => setView( "grid" )}
testID="ObsList.toggleGridView"
accessibilityRole="button"
>
<IconMaterial name="grid-view" size={30} />
</Pressable>
</View>
</View>
);
};
export default Toolbar;

View File

@@ -1,64 +0,0 @@
// @flow
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
import { t } from "i18next";
import type { Node } from "react";
import React, { useMemo, useRef } from "react";
import {
Button, ProgressBar, Text
} from "react-native-paper";
import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations";
import { textStyles, viewStyles } from "styles/observations/uploadProgressBar";
import colors from "styles/tailwindColors";
type Props = {
allObsToUpload: Array<Object>,
stopUpload: () => void
}
const UploadProgressBar = ( {
allObsToUpload,
stopUpload
}: Props ): Node => {
const numUnuploadedObs = useNumUnuploadedObservations( );
const totalObsToUpload = Math.max( allObsToUpload.length, numUnuploadedObs );
const calculateProgress = ( ) => ( totalObsToUpload - numUnuploadedObs ) / totalObsToUpload;
const progressFraction = calculateProgress( );
const sheetRef = useRef( null );
const snapPoints = useMemo( () => ["25%"], [] );
// eslint-disable-next-line react/jsx-no-useless-fragment
const noHandle = ( ) => <></>;
return (
<BottomSheet
ref={sheetRef}
snapPoints={snapPoints}
handleComponent={noHandle}
backgroundStyle={viewStyles.bottomSheet}
>
<BottomSheetView style={viewStyles.grayContainer}>
<Button
icon="close-circle"
onPress={stopUpload}
textColor={colors.white}
style={viewStyles.closeButton}
/>
<Text style={textStyles.whiteText} variant="titleMedium">
{t( "Uploading-X-Observations", { count: numUnuploadedObs } )}
</Text>
<ProgressBar
progress={progressFraction}
style={viewStyles.progressBar}
color={colors.white}
/>
</BottomSheetView>
</BottomSheet>
);
};
export default UploadProgressBar;

View File

@@ -1,29 +0,0 @@
// @flow
import Button from "components/SharedComponents/Buttons/Button";
import { t } from "i18next";
import type { Node } from "react";
import React from "react";
import { Text, View } from "react-native";
import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations";
type Props = {
startUpload: Function
}
const UploadPrompt = ( { startUpload }: Props ): Node => {
const numUnuploadedObs = useNumUnuploadedObservations( );
return (
<View testID="UploadPrompt">
<Text>{t( "Whenever-you-get-internet-connection-you-can-upload" )}</Text>
<Button
level="neutral"
text={t( "UPLOAD-X-OBSERVATIONS", { count: numUnuploadedObs } )}
className="py-1 mt-5"
onPress={startUpload}
/>
</View>
);
};
export default UploadPrompt;

View File

@@ -1,41 +1,22 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import UserIcon from "components/SharedComponents/UserIcon";
import { Pressable, Text, View } from "components/styledComponents";
import { Text, View } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import IconMaterial from "react-native-vector-icons/MaterialIcons";
import User from "realmModels/User";
import useCurrentUser from "sharedHooks/useCurrentUser";
import colors from "styles/tailwindColors";
const UserCard = ( ): Node => {
const navigation = useNavigation( );
const currentUser = useCurrentUser( );
const { t } = useTranslation( );
if ( !currentUser ) { return <View className="flex-row mx-5 items-center" />; }
const navToUserProfile = ( ) => navigation.navigate( "UserProfile", { userId: currentUser.id } );
const uri = User.uri( currentUser );
return (
<View className="flex-row px-5 items-center rounded-bl-3xl rounded-br-3xl bg-focusGreen h-24">
{uri && <UserIcon uri={uri} />}
<View className="flex-row px-5 items-center rounded-bl-3xl rounded-br-3xl bg-white h-24">
<View className="ml-3">
<Text className="color-white my-1">{User.userHandle( currentUser )}</Text>
<Text className="color-white my-1">
{ t( "X-Observations", { count: currentUser.observations_count || 0 } )}
</Text>
<Text className="mt-1 text-3xl font-light">{t( "Welcome-back" )}</Text>
<Text className="mb-1 text-3xl font-semibold">{User.userHandle( currentUser )}</Text>
</View>
<Pressable
onPress={navToUserProfile}
className="absolute right-5"
accessibilityRole="button"
>
<IconMaterial name="edit" size={30} color={colors.white} />
</Pressable>
</View>
);
};

View File

@@ -30,6 +30,7 @@ const AddObsButton = ( ): React.Node => {
accessibilityLabel={t( "Open-add-evidence-modal" )}
className="m-0"
disabled={false}
testID="add-obs-button"
/>
</>
);

View File

@@ -1,6 +1,7 @@
// @flow
import { Pressable, Text } from "components/styledComponents";
import Heading4 from "components/SharedComponents/Typography/Heading4";
import { Pressable } from "components/styledComponents";
import * as React from "react";
import { ActivityIndicator } from "react-native-paper";
@@ -22,7 +23,7 @@ const setStyles = ( {
className
} ) => {
let buttonClass = "rounded flex-row justify-center items-center py-1.5 px-8";
let textClass = "text-lg text-white font-semibold";
let textClass = "text-white tracking-widest";
if ( className ) {
buttonClass = buttonClass.concat( " ", className );
@@ -70,7 +71,9 @@ const Button = ( {
accessibilityState={{ disabled }}
>
{loading && <ActivityIndicator size={18} className="mr-2" />}
<Text className={textClass}>{text}</Text>
<Heading4 className={textClass}>
{text}
</Heading4>
</Pressable>
);
};

View File

@@ -53,15 +53,13 @@ const InlineUser = ( { user }: Props ): Node => {
testID="InlineUser"
className="flex flex-row items-center"
accessibilityRole="link"
accessibilityLabel={t( "Navigate-to-user-profile" )}
accessibilityValue={{ text: userHandle }}
accessibilityLabel={t( "User", { userHandle } )}
accessibilityHint={t( "Navigates-to-user-profile" )}
onPress={() => {
navigation.navigate( "UserProfile", { userId: user.id } );
}}
>
<View className="mr-[7px]">
{renderUserIcon()}
</View>
<View className="mr-[7px]">{renderUserIcon()}</View>
<Text>{userHandle}</Text>
</Pressable>
);

View File

@@ -1,6 +1,7 @@
// @flow
import { Text, View } from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React from "react";
import { TouchableOpacity } from "react-native";
@@ -39,8 +40,9 @@ const Tabs = ( { tabs = DEFAULT_TABS, activeId }: Props ): Node => (
}
}}
testID={testID || `${id}-tab`}
accessibilityLabel={text}
accessibilityRole="tab"
accessibilityLabel={text}
accessibilityHint={t( "Switch-to-tab", { tab: text } )}
accessibilityState={{
selected: active,
expanded: active

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
// eslint-disable-next-line react/jsx-props-no-spreading
const Body1 = ( props: any ): Node => <INatTextLight className="text-base" {...props} />;
export default Body1;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
// eslint-disable-next-line react/jsx-props-no-spreading
const Body2 = ( props: any ): Node => <INatTextLight {...props} />;
export default Body2;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
// eslint-disable-next-line react/jsx-props-no-spreading
const Body3 = ( props: any ): Node => <INatTextLight className="text-sm" {...props} />;
export default Body3;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
// eslint-disable-next-line react/jsx-props-no-spreading
const Body4 = ( props: any ): Node => <INatTextLight className="text-xs" {...props} />;
export default Body4;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatText from "./INatText";
// eslint-disable-next-line react/jsx-props-no-spreading
const Heading1 = ( props: any ): Node => <INatText className="text-3xl" {...props} />;
export default Heading1;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatText from "./INatText";
// eslint-disable-next-line react/jsx-props-no-spreading
const Heading2 = ( props: any ): Node => <INatText className="text-2xl" {...props} />;
export default Heading2;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatText from "./INatText";
// eslint-disable-next-line react/jsx-props-no-spreading
const Heading3 = ( props: any ): Node => <INatText className="text-lg" {...props} />;
export default Heading3;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatText from "./INatText";
// eslint-disable-next-line react/jsx-props-no-spreading
const Heading4 = ( props: any ): Node => <INatText {...props} />;
export default Heading4;

View File

@@ -0,0 +1,22 @@
// @flow
import { Text } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
type Props = {
children: any,
testID?: string,
style?: any
}
const INatText = ( { children, testID, style }: Props ): Node => (
<Text
style={style}
testID={testID}
>
{children}
</Text>
);
export default INatText;

View File

@@ -0,0 +1,22 @@
// @flow
import { LightText } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
type Props = {
children: any,
testID?: string,
style?: any
}
const INatTextLight = ( { children, testID, style }: Props ): Node => (
<LightText
style={style}
testID={testID}
>
{children}
</LightText>
);
export default INatTextLight;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
// eslint-disable-next-line react/jsx-props-no-spreading
const List1 = ( props: any ): Node => <INatTextLight className="text-base" {...props} />;
export default List1;

View File

@@ -0,0 +1,13 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
const List2 = (
props: any
// eslint-disable-next-line react/jsx-props-no-spreading
): Node => <INatTextLight className="text-sm leading-[17px]" {...props} />;
export default List2;

View File

@@ -0,0 +1,11 @@
// @flow
import type { Node } from "react";
import React from "react";
import INatTextLight from "./INatTextLight";
// eslint-disable-next-line react/jsx-props-no-spreading
const Subheading1 = ( props: any ): Node => <INatTextLight className="text-xl" {...props} />;
export default Subheading1;

View File

@@ -17,6 +17,7 @@ const UserIcon = ( { uri, small }: Props ): React.Node => {
testID="UserIcon.photo"
className={className}
source={uri}
accessibilityIgnoresInvertColors
/>
);
};

View File

@@ -1,7 +1,24 @@
import INatIcon, { glyphMap } from "components/INatIcon";
import AddObsButton from "components/SharedComponents/Buttons/AddObsButton";
import Button from "components/SharedComponents/Buttons/Button";
import EvidenceButton from "components/SharedComponents/Buttons/EvidenceButton";
import SecondaryCTAButton from "components/SharedComponents/Buttons/SecondaryCTAButton";
import Body1 from "components/SharedComponents/Typography/Body1";
import Body2 from "components/SharedComponents/Typography/Body2";
import Body3 from "components/SharedComponents/Typography/Body3";
import Body4 from "components/SharedComponents/Typography/Body4";
import Heading1 from "components/SharedComponents/Typography/Heading1";
import Heading2 from "components/SharedComponents/Typography/Heading2";
import Heading3 from "components/SharedComponents/Typography/Heading3";
import Heading4 from "components/SharedComponents/Typography/Heading4";
import List1 from "components/SharedComponents/Typography/List1";
import List2 from "components/SharedComponents/Typography/List2";
import Subheading1 from "components/SharedComponents/Typography/Subheading1";
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
import InlineUser from "components/SharedComponents/InlineUser";
import QualityGradeStatus from "components/SharedComponents/QualityGradeStatus";
import {
ScrollView,
Text,
View
} from "components/styledComponents";
import React from "react";
@@ -9,13 +26,7 @@ import { Alert } from "react-native";
import { IconButton, useTheme } from "react-native-paper";
import useCurrentUser from "sharedHooks/useCurrentUser";
import AddObsButton from "./SharedComponents/Buttons/AddObsButton";
import Button from "./SharedComponents/Buttons/Button";
import EvidenceButton from "./SharedComponents/Buttons/EvidenceButton";
import SecondaryCTAButton from "./SharedComponents/Buttons/SecondaryCTAButton";
import InlineUser from "./SharedComponents/InlineUser";
import QualityGradeStatus from "./SharedComponents/QualityGradeStatus";
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
/* eslint-disable i18next/no-literal-string */
/* eslint-disable react/no-unescaped-entities */
@@ -26,15 +37,13 @@ const UiLibrary = ( ) => {
<ViewWithFooter>
<ScrollView className="px-5">
{/* TODO replace these text components with our typography header components */}
<Text>
<Body1>
All the re-usable UI components we've got. If you're making a new UI
component, please put it here first and try to show what it looks
like with different property configurations.
</Text>
<Text className="text-xl">Buttons</Text>
<Text className="text-lg">Button</Text>
</Body1>
<Heading1>Buttons</Heading1>
<Heading2>Button</Heading2>
<Button className="mb-2" level="primary" text="PRIMARY BUTTON" />
<Button className="mb-2" text="NEUTRAL BUTTON" />
<Button
@@ -52,7 +61,6 @@ const UiLibrary = ( ) => {
<Button className="mb-2" level="focus" text="FOCUS DISABLED" disabled />
<Button className="mb-2" level="warning" text="WARNING DISABLED" disabled />
<Button className="mb-2" loading text="LOADING BUTTON" />
<Button
className="mb-2"
text="Tap to show alert"
@@ -61,52 +69,67 @@ const UiLibrary = ( ) => {
"Or did you click it? Fight me."
)}
/>
<Text className="text-xl">Multiple Buttons With Focus</Text>
<Heading2>Multiple Buttons With Focus</Heading2>
<View className="flex-row justify-between">
<Button className="my-2" text="LEFT" />
<Button className="my-2 grow ml-3" level="focus" text="RIGHT" />
</View>
<Text className="text-xl">Multiple Buttons Without Focus</Text>
<Heading2>Multiple Buttons Without Focus</Heading2>
<View className="flex-row">
<Button className="my-2 grow" text="LEFT" />
<Button className="my-2 ml-3 grow" text="RIGHT" />
</View>
<Text className="text-lg">AddObsButton</Text>
<Text>
<Heading2>AddObsButton</Heading2>
<Body1 className="my-2">
You probably don't want to tap this because you can't escape the
modal. Probably need to refactor to separate form from function.
</Text>
</Body1>
<AddObsButton />
<Text className="text-lg">EvidenceButton</Text>
<Heading2 className="my-2">EvidenceButton</Heading2>
<View className="flex flex-row justify-between">
<View>
<Text className="text-center">Default</Text>
<Body2>Default</Body2>
<EvidenceButton icon="camera" />
</View>
<View>
<Text className="text-center">Disabled</Text>
<Body2>Disabled</Body2>
<EvidenceButton icon="microphone" disabled />
</View>
<View>
<Text className="text-center">With Icon</Text>
<Body2>With Icon</Body2>
<EvidenceButton icon="microphone" />
</View>
</View>
<Text className="text-lg">SecondaryCTAButton</Text>
<Heading2 className="my-2">SecondaryCTAButton</Heading2>
<SecondaryCTAButton>
<Text>SecondaryCTAButton</Text>
<Body1>SecondaryCTAButton</Body1>
</SecondaryCTAButton>
<SecondaryCTAButton disabled>
<Text>Disabled SecondaryCTAButton</Text>
<Body1>Disabled SecondaryCTAButton</Body1>
</SecondaryCTAButton>
<Text className="text-lg">Icon Button w/ Custom iNaturalist Icons</Text>
<Heading2 className="my-2">Typography</Heading2>
<Heading1 className="my-2">Heading1</Heading1>
<Heading2 className="my-2">Heading2</Heading2>
<Heading3 className="my-2">Heading3</Heading3>
<Heading4 className="my-2">Heading4</Heading4>
<Subheading1 className="my-2">Subheading1</Subheading1>
<Body1 className="my-2">Body1</Body1>
<Body2 className="my-2">Body2</Body2>
<Body3 className="my-2">Body3</Body3>
<Body4 className="my-2">Body4</Body4>
<List1 className="my-2">List1</List1>
<List2 className="my-2">List2</List2>
<Heading2>Icon Button w/ Custom iNaturalist Icons</Heading2>
<View className="flex flex-row justify-between">
<View>
<Text className="text-center">Primary</Text>
<Body2>Primary</Body2>
<IconButton
icon="compass-rose"
className="my-2"
@@ -117,7 +140,7 @@ const UiLibrary = ( ) => {
/>
</View>
<View>
<Text className="text-center">Focused</Text>
<Body2>Focused</Body2>
<IconButton
icon="plus-sign"
className="my-2"
@@ -131,7 +154,7 @@ const UiLibrary = ( ) => {
/>
</View>
<View>
<Text className="text-center">Warning</Text>
<Body2>Warning</Body2>
<IconButton
icon="notifications-bell"
className="my-2"
@@ -143,10 +166,10 @@ const UiLibrary = ( ) => {
/>
</View>
</View>
<Text className="text-lg">Custom iNaturalist Icons</Text>
<Text>
<Heading2>Custom iNaturalist Icons</Heading2>
<Body1>
Make sure you're exporting glyphMap from components/INatIcon.js to see all custom icons
</Text>
</Body1>
<View className="flex flex-row flex-wrap justify-center">
{Object.keys( glyphMap ).map( iconName => (
<INatIcon
@@ -161,8 +184,8 @@ const UiLibrary = ( ) => {
/>
) )}
</View>
<Text className="text-lg">InlineUser</Text>
<Text>InlineUser component</Text>
<Heading2 className="my-2">InlineUser</Heading2>
<Body2 className="my-2">InlineUser component</Body2>
<InlineUser
user={currentUser || {
icon_url:
@@ -170,21 +193,21 @@ const UiLibrary = ( ) => {
login: "turtletamer74"
}}
/>
<Text>InlineUser component for a user that has no icon set</Text>
<Body2 className="my-2">InlineUser component for a user that has no icon set</Body2>
<InlineUser user={{ login: "frogfinder23" }} />
<Text className="text-lg">Quality Grade Status</Text>
<Heading2 className="my-2">Quality Grade Status</Heading2>
<View className="flex flex-row justify-between">
<View>
<Text className="text-center">Research</Text>
<Body2 className="text-center">Research</Body2>
<QualityGradeStatus qualityGrade="research" color="black" />
</View>
<View>
<Text className="text-center">Needs Id</Text>
<Body2 className="text-center">Needs Id</Body2>
<QualityGradeStatus qualityGrade="needs_id" color="black" />
</View>
<View>
<Text className="text-center">Casual</Text>
<Body2 className="text-center">Casual</Body2>
<QualityGradeStatus qualityGrade="casual" color="black" />
</View>
</View>
@@ -200,10 +223,10 @@ const UiLibrary = ( ) => {
</View>
</View>
<Text className="text-lg">More Stuff!</Text>
<Text className="h-[100px]">
<Heading2 className="my-2">More Stuff!</Heading2>
<Body1 className="h-[400px]">
Useless spacer at the end because height in NativeWind is confusing.
</Text>
</Body1>
</ScrollView>
</ViewWithFooter>
);

View File

@@ -39,6 +39,11 @@ const Text = styled(
Platform.OS === "ios" ? "font-Whitney-Medium" : "font-Whitney-Medium-Pro"
);
// $FlowIgnore
const LightText = styled(
StyledText,
Platform.OS === "ios" ? "font-Whitney-Light" : "font-Whitney-Light-Pro"
);
// $FlowIgnore
const TextInput = styled( StyledTextInput );
// $FlowIgnore
const Pressable = styled( StyledPressable );
@@ -58,6 +63,7 @@ export {
HeaderText,
Image,
KeyboardAvoidingView,
LightText,
Modal,
Pressable,
SafeAreaView,

View File

@@ -565,7 +565,7 @@ Unreviewed-only = Unreviewed only
UPLOAD-OBSERVATION = UPLOAD OBSERVATION
# Shows the number of observations a user can upload to iNat from my observations page
UPLOAD-X-OBSERVATIONS = UPLOAD {$count ->
Upload-x-observations = UPLOAD {$count ->
[one] 1 OBSERVATION
*[other] {$count} OBSERVATIONS
}
@@ -585,6 +585,8 @@ Username-or-Email = Username or Email
Visually-search-iNaturalist-data = Visually search iNaturalists wealth of data. Search by a taxon in a location
Welcome-back = Welcome back,
Whenever-you-get-internet-connection-you-can-upload = Whenever you get internet connection, you can upload your observations to iNaturalist.
Which-traditional-projects-can-add-your-observations = Which traditional projects can add your observations?
@@ -687,7 +689,15 @@ No = No
Discard-Comment = Discard Comment
Are-you-sure-discard-comment = Are you sure you want to discard this comment?
## Accessibility labels: these are used by screen readers to describe actionable elements
## Accessibility labels: these are used by screen readers to label actionable elements iOS: https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619577-accessibilitylabel
## iOS Guidelines "A string that succinctly identifies the accessibility element." Starts with capital letter, no ending punctuation.
User = User { $userHandle }
## Accessibility hints: these are used by screen readers to describe what happens when the user interacts with an element iOS: https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619585-accessibilityhint
## iOS Guidelines "A string that briefly describes the result of performing an action on the accessibility element." Third person singular ending with a period.
Navigates-to-user-profile = Navigates to user profile.
## The following are actually more like "accessibility hints" than labels we should probably refactor
Add-this-ID = Add this identification
# Accessible label for the camera button
Camera-button-label-switch-camera = Use the device's other camera.
@@ -712,12 +722,12 @@ Navigate-to-login-screen = Navigate to login screen
Navigate-to-observation-details = Navigate to observation details screen
Navigate-to-project-details = Navigate to project details
Navigate-to-taxon-details = Navigate to taxon details
Navigate-to-user-profile = Navigate to user profile
Number-of-comments = Number of comments
Number-of-identifications = Number of identifications
Observation-has-no-photos-and-no-sounds = This observation has no photos and no sounds.
Take-photo = Take photo
Photo-taken-at = Photo taken at { $date }
Switch-to-tab = Switch to { $tab } tab
Take-photo = Take photo
# Accessibility labels for no internet state in ObsDetails
Location-map-unavailable-without-internet = Location map unavailable without internet
Observation-photos-unavailable-without-internet = Observation photos unavailable without internet

View File

@@ -382,7 +382,7 @@
"Unmute": "Unmute",
"Unreviewed-only": "Unreviewed only",
"UPLOAD-OBSERVATION": "UPLOAD OBSERVATION",
"UPLOAD-X-OBSERVATIONS": {
"Upload-x-observations": {
"comment": "Shows the number of observations a user can upload to iNat from my observations page",
"val": "UPLOAD { $count ->\n [one] 1 OBSERVATION\n *[other] { $count } OBSERVATIONS\n}"
},
@@ -390,13 +390,14 @@
"comment": "Shows the number of observations a user is currently uploading on my observations page",
"val": "Uploading { $count ->\n [one] 1 Observation\n *[other] { $count } Observations\n}"
},
"User": "User",
"User": "User { $userHandle }",
"Username": "Username",
"Username-or-Email": {
"comment": "Appears above the text fields",
"val": "Username or Email"
},
"Visually-search-iNaturalist-data": "Visually search iNaturalists wealth of data. Search by a taxon in a location",
"Welcome-back": "Welcome back,",
"Whenever-you-get-internet-connection-you-can-upload": "Whenever you get internet connection, you can upload your observations to iNaturalist.",
"Which-traditional-projects-can-add-your-observations": "Which traditional projects can add your observations?",
"Who-can-add-observation-fields-to-my-observations": "Who can add observation fields to my observations?",
@@ -490,6 +491,7 @@
},
"Discard-Comment": "Discard Comment",
"Are-you-sure-discard-comment": "Are you sure you want to discard this comment?",
"Navigates-to-user-profile": "Navigates to user profile.",
"Add-this-ID": "Add this identification",
"Camera-button-label-switch-camera": {
"comment": "Accessible label for the camera button",
@@ -524,12 +526,12 @@
"Navigate-to-observation-details": "Navigate to observation details screen",
"Navigate-to-project-details": "Navigate to project details",
"Navigate-to-taxon-details": "Navigate to taxon details",
"Navigate-to-user-profile": "Navigate to user profile",
"Number-of-comments": "Number of comments",
"Number-of-identifications": "Number of identifications",
"Observation-has-no-photos-and-no-sounds": "This observation has no photos and no sounds.",
"Take-photo": "Take photo",
"Photo-taken-at": "Photo taken at { $date }",
"Switch-to-tab": "Switch to { $tab } tab",
"Take-photo": "Take photo",
"Location-map-unavailable-without-internet": {
"comment": "Accessibility labels for no internet state in ObsDetails",
"val": "Location map unavailable without internet"

View File

@@ -565,7 +565,7 @@ Unreviewed-only = Unreviewed only
UPLOAD-OBSERVATION = UPLOAD OBSERVATION
# Shows the number of observations a user can upload to iNat from my observations page
UPLOAD-X-OBSERVATIONS = UPLOAD {$count ->
Upload-x-observations = UPLOAD {$count ->
[one] 1 OBSERVATION
*[other] {$count} OBSERVATIONS
}
@@ -585,6 +585,8 @@ Username-or-Email = Username or Email
Visually-search-iNaturalist-data = Visually search iNaturalists wealth of data. Search by a taxon in a location
Welcome-back = Welcome back,
Whenever-you-get-internet-connection-you-can-upload = Whenever you get internet connection, you can upload your observations to iNaturalist.
Which-traditional-projects-can-add-your-observations = Which traditional projects can add your observations?
@@ -687,7 +689,15 @@ No = No
Discard-Comment = Discard Comment
Are-you-sure-discard-comment = Are you sure you want to discard this comment?
## Accessibility labels: these are used by screen readers to describe actionable elements
## Accessibility labels: these are used by screen readers to label actionable elements iOS: https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619577-accessibilitylabel
## iOS Guidelines "A string that succinctly identifies the accessibility element." Starts with capital letter, no ending punctuation.
User = User { $userHandle }
## Accessibility hints: these are used by screen readers to describe what happens when the user interacts with an element iOS: https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619585-accessibilityhint
## iOS Guidelines "A string that briefly describes the result of performing an action on the accessibility element." Third person singular ending with a period.
Navigates-to-user-profile = Navigates to user profile.
## The following are actually more like "accessibility hints" than labels we should probably refactor
Add-this-ID = Add this identification
# Accessible label for the camera button
Camera-button-label-switch-camera = Use the device's other camera.
@@ -712,12 +722,12 @@ Navigate-to-login-screen = Navigate to login screen
Navigate-to-observation-details = Navigate to observation details screen
Navigate-to-project-details = Navigate to project details
Navigate-to-taxon-details = Navigate to taxon details
Navigate-to-user-profile = Navigate to user profile
Number-of-comments = Number of comments
Number-of-identifications = Number of identifications
Observation-has-no-photos-and-no-sounds = This observation has no photos and no sounds.
Take-photo = Take photo
Photo-taken-at = Photo taken at { $date }
Switch-to-tab = Switch to { $tab } tab
Take-photo = Take photo
# Accessibility labels for no internet state in ObsDetails
Location-map-unavailable-without-internet = Location map unavailable without internet
Observation-photos-unavailable-without-internet = Observation photos unavailable without internet

View File

@@ -1,6 +1,9 @@
// @flow
import { activateKeepAwake, deactivateKeepAwake } from "@sayem314/react-native-keep-awake";
import {
activateKeepAwake,
deactivateKeepAwake
} from "@sayem314/react-native-keep-awake";
import { RealmContext } from "providers/contexts";
import { useEffect, useState } from "react";
import Observation from "realmModels/Observation";
@@ -12,19 +15,40 @@ const useUploadObservations = ( allObsToUpload: Array<Object> ): Object => {
const [uploadInProgress, setUploadInProgress] = useState( false );
const [shouldUpload, setShouldUpload] = useState( false );
const [currentUploadIndex, setCurrentUploadIndex] = useState( 0 );
const realm = useRealm();
const apiToken = useApiToken();
const [progress, setProgress] = useState( 0 );
const [error, setError] = useState( null );
const realm = useRealm( );
const apiToken = useApiToken( );
const cleanup = ( ) => {
setUploadInProgress( false );
setShouldUpload( false );
setCurrentUploadIndex( 0 );
setError( null );
deactivateKeepAwake( );
setProgress( 0 );
};
useEffect( ( ) => {
const upload = async observationToUpload => {
await Observation.uploadObservation( observationToUpload, apiToken, realm );
const increment = ( 1 / allObsToUpload.length ) / 2;
setProgress( currentProgress => currentProgress + increment );
try {
await Observation.uploadObservation(
observationToUpload,
apiToken,
realm
);
} catch ( e ) {
console.warn( e );
setError( e.message );
}
setProgress( currentProgress => {
if ( currentUploadIndex === allObsToUpload.length - 1 ) {
return 1;
}
return currentProgress + increment;
} );
setCurrentUploadIndex( currentIndex => currentIndex + 1 );
};
@@ -35,6 +59,7 @@ const useUploadObservations = ( allObsToUpload: Array<Object> ): Object => {
cleanup( );
return;
}
activateKeepAwake( );
setUploadInProgress( true );
upload( observationToUpload );
@@ -48,6 +73,8 @@ const useUploadObservations = ( allObsToUpload: Array<Object> ): Object => {
return {
uploadInProgress,
error,
progress,
stopUpload: cleanup,
startUpload: ( ) => setShouldUpload( true )
};

View File

@@ -8,6 +8,40 @@ module.exports = {
content: ["index.js", "./src/**/*.js"],
theme: {
extend: {
fontSize: {
"3xl": ["26px", {
lineHeight: "31px"
}
],
"2xl": ["22px", {
lineHeight: "26px"
}
],
xl: ["21px", {
lineHeight: "25px"
}
],
lg: ["19px", {
lineHeight: "23px"
}
],
base: ["18px", {
lineHeight: "22px"
}
],
md: ["16px", {
lineHeight: "18px"
}
],
sm: ["14px", {
lineHeight: "18px"
}
],
xs: ["12px", {
lineHeight: "14px"
}
]
},
height: {
22: "5.5rem"
},
@@ -15,6 +49,7 @@ module.exports = {
"Whitney-Medium": ["Whitney-Medium"],
"Whitney-Medium-Pro": ["Whitney-Medium-Pro"], // Android naming convention
"Whitney-Light": ["Whitney-Light"],
"Whitney-Light-Pro": ["Whitney-Light-Pro"], // Android naming convention
// selected from list of fonts already available in RN
// https://infinitbility.com/react-native-font-family-list/
"Papyrus-Condensed": ["Papyrus-Condensed"],

4
tests/.eslintrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
plugins: ["testing-library"],
extends: ["plugin:testing-library/react"]
};

View File

@@ -1,7 +1,7 @@
// These test ensure that My Observation integrates with other systems like
// remote data retrieval and local data persistence
import { waitFor } from "@testing-library/react-native";
import { screen, waitFor } from "@testing-library/react-native";
import ObsList from "components/Observations/ObsList";
import inatjs from "inaturalistjs";
import React from "react";
@@ -28,21 +28,24 @@ describe( "MyObservations", ( ) => {
// await signIn( mockUser );
// const observations = [factory( "RemoteObservation" )];
// inatjs.observations.search.mockResolvedValue( makeResponse( observations ) );
// const { queryByTestId } = renderAppWithComponent( <ObsList /> );
// renderAppWithComponent( <ObsList /> );
// const { findByTestId } = renderAppWithComponent( <ObsList /> );
// expect( await findByTestId( "ObservationViews.myObservations" ) ).toBeAccessible( );
// expect( await screen.findByTestId( "ObservationViews.myObservations" ) ).toBeAccessible( );
// } );
// } );
describe( "when signed out", ( ) => {
async function testApiMethodNotCalled( apiMethod ) {
async function testApiMethodNotCalled( apiMethod, language ) {
// Let's make sure the mock hasn't already been used
expect( apiMethod ).not.toHaveBeenCalled( );
const signedInUsers = global.realm.objects( "User" ).filtered( "signedIn == true" );
expect( signedInUsers.length ).toEqual( 0 );
const { getByText } = renderAppWithComponent( <ObsList /> );
renderAppWithComponent( <ObsList /> );
// TODO: We should really address this globally in the test suite. On first render,
// text doesn't have a language set, but on second render, text will default to English.
const textByLanguage = language === "en" ? "Log in to iNaturalist" : "Log-in-to-iNaturalist";
await waitFor( ( ) => {
expect( getByText( "Log in to iNaturalist" ) ).toBeTruthy( );
expect( screen.getByText( textByLanguage ) ).toBeTruthy( );
} );
// Unpleasant, but without adjusting the timeout it doesn't seem like
// all of these requests get caught
@@ -51,10 +54,10 @@ describe( "MyObservations", ( ) => {
}, { timeout: 3000, interval: 500 } );
}
it( "should not make a request to users/me", async ( ) => {
await testApiMethodNotCalled( inatjs.users.me );
await testApiMethodNotCalled( inatjs.users.me, undefined );
} );
it( "should not make a request to observations/updates", async ( ) => {
await testApiMethodNotCalled( inatjs.observations.updates );
await testApiMethodNotCalled( inatjs.observations.updates, "en" );
} );
} );
@@ -63,27 +66,27 @@ describe( "MyObservations", ( ) => {
const mockUser = factory( "LocalUser" );
expect( mockUser.locale ).toEqual( "en" );
await signIn( mockUser );
const { queryByText } = renderAppWithComponent( <ObsList /> );
renderAppWithComponent( <ObsList /> );
await waitFor( ( ) => {
expect( queryByText( / Observations/ ) ).toBeTruthy( );
expect( screen.getByText( /Welcome back/ ) ).toBeTruthy( );
} );
expect( queryByText( /X-Observations/ ) ).toBeFalsy( );
expect( screen.queryByText( /Welcome-back/ ) ).toBeFalsy( );
} );
it( "should be Spanish if signed in user's locale is Spanish", async ( ) => {
it.skip( "should be Spanish if signed in user's locale is Spanish", async ( ) => {
const mockSpanishUser = factory( "LocalUser", {
locale: "es"
} );
expect( mockSpanishUser.locale ).toEqual( "es" );
await signIn( mockSpanishUser );
const { queryByText } = renderAppWithComponent( <ObsList /> );
renderAppWithComponent( <ObsList /> );
await waitFor( ( ) => {
expect( queryByText( / Observaciones/ ) ).toBeTruthy( );
expect( screen.getByText( / Observaciones/ ) ).toBeTruthy();
} );
expect( queryByText( /X-Observations/ ) ).toBeFalsy( );
expect( screen.queryByText( /X-Observations/ ) ).toBeFalsy( );
} );
it(
it.skip(
"should change to es when local user locale is en but remote user locale is es",
async ( ) => {
const mockUser = factory( "LocalUser" );
@@ -95,15 +98,15 @@ describe( "MyObservations", ( ) => {
} );
inatjs.users.me.mockResolvedValue( makeResponse( [mockSpanishUser] ) );
const { findByText, queryByText } = renderAppWithComponent( <ObsList /> );
renderAppWithComponent( <ObsList /> );
// I'd prefer to wait for the Spanish text to appear, but that never
// seems to wait long enough. This waits for the relevant API call to
// have been made
await waitFor( ( ) => {
expect( inatjs.users.me ).toHaveBeenCalled( );
} );
expect( await findByText( / Observaciones/ ) ).toBeTruthy( );
expect( queryByText( /X-Observations/ ) ).toBeFalsy( );
expect( await screen.findByText( / Observaciones/ ) ).toBeTruthy( );
expect( screen.queryByText( /X-Observations/ ) ).toBeFalsy( );
}
);
} );

View File

@@ -1,4 +1,4 @@
import { waitFor } from "@testing-library/react-native";
import { screen, waitFor } from "@testing-library/react-native";
import ObsEdit from "components/ObsEdit/ObsEdit";
import faker from "faker";
import { ObsEditContext } from "providers/contexts";
@@ -90,12 +90,12 @@ test( "renders observation photo from photo gallery", ( ) => {
} )];
mockObsEditProviderWithObs( observations );
const { getByText } = renderObsEdit( );
renderObsEdit( );
const obs = observations[0];
expect( getByText( obs.place_guess ) ).toBeTruthy( );
expect( getByText( new RegExp( obs.longitude ) ) ).toBeTruthy( );
expect( screen.getByText( obs.place_guess ) ).toBeTruthy( );
expect( screen.getByText( new RegExp( obs.longitude ) ) ).toBeTruthy( );
} );
describe( "location fetching", () => {
@@ -124,9 +124,9 @@ describe( "location fetching", () => {
expect( observation.created_at ).toBeFalsy( );
expect( observation._created_at ).toBeTruthy( );
mockObsEditProviderWithObs( [observation] );
const { queryByText } = renderObsEdit( );
renderObsEdit( );
expect( queryByText( new RegExp( `Lat: ${observation.latitude}` ) ) ).toBeTruthy( );
expect( screen.getByText( new RegExp( `Lat: ${observation.latitude}` ) ) ).toBeTruthy( );
expect( mockFetchUserLocation ).not.toHaveBeenCalled();
} );
@@ -139,9 +139,9 @@ describe( "location fetching", () => {
expect( observation.id ).toBeTruthy( );
expect( observation.created_at ).toBeTruthy( );
mockObsEditProviderWithObs( [observation] );
const { queryByText } = renderObsEdit( );
renderObsEdit( );
expect( queryByText( new RegExp( `Lat: ${observation.latitude}` ) ) ).toBeTruthy( );
expect( screen.getByText( new RegExp( `Lat: ${observation.latitude}` ) ) ).toBeTruthy( );
expect( mockFetchUserLocation ).not.toHaveBeenCalled();
} );
} );

View File

@@ -1,5 +1,5 @@
import { useRoute } from "@react-navigation/native";
import { waitFor } from "@testing-library/react-native";
import { screen, waitFor } from "@testing-library/react-native";
import ObsEdit from "components/ObsEdit/ObsEdit";
import ObsEditProvider from "providers/ObsEditProvider";
import React from "react";
@@ -40,9 +40,9 @@ describe( "UUID in params", ( ) => {
global.realm
);
useRoute.mockImplementation( ( ) => ( { params: { uuid: observation.uuid } } ) );
const { queryByText } = renderObsEdit( );
renderObsEdit( );
await waitFor( ( ) => {
expect( queryByText( observation.taxon.name ) ).toBeTruthy( );
expect( screen.getByText( observation.taxon.name ) ).toBeTruthy( );
} );
} );
@@ -55,23 +55,23 @@ describe( "UUID in params", ( ) => {
factory( "LocalObservation" ),
global.realm
);
useRoute.mockImplementation( ( ) => ( { params: { uuid: observation.uuid } } ) );
const { queryByText, update } = renderObsEdit( );
await waitFor( async ( ) => {
expect( queryByText( observation.taxon.name ) ).toBeTruthy( );
// Up to this point we're just repeating the prior test to ensure that the
// observation in the params gets inserted into the context
useRoute.mockImplementation( () => ( { params: { uuid: observation.uuid } } ) );
const { update } = renderObsEdit();
expect( await screen.findByText( observation.taxon.name ) ).toBeTruthy();
// Up to this point we're just repeating the prior test to ensure that the
// observation in the params gets inserted into the context
// Now we alter the params so they specify a different observation
const newObservation = await Observation.saveLocalObservationForUpload(
factory( "LocalObservation" ),
global.realm
);
useRoute.mockImplementation( ( ) => ( { params: { uuid: newObservation.uuid } } ) );
await renderObsEdit( update );
expect( queryByText( newObservation.taxon.name ) ).toBeTruthy( );
expect( queryByText( observation.taxon.name ) ).toBeFalsy( );
} );
// Now we alter the params so they specify a different observation
const newObservation = await Observation.saveLocalObservationForUpload(
factory( "LocalObservation" ),
global.realm
);
useRoute.mockImplementation( () => ( {
params: { uuid: newObservation.uuid }
} ) );
await renderObsEdit( update );
expect( screen.getByText( newObservation.taxon.name ) ).toBeTruthy();
expect( screen.queryByText( observation.taxon.name ) ).toBeFalsy();
} );
it( "should not reset the observation in context when context has "
@@ -81,12 +81,10 @@ describe( "UUID in params", ( ) => {
global.realm
);
useRoute.mockImplementation( ( ) => ( { params: { uuid: observation.uuid } } ) );
const { queryByText, update } = renderObsEdit( );
await waitFor( async ( ) => {
expect( queryByText( observation.taxon.name ) ).toBeTruthy( );
useRoute.mockImplementation( ( ) => ( { params: { uuid: observation.uuid } } ) );
await renderObsEdit( update );
expect( queryByText( observation.taxon.name ) ).toBeTruthy( );
} );
const { update } = renderObsEdit( );
expect( await screen.findByText( observation.taxon.name ) ).toBeTruthy( );
useRoute.mockImplementation( ( ) => ( { params: { uuid: observation.uuid } } ) );
await renderObsEdit( update );
expect( await screen.findByText( observation.taxon.name ) ).toBeTruthy( );
} );
} );

View File

@@ -1,11 +1,14 @@
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
import { NavigationContainer } from "@react-navigation/native";
import { fireEvent, render, waitFor } from "@testing-library/react-native";
import {
fireEvent,
screen
} from "@testing-library/react-native";
import PhotoGallery from "components/PhotoImporter/PhotoGallery";
import ObsEditProvider from "providers/ObsEditProvider";
import React from "react";
import factory from "../factory";
import { renderComponent } from "../helpers/render";
import { signIn, signOut } from "../helpers/user";
beforeEach( signOut );
@@ -26,17 +29,13 @@ test( "shows a selected checkmark when a photo is tapped", async ( ) => {
},
edges: [{ node: photo }]
} ) );
const { queryByTestId } = render(
<NavigationContainer>
<ObsEditProvider>
<PhotoGallery />
</ObsEditProvider>
</NavigationContainer>
renderComponent(
<ObsEditProvider>
<PhotoGallery />
</ObsEditProvider>
);
await waitFor( ( ) => {
const renderedPhoto = queryByTestId( `PhotoGallery.${photo.image.uri}` );
expect( queryByTestId( `PhotoGallery.selected.${photo.image.uri}` ) ).toBeFalsy( );
fireEvent.press( renderedPhoto );
expect( queryByTestId( `PhotoGallery.selected.${photo.image.uri}` ) ).toBeTruthy( );
} );
const renderedPhoto = await screen.findByTestId( `PhotoGallery.${photo.image.uri}` );
expect( screen.queryByTestId( `PhotoGallery.selected.${photo.image.uri}` ) ).toBeFalsy( );
fireEvent.press( renderedPhoto );
expect( await screen.findByTestId( `PhotoGallery.selected.${photo.image.uri}` ) ).toBeTruthy( );
} );

View File

@@ -1,4 +1,4 @@
import { fireEvent } from "@testing-library/react-native";
import { fireEvent, screen } from "@testing-library/react-native";
import About from "components/About";
import React from "react";
import Mailer from "react-native-mail";
@@ -10,8 +10,8 @@ jest.mock( "react-native-mail", ( ) => ( {
} ) );
test( "native email client is opened on button press", ( ) => {
const { getByText } = renderComponent( <About /> );
const debugLogButton = getByText( /EMAIL-DEBUG-LOGS/ );
renderComponent( <About /> );
const debugLogButton = screen.getByText( /EMAIL-DEBUG-LOGS/ );
expect( debugLogButton ).toBeTruthy( );
fireEvent.press( debugLogButton );
expect( Mailer.mail ).toHaveBeenCalled( );

View File

@@ -1,4 +1,4 @@
import { fireEvent, screen, waitFor } from "@testing-library/react-native";
import { fireEvent, screen } from "@testing-library/react-native";
import AddID from "components/ObsEdit/AddID";
import { t } from "i18next";
import inatjs from "inaturalistjs";
@@ -52,6 +52,8 @@ jest.mock( "react-native-vector-icons/MaterialIcons", ( ) => {
}
render( ) {
// I have disabled the eslint rule here because it is about a mock and not the test
// eslint-disable-next-line testing-library/no-node-access
return InnerReact.createElement( "MaterialIcons", this.props, this.props.children );
}
}
@@ -75,20 +77,18 @@ describe( "AddID", ( ) => {
it( "should render inside mocked container", ( ) => {
renderComponent( <AddID route={mockRoute} /> );
expect( screen.queryByTestId( "mock-view-no-footer" ) ).toBeTruthy( );
expect( screen.getByTestId( "mock-view-no-footer" ) ).toBeTruthy( );
} );
it( "show taxon search results", async ( ) => {
inatjs.search.mockResolvedValue( makeResponse( mockTaxaList ) );
const { getByTestId } = renderComponent( <AddID route={mockRoute} /> );
const input = getByTestId( "SearchTaxon" );
renderComponent( <AddID route={mockRoute} /> );
const input = screen.getByTestId( "SearchTaxon" );
const taxon = mockTaxaList[0];
await waitFor( () => {
fireEvent.changeText( input, "Some taxon" );
expect( getByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy( );
} );
fireEvent.changeText( input, "Some taxon" );
expect( await screen.findByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy( );
expect(
getByTestId( `Search.taxa.${taxon.id}.photo` ).props.source
screen.getByTestId( `Search.taxa.${taxon.id}.photo` ).props.source
).toStrictEqual( { uri: taxon.default_photo.square_url } );
} );

View File

@@ -1,4 +1,4 @@
import { render } from "@testing-library/react-native";
import { render, screen } from "@testing-library/react-native";
import DisplayTaxonName from "components/DisplayTaxonName";
import React from "react";
@@ -40,60 +40,60 @@ describe( "when common name is first", () => {
const user = { prefers_scientific_name_first: false };
test( "renders correct taxon for species", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: speciesTaxon, user }} />
);
expect(
getByText( `${speciesTaxon.preferred_common_name} (${speciesTaxon.name})` )
screen.getByText( `${speciesTaxon.preferred_common_name} (${speciesTaxon.name})` )
).toBeTruthy();
} );
test( "renders correct taxon w/o common name", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: noCommonNameTaxon, user }} />
);
expect( getByText( noCommonNameTaxon.name ) ).toBeTruthy();
expect( screen.getByText( noCommonNameTaxon.name ) ).toBeTruthy();
} );
test( "renders correct taxon w/o common name and no species", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: highRankTaxon, user }} />
);
expect(
getByText( `${highRankTaxon.rank} ${highRankTaxon.name}` )
screen.getByText( `${highRankTaxon.rank} ${highRankTaxon.name}` )
).toBeTruthy();
} );
test( "renders correct taxon for a subspecies", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: highRankTaxon, user }} />
);
expect(
getByText( `${highRankTaxon.rank} ${highRankTaxon.name}` )
screen.getByText( `${highRankTaxon.rank} ${highRankTaxon.name}` )
).toBeTruthy();
} );
test( "renders correct taxon for species", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: subspeciesTaxon, user }} />
);
expect(
getByText( "Silver Lupine (Lupinus albifrons var. collinus)" )
screen.getByText( "Silver Lupine (Lupinus albifrons var. collinus)" )
).toBeTruthy();
} );
test( "renders correct taxon for improperly capitalized common name", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: uncapitalizedTaxon, user }} />
);
expect(
getByText( "Crown-of-thorns Blue Sea-Stars (Acanthaster planci)" )
screen.getByText( "Crown-of-thorns Blue Sea-Stars (Acanthaster planci)" )
).toBeTruthy();
} );
} );
@@ -102,40 +102,40 @@ describe( "when scientific name is first", () => {
const user = { prefers_scientific_name_first: true };
test( "renders correct taxon for species", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: speciesTaxon, user }} />
);
expect(
getByText( `${speciesTaxon.name} (${speciesTaxon.preferred_common_name})` )
screen.getByText( `${speciesTaxon.name} (${speciesTaxon.preferred_common_name})` )
).toBeTruthy();
} );
test( "renders correct taxon w/o common name", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: noCommonNameTaxon, user }} />
);
expect( getByText( noCommonNameTaxon.name ) ).toBeTruthy();
expect( screen.getByText( noCommonNameTaxon.name ) ).toBeTruthy();
} );
test( "renders correct taxon w/o common name and no species", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: highRankTaxon, user }} />
);
expect(
getByText( `${highRankTaxon.rank} ${highRankTaxon.name}` )
screen.getByText( `${highRankTaxon.rank} ${highRankTaxon.name}` )
).toBeTruthy();
} );
test( "renders correct taxon for species", () => {
const { getByText } = render(
render(
<DisplayTaxonName item={{ taxon: subspeciesTaxon, user }} />
);
expect(
getByText( "Lupinus albifrons var. collinus (Silver Lupine)" )
screen.getByText( "Lupinus albifrons var. collinus (Silver Lupine)" )
).toBeTruthy();
} );
} );

View File

@@ -1,5 +1,5 @@
import { NavigationContainer } from "@react-navigation/native";
import { render } from "@testing-library/react-native";
import { render, screen } from "@testing-library/react-native";
import Messages from "components/Messages/Messages";
import React from "react";
@@ -65,8 +65,8 @@ describe( "when loading", ( ) => {
} );
it( "displays activity indicator when loading", ( ) => {
const { getByTestId } = renderMessages( );
expect( getByTestId( "Messages.activityIndicator" ) ).toBeTruthy( );
renderMessages( );
expect( screen.getByTestId( "Messages.activityIndicator" ) ).toBeTruthy( );
} );
} );
@@ -80,8 +80,8 @@ describe( "when loading complete", ( ) => {
} );
it( "displays message subject and not activity indicator when loading complete", ( ) => {
const { getByText, queryByTestId } = renderMessages( );
expect( getByText( mockMessage.subject ) ).toBeTruthy( );
expect( queryByTestId( "Messages.activityIndicator" ) ).toBeNull( );
renderMessages( );
expect( screen.getByText( mockMessage.subject ) ).toBeTruthy( );
expect( screen.queryByTestId( "Messages.activityIndicator" ) ).toBeNull( );
} );
} );

View File

@@ -103,10 +103,10 @@ describe( "ObsDetails", () => {
test( "renders obs details from remote call", async ( ) => {
useIsConnected.mockImplementation( ( ) => true );
const { getByText, findByTestId } = renderComponent( <ObsDetails /> );
renderComponent( <ObsDetails /> );
expect( await findByTestId( `ObsDetails.${mockObservation.uuid}` ) ).toBeTruthy( );
expect( getByText( mockObservation.taxon.name ) ).toBeTruthy( );
expect( await screen.findByTestId( `ObsDetails.${mockObservation.uuid}` ) ).toBeTruthy( );
expect( screen.getByText( mockObservation.taxon.name ) ).toBeTruthy( );
} );
test( "renders data tab on button press", async ( ) => {
@@ -143,8 +143,8 @@ describe( "Observation with no evidence", () => {
describe( "activity tab", ( ) => {
test( "navigates to taxon details on button press", async ( ) => {
const { findByTestId } = renderComponent( <ObsDetails /> );
fireEvent.press( await findByTestId( `ObsDetails.taxon.${mockObservation.taxon.id}` ) );
renderComponent( <ObsDetails /> );
fireEvent.press( await screen.findByTestId( `ObsDetails.taxon.${mockObservation.taxon.id}` ) );
expect( mockNavigate ).toHaveBeenCalledWith( "TaxonDetails", {
id: mockObservation.taxon.id
} );

View File

@@ -1,4 +1,4 @@
import { fireEvent, waitFor } from "@testing-library/react-native";
import { fireEvent, screen, waitFor } from "@testing-library/react-native";
import DeleteObservationDialog from "components/ObsEdit/DeleteObservationDialog";
import inatjs from "inaturalistjs";
import { ObsEditContext } from "providers/contexts";
@@ -74,8 +74,8 @@ describe( "delete observation", ( ) => {
const localObservation = getLocalObservation( observations[0].uuid );
expect( localObservation ).toBeTruthy( );
mockObsEditProviderWithObs( observations );
const { queryByText } = renderDeleteDialog( );
const deleteButton = queryByText( /Yes-delete-observation/ );
renderDeleteDialog( );
const deleteButton = screen.queryByText( /Yes-delete-observation/ );
expect( deleteButton ).toBeTruthy( );
fireEvent.press( deleteButton );
expect( getLocalObservation( observations[0].uuid ) ).toBeFalsy( );
@@ -97,17 +97,17 @@ describe( "delete observation", ( ) => {
const localObservation = getLocalObservation( observations[0].uuid );
expect( localObservation ).toBeTruthy( );
mockObsEditProviderWithObs( observations );
const { queryByText } = renderDeleteDialog( );
renderDeleteDialog( );
// TODO: figure out why this needs English text and why the one above needs
// the generic text. Probably has to do with User object still being stored in global realm
// between tests
const deleteButton = queryByText( /delete/ );
const deleteButton = screen.queryByText( /delete/ );
expect( deleteButton ).toBeTruthy( );
fireEvent.press( deleteButton );
await waitFor( ( ) => {
expect( inatjs.observations.delete ).toHaveBeenCalledTimes( 1 );
expect( getLocalObservation( observations[0].uuid ) ).toBeFalsy( );
} );
expect( getLocalObservation( observations[0].uuid ) ).toBeFalsy( );
} );
} );
@@ -120,9 +120,9 @@ describe( "delete observation", ( ) => {
const localObservation = getLocalObservation( observations[0].uuid );
expect( localObservation ).toBeTruthy( );
mockObsEditProviderWithObs( observations );
const { queryByText } = renderDeleteDialog( );
renderDeleteDialog( );
const cancelButton = queryByText( /Cancel/ );
const cancelButton = screen.queryByText( /Cancel/ );
expect( cancelButton ).toBeTruthy( );
fireEvent.press( cancelButton );
expect( getLocalObservation( observations[0].uuid ) ).toBeTruthy( );

View File

@@ -1,4 +1,4 @@
import { fireEvent, render } from "@testing-library/react-native";
import { fireEvent, render, screen } from "@testing-library/react-native";
import ObsCard from "components/Observations/ObsCard";
import React from "react";
@@ -9,23 +9,22 @@ const testObservation = factory( "LocalObservation", {
} );
test( "renders text passed into observation card", ( ) => {
const { getByTestId, getByText } = render(
render(
<ObsCard
item={testObservation}
handlePress={( ) => jest.fn()}
/>
);
expect( getByTestId( `ObsList.obsCard.${testObservation.uuid}` ) ).toBeTruthy( );
expect( getByTestId( "ObsList.photo" ).props.source )
expect( screen.getByTestId( `ObsList.obsCard.${testObservation.uuid}` ) ).toBeTruthy( );
expect( screen.getByTestId( "ObsList.photo" ).props.source )
.toStrictEqual( { uri: testObservation.observationPhotos[0].photo.url } );
expect( getByText(
expect( screen.getByText(
`${testObservation.taxon.preferred_common_name} (${testObservation.taxon.name})`
) ).toBeTruthy( );
expect( getByText( testObservation.placeGuess ) ).toBeTruthy( );
expect( getByText( testObservation.comments.length.toString( ) ) ).toBeTruthy( );
expect( getByText( testObservation.identifications.length.toString( ) ) ).toBeTruthy( );
// add grade tests
expect( screen.getByText( testObservation.placeGuess ) ).toBeTruthy( );
expect( screen.getByText( testObservation.comments.length.toString( ) ) ).toBeTruthy( );
expect( screen.getByText( testObservation.identifications.length.toString( ) ) ).toBeTruthy( );
} );
test( "navigates to ObsDetails on button press", ( ) => {
@@ -33,14 +32,14 @@ test( "navigates to ObsDetails on button press", ( ) => {
navigate: jest.fn( )
};
const { getByTestId } = render(
render(
<ObsCard
item={testObservation}
handlePress={( ) => fakeNavigation.navigate( "ObsDetails" )}
/>
);
const button = getByTestId( `ObsList.obsCard.${testObservation.uuid}` );
const button = screen.getByTestId( `ObsList.obsCard.${testObservation.uuid}` );
fireEvent.press( button );
expect( fakeNavigation.navigate ).toBeCalledWith( "ObsDetails" );

View File

@@ -1,4 +1,4 @@
import { render } from "@testing-library/react-native";
import { render, screen } from "@testing-library/react-native";
import ObsCardDetails from "components/Observations/ObsCardDetails";
import React from "react";
@@ -9,14 +9,14 @@ const testObservation = factory( "LocalObservation", {
} );
test( "renders correct taxon and observation details", () => {
const { getByText } = render(
render(
<ObsCardDetails view="list" item={testObservation} />
);
expect(
getByText(
screen.getByText(
`${testObservation.taxon.preferred_common_name} (${testObservation.taxon.name})`
)
).toBeTruthy();
expect( getByText( testObservation.placeGuess ) ).toBeTruthy();
expect( screen.getByText( testObservation.placeGuess ) ).toBeTruthy();
} );

View File

@@ -1,5 +1,5 @@
import {
fireEvent, screen, waitFor, within
fireEvent, screen, within
} from "@testing-library/react-native";
import ObsList from "components/Observations/ObsList";
import React from "react";
@@ -55,39 +55,46 @@ jest.mock( "@react-navigation/native", ( ) => {
} );
it( "renders an observation", async ( ) => {
await waitFor( ( ) => {
const { getByTestId } = renderComponent( <ObsList /> );
const obs = mockObservations[0];
const list = getByTestId( "ObservationViews.myObservations" );
renderComponent( <ObsList /> );
const obs = mockObservations[0];
// Test that there isn't other data lingering
expect( list.props.data.length ).toEqual( mockObservations.length );
// Test that a card got rendered for the our test obs
const card = getByTestId( `ObsList.obsCard.${obs.uuid}` );
expect( card ).toBeTruthy( );
// Test that the card has the correct comment count
const commentCount = within( card ).getByTestId( "ObsList.obsCard.commentCount" );
expect( commentCount.children[0] ).toEqual( obs.comments.length.toString( ) );
} );
const list = await screen.findByTestId( "ObservationViews.myObservations" );
// Test that there isn't other data lingering
expect( list.props.data.length ).toEqual( mockObservations.length );
// Test that a card got rendered for the our test obs
const card = await screen.findByTestId( `ObsList.obsCard.${obs.uuid}` );
expect( card ).toBeTruthy( );
// Test that the card has the correct comment count
const commentCount = within( card ).getByTestId( "ObsList.obsCard.commentCount" );
// TODO: I disabled node eslint rule here because we will soon have to refactor this
// test into it's own unit test, because the comment count will be a component
// after the refactor we should change this line to be in compliance with the eslint rule
// eslint-disable-next-line testing-library/no-node-access
expect( commentCount.children[0] ).toEqual( obs.comments.length.toString( ) );
} );
it( "renders multiple observations", async ( ) => {
await waitFor( ( ) => {
const { getByTestId } = renderComponent( <ObsList /> );
mockObservations.forEach( obs => {
expect( getByTestId( `ObsList.obsCard.${obs.uuid}` ) ).toBeTruthy( );
} );
renderComponent( <ObsList /> );
// Awaiting the first observation because using await in the forEach errors out
const firstObs = mockObservations[0];
await screen.findByTestId( `ObsList.obsCard.${firstObs.uuid}` );
mockObservations.forEach( obs => {
expect( screen.getByTestId( `ObsList.obsCard.${obs.uuid}` ) ).toBeTruthy();
} );
// TODO: some things are still happening in the background so I unmount here,
// better probably to mock away those things happening in the background for this test
screen.unmount();
} );
it( "renders grid view on button press", async ( ) => {
await waitFor( ( ) => {
const { getByTestId } = renderComponent( <ObsList /> );
const button = getByTestId( "ObsList.toggleGridView" );
fireEvent.press( button );
mockObservations.forEach( obs => {
expect( getByTestId( `ObsList.gridItem.${obs.uuid}` ) ).toBeTruthy( );
} );
renderComponent( <ObsList /> );
const button = await screen.findByTestId( "ObsList.toggleGridView" );
fireEvent.press( button );
// Awaiting the first observation because using await in the forEach errors out
const firstObs = mockObservations[0];
await screen.findByTestId( `ObsList.gridItem.${firstObs.uuid}` );
mockObservations.forEach( obs => {
expect( screen.getByTestId( `ObsList.gridItem.${obs.uuid}` ) ).toBeTruthy( );
} );
} );

View File

@@ -63,13 +63,13 @@ const renderPhotoGallery = ( ) => renderComponent(
);
test( "renders photos from photo gallery", ( ) => {
const { getByTestId } = renderPhotoGallery( );
renderPhotoGallery( );
const { uri } = mockPhoto.image;
expect( getByTestId( "PhotoGallery.list" ) ).toBeTruthy( );
expect( getByTestId( `PhotoGallery.${uri}` ) ).toBeTruthy( );
expect( getByTestId( "PhotoGallery.photo" ).props.source )
expect( screen.getByTestId( "PhotoGallery.list" ) ).toBeTruthy( );
expect( screen.getByTestId( `PhotoGallery.${uri}` ) ).toBeTruthy( );
expect( screen.getByTestId( "PhotoGallery.photo" ).props.source )
.toStrictEqual( { uri } );
} );

View File

@@ -34,15 +34,15 @@ describe( "ProjectDetails", () => {
} );
test( "displays project details", ( ) => {
const { getByTestId, getByText } = renderComponent( <ProjectDetails /> );
renderComponent( <ProjectDetails /> );
expect( getByText( mockProject.title ) ).toBeTruthy( );
expect( getByText( mockProject.description ) ).toBeTruthy( );
expect( screen.getByText( mockProject.title ) ).toBeTruthy( );
expect( screen.getByText( mockProject.description ) ).toBeTruthy( );
expect(
getByTestId( "ProjectDetails.headerImage" ).props.source
screen.getByTestId( "ProjectDetails.headerImage" ).props.source
).toStrictEqual( { uri: mockProject.header_image_url } );
expect(
getByTestId( "ProjectDetails.projectIcon" ).props.source
screen.getByTestId( "ProjectDetails.projectIcon" ).props.source
).toStrictEqual( { uri: mockProject.icon } );
} );
} );

View File

@@ -38,9 +38,9 @@ describe( "ProjectObservations", () => {
} );
test( "displays project observations", ( ) => {
const { getByTestId, getByText } = renderComponent( <ProjectObservations /> );
renderComponent( <ProjectObservations /> );
expect( getByText(
expect( screen.getByText(
`${
mockObservation.taxon.preferred_common_name
} (${
@@ -49,6 +49,7 @@ test( "displays project observations", ( ) => {
mockObservation.taxon.name
})`
) ).toBeTruthy( );
expect( getByTestId( "ObsList.photo" ).props.source )
.toStrictEqual( { uri: mockObservation.observation_photos[0].photo.url } );
expect( screen.getByTestId( "ObsList.photo" ).props.source ).toStrictEqual( {
uri: mockObservation.observation_photos[0].photo.url
} );
} );

View File

@@ -26,15 +26,15 @@ jest.mock( "@react-navigation/native", ( ) => {
} );
test( "displays project search results", ( ) => {
const { getByTestId, getByText } = renderComponent( <Projects /> );
renderComponent( <Projects /> );
const input = getByTestId( "ProjectSearch.input" );
const input = screen.getByTestId( "ProjectSearch.input" );
fireEvent.changeText( input, "butterflies" );
expect( getByText( mockProject.title ) ).toBeTruthy( );
expect( getByTestId( `Project.${mockProject.id}.photo` ).props.source )
expect( screen.getByText( mockProject.title ) ).toBeTruthy( );
expect( screen.getByTestId( `Project.${mockProject.id}.photo` ).props.source )
.toStrictEqual( { uri: mockProject.icon } );
fireEvent.press( getByTestId( `Project.${mockProject.id}` ) );
fireEvent.press( screen.getByTestId( `Project.${mockProject.id}` ) );
expect( mockedNavigate ).toHaveBeenCalledWith( "ProjectDetails", {
id: mockProject.id
} );

View File

@@ -1,4 +1,4 @@
import { fireEvent } from "@testing-library/react-native";
import { fireEvent, screen } from "@testing-library/react-native";
import Search from "components/Search/Search";
import React from "react";
@@ -27,14 +27,14 @@ jest.mock( "@react-navigation/native", ( ) => {
} );
test( "renders taxon search results from API call", ( ) => {
const { getByTestId, getByText } = renderComponent( <Search /> );
renderComponent( <Search /> );
const commonName = mockTaxon.preferred_common_name;
expect( getByTestId( "Search.taxa" ) ).toBeTruthy( );
expect( getByTestId( `Search.${mockTaxon.id}.photo` ).props.source )
expect( screen.getByTestId( "Search.taxa" ) ).toBeTruthy( );
expect( screen.getByTestId( `Search.${mockTaxon.id}.photo` ).props.source )
.toStrictEqual( { uri: mockTaxon.default_photo.square_url } );
// using RegExp to be able to search within a string
expect( getByText( new RegExp( commonName ) ) ).toBeTruthy( );
expect( screen.getByText( new RegExp( commonName ) ) ).toBeTruthy();
} );
// right now this is failing on react-native-modal, since there's a TouchableWithFeedback
@@ -42,8 +42,8 @@ test( "renders taxon search results from API call", ( ) => {
test.todo( "should not have accessibility errors" );
test( "navigates to TaxonDetails on button press", ( ) => {
const { getByTestId } = renderComponent( <Search /> );
renderComponent( <Search /> );
fireEvent.press( getByTestId( `Search.taxa.${mockTaxon.id}` ) );
fireEvent.press( screen.getByTestId( `Search.taxa.${mockTaxon.id}` ) );
expect( mockedNavigate ).toHaveBeenCalledWith( "TaxonDetails", { id: mockTaxon.id } );
} );

View File

@@ -1,4 +1,4 @@
import { fireEvent } from "@testing-library/react-native";
import { fireEvent, screen } from "@testing-library/react-native";
import Search from "components/Search/Search";
import React from "react";
@@ -32,22 +32,22 @@ jest.mock( "@react-navigation/native", ( ) => {
const { login } = mockUser;
test( "displays user search results on button press", ( ) => {
const { getByTestId, getByText } = renderComponent( <Search /> );
const button = getByTestId( "Search.users" );
renderComponent( <Search /> );
const button = screen.getByTestId( "Search.users" );
fireEvent.press( button );
expect( getByTestId( `Search.user.${login}` ) ).toBeTruthy( );
expect( getByTestId( `Search.${login}.photo` ).props.source ).toStrictEqual( {
expect( screen.getByTestId( `Search.user.${login}` ) ).toBeTruthy( );
expect( screen.getByTestId( `Search.${login}.photo` ).props.source ).toStrictEqual( {
uri: mockUser.icon
} );
expect( getByText( new RegExp( login ) ) ).toBeTruthy( );
expect( screen.getByText( new RegExp( login ) ) ).toBeTruthy();
} );
test( "navigates to user profile on button press", ( ) => {
const { getByTestId } = renderComponent( <Search /> );
const button = getByTestId( "Search.users" );
renderComponent( <Search /> );
const button = screen.getByTestId( "Search.users" );
fireEvent.press( button );
fireEvent.press( getByTestId( `Search.user.${login}` ) );
fireEvent.press( screen.getByTestId( `Search.user.${login}` ) );
expect( mockedNavigate ).toHaveBeenCalledWith( "UserProfile", { userId: mockUser.id } );
} );

View File

@@ -1,5 +1,5 @@
import { NavigationContainer } from "@react-navigation/native";
import { fireEvent, render } from "@testing-library/react-native";
import { fireEvent, render, screen } from "@testing-library/react-native";
import TaxonDetails from "components/TaxonDetails/TaxonDetails";
import React from "react";
import { Linking } from "react-native";
@@ -50,12 +50,12 @@ jest.mock(
);
test( "renders taxon details from API call", async ( ) => {
const { getByTestId, getByText } = renderTaxonDetails( );
expect( getByTestId( `TaxonDetails.${mockTaxon.id}` ) ).toBeTruthy( );
expect( getByTestId( "PhotoScroll.photo" ).props.source )
renderTaxonDetails( );
expect( screen.getByTestId( `TaxonDetails.${mockTaxon.id}` ) ).toBeTruthy( );
expect( screen.getByTestId( "PhotoScroll.photo" ).props.source )
.toStrictEqual( { uri: mockTaxon.taxonPhotos[0].photo.url } );
expect( getByText( mockTaxon.preferred_common_name ) ).toBeTruthy( );
expect( getByText( mockTaxon.wikipedia_summary ) ).toBeTruthy( );
expect( screen.getByText( mockTaxon.preferred_common_name ) ).toBeTruthy( );
expect( screen.getByText( mockTaxon.wikipedia_summary ) ).toBeTruthy( );
} );
test( "should not have accessibility errors", ( ) => {
@@ -68,8 +68,8 @@ test( "should not have accessibility errors", ( ) => {
} );
test( "navigates to Wikipedia on button press", async ( ) => {
const { getByTestId } = renderTaxonDetails( );
fireEvent.press( getByTestId( "TaxonDetails.wikipedia" ) );
renderTaxonDetails( );
fireEvent.press( screen.getByTestId( "TaxonDetails.wikipedia" ) );
expect( Linking.openURL ).toHaveBeenCalledTimes( 1 );
expect( Linking.openURL ).toHaveBeenCalledWith( mockTaxon.wikipedia_url );
} );

View File

@@ -39,11 +39,11 @@ describe( "UserProfile", () => {
} );
test( "renders user profile from API call", async () => {
const { getByTestId, getByText } = renderComponent( <UserProfile /> );
renderComponent( <UserProfile /> );
expect( getByTestId( `UserProfile.${mockUser.id}` ) ).toBeTruthy();
expect( getByText( `iNaturalist ${mockUser.roles[0]}` ) ).toBeTruthy();
expect( getByTestId( "UserIcon.photo" ).props.source ).toStrictEqual( {
expect( screen.getByTestId( `UserProfile.${mockUser.id}` ) ).toBeTruthy();
expect( screen.getByText( `iNaturalist ${mockUser.roles[0]}` ) ).toBeTruthy();
expect( screen.getByTestId( "UserIcon.photo" ).props.source ).toStrictEqual( {
uri: mockUser.icon_url
} );
} );

View File

@@ -1,4 +1,4 @@
import { getDefaultNormalizer, render } from "@testing-library/react-native";
import { getDefaultNormalizer, render, screen } from "@testing-library/react-native";
import UserText from "components/SharedComponents/UserText";
import { trim } from "lodash";
import React from "react";
@@ -15,68 +15,64 @@ describe( "Sanitization", () => {
<script>${scriptTagContent}</script>
</div>`;
const {
queryByText
} = render(
render(
<UserText text={testText} />
);
expect( queryByText( paragraphTagContent ) ).toBeTruthy();
expect( queryByText( quoteTagContent ) ).toBeFalsy();
expect( queryByText( scriptTagContent ) ).toBeFalsy();
expect( screen.getByText( paragraphTagContent ) ).toBeTruthy();
expect( screen.queryByText( quoteTagContent ) ).toBeFalsy();
expect( screen.queryByText( scriptTagContent ) ).toBeFalsy();
} );
it( "only allows the HTML attributes we support on the web", () => {
it( "only allows the HTML attributes we support on the web", async () => {
const pTagText = "Welcome to iNaturalist";
const altText = "Girl in a jacket";
const testText = `<div>
<p style="font-size:100px">${pTagText}</p>
<img src="img_girl.jpg" alt=${altText} width="500" height="600">
<img src="img_girl.jpg" alt="${altText}" width="500" height="600">
<p>fontSize</p>
</div>`;
const { queryByText, findByLabelText } = render(
render(
<UserText text={testText} />
);
// alt text renders as accessibilityLabel
expect( findByLabelText( altText ) ).toBeTruthy();
expect( await screen.findByLabelText( altText ) ).toBeTruthy();
// default font size is 14, check if no change
expect( queryByText( pTagText ) ).toHaveProperty( "props.style.0.fontSize", 14 );
expect( screen.queryByText( pTagText ) ).toHaveProperty( "props.style.0.fontSize", 14 );
} );
it( "links all @ mentions", () => {
const testText = "@anglantis";
const {
queryByText
} = render(
render(
<UserText text={testText} />
);
expect( queryByText( testText ) ).toHaveProperty( "props.accessibilityRole", "link" );
expect( screen.queryByText( testText ) ).toHaveProperty( "props.accessibilityRole", "link" );
} );
it( "links all URLs", () => {
const testText = "https://www.inaturalist.org";
const { getByRole, queryByText } = render(
render(
<UserText text={testText} />
);
expect( getByRole( "link" ) ).toBeTruthy();
expect( queryByText( testText ) ).toBeTruthy();
expect( screen.getByRole( "link" ) ).toBeTruthy();
expect( screen.getByText( testText ) ).toBeTruthy();
} );
it( "closes unclosed tags", () => {
const testText = "<p>Welcome to iNat";
const { queryByText } = render(
render(
<UserText text={testText} />
);
expect( queryByText( "Welcome to iNat" ) ).toBeTruthy();
expect( screen.getByText( "Welcome to iNat" ) ).toBeTruthy();
} );
it( "strips leading and trailing whitespace", () => {
const testText = " This is a single line with a lloooooot of whitespace \n\n\n\n\n\n\n ";
const { queryByText } = render(
render(
<UserText text={testText} />
);
@@ -86,8 +82,8 @@ describe( "Sanitization", () => {
// https://callstack.github.io/react-native-testing-library/docs/api-queries/#normalization
const normalizer = getDefaultNormalizer( { trim: false } );
expect( queryByText( trim( testText ), { normalizer } ) ).toBeTruthy();
expect( queryByText( testText, { normalizer } ) ).toBeFalsy();
expect( screen.getByText( trim( testText ), { normalizer } ) ).toBeTruthy();
expect( screen.queryByText( testText, { normalizer } ) ).toBeFalsy();
} );
} );
@@ -101,34 +97,36 @@ describe( "Basic Rendering", () => {
it( "renders text", () => {
const testText = "foo bar baz";
const { queryByText, getByText } = render(
render(
<UserText text={testText} />
);
expect( getByText( testText ) ).toBeTruthy();
expect( queryByText( "asdfgh" ) ).toBeFalsy();
expect( screen.getByText( testText ) ).toBeTruthy();
expect( screen.queryByText( "asdfgh" ) ).toBeFalsy();
} );
it( "renders markdown", () => {
const testText = "# This is Heading 1";
const { queryByText } = render(
render(
<UserText text={testText} />
);
expect( queryByText( testText ) ).toBeFalsy();
expect( queryByText( "This is Heading 1" ) )
expect( screen.queryByText( testText ) ).toBeFalsy();
expect( screen.queryByText( "This is Heading 1" ) )
.toHaveProperty( "props.style.0.fontWeight", "bold" );
} );
it( "renders html", () => {
const testText = "<p>Welcome to <b>iNaturalist</b></p>";
const { queryByText } = render(
render(
<UserText text={testText} />
);
expect( queryByText( testText ) ).toBeFalsy();
expect( queryByText( "Welcome to" ) ).not.toHaveProperty( "props.style.0.fontWeight", "bold" );
expect( queryByText( "iNaturalist" ) ).toHaveProperty( "props.style.0.fontWeight", "bold" );
expect( screen.queryByText( testText ) ).toBeFalsy();
expect( screen.queryByText( "Welcome to" ) )
.not.toHaveProperty( "props.style.0.fontWeight", "bold" );
expect( screen.queryByText( "iNaturalist" ) )
.toHaveProperty( "props.style.0.fontWeight", "bold" );
} );
// Cannot test table and list rendering, at least using this type of test.