mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-03 21:23:04 -04:00
Merge branch 'main' into 397-qualitygradestatus-component
This commit is contained in:
73
.eslintrc.js
73
.eslintrc.js
@@ -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: {
|
||||
|
||||
4
.github/workflows/e2e_ios.yml
vendored
4
.github/workflows/e2e_ios.yml
vendored
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
55
package-lock.json
generated
@@ -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",
|
||||
|
||||
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
159
src/components/Observations/ObsListToolbar.js
Normal file
159
src/components/Observations/ObsListToolbar.js
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ const AddObsButton = ( ): React.Node => {
|
||||
accessibilityLabel={t( "Open-add-evidence-modal" )}
|
||||
className="m-0"
|
||||
disabled={false}
|
||||
testID="add-obs-button"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
11
src/components/SharedComponents/Typography/Body1.js
Normal file
11
src/components/SharedComponents/Typography/Body1.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Body2.js
Normal file
11
src/components/SharedComponents/Typography/Body2.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Body3.js
Normal file
11
src/components/SharedComponents/Typography/Body3.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Body4.js
Normal file
11
src/components/SharedComponents/Typography/Body4.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Heading1.js
Normal file
11
src/components/SharedComponents/Typography/Heading1.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Heading2.js
Normal file
11
src/components/SharedComponents/Typography/Heading2.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Heading3.js
Normal file
11
src/components/SharedComponents/Typography/Heading3.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Heading4.js
Normal file
11
src/components/SharedComponents/Typography/Heading4.js
Normal 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;
|
||||
22
src/components/SharedComponents/Typography/INatText.js
Normal file
22
src/components/SharedComponents/Typography/INatText.js
Normal 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;
|
||||
22
src/components/SharedComponents/Typography/INatTextLight.js
Normal file
22
src/components/SharedComponents/Typography/INatTextLight.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/List1.js
Normal file
11
src/components/SharedComponents/Typography/List1.js
Normal 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;
|
||||
13
src/components/SharedComponents/Typography/List2.js
Normal file
13
src/components/SharedComponents/Typography/List2.js
Normal 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;
|
||||
11
src/components/SharedComponents/Typography/Subheading1.js
Normal file
11
src/components/SharedComponents/Typography/Subheading1.js
Normal 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;
|
||||
@@ -17,6 +17,7 @@ const UserIcon = ( { uri, small }: Props ): React.Node => {
|
||||
testID="UserIcon.photo"
|
||||
className={className}
|
||||
source={uri}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 iNaturalist’s 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
|
||||
|
||||
@@ -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 iNaturalist’s 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"
|
||||
|
||||
@@ -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 iNaturalist’s 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
|
||||
|
||||
@@ -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 )
|
||||
};
|
||||
|
||||
@@ -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
4
tests/.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
plugins: ["testing-library"],
|
||||
extends: ["plugin:testing-library/react"]
|
||||
};
|
||||
@@ -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( );
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
@@ -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();
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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( );
|
||||
} );
|
||||
|
||||
@@ -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( );
|
||||
|
||||
@@ -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 } );
|
||||
} );
|
||||
|
||||
|
||||
@@ -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();
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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
|
||||
} );
|
||||
|
||||
@@ -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( );
|
||||
|
||||
@@ -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" );
|
||||
|
||||
@@ -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();
|
||||
} );
|
||||
|
||||
@@ -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( );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
@@ -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 } );
|
||||
} );
|
||||
|
||||
|
||||
@@ -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 } );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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
|
||||
} );
|
||||
|
||||
@@ -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 } );
|
||||
} );
|
||||
|
||||
@@ -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 } );
|
||||
} );
|
||||
|
||||
@@ -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 );
|
||||
} );
|
||||
|
||||
@@ -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
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user